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.