Skip to content
Control Plane Labs

402 Payment Required

Client error response defined by RFC 9110 §15.5.3.

Last updated July 27, 2026

Common causes at a glance

  • A payment provider rejecting the card behind the request, as Stripe does
  • A metered API plan whose included quota or credit balance is exhausted
  • A free trial that expired while the API key stayed valid
  • An unpaid invoice putting the account into a suspended-but-authenticated state

Reproduce it

curl -i -H "Authorization: Bearer $TOKEN" https://api.example.com/v1/render

What 402 Payment Required means

402 is the only 4xx in the core specification that says nothing at all about what it means. The original intent was some form of digital-cash negotiation that never materialised, and rather than reassign the number, successive revisions of HTTP have left it parked. That makes it a squatter’s code: available, memorable, undefined.

What it means on the wire is therefore whatever the API you are calling decided. Stripe uses it for card errors — declines, insufficient funds, expired cards. Other SaaS APIs use it for quota exhaustion on a paid plan, trial expiry, or an unpaid invoice. Those are different situations and a client cannot distinguish them from the status alone, so any handling you write has to read the response body.

What the spec says

RFC 9110 §15.5.3 is one sentence: the code “is reserved for future use”. No required header, no defined body, no caching guidance beyond the general 4xx rules in §15.5, and no registration process for the ad-hoc meanings vendors have attached to it. MDN’s 402 reference documents the same non-status. Because the code carries no semantics, the machine-readable part has to live in the body, and RFC 9457 problem details is the sensible container: a stable type URI distinguishing “card declined” from “quota exceeded” is the thing clients can branch on.

What actually causes it

Almost every real 402 is a billing state, and the giveaway is that authentication succeeded — the credential is fine, the account is not. Stripe documents 402 for card errors in its error handling guide, where the useful information is the decline code in the body rather than the status. Metered APIs are the other family: exhaust the included credit on an inference or SMS endpoint and you get 402 rather than 429, because the limit is commercial rather than rate-based. That distinction drives client behaviour. A 429 is worth retrying after a delay; a 402 keeps failing until a human updates a payment method.

How to debug it

Confirm first that this is a billing state and not a misclassified authorization failure. If the same token works on other endpoints, the credential is valid and the account is the problem. Then read the body, because the status alone carries nothing:

curl -s -o body.json -w '%{http_code}\n' -H "Authorization: Bearer $TOKEN" \
https://api.example.com/v1/render && jq . body.json

Look for a vendor error code, a type URI, or a link to a billing page, then check the provider’s dashboard for a failed charge or an exhausted balance. Make sure your retry layer excludes 402: generic backoff wrappers treat every non-2xx as retryable, and a suspended account then produces a stream of doomed requests. Alert once and stop calling.

Working examples

curl

# The status carries no meaning; the body does. Capture both.
curl -s -o body.json -w 'status=%{http_code}\n' \
  -H "Authorization: Bearer $TOKEN" \
  https://api.example.com/v1/render
jq . body.json

# Prove the credential is valid by hitting a non-metered endpoint.
curl -i -H "Authorization: Bearer $TOKEN" https://api.example.com/v1/me

Python (requests)

import requests

resp = requests.post(
    "https://api.example.com/v1/render",
    headers={"Authorization": f"Bearer {token}"},
    json={"template": "invoice"},
    timeout=30,
)

if resp.status_code == 402:
    # 402 is not retryable: no amount of backoff fixes an unpaid invoice.
    detail = resp.json()
    print("billing failure:", detail.get("type"), detail.get("detail"))
    raise SystemExit("payment method or plan needs human attention")

resp.raise_for_status()

Node (fetch)

const res = await fetch("https://api.example.com/v1/render", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + token,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ template: "invoice" }),
});

if (res.status === 402) {
  const body = await res.json().catch(() => ({}));
  // Branch on the body's type/code, never on 402 alone.
  console.error("billing:", body.type ?? body.code, body.detail ?? body.message);
  // Do not retry. Escalate.
}

Reference and tooling

The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.

Other 4xx codes