For a merchant's agent. Read this page (or its Markdown form, Accept: text/markdown) and run the steps in order. Every error carries error_code, message, suggested_action and documentation_url; the documentation_url anchors are the codes listed under "Error codes" below. Fee: 0% (pilot). Humans can use the form at /integrator/connect instead. To see whether your site is already visible to agents, try AIpush.app: https://aipush.app
payment_verified. The merchant's own wallet signs steps 1 to 3; smart-contract wallets (ERC-1271) are accepted. APIbase issues no wallets: see /integrator/wallet.alternatives[]).webhook_ping without it).Call GET /api/v1/shop/auth/nonce?wallet=<wallet>&purpose=register. The reply is {nonce, issued_at, expires_in, message}. A nonce is single-use and lives 300 seconds. Sign message exactly (EIP-191 personal_sign).
Errors: rate_limited (429), validation_failed (422).
Call POST /api/v1/shop/merchants (MCP tool shop.merchant.register) with the shop fields from the tool's input schema (slug, name, category, country, contact email, site_url, payout wallets) and encryption_key {kid, alg: "x25519", pub, sig_by_wallet}, plus message and signature from step 1. sig_by_wallet signs the text apibase.pro merchant encryption key, a newline, kid: <kid>, a newline, alg: <alg>, a newline, pub: <pub>.
The reply is {merchant_id, slug, status: "pending", docs_to_accept: [{doc_id, version, sha256, url}]}. Without a signature the call is 401.
Errors: country_not_supported (403), category_prohibited (403, with alternatives[]), slug_taken and wallet_registered (409), payout_wallet_sanctioned (403), payout_wallet_platform (400).
The payout wallet must be the merchant's own; platform addresses are refused with payout_wallet_platform (400).
Get a fresh nonce with purpose=accept_terms; its message is the acceptance text with the four document hashes. Sign it and call POST /api/v1/shop/merchants/me/acceptances (MCP tool shop.merchant.accept_terms) with {wallet, doc_hashes: [{doc_id, version, sha256}], message, signature}. All four documents are required: merchant-agreement, aup, dpa, refund-framework (see /legal/index.json).
The reply is {status: "active", api_key: "mk_live_..."}. The key is shown once; store it. Repeating the call returns {status: "active"} without a new key.
Errors: terms_not_accepted (428, docs[] holds the current hashes), merchant_unavailable (410).
Call PUT /api/v1/shop/merchants/me/catalog with Authorization: Bearer <api_key> and {items: [...]} (MCP tool shop.merchant.catalog_upsert). Up to 500 items per call. Include exactly one test item: sku: "__apibase_test", is_test: true, price_usd: 0.01. The reply lists upserted, flagged and rejected; a bad item never blocks the others.
Errors: terms_not_accepted (428), merchant_unavailable (410), category_prohibited (an item in rejected).
Call PUT /api/v1/shop/merchants/me/webhooks with {url, events[]} (MCP tool shop.merchant.webhook_set). The reply shows the whsec_ signing secret once. Deliveries carry X-APIbase-Signature: t=<unix>,v1=<hex> where v1 is HMAC-SHA256 of t + "." + body with that secret. Answer any 2xx within 10 seconds.
Errors: validation_failed (422, for example a private address or a non-https URL), merchant_unavailable (410).
Call GET /api/v1/shop/merchants/me/check (MCP tool shop.merchant.check). The reply is {status: "connected" | "incomplete", steps[], payment_verified, public_url, mcp_url}. connected needs storefront_initialize, tools_list_6, quote_test_sku and webhook_ping without a failure. The same statuses, without details, are public at /integrator/check/<slug>.
Sandbox: https://sandbox.apibase.pro — the same image on Base Sepolia and Tempo testnet with its own database; register and run the connection check there first, then repeat on mainnet with the $0.01 test SKU.
Errors: merchant_unavailable (410).
With the wallet from the preconditions, act as a buyer of your own shop:
POST /api/v1/shop/quotes with {merchant: "<slug>", items: [{sku: "__apibase_test", qty: 1}]}. The reply carries quote_id, total_usd and pay.pay.x402 and send it as X-Payment to POST /api/v1/shop/quotes/<quote_id>/pay (on the storefront MCP endpoint /mcp/m/<slug> the tool is shop.order.pay). On Tempo use the MPP challenge from the same URL.200 with state of FULFILLED or CLOSED and fulfillment text test ok means the order is paid. At most three paid test orders per day are allowed.extensions["payment-identifier"].info.id (16-128 characters, listed in the 402 extensions) to the payment payload. The same id with the same payment returns the stored answer (order_id, state, tx_hash; read the fulfillment with GET /api/v1/shop/orders/<order_id>) without a second verify or settle for 24 hours. Without an id nothing changes.Errors: quote_expired, out_of_stock, test_sku_daily_cap, payment_required, payment_amount_mismatch, payment_pending, quote_already_paying, payment_identifier_conflict, payment_in_flight, already_placed, pii_required, pii_plaintext_rejected.
Call the check again (step 6). payment_verified is true once a PAID test order made by your own agent exists. Then place link A, B, C or D on the merchant's site: /integrator#options.
A shop that sells streams (fulfillment_mode: "stream") is paid over an on-chain payment channel. Only the channel payee, your payout_wallet_tempo, can settle or close it on-chain. APIbase never holds your key: you run the settler yourself, and APIbase keeps the accounting and verifies the buyer's vouchers.
PATCH /api/v1/shop/merchants/me/stream-settings with {"settler": "merchant"} (scope catalog:write). The shop must be active.packages/stream-settler): apibase-stream-settler --key-env MERCHANT_TEMPO_KEY --api https://apibase.pro --mk mk_live_... --interval 60. The key of your payout wallet is read from your own environment variable or file and signs on your machine only; it is never sent to APIbase. Your merchant key needs the scopes orders:read and orders:write.GET /api/v1/shop/merchants/me/streams/settle-queue (scope orders:read): your channels with at least $0.50 unsettled, or an hour since the last settle, or a close requested. Each item is {channel_id, escrow_contract, chain_id, cumulative_amount, signature, action} with action = settle or close. The CLI sends the transaction from your wallet, then calls POST /api/v1/shop/merchants/me/streams/:channel_id/settled with {tx_hash} (scope orders:write). APIbase reads the chain: the transaction must come from your payout wallet and the channel's settled amount must have advanced (for a close, the channel must be finalized).202 {"status": "close_pending", "channel_id": ..., "note": ...}: you settle within the grace period; the buyer may request a close and withdraw on-chain after CLOSE_GRACE_PERIOD.close_pending channel, or one the buyer asked to close on-chain, that you have not settled for an hour raises the stream.settle_due event (webhook and events feed) and a notice by e-mail. Keep the CLI running.The platform fee of settled amounts is a receivable, as for any stream.
A subscription paid on Tempo can be renewed by your shop with an access key that you generate and keep; APIbase never sees a private key and sends no transaction, and you pay the gas.
apibase-merchant-renewer init (MIT, source in packages/merchant-renewer): it writes the key to a local file and prints the address. Register the address with PUT /api/v1/shop/merchants/me/renewer-key and {"key_id": "0x...", "expires_at": "<ISO 8601>"} (scope catalog:write; a private key in the body is refused with 422). DELETE on the same path removes it.pull_setup in shop.subscription.get, authorizes your key address with accessKey.authorize (limit = price x remaining periods) and calls shop.subscription.confirm_pull.apibase-merchant-renewer run --key-file ... --mk mk_live_... --interval 60. Each pass reads GET /api/v1/shop/merchants/me/subscriptions/renew-queue (scope orders:read), sends transferWithMemo transfers from the payer's account with your key (amount - fee to your payout wallet and, when the platform fee is on, fee to the fee wallet) and reports them with POST /api/v1/shop/merchants/me/subscriptions/:id/renewed {period_no, tx_hashes} (scope orders:write). APIbase verifies recipient, amount, memo and payer on-chain; a wrong transfer is 400 renewal_not_proven.subscription.pull_failed and the buyer's agent renews explicitly. A canceled subscription leaves nothing in the queue. Details: /integrator/docs#tempo-keychain-subscriptions.When the fee is on, the Base 402 of POST /api/v1/shop/quotes/:id/pay carries accepts[0].extra.fee_split:
{"v": 1, "fee_to": "0x...", "fee_amount": "1340000", "merchant_amount": "87660000", "how": "..."}
A client that supports it signs two EIP-3009 TransferWithAuthorization messages with the same wallet and different random nonces: the first to accepts[0].payTo for merchant_amount, the second to fee_to for fee_amount. It sends the second one inside the payment payload:
{
"x402Version": 2,
"accepted": {"scheme": "exact", "network": "eip155:8453"},
"payload": {
"authorization": {"from": "0x...", "to": "<payTo>", "value": "<merchant_amount>", "validAfter": "0", "validBefore": "<unix>", "nonce": "0x<32 bytes>"},
"signature": "0x...",
"feeAuthorization": {
"authorization": {"from": "0x...", "to": "<fee_to>", "value": "<fee_amount>", "validAfter": "0", "validBefore": "<unix>", "nonce": "0x<32 bytes>"},
"signature": "0x..."
}
}
}
Base64-encode the JSON into the X-Payment header as usual. Any other amount, recipient or payer is refused with payment_amount_mismatch and nothing is claimed. Without feeAuthorization the single authorization for the full total still works and the fee is invoiced. Example client: scripts/shop/examples/x402-fee-split-client.ts.
quote_expiredHTTP 410. The quote is past expires_at. The reply carries a fresh quote in extra.quote with current prices. Pay that one.
out_of_stockHTTP 409. Another buyer took the last unit. The reply has available and up to three alternatives.
payment_requiredHTTP 402. The settle was refused or no payment was sent. Nothing was delivered. Sign a new authorization (new nonce) and pay again before the quote expires.
payment_amount_mismatchHTTP 402. The signed amount or address differs from the quote. Sign again for exactly total_usd to the payTo address in the quote.
payment_pendingHTTP 202 with order_id. The receipt was not seen in time. Do not pay again; poll GET /api/v1/shop/orders/<order_id> until the state changes. A job reconciles the chain.
quote_already_payingHTTP 409. An MPP payment for this quote is already in progress. Wait and read the order.
payment_identifier_conflictHTTP 409. The payment-identifier id was already used with a different quote, amount, recipient or payer. Use a fresh id for each distinct payment.
payment_in_flightHTTP 409. A request with this payment-identifier id is still running. Wait, then retry with the same id to get the stored answer, or read the order.
pii_requiredHTTP 422. The product needs personal data, encrypted under the merchant's key from shop.catalog.get. Add it and retry.
pii_plaintext_rejectedHTTP 400. Personal data was sent in the clear; the value is not echoed. Send it encrypted.
merchant_unavailableHTTP 410 or 503. The shop is deactivated, suspended or temporarily unavailable. The reason is not disclosed.
country_not_supportedHTTP 403. The entity country or the sign-up address country is not served. See /legal/aup.
category_prohibitedHTTP 403. The category is not supported. alternatives[] lists the nearest allowed categories. See /legal/aup.
terms_not_acceptedHTTP 428. The merchant is pending, a document hash or version is not the current one, or a re-acceptance is overdue. docs[] holds the current hashes: redo step 3.
already_placedHTTP 200. The same buyer already paid this quote; the reply is the same order. Nothing is charged twice.
test_sku_daily_capHTTP 429. The daily limit of paid test orders is reached. Try again after 24 hours.