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.

4 min readSume
All posts

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:

Service-account usability checks (Sume API source, read 2026-10-05)
OrderCodeMessage in the sourceWhat it means for you
1service_account_key_revokedService account credential is revoked.This one key is dead; a different key on the same account may still work
2service_account_revokedService account is revoked.The account is gone; every key on it fails
3service_account_disabledService 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

All Developers posts

Written by Sume