DNAbyD3Docs

Pay with DNA

How an agent handles a `402` that asks a person to approve payment with DNA.

A 402 from a site on DNA means the page has a price and a person must approve it. The agent can't sign in or pay. It shows the person a link, waits for approval, then fetches the page. A whole-site approval mints a session, and later pages are charged against it; a one-page approval covers that page only.

A 402 can list two methods: d3-pay (this page) and x402. Use the first one you support.

The 402 response

Only a person's own browser, navigating, is redirected to payUrl. A recognised agent, or any request carrying a credential, gets the 402.

For some verified agents, DNA answers an unpaid request with a 302 to payUrl, or a 200 carrying the same JSON body without error. Handle a body with payUrl and handoff the same way whatever its status. A refused credential always gets the 402.

Headers

HeaderValue
WWW-AuthenticatePayment id="<resource>", realm="<host>", method="d3-pay", header="Payment-Authorization", request="<base64url>", description="<a sentence>". request decodes to { "priceUsd", "payUrl", "docsUrl" }.
Link<payUrl>; rel="payment"
Retry-AfterOn 402 handoff_pending only: seconds before the next poll. Same 5 as handoff.retryAfterSeconds.
payment-requiredThe x402 terms. Only present when the site also offers x402.
access-control-expose-headersNames the headers above so a browser client can read them.

Body

FieldDescription
errorpayment_required on a first ask. Otherwise a code from Errors.
preferred, methodsd3-pay, x402, most preferred first.
priceUsdPage price in dollars, as a string: "0.001".
payUrlApproval page for this handoff, on pay.d3.com. Show it to the person.
handoff.token22 base64url characters, a dot, 43 more. A bearer token for the window. Don't log it.
handoff.fetchUrlThe requested page with ?d3pay_handoff=<token> appended. Poll it, then fetch it.
handoff.paramd3pay_handoff, the query parameter above.
handoff.statuspending, approved, or spent.
handoff.expiresAtISO instant, 24 hours after the refused fetch. The request stops standing then.
handoff.retryAfterSecondsMinimum seconds between fetches of fetchUrl while pending: 5.
howThe steps below as one paragraph, for a model that sees only the body. A site may serve its own wording in other fields instead.
balanceMicrosWith insufficient_balance only: the account balance, in millionths of a dollar.
{
  "error": "payment_required",
  "preferred": "d3-pay",
  "methods": ["d3-pay", "x402"],
  "priceUsd": "0.001",
  "payUrl": "https://pay.d3.com/pay?handoff=kQ7mP2vXr9LwZ4tB8nHcYs.aG9uZXN0bHktdGhpcy1pcy1qdXN0LWFuLWV4YW1wbGU",
  "handoff": {
    "token": "kQ7mP2vXr9LwZ4tB8nHcYs.aG9uZXN0bHktdGhpcy1pcy1qdXN0LWFuLWV4YW1wbGU",
    "fetchUrl": "https://example.com/premium?d3pay_handoff=kQ7mP2vXr9LwZ4tB8nHcYs.aG9uZXN0bHktdGhpcy1pcy1qdXN0LWFuLWV4YW1wbGU",
    "param": "d3pay_handoff",
    "status": "pending",
    "expiresAt": "2026-10-14T18:02:11.000Z",
    "retryAfterSeconds": 5
  },
  "how": "This response is meant for software. ..."
}

Handle the 402

  1. Give the person payUrl to open in their own browser. They haven't seen the site's response. Don't sign in or pay for them, and don't fetch payUrl as the page.
  2. Re-fetch handoff.fetchUrl every retryAfterSeconds, or re-send the page with Payment-Authorization: Payment <handoff.token>. Until the person approves, it returns 402 handoff_pending with the same handoff.
    • Don't retry the bare page. That mints a new handoff, and the person is asked twice.
  3. The first response from fetchUrl that isn't a 402 is the page. Read its headers before fetching anything else on the site; they carry the session.

To get these steps as JSON, either:

  • GET <payUrl> with Accept: application/json and no text/html.
  • GET https://pay.d3.com/pay/instructions?handoff=<token>.

The JSON fields are state, site, page, priceUsd, approveBy, approvalUrl, fetchUrl, scope, instructions (one string), and steps (the same text as a list). state is ready, approved, taken, expired, invalid, or unavailable.

Use the session

The page returned from fetchUrl sets a cookie for the site and carries a receipt.

HeaderValue
Set-Cookie__Host-d3pay=<session>, scoped to the site, valid until handoff.expiresAt.
Payment-ReceiptBase64 JSON: status (success), method (d3-pay), reference (the request id), charged and balance (millionths of a dollar), and session on this first response only.

Send the cookie on later requests to the site. If your client doesn't keep cookies, send the session token from the receipt:

Payment-Authorization: Payment <session>
  • A session token is 43 base64url characters. The header value is capped at 128.
  • One session covers one site. Each page is charged at its own price.
  • Every fetch of fetchUrl after the first is another paid request. Fetch it once, then use the cookie.
  • With more than one credential, the order is: handoff in the URL, then Payment-Authorization, then the cookie. A stale cookie never overrides a new approval.
  • On a paid response, the site's Cache-Control is narrowed to private. A read answered from your client's cache isn't charged, and the receipt never enters a shared cache.

Scope

The person chooses the scope when approving.

ScopeCoversChargedEnds
SiteThe whole host, through a sessionEach page at its own priceAt handoff.expiresAt or when the person ends it. A 402 restarts the flow.
Single requestThe one URL in handoff.fetchUrl; no cookie, no sessionOnceAt handoff.expiresAt

With a single-request grant:

  • A retry after a lost response serves the page again without a second charge.
  • Any other page on the site returns wrong_page. A new 402 for that page asks the person again.

Errors

  • Every 402 carries a new handoff, except handoff_pending, which echoes the one you sent.
  • Unknown and expired handoffs also return handoff_pending, so the response never reveals whether a handoff exists.
  • A request carrying a credential never fails open. A 502 is a refusal.
StatuserrorMeaningChargedAction
402handoff_pendingNot approved yet, unknown, or expired.noKeep polling fetchUrl. Past expiresAt, fetch the bare page.
402invalid_handoffForged, or minted for another host.noFetch the bare page for a new handoff.
402wrong_pageA single-request grant used on a page it wasn't approved for.noFetch the page it was approved for.
402insufficient_balanceBalance is below the price. balanceMicros says by how much.noTell the person to add credit on payUrl.
402invalid_credentialSession is malformed, unknown, or for another host.noFetch the bare page and start over.
402revoked_sessionThe person ended this site's access from their account.noFetch the bare page. The person is asked again.
402expired_sessionThe session's window has passed.noFetch the bare page. The person is asked again.
502payment_unavailableDNA couldn't settle the payment. The site wasn't reached.unknownRetry shortly with the same credential.

On this page