All articles Guides

How to read a refusal

Nine reasons a call can be refused, what each one means, and what to actually do about it.

Photo: Aleksi Tappura a (CC0) / Wikimedia Commons

Every refusal from the gateway carries a reason. It is a short machine-readable string and it is precise. Here is what each one is telling you, in the order the gateway checks them.

missing_key — no key arrived at all. Usually a request built without the parameter, or a proxy stripping it. Check what actually left your server, not what your code intended to send.

invalid_key — the key does not exist, or has been revoked. Check for a copy-paste truncation first; it is by far the common cause, and a key missing its last four characters looks fine at a glance. A revoked key never comes back.

expired — past its thirty-day window. If the subscription is live the window rolls by itself, so this usually means billing rather than keys. Check the subscription before you touch the key.

ip_not_allowed — the caller's IP is not on the key's allowlist. Remember your server's egress address is not your office address, and that it may change. If you are on a platform with dynamic egress, an allowlist is the wrong tool.

insufficient_scope — the key lacks the scope the call asked for. Either grant it, or stop sending the scope parameter if you do not need the check. A key with no scopes selected has full access, not zero — "unscoped", not "permitted nothing".

domain_mismatch — the call came from a domain the key is not bound to. One key, one domain. If you need a second domain, that is a second key, or a transfer.

domain_not_verified — the domain is bound but ownership was never proved, or the proof lapsed. Add the record shown on the key's page and press Check.

usage_limit_exceeded — the account's allowance for the cycle is spent. Pooled across all your keys, resets on your subscription anniversary rather than the first of the month. Adding keys does not add allowance.

rate_limited — too many calls from this key in one minute. A burst guard, separate from the monthly allowance, and it clears within the minute.

The one thing to build

Branch on reason, not on the status code.

A 403 covers five different situations with five different fixes. The difference between *your domain is wrong* and *your subscription lapsed* matters enormously to whoever is reading your logs at two in the morning, and a log line that says 403 tells them nothing at all.

switch (body.reason) {
  case 'domain_not_verified':
  case 'domain_mismatch':
    alert('config');    // somebody has to fix a setting
    break;
  case 'expired':
  case 'usage_limit_exceeded':
    alert('billing');   // somebody has to spend money
    break;
  case 'rate_limited':
    backOff();          // this one fixes itself
    break;
  default:
    alert('integration');
}

Three different people fix those three categories. Routing them apart at the point of failure is the difference between a five-minute resolution and an hour of triage.

And do not parse the message

The message field is a sentence for a human. We reword it when we find a clearer way to say something. The reason is a contract and does not change.

Start building

Lock a key to your domain in about five minutes.

Get started