Service-account 403: key revoked, account revoked or disabled
Three 403 codes stop a service-account key before any other check: key_revoked, revoked and disabled. None is retryable; create or request a replacement.

A service-account key can be refused for three reasons before the API looks at models, caps or rates: service_account_key_revoked (this credential is revoked), service_account_revoked (the whole service account is revoked) and service_account_disabled (the account is disabled). All three are 403. None of them is a rate or capacity problem, so waiting does not help.
Read the code, pick the fix
The source checks them in this order, and returns the first that applies:
| Order | Code | Message in the source | What it means for you |
|---|---|---|---|
| 1 | service_account_key_revoked | Service account credential is revoked. | This one key is dead; a different key on the same account may still work |
| 2 | service_account_revoked | Service account is revoked. | The account is gone; every key on it fails |
| 3 | service_account_disabled | Service account is disabled. | The account is paused by its owner or an operator |
A trap in the envelope
These 403s do not have their own entry in the actionability mapping, so in the current source they take the default branch for a 4xx: category: validation, retryable: false, next_action: fix_input. The fix_input hint is not about your request body. The request is fine and the credential is not. Branch on the code and treat all three as a credential problem, so your alerting does not tell an engineer to look at a payload that is correct.
Alert, do not retry
A revoked key will never come back, so a retry loop only produces noise and denial records. Pause the worker, page the owner and keep the request_id:
import json
CRED = {
"service_account_key_revoked": "rotate this key",
"service_account_revoked": "ask for a new account",
"service_account_disabled": "ask the owner to re-enable",
}
BODY = '{"error": {"code": "service_account_disabled", "request_id": "req_example"}}'
err = json.loads(BODY)["error"]
if err["code"] in CRED:
print("PAUSE WORKER:", CRED[err["code"]], "| request_id", err["request_id"])
else:
print("not a credential error")What to record when it happens
Write the code, the request_id, the time and the key's label (not the key) to your incident log. The API also records a policy denial on its side for each refusal, so the same request_id lets the owner of the account match your log to theirs. If several workers share the key, the first one to see the 403 should flip a shared flag so that the others stop, instead of each one discovering the problem on its own.
Telling it apart from a bad key
A key that was never valid fails earlier, as an authentication error, and a key that lacks a scope fails as insufficient_scope. The three codes here mean that Sume recognised a service-account credential and that its own status says no. That difference decides who acts: a typo is yours to fix, and a revoked account is the owner's call.
Rotation without downtime
Keep two keys active during a planned change, switch the worker's environment variable, then revoke the old one. Never log the key itself; log only its last few characters if you must. The Errors and credits page lists the other auth errors, and the related posts cover insufficient_scope.
Sources
Related posts
More in Developers
- service_account_metadata_required 400: which source headers to send
The 400 lists missing fields in details.missing and the header for each in details.headers. Five x-sume-source headers exist; send the ones named.
- service_account_model_not_allowed 403: the key's model allowlist
The 403 says the model, or the operation, is not on the service-account key's allowlist. details.model or details.operation names the value to add or avoid.
- service_account_policy_unavailable 503: the message says temporary
The message says temporarily unavailable, but the envelope marks this 503 retryable false with contact_support. Retry cautiously and alert on repeats.
- service_account_rate_limited 429: read the minute and hour headers
A service-account key has an optional minute window and hour window. The 429 names the window, and x-sume-service-account headers show what is left.
Written by Sume