Pay by agent — coming
The pay family is described in the contract and is not switched on here yet. Both endpoints answer not_enabled until it is. We are not giving a date; when it answers, this page says so on its own — it reads the API’s enablement, the same answer the API’s own gate gives.
What it is
An invoice sent by a payee who opted in carries a pay reference — on the PDF (“Pay by agent”), on the payer’s GET /v1/documents/{id} as pay_url, and as pay_url on the payer’s webhook deliveries of it. A delivery’s pay_url is null on the send’s own delivery (the reference is issued just after it) and present from the next event on; the payee’s copy never carries it. The reference is the capability: it lets its holder pay that one invoice and nothing else. No account, no API key. The protocol is x402 version 2: the server answers 402 with the terms, the payer’s wallet signs an EIP-3009 TransferWithAuthorization for exactly that amount, and Coinbase’s facilitator submits the transfer payer → payee on-chain.
1. Read the challenge
GET /v1/pay/{payRef} is unauthenticated and rate-limited per IP (30 a minute by default; 429 carries Retry-After). A payable invoice answers 402 with the x402 header and a body that mirrors it for humans.
curl -i "https://api.rivet.network/v1/pay/8mYv4sB2kQ9xL0pT3wZr6cHn1jFd5aUe7oGi2bKs0Vg"{
"x402Version": 2,
"resource": {
"url": "https://api.rivet.network/v1/pay/8mYv4sB2kQ9xL0pT3wZr6cHn1jFd5aUe7oGi2bKs0Vg",
"description": "INV-0042 from Northwind Freight",
"mimeType": "application/json"
},
"accepts": [
{
"scheme": "exact",
"network": "eip155:8453",
"amount": "1250000000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0x9f2c…e41b",
"maxTimeoutSeconds": 300,
"extra": { "name": "USD Coin", "version": "2" }
}
]
}| Field | Type | Notes |
|---|---|---|
| scheme* | string | exact |
| network* | string | CAIP-2 network id — `eip155:8453` (Base) or `eip155:84532` (Base Sepolia). |
| amount* | string | The debt in ATOMIC units of `asset` (USDC has 6 decimals: balance due × 10^6). |
| asset* | string | The USDC contract address on `network` (Circle's published address). |
| payTo* | string | The payee's receiving address — attested by Rivet Money, never typed. |
| maxTimeoutSeconds* | integer | The validity window the wallet should sign for (300). |
| extra | object | The token's EIP-712 domain (`name`, `version`) — differs per network (Base `USD Coin`, Base Sepolia `USDC`; both version `2`). |
{
"x402Version": 2,
"code": "payment_required",
"invoice": {
"id": "6a5eed00000000000000d001",
"number": "INV-0042",
"payee": "Northwind Freight",
"amount": 1250,
"currency": "USD",
"due_at": "2026-10-31T00:00:00.000Z"
},
"accepts": [ { "scheme": "exact", "network": "eip155:8453", "amount": "1250000000", "…": "…" } ]
}- Amount is atomic.
accepts[0].amountis the balance due in USDC’s six decimals: $1,250.00 is1250000000. The body’sinvoice.amountis the same debt in fiat, for a person. - Sign the token’s domain, not a constant.
extracarries the EIP-712 domain the USDC contract on that network expects — Base isUSD Coinversion2, Base Sepolia isUSDCversion2. Use what the challenge says. - A paid invoice answers 200
{status: "paid"}, never a second challenge. An unknown or rotated reference, or a payee that opted out, is404— nothing about the invoice is acknowledged.
2. Sign and settle
Sign TransferWithAuthorization with from your address, to the challenge’s payTo, value its amount, a 32-byte random nonce and a validBefore inside maxTimeoutSeconds. Wrap it as a PaymentPayload, base64 it, and POST it back as PAYMENT-SIGNATURE — or send the same object as the body’s payment if you cannot set headers.
{
"x402Version": 2,
"accepted": { "scheme": "exact", "network": "eip155:8453", "amount": "1250000000", "asset": "0x8335…2913", "payTo": "0x9f2c…e41b", "maxTimeoutSeconds": 300, "extra": { "name": "USD Coin", "version": "2" } },
"payload": {
"signature": "0x<EIP-712 signature of TransferWithAuthorization>",
"authorization": {
"from": "0x<the payer's address>",
"to": "0x9f2c…e41b",
"value": "1250000000",
"validAfter": "0",
"validBefore": "1793491200",
"nonce": "0x<32 random bytes>"
}
}
}curl -X POST "https://api.rivet.network/v1/pay/8mYv4sB2kQ9xL0pT3wZr6cHn1jFd5aUe7oGi2bKs0Vg" \
-H "PAYMENT-SIGNATURE: <base64 PaymentPayload>"{
"status": "paid",
"document": { "id": "6a5eed00000000000000d001", "number": "INV-0042" },
"transaction": "0x3fa1…c09d",
"network": "eip155:8453"
}| Field | Type | Notes |
|---|---|---|
| status* | string | paid |
| document* | object | |
| transaction* | string | null | The on-chain transaction hash. |
| network* | string | null | CAIP-2 network id the transfer settled on. |
- PAYMENT-RESPONSE rides the
200: base64{success, transaction, network, payer}, the x402 settlement response. - Idempotent. The same authorization sent twice answers
200with the original transaction — the nonce was spent once. - The amount is checked again at settle. If the balance changed since the challenge,
409 payment_amount_stalecarriesdetails.current_amount; fetch a fresh challenge and sign that.
Refusals, by name
409 approval_required— the payer’s own workflow holds the invoice on a pending approval. An outside payment never bypasses a company’s controls.409 financed_invoice_rivet_rail_only— a financed invoice repays from its settlement on the Rivet rail only.409 not_payable— a draft, a cancelled or disputed invoice, or one with a payment already scheduled through Rivet.402witherrorset — the facilitator refused the signature (wrong domain, expired window, insufficient balance). Sign the re-issued challenge.502 x402_settle_failed— the facilitator could not settle after verifying. Nothing was booked; the invoice is unchanged;details.reasonsays why.400— the x402 version 1 wire (X-PAYMENT) or a payload that is not version 2. This endpoint speaks version 2 only.
Paying with your own key
The payer organization’s key takes the same road: POST /v1/documents/{id}/pay with payment_method usdc answers the same 402, accepts the same PAYMENT-SIGNATURE, and applies the key’s payment_limits to the balance due first. A member’s agent and a stranger’s agent pay the same way.
curl -X POST "https://api.rivet.network/v1/documents/6a5eed00000000000000d001/pay" \
-H "Authorization: Bearer rk_live_…" \
-H "Idempotency-Key: pay-inv-0042-usdc" \
-H "Content-Type: application/json" \
-d '{"payment_method": "usdc"}'Sandbox
A sandbox payee challenges on Base Sepolia (eip155:84532) with Circle’s testnet USDC and settles through the public testnet facilitator — fund a test wallet with Sepolia USDC and the whole loop runs end to end with no live funds. The live network is Base (eip155:8453).