Complete a checkout
Hand AgentPay a plain-English task and a spend cap. It drives the merchant site in a real browser, mints the card at the review, pays, and clears 3DS. You poll one id until it ends.
Start the run
POST /v1/checkout needs a task, a merchant and an amount (the cap, in dollars). Always send an idempotency_key: a retry with the same key returns the same checkout instead of buying twice.
curl https://api.joomjoo.com/v1/checkout \
-H "Authorization: Bearer $AGENTPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"task": "buy the domain example.click for 1 year",
"merchant": "namecheap",
"amount": 20,
"confirm_pay": false,
"idempotency_key": "order-2026-11-03-001"
}'const run = await agentpay.checkout.create({
task: "buy the domain example.click for 1 year",
merchant: "namecheap",
amount: 20,
confirm_pay: false,
idempotency_key: "order-2026-11-03-001",
});run = agentpay.checkout.create(
task="buy the domain example.click for 1 year",
merchant="namecheap",
amount=20,
confirm_pay=False,
idempotency_key="order-2026-11-03-001",
){
"checkout_id": "run_7a1e...",
"status": "queued",
"confirm_pay": false,
"host": "www.namecheap.com",
"poll": "/v1/checkout/run_7a1e...",
"request_id": "req_a1b2c3"
}confirm_pay in the response is what was granted. The agent's approval mode in the console can only narrow what you asked for.
Poll until it ends
Read GET /v1/checkout/{checkout_id} every five to ten seconds. submitted, canceled, blocked and error are terminal.
status | Meaning | What you do |
|---|---|---|
queued, running, paying | AgentPay is working. | Keep polling. |
needs_login | The merchant wants a signed-in session. | Read login_check, login_url and connect_url. See the login wall. |
needs_answer | The run met a choice the task does not settle and asked the account owner in the console. | Wait, keep polling. One open question at a time, at most three per run. |
awaiting_approval | The final review is ready. review_total is the shop's total, as a display string such as "1.98 USD". | Tell the person to approve in the console, keep polling. Only in ask mode. |
needs_card_approval | The run pays with the person's own connected card and waits for their approval on their phone. | Send approval_url to the cardholder. See the person's own card. |
submitted | Paid. order_id and order_total are set. | Done. Report the order. |
canceled | The person stopped it in the console. | Stop. Nothing was charged. |
blocked | AgentPay stopped: a wall it could not pass, a timeout, a total above the cap. error_reason says why. | Report the reason. Do not retry blindly. |
error | The engine failed. Nothing was charged and any card was closed. | Report. Retry later with the same idempotency_key. |
{
"checkout_id": "run_7a1e...",
"status": "submitted",
"task": "buy the domain example.click for 1 year",
"merchant": "namecheap",
"amount": 20,
"card_last4": "4775",
"card_source": "issued",
"host": "www.namecheap.com",
"review_total": "1.98 USD",
"order_id": "123456789",
"order_total": "1.98 USD",
"needs_login": false,
"login_check": null,
"login_url": null,
"connect_url": null,
"login_layer": "session",
"needs_card_approval": false,
"approval_url": null,
"approval_expires_at": null,
"payment_unknown": false,
"error_reason": null,
"request_id": "req_d4e5f6"
}Prefer webhooks over polling in production. checkout.submitted, checkout.needs_login, checkout.needs_card_approval and checkout.blocked fire the moment the run changes.
Which site opens
The engine opens the merchant the account already knows: a connection for that merchant, or the login the owner connected in the console. When the account has none, pass start_url (a full https address or a bare domain) so the run knows where to go. A 400 site_required or site_ambiguous means exactly that: pass start_url, or connect the merchant once.
One run at a time per merchant per account: a second run while the merchant session is busy answers 409 busy. Retry shortly.
The login wall
A run that reaches a sign-in page stops at needs_login and tells you why:
login_check | What it means | What to do |
|---|---|---|
signed_out | The merchant session ended, or there never was one. | The person signs in once at connect_url (a live window on the merchant's own page). Then start the checkout again on the same connection. |
unknown | The page gives no sign-in evidence either way. | Same as above. |
passkey_password | The person is signed in; the merchant wants the password once so AgentPay can add its own passkey. | The person types it at connect_url and you start again, or you skip with POST /v1/connections/{id}/passkey and the connection stays on the plain session. |
login_url is the page the browser stopped on. login_layer says whether the run signed in with the saved session or the AgentPay passkey.
Continue from the basket
A run that ended (canceled, blocked or error) with its own items still in the basket can be continued. Pass its id as resume_of on a new checkout: the engine opens at that run's checkout page, knows the basket is its own, and does not have to ask whether to keep it. A run with nothing to continue from answers 409 not_resumable.
Pay with the person's own card
Pass own_card_id (from GET /v1/cards, source: "own") and the run pays with the person's connected card instead of minting one. The review always waits for the cardholder's approval on their phone; confirm_pay cannot skip it. See the person's own card.
Statuses in the SDKs
Both SDKs return the same fields as the REST response and type status as the union above.
const TERMINAL = new Set(["submitted", "canceled", "blocked", "error"]);
let status;
do {
await new Promise((r) => setTimeout(r, 8000));
status = await agentpay.checkout.get(run.checkout_id);
} while (!TERMINAL.has(status.status));import time
TERMINAL = {"submitted", "canceled", "blocked", "error"}
while True:
status = agentpay.checkout.get(run["checkout_id"])
if status["status"] in TERMINAL:
break
time.sleep(8)