Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save jbruchanov/c42cf93f2957734b811018d0097172a6 to your computer and use it in GitHub Desktop.

Select an option

Save jbruchanov/c42cf93f2957734b811018d0097172a6 to your computer and use it in GitHub Desktop.
Google billing key verifier
<!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