Admin API for agents
This page is for an agent or script that does SocialScope admin setup on behalf of a SocialScope admin: listing clients, adding an origin or a return URL, changing a client's providers. It is not for integrating an app. An app uses the consumer API with its application key, described in Authentication.
An admin API key acts as the admin who minted it. Every change it makes is recorded under that admin's name with the key's ID.
Get a key
Only a SocialScope admin can mint a key. In the admin panel, open API keys, choose Create key, and confirm with Google when asked. Pick:
- a label that says where the key will live, such as
ci setup agent - a scope,
READorWRITE(see Scopes) - a lifetime in days. Every key expires. On the development deployment the maximum is 90 days. On production it is 30.
The panel shows the key once, with the endpoint URL to use. Nothing can show it again. If it is lost, revoke it and mint a new one.
Each admin can hold at most 5 active keys.
Store it
The key looks like this:
ssa_<key-id>.<secret>
The key ID is a UUID and the secret is 64 lowercase hexadecimal characters. Keep the whole value in a secret store or an environment variable, next to the endpoint:
SOCIALSCOPE_ADMIN_GRAPHQL_URL=https://<admin host>/api/admin/graphql
SOCIALSCOPE_ADMIN_API_KEY=ssa_<key-id>.<secret>
Rules:
- Read the key from the environment. Never paste it into a prompt, a chat, a commit, a test fixture or a log.
- Never print it, echo it or include it in an error message. If your tool logs request headers, turn that off.
- Never put it in browser code, browser storage or any variable bundled into a browser or mobile app.
- If the key may have been exposed, ask an admin to revoke it now. Revoking costs nothing.
The admin host is in the panel's one-time key dialog and in your team's runbook. This page does not list it.
Call the API
Send POST to /api/admin/graphql on the admin host, over HTTPS, with a JSON body and the key as a bearer token:
POST /api/admin/graphql
Content-Type: application/json
Authorization: Bearer ssa_<key-id>.<secret>
Send exactly one Authorization header and no Cookie header. A request that carries a cookie is refused, so use a client with no cookie jar. You do not need an Origin header or a CSRF token.
With curl, pass the key through a header file read from standard input, not as a command-line argument. Arguments are visible to other users of the machine in the process list and often land in shell history:
printf 'Authorization: Bearer %s\n' "$SOCIALSCOPE_ADMIN_API_KEY" |
curl --silent --fail-with-body "$SOCIALSCOPE_ADMIN_GRAPHQL_URL" \
--header @- \
--header "Content-Type: application/json" \
--data '{"query":"{ adminClients { id slug webOrigins returnUrls enabled } }"}'
printf is a shell builtin, so the key never appears as an argument of a separate process.
With fetch in Node.js:
const response = await fetch(process.env.SOCIALSCOPE_ADMIN_GRAPHQL_URL!, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.SOCIALSCOPE_ADMIN_API_KEY}`,
},
body: JSON.stringify({
query: `mutation($id: ID!, $origin: String!) {
addClientOrigin(id: $id, origin: $origin) { id webOrigins }
}`,
variables: { id: "<client id>", origin: "https://app.example.com" },
}),
});
const { data, errors } = await response.json();
Read errors on every response. A 200 status alone does not mean the operation succeeded.
Introspection is off. The operations and their arguments are listed below. Send one operation per request.
Send the full query text every time. Do not rely on persisted-query hashes: the server keeps no guaranteed cache of them, and a hash-only request it does not recognise answers UPSTREAM_UNAVAILABLE, which retrying will not fix.
Scopes
| Scope | Can call |
|---|---|
READ |
Every READ operation below |
WRITE |
Every READ and WRITE operation below |
A WRITE key does not need the Google confirmation that the panel asks for before these changes. The key itself is that permission, which is why every key expires and every write names it.
Operations
No key can call a panel-only operation, whatever its scope.
| Operation | Kind | Class |
|---|---|---|
previewTemplate |
Query | READ |
adminMembers |
Query | READ |
adminAuditEntries(first, after, apiKeyId) |
Query | READ |
adminClients |
Query | READ |
adminClient(id) |
Query | READ |
adminProviderAvailability |
Query | READ |
adminRegistrations |
Query | READ |
createClient(input) |
Mutation | WRITE |
setClientEnabled(id, enabled) |
Mutation | WRITE |
setClientDisplayName(id, displayName) |
Mutation | WRITE |
setClientPolicy(id, providers, capabilities) |
Mutation | WRITE |
addClientTenantKey(id, tenantKey) |
Mutation | WRITE |
removeClientTenantKey(id, tenantKey) |
Mutation | WRITE |
addClientOrigin(id, origin) |
Mutation | WRITE |
removeClientOrigin(id, origin) |
Mutation | WRITE |
addClientReturnUrl(id, url, confirmedNoOnwardRedirect) |
Mutation | WRITE |
removeClientReturnUrl(id, url) |
Mutation | WRITE |
setClientPreviewOrigins(id, enabled) |
Mutation | WRITE |
addClientPreviewReturnPath(id, path) |
Mutation | WRITE |
removeClientPreviewReturnPath(id, path) |
Mutation | WRITE |
apiKeys |
Query | Panel only |
mintApiKey, revokeApiKey |
Mutation | Panel only |
addAdminMember, removeAdminMember |
Mutation | Panel only |
issueClientCredential, revokeClientCredential |
Mutation | Panel only |
publishRegistration, disableRegistration |
Mutation | Panel only |
setClientGoogleIdentity |
Mutation | Panel only |
createClient and addClientReturnUrl take confirmedNoOnwardRedirect: true. Pass it only after you have read the app's route for that return URL and confirmed it never redirects onward to an address taken from the request.
The operation is checked as a whole before anything runs. If one field in it is refused, nothing in it runs. Every root field in the document counts, including one marked @skip or @include(if: false), so a refused field cannot be hidden behind a directive.
What a key cannot do
A key cannot list, mint or revoke admin API keys, add or remove admins, issue or revoke an app's application key, publish or disable a provider registration, or turn Google sign-in on or off for a client. Ask an admin to do those in the panel. A leaked key therefore cannot create more access.
Errors
Errors follow the shape in Errors and result codes: read extensions.code, and extensions.reason where one is listed.
| Code | Reason | Meaning | What to do |
|---|---|---|---|
UNAUTHENTICATED |
The key is malformed, unknown, wrong, expired or revoked, or its admin was removed or changed Google account. Every case looks the same | Stop. Ask an admin for a new key. Do not retry | |
FORBIDDEN |
API_KEY_READ_ONLY |
A READ key sent a mutation |
Use a WRITE key, or ask an admin to make the change |
FORBIDDEN |
PANEL_ONLY |
The operation is panel only | Ask an admin to do it in the panel |
FORBIDDEN |
Plain HTTP, a Cookie header, two Authorization headers, or the wrong host |
Fix the request. Use HTTPS, one header and no cookies | |
BAD_USER_INPUT |
An argument failed validation, or the document is invalid | Fix the input. The message does not name the field | |
NOT_FOUND |
The ID does not exist | Re-read the list | |
CONFLICT |
Sometimes | The change breaks a rule, such as removing a client's last origin, or the row was busy | Read reason if present, re-read the current state, then decide. A busy row can be retried once |
UPSTREAM_UNAVAILABLE |
The server could not complete the request | Retry later with backoff. Re-read state first, because the change may have applied |
Expiry and revocation
A key stops working the moment it expires or an admin revokes it. Any admin can revoke any key. Removing an admin also revokes all their keys. Keys are never extended, reactivated or rescoped. Mint a new one instead.
Every write a key makes appears in the panel's audit log as "Via API key" followed by the first 8 characters of the key ID. The key's first authenticated request is recorded as "API key first used", even when its operation is refused for the key's scope. An admin can filter the audit log to one key, so after a leak they can see everything it changed. Audit entries are kept for 30 days.
Checklist for an agent
- Read the key and the endpoint from the environment. Never print either secret value.
- Start with a
READquery, such asadminClients, to confirm the key works. - Make one change per request and check
errorseach time. - After a write, read the object back, and check
adminAuditEntries(apiKeyId: "<your key ID>")when you need proof the change landed. - Report what you changed, by client slug and field, to the admin who gave you the key.