Gateway
Invoke a resource
POST /api/resources/{id}/invoke — call a free resource, or complete a paid one with an x402 or MPP proof.
/api/resources/{id}/invokepayment, not authenticationInvoke a resource exposed over http. The JSON body is the resource input. Free resources are delivered directly; paid ones answer 402 until the same request is repeated with a valid proof.
Request
Path
idstringrequiredHeaders
content-typeapplication/jsonPAYMENT-SIGNATUREbase64AuthorizationPayment …Agent-Authorizationbase64urlX-Request-IdstringBody
…objectExamples
curl -s http://localhost:8080/api/resources/weather_basic/invoke \
-X POST -H 'content-type: application/json' \
-d '{"city":"Berlin"}'Responses
Delivered
The merchant backend's status and body, with the backend's Content-Type when it is a text type that fits the body, relayed with cache-control: private when the call was paid. Text is always sent as UTF-8 and carries Content-Security-Policy: sandbox and X-Content-Type-Options: nosniff, so a page from the backend cannot act as the gateway's origin. Binary responses are not supported. After a settlement, the PAYMENT-RESPONSE header carries a base64 JSON summary, with amount in the asset's base units when the provider reports it:
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
PAYMENT-RESPONSE: eyJzdWNjZXNzIjp0cnVlLCJ0cmFuc2FjdGlvbiI6IjB4NGYy…
{ "report": "…" }{
"success": true,
"transaction": "0x4f2c…9ab1",
"network": "eip155:31337",
"payer": "0x3C44…93BC",
"amount": "10000",
"status": "settled",
"provider": "x402",
"currency": "USDC",
"externalReference": "0x4f2c…9ab1"
}402 Payment Required
The body is the envelope below; the PAYMENT-REQUIRED header carries the x402 v2 PaymentRequired document as base64, which off-the-shelf x402 clients read. Sent with cache-control: no-store.
{
"status": "payment-required",
"code": "PAYMENT_REQUIRED",
"requestId": "…",
"resourceId": "market_report",
"message": "Payment of 0.01 USDC is required for resource \"market_report\". …",
"payment": {
"provider": "x402",
"version": "2",
"amount": "0.01",
"currency": "USDC",
// destination, network and asset are deprecated, and absent when
// the challenge lists several offers: read each one in accepts
"destination": "0x…",
"network": "eip155:31337",
"asset": "0x…",
"expiresAt": "…",
"accepts": [ /* x402 v2 PaymentRequirements, verbatim */ ],
"envelope": { /* x402 v2 PaymentRequired, verbatim */ }
}
}With several x402 offers, accepts holds one requirement per offer and the proof's accepted echo selects one; zero or several matches are invalid_payment_requirements. The envelope then omits destination, network and asset, which are deprecated in favor of each offer in accepts, and the challenge names the resource as resource://agent-commerce/resources/<id>.
With experimental ERC-8409 quotes enabled, the PaymentRequired document also carries one signed quote per EVM offer in extensions["erc-8409"].info.quotes. Each quoted offer adds about 0.9 KB of base64 to the header; see Check a signed quote.
Headers by rail
| x402 | MPP | |
|---|---|---|
| proof | PAYMENT-SIGNATURE | Authorization: Payment … |
| challenge on 402 | PAYMENT-REQUIRED | WWW-Authenticate: Payment … |
| refused proof or spent authorization (402) | a fresh PAYMENT-REQUIRED with error | a fresh WWW-Authenticate |
| refused settlement (402) | PAYMENT-RESPONSE with success: false | a fresh WWW-Authenticate and PAYMENT-RESPONSE with success: false |
| delivered | PAYMENT-RESPONSE | Payment-Receipt and PAYMENT-RESPONSE |
| backend error after settlement | PAYMENT-RESPONSE | PAYMENT-RESPONSE |
When the pipeline offers another attempt, the error body includes its challenge in details.challenge. Other paid outcomes:
- A replay returns
409without a challenge if the first attempt is unfinished, has the legacyfailedstatus, or its status cannot be read. A new challenge in those cases could lead to a second payment. Since 2.6.0, an unfinished attempt also blocks a replay whose proof fails verification after the transfer lands or its MPP challenge expires. - A replay's
details.networknames the network the first attempt paid on. The fresh x402 challenge on a 402 uses that family's replayerror:invalid_exact_evm_nonce_already_usedon EVM,invalid_exact_near_payload_delegate_action_nonce_already_usedon NEAR andduplicate_settlementon Solana. - A missing Permit2 token allowance (
permit2_allowance_required) answers412 Precondition Failedwith a freshPAYMENT-REQUIRED. The buyer approves Permit2 and retries.x402HTTPClient.parsePaymentResultdecodes that header only on 402; for 412, callgetPaymentRequiredResponseor readdetails.challenge. PAYMENT_PROVIDER_UNAVAILABLEandAUTHORIZATION_PROVIDER_UNAVAILABLEanswer503withRetry-After: 5. An outage is no verdict on the payment, so the same proof may be sent again.- A settlement with no verdict answers
502withPAYMENT-RESPONSE. ItserrorReasonissettlement_pendingwhen a transaction hash is known, andunexpected_settle_errorotherwise. - x402 v1 is refused. A request carrying only the v1
X-PAYMENTheader gets the 402 challenge witherror: "invalid_x402_version"inPAYMENT-REQUIRED. - An MPP challenge for a request with a body binds an RFC 9530
sha-256digest of the body bytes. The paid retry must send the same bytes; even reformatted JSON getsbody_digest_mismatch.
Errors
The error envelope with the code's HTTP status. A backend failure under upfront, the MPP default, happens after settlement, so the response still includes PAYMENT-RESPONSE. A backend failure under authorization, the x402 default, settles nothing. So does a refused x402 settlement, and the client can send the same x402 proof again; an MPP credential is single-use, so its retry gets 402 PAYMENT_REPLAYED with a fresh challenge.
Over HTTP, MPP's unpaid 402 and its verification, replay, settlement and provider errors use application/problem+json with the core draft's problem types. The envelope's code, message and details stay as extension members, and status is the numeric HTTP status. detail is a sentence; the reason token stays in message and details.reason. A successful Payment-Receipt includes challengeId and chainId.
BACKEND_ERROR; redirects are not followed.