Created
May 11, 2026 20:34
-
-
Save jbruchanov/c42cf93f2957734b811018d0097172a6 to your computer and use it in GitHub Desktop.
Google billing key verifier
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Play Store API Test</title> | |
| <style> | |
| body { font-family: sans-serif; max-width: 700px; margin: 40px auto; padding: 0 20px; } | |
| label { display: block; margin-top: 16px; font-weight: bold; } | |
| textarea { width: 100%; height: 150px; font-family: monospace; font-size: 12px; } | |
| input[type=text] { width: 100%; padding: 6px; font-size: 14px; } | |
| button { margin-top: 16px; padding: 10px 24px; font-size: 16px; cursor: pointer; } | |
| #result { margin-top: 20px; padding: 16px; background: #f4f4f4; border-radius: 8px; white-space: pre-wrap; font-family: monospace; font-size: 13px; display: none; } | |
| .ok { border-left: 4px solid #2e7d32; } | |
| .err { border-left: 4px solid #c62828; } | |
| </style> | |
| </head> | |
| <body> | |
| <h2>Google Play Developer API — Key Verification</h2> | |
| <label for="jsonKey">Service Account JSON Key (paste entire file content):</label> | |
| <textarea id="jsonKey" placeholder='{"type":"service_account","project_id":"...","private_key":"-----BEGIN PRIVATE KEY-----\n...'></textarea> | |
| <label for="appId">Application ID (package name):</label> | |
| <input type="text" id="appId" value=""> | |
| <button onclick="verify()">Verify</button> | |
| <div id="result"></div> | |
| <script> | |
| const resultEl = document.getElementById('result'); | |
| function show(msg, ok) { | |
| resultEl.style.display = 'block'; | |
| resultEl.className = ok ? 'ok' : 'err'; | |
| resultEl.textContent = msg; | |
| } | |
| function b64url(buf) { | |
| const bytes = buf instanceof ArrayBuffer ? new Uint8Array(buf) : new TextEncoder().encode(buf); | |
| let s = ''; | |
| bytes.forEach(b => s += String.fromCharCode(b)); | |
| return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); | |
| } | |
| async function importKey(pem) { | |
| const pemBody = pem.replace(/-----BEGIN PRIVATE KEY-----/, '') | |
| .replace(/-----END PRIVATE KEY-----/, '') | |
| .replace(/\s/g, ''); | |
| const binary = Uint8Array.from(atob(pemBody), c => c.charCodeAt(0)); | |
| return crypto.subtle.importKey('pkcs8', binary, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['sign']); | |
| } | |
| async function createJwt(email, privateKey) { | |
| const now = Math.floor(Date.now() / 1000); | |
| const header = b64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' })); | |
| const payload = b64url(JSON.stringify({ | |
| iss: email, | |
| scope: 'https://www.googleapis.com/auth/androidpublisher', | |
| aud: 'https://oauth2.googleapis.com/token', | |
| iat: now, | |
| exp: now + 3600 | |
| })); | |
| const key = await importKey(privateKey); | |
| const sig = await crypto.subtle.sign('RSASSA-PKCS1-v1_5', key, new TextEncoder().encode(header + '.' + payload)); | |
| return header + '.' + payload + '.' + b64url(sig); | |
| } | |
| async function getAccessToken(jwt) { | |
| const resp = await fetch('https://oauth2.googleapis.com/token', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, | |
| body: `grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=${jwt}` | |
| }); | |
| const data = await resp.json(); | |
| if (data.error) throw new Error(`Token error: ${data.error_description || data.error}`); | |
| return data.access_token; | |
| } | |
| async function verify() { | |
| resultEl.style.display = 'block'; | |
| resultEl.className = ''; | |
| resultEl.textContent = 'Working...'; | |
| try { | |
| const keyData = JSON.parse(document.getElementById('jsonKey').value.trim()); | |
| const appId = document.getElementById('appId').value.trim(); | |
| if (!keyData.client_email || !keyData.private_key) { | |
| throw new Error('Invalid JSON key — missing client_email or private_key'); | |
| } | |
| if (!appId) throw new Error('Please enter an Application ID'); | |
| // 1. Create JWT and get access token | |
| show('Signing JWT...', true); | |
| const jwt = await createJwt(keyData.client_email, keyData.private_key); | |
| show('Getting access token...', true); | |
| const token = await getAccessToken(jwt); | |
| // 2. Create an edit (proves API access to the app) | |
| show('Creating edit for ' + appId + '...', true); | |
| const editResp = await fetch( | |
| `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${appId}/edits`, | |
| { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: '{}' } | |
| ); | |
| const editData = await editResp.json(); | |
| if (editData.error) throw new Error(`API error ${editData.error.code}: ${editData.error.message}`); | |
| // 3. List tracks | |
| const tracksResp = await fetch( | |
| `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${appId}/edits/${editData.id}/tracks`, | |
| { headers: { Authorization: `Bearer ${token}` } } | |
| ); | |
| const tracksData = await tracksResp.json(); | |
| // 4. Cleanup — delete the edit | |
| await fetch( | |
| `https://androidpublisher.googleapis.com/androidpublisher/v3/applications/${appId}/edits/${editData.id}`, | |
| { method: 'DELETE', headers: { Authorization: `Bearer ${token}` } } | |
| ); | |
| // 5. Show results | |
| const tracks = (tracksData.tracks || []).map(t => { | |
| const releases = (t.releases || []).map(r => ` ${r.status}: v${r.versionCodes?.[0] || '?'} — ${r.name || 'no name'}`).join('\n'); | |
| return ` ${t.track}:\n${releases || ' (empty)'}`; | |
| }).join('\n'); | |
| show(`✅ SUCCESS — Key is valid!\n\nService account: ${keyData.client_email}\nApp: ${appId}\nEdit ID: ${editData.id}\n\nTracks:\n${tracks || ' (no tracks found)'}`, true); | |
| } catch (e) { | |
| show(`❌ FAILED\n\n${e.message}`, false); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment