Skip to main content

The key_expired error

When you send a request with an expired or expired-disabled key, Nuntiq deliberately returns more information than a generic 401. This page documents the response and the recovery path.

The motivation is straightforward: if a partner's recipients muted every reminder email, the only signal a key is gone is the next request breaking. This response tells your integration log exactly where to send a human to get it back.

What it looks like

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
"error": "key_expired",
"message": "This API key expired on 2026-08-18. Generate a new key at https://acme.dev.apquery.com/supplier-access/regenerate",
"regenerate_url": "https://acme.dev.apquery.com/supplier-access/regenerate"
}

Three fields beyond the standard message:

FieldMeaning
errorAlways "key_expired". Use this to branch on the error type without parsing the message.
messageHuman-readable, includes the expiry date and the regenerate URL.
regenerate_urlPer-tenant URL of the supplier portal regen page. Send a human here.

When you'll see it

Either of:

  • expires_at on the key has passed.
  • The daily supplier_api_maintenance task has stamped expired_at on the key (which it does for every key whose expires_at is in the past).

Both conditions resolve to the same response.

What it doesn't tell you

  • Which key (no key_id, no last_4). The 401 is generated by the auth path, which deliberately does not enumerate keys.
  • Whether the key was revoked. A revoked key returns the standard "Invalid API Key" 401, not this one. (Revocation is a manual kill; expiry is a scheduled lapse; they have separate audit trails.)
  • Whether your rotation_secret is still valid. It is not — the same hard-delete that removes the key row also removes the rotation secret. You cannot rotate an expired key. You can only regenerate.

Recovery

  1. Log loudly. Your integration's error log should make this response visible — it is the signal that everything stops working in less than 30 days unless someone acts.
  2. Send a human to regenerate_url. They follow the same claim flow as at first issue: enter email, request 6-digit code, enter code, pick a new expiration interval, claim. Both api_key and rotation_secret are issued fresh.
  3. Replace your stored credentials. The old key_id is gone (the regen flow always mints a new row). Update your vault, restart your process, retry.

There is no programmatic recovery path. By design — an expired key means the partner organization needs to make a deliberate choice about who is responsible for the new one.

Suggested client-side handling

def call_partner_api(method, path, **kwargs):
resp = requests.request(method, NUNTIQ_BASE + path, **kwargs)
if resp.status_code == 401:
body = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {}
if body.get("error") == "key_expired":
log.error(
"Nuntiq partner key expired. Regenerate at %s. Message: %s",
body.get("regenerate_url"),
body.get("message"),
)
# Page the on-call human, do not retry.
raise NuntiqKeyExpired(body.get("regenerate_url"), body.get("message"))
resp.raise_for_status()
return resp.json()

The pageable NuntiqKeyExpired exception is the point: this is not a transient failure. Exponential backoff will burn CPU forever. A human has to claim a new key.