HTTP 402 Payment Required sat reserved in the HTTP specification for three decades. DurableFile uses it for exactly what the name says: the request is valid, the account is real, and the only missing thing is payment.

For an agent, 402 is not an error to log and abandon. It is a state with a documented exit.

When 402 happens

Two situations produce it:

  1. payment_required — the account has never bought storage. New accounts start with zero quota, because account creation is free and unlimited; payment is the abuse gate that replaces email confirmation and CAPTCHAs.
  2. quota_exceeded — the account has quota, but this upload does not fit in what remains.

Both bodies follow the same shape:

{
  "error": "payment_required",
  "fix": "buy storage with POST https://durablefile.com/v1/credit and {\"gb\":1}, complete one payment option, then retry the upload",
  "topup_url": "https://durablefile.com/v1/credit",
  "suggested_gb": 1,
  "gb_price_usd": "1.00",
  "storage_quota_bytes": 0,
  "checkout_url": "https://checkout.stripe.com/…",
  "machine_payment_options": []
}

The fix field is the contract: a plain-language instruction naming the exact next call. An agent should surface it or act on it, never retry the same request unchanged.

Two rails, two kinds of caller

The 402 body and the POST /v1/credit response both carry two payment paths, because callers come in two kinds:

Stripe — the human path. checkout_url is a hosted card page. A browser, a card, a person. If the agent works for a human, the correct move is to hand this URL over and wait.

usevig — the machine path. When the stablecoin rail is available, the response also carries machine_payment_options: entries with an exact token amount, a network, a destination address, and a pay instruction string. An agent holding a wallet can execute an ordinary token transfer and needs no browser, no card, and no human. Each quote has an expiry timestamp, so request the checkout when ready to pay, not hours before.

Both rails grant the same thing: $1 buys 1 GiB of one-time, stacking quota.

The recovery loop

The grant arrives through a signed payment webhook — never from a browser return page — so the agent's loop is:

# 1. Ask for payment options.
curl -X POST https://durablefile.com/v1/credit \
  -H "Authorization: Bearer $DURABLEFILE_KEY" \
  -H "Content-Type: application/json" -d '{"gb":1}'

# 2. Complete one option (hand checkout_url to a human, or pay a
#    machine_payment_options entry from a wallet).

# 3. Poll until the quota lands.
curl https://durablefile.com/v1/me \
  -H "Authorization: Bearer $DURABLEFILE_KEY"

# 4. Retry the original upload unchanged.
curl -X POST https://durablefile.com/v1/upload \
  -H "Authorization: Bearer $DURABLEFILE_KEY" \
  -F "file=@report.pdf"

The retry in step 4 is safe by design: uploads are content-addressed, so if the earlier attempt partially succeeded somewhere, the retry converges on the same URL. See same bytes, same URL.

Handling quota_exceeded

The over-quota variant includes the numbers needed to decide: incoming_bytes, storage_remaining_bytes, and a suggested_gb that covers the shortfall. The agent has two honest options — buy suggested_gb through the same flow, or free space by deleting files it no longer needs (delete is the only lifecycle operation that frees quota; see the lifecycle contract).

A well-behaved agent checks GET /v1/me before large uploads and treats 402 as a budget question for its operator: is this artifact worth a dollar? That is a decision, not an exception — which is exactly why the status code exists.

One last framing note before paying: everything uploaded after the 402 clears is public to anyone holding the URL. The dollar buys hosting for artifacts that are meant to be shared — never route secrets or personal data through it.