Skip to main content

Request Payment

The Request Payment capability creates a payment intent that is fulfilled by a human scanning a QR code with their wallet app. This is the core payment flow in ItPay — it supports the full lifecycle from intent creation through settlement.

This capability also supports the HTTP 402 (x402) mode, where a service responds with 402 Payment Required instead of requiring a separate API call.


Swimlane Diagram — Request Payment Lifecycle

The following swimlane shows the complete flow from user request to service delivery. All four actors participate.

┌──────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Human │ │ Agent │ │ ItPay │ │ Payment │
│ (Payer) │ │ (Buyer) │ │ Protocol │ │ Channel │
└────┬─────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │ │
│ 1. User says: │ │ │
│ 'Summarize this │ │ │
│ PDF for $6.99'│ │ │
├──────────────────▶ │ │
│ │ │ │
│ │ 2. POST /v1/payment-intents │
│ │ {service_id, amount, description} │
│ ├──────────────────▶ │
│ │ │ │
│ │ │ 3. Create QR │
│ │ │ via channel API │
│ │ ├──────────────────▶│
│ │ │ │
│ │ │ 4. QR URI │
│ │ │ ◀────────────────┤
│ │ │ │
│ │ 5. 201 Created │ │
│ │ {id, qr, status} │ │
│ │ ◀────────────────┤ │
│ │ │ │
│ 6. Show QR │ │ │
│ 'Scan with │ │ │
│ Alipay' │ │ │
│ ◀───────────────┤ │ │
│ │ │ │
│ 7. Human scans │ │ │
│ QR with wallet │ │ │
├─────────────────────────────────────────────────────────▶│
│ │ │ │
│ │ │ 8. Channel │
│ │ │ callback/webhook │
│ │ │ {status: auth} │
│ │ │ ◀────────────────┤
│ │ │ │
│ 9. Human auths │ │ │
│ in wallet app │ │ │
├─────────────────────────────────────────────────────────▶│
│ │ │ │
│ │ │ 10. Authorized │
│ │ │ callback │
│ │ │ ◀─────────────┤
│ │ │ │
│ │ 11. Poll GET │ │
│ │ /v1/... │ │
│ ├──────────────────▶│ │
│ │ │ │
│ │ 12. Status: auth │ │
│ │ ◀────────────────┤ │
│ │ │ │
│ │ 13. POST /v1/... │ │
│ │ /capture │ │
│ ├──────────────────▶│ │
│ │ │ │
│ │ │ 14. Channel │
│ │ │ settle/capture│
│ │ ├──────────────────▶│
│ │ │ │
│ │ │ 15. Settlement │
│ │ │ confirmed │
│ │ │ ◀────────────────┤
│ │ │ │
│ │ 16. Webhook: │ │
│ │ succeeded │ │
│ │ ◀────────────────┤ │
│ │ │ │
│ 17. 'Here's your │ │ │
│ summary' │ │ │
│ ◀───────────────┤ │ │

How QR Payment Works in Practice When a user asks an AI agent for a paid service, the agent creates a PaymentIntent via the ItPay Protocol. This triggers the generation of a QR code through the configured payment channel (e.g., Alipay, WeChat Pay). The human scans the QR with their wallet app, authorizes the payment, and the agent receives a webhook confirming settlement — at which point the service is delivered. The entire lifecycle from request to delivery takes under 30 seconds in the happy path.


Endpoint

MethodEndpointDescription
POST/v1/payment-intentsCreate a payment intent with QR code

Request Fields

FieldTypeRequiredDescription
service_idstring (UUIDv7)yesThe ServiceManifest id this intent is paying for
typestringyesPayment type — must be "one_time"
amountMoneyyesAmount to charge in the payer's local currency
amount.currencystring (ISO 4217)yesCurrency code (e.g. CNY, USD, THB)
amount.valuenumberyesAmount in minor units (e.g. cents)
descriptionstringyesHuman-readable description of what this payment is for
payer_channelstringnoChannel the payer will use (e.g. alipay, wechat). If omitted, the service's default channel is used.
return_urlstring (URL)noURL to redirect the user to after payment completes
metadataobjectnoArbitrary key-value data (max 4 KB)

Money Object

FieldTypeDescription
currencystring (ISO 4217)Currency code
valuenumberAmount in minor currency units (e.g. cents)
tip

Always use minor currency units (cents, fen, satang) for the value field. This avoids floating-point precision issues. For example, CNY 6.99 becomes { "currency": "CNY", "value": 699 }.


1. Request & Session Context

Human-side dialog:

Human: "Hey, can you summarize this 42-page PDF for me?" Agent: "Sure! That will cost CNY 6.99 (about $0.99). I'll need a payment to proceed." Human: "OK, go ahead."

Agent-side behavior: The agent recognizes this is a paid service. It validates the human's intent to pay, checks if the service is installed, and constructs the PaymentIntent payload. The agent holds the conversation state — it does not proceed without explicit human consent.

warning

Never create a PaymentIntent without explicit human consent. The agent must confirm the amount, currency, and description with the human before making any API call.

Error: Human declines payment

  • Agent responds: "No problem, I won't create the payment. Let me know if you change your mind."
  • No API call is made. State remains unchanged.

2. PaymentIntent Creation

HTTP Request

POST /v1/payment-intents
Content-Type: application/json
Authorization: Bearer ag_sk_...k1l2
Idempotency-Key: 01J7XYKZ1A2B3C4D5E6F7G8H9I

{
"service_id": "01J7XYKZ1A2B3C4D5E6F7G8H9I",
"type": "one_time",
"amount": {
"currency": "CNY",
"value": 699
},
"description": "AI document summary (42 pages, PDF)",
"payer_channel": "alipay",
"return_url": "https://summarybot.ai/thank-you",
"metadata": {
"session_id": "sess_xyz_456",
"file_hash": "sha256:aabbccddeeff00112233445566778899"
}
}

HTTP Response (Success)

HTTP/1.1 201 Created
Content-Type: application/json
X-Request-Id: req_01J7XZ1A2B3C4D5E6F7G8H9IK

{
"id": "pi_01J7XZ1A2B3C4D5E6F7G8H9IK",
"service_id": "01J7XYKZ1A2B3C4D5E6F7G8H9I",
"type": "one_time",
"amount": {
"currency": "CNY",
"value": 699
},
"settlement": {
"currency": "USD",
"value": 99,
"rate": 0.1416
},
"description": "AI document summary (42 pages, PDF)",
"payer": {
"agent_id": "agent_cli_a1b2c3d4",
"human_id": null
},
"payee": {
"agent_id": "agent_srv_9x8y7z6w",
"merchant_account": "summarybot@alipay"
},
"channel": "alipay",
"qr": {
"charge_id": "qr_01J7XZ2C3D4E5F6G7H8I9J0K",
"scan_url": "https://pay.summarybot.ai/qr/qr_01J7XZ2C3D4E5F6G7H8I9J0K"
},
"status": "pending",
"metadata": {
"session_id": "sess_xyz_456",
"file_hash": "sha256:aabbccddeeff00112233445566778899"
},
"created_at": "2026-05-27T09:00:05Z",
"expires_at": "2026-05-27T09:15:05Z"
}

Understanding the PaymentIntent Object

The PaymentIntent object is the core data structure of the Request Payment flow. Key fields:

  • id — Unique identifier (pi_ prefix, UUIDv7). Used for all subsequent operations (polling, capture, refund).
  • qr — Contains the scan_url which must be rendered as a QR code image for the human payer.
  • status — Tracks the lifecycle state. Starts as pending and transitions through qr_generatedscanningauthorizedcapturedsucceeded.
  • settlement — Shows the converted amount in the payee's settlement currency, along with the exchange rate applied.
  • payer / payee — Identifies the participating agents. human_id is populated only after the human authorizes payment.
  • expires_at — Hard deadline (typically 15 minutes). After this time, no further state transitions are possible.
note

The human_id field in the payer object is null at creation time. It is populated after the human authorizes payment in their wallet app, enabling the agent to reconcile the payment to a specific user.

Error Responses

400 Bad Request — Validation failure

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
"error": "validation_error",
"code": "INVALID_AMOUNT",
"message": "amount.value must be a positive integer in minor units",
"details": {
"field": "amount.value",
"value": -699,
"constraint": "minimum: 1"
}
}

Handling: Agent logs the error, presents a friendly message to the human ("Something went wrong creating the payment, please try again."), and does not retry automatically for validation errors.

401 Unauthorized — Invalid API key

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
"error": "authentication_error",
"code": "INVALID_API_KEY",
"message": "The provided API key is invalid or expired"
}

Handling: Agent rotates credentials, retries once. If still failing, surfaces to the human: "I'm having trouble connecting to the payment system. Please try again later."

402 Payment Required (if using x402 mode instead of explicit API call) Handled via the HTTP 402 flow — see x402 Semantics.

409 Conflict — Duplicate idempotency key

HTTP/1.1 409 Conflict
Content-Type: application/json

{
"error": "conflict",
"code": "IDEMPOTENCY_KEY_USED",
"message": "This idempotency key was already used for a different request"
}

Handling: Agent generates a fresh idempotency key and retries the request.

429 Too Many Requests — Rate limited

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 5

{
"error": "rate_limited",
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests. Please retry after 5 seconds."
}

Handling: Agent waits Retry-After seconds, then retries with exponential backoff (1×, 2×, 4×, max 30s).

500/502/503 — Server error

HTTP/1.1 503 Service Unavailable
Content-Type: application/json

{
"error": "service_unavailable",
"code": "TEMPORARILY_UNAVAILABLE",
"message": "The payment service is temporarily unavailable. Please retry."
}

Handling: Agent retries with exponential backoff (1s, 2s, 4s, 8s), up to 3 attempts. If all fail: "The payment service is currently unavailable. Please try again in a few minutes."

tip

Always include an Idempotency-Key header (UUIDv7) when creating PaymentIntents. This guarantees safe retries — if the request succeeds server-side but the response is lost, retrying with the same key returns the original result instead of creating a duplicate payment.


3. Channel Selection & Routing

The payer_channel field determines which downstream payment channel processes the QR code. The routing algorithm follows this priority:

1. Explicit payer_channel → must be in service's `accepted_channels`
2. Channel preference stored on Install → if payer has installed this service
3. Payer's wallet auto-detection → prompt user to choose
4. Service's default_channel → set at manifest creation

Understanding Channel Routing

Channel routing is the process of mapping a PaymentIntent to a specific payment gateway adapter. Each channel adapter handles:

  • Credential management (API keys, certificates, signing)
  • Request/response translation between ItPay's canonical format and the channel's native API
  • Webhook signature verification
  • Error code normalization
ScenarioPayer provides payer_channelUser's wallet detectedRouting Decision
Explicit channel"alipay"Route to Alipay adapter. Must be in service's accepted_channels.
Auto-detect"auto" or omittedAlipayRoute to channel matching detected wallet
FallbackOmittedWallet not detectedUse service default_channel or return CHANNEL_UNAVAILABLE

Channel Resolution (PaaS Layer)

When routed to a channel, the ItPay PaaS Layer:

  1. Loads the channel adapter matching payer_channel
  2. Checks channel credentials are valid (mchid + certificate for WeChat, app_id + private key for Alipay)
  3. Translates the ItPay PaymentIntent into the channel's native request format
  4. Calls the channel's QR creation endpoint (e.g., POST /v3/pay/transactions/native for WeChat, alipay.trade.precreate for Alipay)
  5. Returns the normalized QR response
note

Channel adapters are pluggable. Adding support for a new payment channel requires implementing the ChannelAdapter interface — translating ItPay's canonical PaymentIntent to the channel's native format, and vice versa for webhooks.


4. QR Generation & Display

This step happens server-side within the ItPay Protocol and is not visible to the agent.

ItPay internal logic:

  1. Validates payer_channel is in the service's accepted_channels
  2. Loads the channel adapter for Alipay
  3. Translates PaymentIntent into Alipay's native format (alipay.trade.precreate)
  4. Calls the Alipay API

Alipay API call (internal — not exposed to agent):

POST https://openapi.alipay.com/gateway.do
Content-Type: application/x-www-form-urlencoded

method=alipay.trade.precreate
&app_id=2021001122334455
&charset=utf-8
&timestamp=2026-05-27+09%3A00%3A05
&version=1.0
&sign=***RSA2_SIGNATURE***
&biz_content={
"out_trade_no": "pi_01J7XZ1A2B3C4D5E6F7G8H9IK",
"total_amount": "6.99",
"subject": "AI document summary (42 pages, PDF)",
"qr_code_timeout_express": "15m"
}

Alipay Response (internal):

{
"alipay_trade_precreate_response": {
"code": "10000",
"msg": "Success",
"out_trade_no": "pi_01J7XZ1A2B3C4D5E6F7G8H9IK",
"qr_code": "https://qr.alipay.com/abc123def456"
},
"sign": "***RSA2_SIGNATURE***"
}

Agent Receives PaymentIntent Response

Status transitions:

  • created_atexpires_at: 15-minute window
  • status: pending

Agent-side behavior:

  1. Stores the id (pi_...) and qr.scan_url in session state
  2. Sets up a polling interval (every 3 seconds) for GET /v1/payment-intents/{id}
  3. Registers a webhook handler for payment_intent.* events
  4. Computes human-readable expiry: "This QR code expires in 15 minutes"

Agent Displays QR Code to Human

Human-side dialog:

Agent: "I've created a payment of CNY 6.99 via Alipay. Please scan this QR code with your Alipay app:" (Agent displays QR code image) Agent: "This QR code expires at 09:15 AM (15 minutes). Let me know once you've scanned it!"

Agent-side behavior:

  • Renders the scan_url as a QR code image (in chat, CLI, or web)
  • If multi-channel: displays all available QR codes side-by-side

Error: Human cannot see QR code (voice/chat-only interface) Agent falls back: "If you can't scan a QR code, I can also send you a payment link: https://pay.summarybot.ai/qr/qr_01J7XZ.... Open it on your phone."

Channel Errors During QR Generation

Error: Channel credentials invalid The protocol returns: CHANNEL_CONFIGURATION_ERROR. Agent receives a 400 with code: "CHANNEL_CONFIGURATION_ERROR". Agent tells human: "The payment channel is not configured correctly. Please contact the service provider."

Error: Channel rate-limited by Alipay The protocol falls back to the service's secondary channel (e.g., WeChat Pay) if configured. If no fallback exists, returns CHANNEL_TEMPORARILY_UNAVAILABLE and the agent retries.

Error: QR generation fails (Alipay timeout) ItPay returns the PaymentIntent with status: "failed" and an appropriate error. Agent surfaces: "The QR code could not be generated. Please try again."

warning

Always check the channel field in the response matches the expected payer_channel. If the requested channel is unavailable and a fallback was used, the QR code rendered may differ from what the human expects.


5. Human Authorization (Scan)

Human-side dialog:

Human takes out their phone, opens Alipay, and scans the QR code. Alipay shows: "Pay CNY 6.99 to SummaryBot — AI document summary (42 pages, PDF)" Human sees the payment details and confirms.

Agent-side behavior: The agent is polling every 3 seconds. It sees the status change:

GET /v1/payment-intents/pi_01J7XZ1A2B3C4D5E6F7G8H9IK
Authorization: Bearer ag_sk_...k1l2

Response when QR is scanned (status: scanning):

HTTP/1.1 200 OK
Content-Type: application/json

{
"id": "pi_01J7XZ1A2B3C4D5E6F7G8H9IK",
"status": "scanning",
"amount": { "currency": "CNY", "value": 699 },
"scanned_at": "2026-05-27T09:02:15Z",
...
}

Agent-side behavior:

  • Updates conversation: "I see you've scanned the QR code!"
  • Continues polling for the next state transition

Error: QR code never scanned (timeout) After expires_at is reached:

GET /v1/payment-intents/pi_01J7XZ1A2B3C4D5E6F7G8H9IK
→ status: "expired"

Agent says: "The QR code has expired. Would you like me to generate a new one?" Agent retries by creating a fresh PaymentIntent (Step 2), up to 2 more times.


6. Channel Processing

Human-side dialog:

Human enters their Alipay PIN (or uses fingerprint/FaceID). Alipay shows: "Payment successful — CNY 6.99 to SummaryBot" Human sees the success screen.

Payment Channel → ItPay callback: The channel sends a notification that the payment has been authorized.

POST /v1/webhooks/channel/alipay (server-to-server call)
Content-Type: application/json
X-Channel-Signature: ***RSA2***

{
"channel": "alipay",
"event": "trade_status",
"data": {
"out_trade_no": "pi_01J7XZ1A2B3C4D5E6F7G8H9IK",
"trade_no": "2026052722001122334455",
"total_amount": "6.99",
"buyer_id": "2088123456789012",
"trade_status": "WAIT_BUYER_PAY"
}
}

Transition: scanningauthorized The ItPay Protocol updates the PaymentIntent status to authorized.

Agent sees (via polling):

HTTP/1.1 200 OK

{
"id": "pi_01J7XZ1A2B3C4D5E6F7G8H9IK",
"status": "authorized",
"authorized_at": "2026-05-27T09:02:45Z",
"payer": {
"human_id": "user_abc_789",
"wallet_id": "2088123456789012"
},
...
}

Error: Payment rejected by human

{
"status": "failed",
"failure_code": "PAYMENT_REJECTED",
"failure_message": "The user cancelled the payment in their wallet"
}

Agent says: "The payment was cancelled in your wallet. Would you like to try again?" Retry by creating a new PaymentIntent.

Error: Insufficient balance

{
"status": "failed",
"failure_code": "INSUFFICIENT_BALANCE",
"failure_message": "The wallet does not have sufficient funds"
}

Agent says: "It looks like your Alipay account doesn't have enough balance. Please top up and try again."


7. Webhook Delivery

ItPay Protocol → Agent webhook:

POST https://agent-endpoint.example.com/webhooks/payments
Content-Type: application/json
X-Webhook-Signature: ***HMAC_SHA256***
X-Webhook-Id: wh_01J7XZ4C5D6E7F8G9H0I1J2K3L
X-Webhook-Timestamp: 2026-05-27T09:03:05Z

{
"type": "payment_intent.succeeded",
"data": {
"id": "pi_01J7XZ1A2B3C4D5E6F7G8H9IK",
"service_id": "01J7XYKZ1A2B3C4D5E6F7G8H9I",
"amount": { "currency": "CNY", "value": 699 },
"settlement": { "currency": "USD", "value": 99, "rate": 0.1416 },
"channel": "alipay",
"channel_txn_id": "2026052722001122334455",
"succeeded_at": "2026-05-27T09:03:00Z",
"metadata": {
"session_id": "sess_xyz_456",
"file_hash": "sha256:aabbccddeeff00112233445566778899"
}
}
}

Agent verifies webhook signature:

  1. Reads X-Webhook-Signature header
  2. Computes HMAC-SHA256 of the raw body using the shared webhook secret
  3. Compares signatures (constant-time comparison)
  4. Rejects if mismatch (returns 401 to the webhook sender)

Agent webhook response:

HTTP/1.1 200 OK
Content-Type: application/json

{ "received": true }

Error: Webhook delivery failure (network error) ItPay retries webhook delivery up to 5 times over 24 hours with exponential backoff (1min, 5min, 30min, 2h, 6h).

Error: Agent returns non-200 status ItPay interprets any non-2xx as delivery failure and retries per the schedule above.

warning

You must respond to webhooks with HTTP 200 OK (or any 2xx) within 5 seconds. If you need more time to process, acknowledge immediately with { "received": true } and process asynchronously. Non-2xx responses trigger retries.

tip

Always verify webhook signatures using the shared webhook secret. Use constant-time comparison (crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python) to prevent timing attacks.


8. Service Delivery

Human-side dialog:

Agent: "Payment received! Here's your document summary: [link to PDF summary]" (Agent delivers the service result)

Agent-side behavior:

  1. Receives the payment_intent.succeeded webhook (or detects via polling)
  2. Looks up the metadata.session_id to find the in-progress session
  3. Executes the paid service (e.g., calls the AI summarization API)
  4. Delivers the result to the human

Error: Service execution fails after payment Agent refunds automatically (via Refund with Revocation):

POST /v1/payment-intents/pi_01J7XZ1A2B3C4D5E6F7G8H9IK/refund

Agent says: "I'm sorry, the service couldn't be delivered. I've initiated a full refund."

note

The metadata.session_id field is critical for correlating webhook events back to active conversations. Always include it when creating a PaymentIntent.


9. Error Handling

Capture Operation

Agent-side behavior: When the agent (or the service's backend) sees status: "authorized", it calls the capture endpoint to finalize the payment.

POST /v1/payment-intents/pi_01J7XZ1A2B3C4D5E6F7G8H9IK/capture
Content-Type: application/json
Authorization: Bearer ag_sk_...k1l2
Idempotency-Key: 01J7XZ3A4B5C6D7E8F9G0H1I2J

{}

Success response:

HTTP/1.1 200 OK
Content-Type: application/json

{
"id": "pi_01J7XZ1A2B3C4D5E6F7G8H9IK",
"status": "captured",
"captured_at": "2026-05-27T09:02:50Z",
...
}

Error: Capture already called (idempotent)

HTTP/1.1 200 OK
Body: same as above — idempotent, safe to call multiple times.

Error: Payment intent not in authorizable state

HTTP/1.1 400 Bad Request

{
"error": "invalid_state",
"code": "INVALID_TRANSITION",
"message": "Cannot capture payment intent in status 'pending'. Must be 'authorized'."
}

Handling: Agent continues polling. If this was a race condition (authorized just happened), the next poll will catch the state.

note

Capture is idempotent — calling it multiple times with the same Idempotency-Key returns the same result. This is safe for retry scenarios.

Settlement Confirmed by Channel

Payment Channel → ItPay callback (server-to-server):

POST /v1/webhooks/channel/alipay
X-Channel-Signature: ***RSA2***

{
"channel": "alipay",
"event": "trade_status",
"data": {
"out_trade_no": "pi_01J7XZ1A2B3C4D5E6F7G8H9IK",
"trade_no": "2026052722001122334455",
"total_amount": "6.99",
"trade_status": "TRADE_SUCCESS",
"send_pay_date": "2026-05-27 09:03:00",
"receipt_amount": "6.99",
"buyer_logon_id": "abc***@alipay.com"
}
}

Transition: capturedsucceeded


10. Timeout & Retry

┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ Human │ │ Agent │ │ ItPay │
└────┬─────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ QR shown │ │
│ ◀───────────────┤ │
│ │ │
│ 10 minutes pass │ │
│ (no scan) │ │
│ │ │
│ │ GET status │
│ ├──────────────────▶│
│ │ status: expired │
│ │◀─────────────────│
│ │ │
│ "The QR code │ │
│ has expired. │ │
│ New one?" │ │
│ ◀───────────────┤ │
│ │ │
│ "Yes, try again"│ │
│ ├───────────────▶ │
│ │ │
│ │ POST (new) │
│ ├──────────────────▶│

Full trace:

  1. PaymentIntent created, QR displayed
  2. Agent polls every 3 seconds for 15 minutes
  3. At expires_at (09:15:05Z), the next poll returns status: "expired"
  4. Agent says: "The QR code has expired. Would you like me to generate a new one?"
  5. If human agrees, agent creates a new PaymentIntent (cannot reuse expired IDs)
  6. New QR code is displayed with a fresh 15-minute window
  7. If the human declines or doesn't respond within 2 minutes, the agent closes the session

Agent-side retry limit: Maximum 3 PaymentIntent creations per session. After 3 failures (timeouts or declines), the agent says: "I've tried several times but the payment has not gone through. Please try again later or contact support."


Multi-Channel Scenario

When a service accepts multiple payment channels and no single channel can be determined, the API returns multiple QR options:

{
"id": "pi_01J7XZ1A2B3C4D5E6F7G8H9IK",
"channels": [
{
"channel": "alipay",
"qr_data": "https://pay.summarybot.ai/qr/qr_alipay_abc",
"scan_url": "https://pay.summarybot.ai/qr/qr_alipay_abc",
"label": "Alipay"
},
{
"channel": "wechat",
"qr_data": "weixin://wxpay/bizpayurl/up?pr=xyz",
"scan_url": "https://pay.summarybot.ai/qr/qr_wechat_xyz",
"label": "WeChat Pay"
},
{
"channel": "promptpay",
"qr_data": "000201010212...",
"scan_url": "https://pay.summarybot.ai/qr/qr_promptpay_def",
"label": "PromptPay"
}
],
"payer_prompt": "Select your payment method"
}

Agent behavior in multi-channel mode:

┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ Human │ │ Agent │ │ ItPay │
└────┬─────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ 1. POST /v1/payment-intents │
│ (no payer_channel) │
│─────────────────▶│ │
│ ├──────────────────▶│
│ │ │
│ 2. 201 Created │ │
│ {channels: [ │ │
│ Alipay QR, │ │
│ WeChat QR, │ │
│ PromptPay]} │ │
│ │◀─────────────────│
│ │ │
│ 3. "Please │ │
│ choose your │ │
│ payment │ │
│ method:" │ │
│ [Alipay][WX] │ │
│ [PromptPay] │ │
│ ◀───────────────┤ │
│ │ │
│ 4. "Alipay" │ │
│ ├───────────────▶ │

Human-side dialog:

Agent: "Please choose your payment method:" (Agent shows QR images for all three channels) Human: "I'll use Alipay." (scans the Alipay QR)

The rest of the flow proceeds identically to single-channel mode.

tip

In multi-channel mode, prefer rendering all available QR codes side-by-side so the human can choose at a glance. If the interface is text-only, list the channels with labels and ask the human to pick one before generating the QR.


HTTP 402 Mode

The PaymentIntent can also be created implicitly via the HTTP 402 flow. When a service endpoint responds with 402 Payment Required, it includes a complete PaymentIntent in the response body and headers.

HTTP/1.1 402 Payment Required
Content-Type: application/json
X-Payment-Intent: pi_01J7XZ1A2B3C4D5E6F7G8H9IK
X-Payment-Channel: alipay
X-Payment-Amount: CNY 6.99
X-Payment-QR: itpay://pay/pi_01J7XZ...?channel=alipay

{
"error": "payment_required",
"payment_intent": {
"id": "pi_01J7XZ1A2B3C4D5E6F7G8H9IK",
"amount": { "currency": "CNY", "value": 699 },
"channel": "alipay",
"qr_uri": "itpay://pay/pi_01J7XZ...?channel=alipay",
"expires_at": "2026-05-27T09:15:05Z"
}
}
note

The 402 mode eliminates the need for a separate API call to create a PaymentIntent. The service endpoint itself triggers payment creation by returning a 402 response. See x402 Semantics for the complete client-side flow.


State Machine

┌─────────────┐
│ pending │
└──────┬──────┘
│ QR generated

┌─────────────────┐
│ qr_generated │
└────────┬────────┘
│ payer scans QR

┌──────────────┐
│ scanning │
└───────┬──────┘
│ payer authorizes

┌──────────────┐
│ authorized │
└───────┬──────┘
│ payee captures

┌──────────────┐
│ captured │
└───────┬──────┘
│ settlement confirmed

┌──────────────┐
│ succeeded │
└──────────────┘

Any state ──expires_at reached──▶ expired
Any state ──manual cancel───────▶ cancelled

Transitions

FromToTrigger
pendingqr_generatedQR code has been rendered by the payment channel
qr_generatedscanningPayer's wallet app has scanned the QR code
scanningauthorizedPayer has confirmed the payment in their wallet
authorizedcapturedPayee captures the authorized funds
capturedsucceededSettlement is confirmed on the payment channel
anyexpiredexpires_at timestamp is reached
anycancelledPayee or protocol cancels the intent

Error States

StateMeaningRecovery
failedPayment rejected by channel or userCreate new PaymentIntent
expiredQR code lifetime exceeded (15 min default)Create new PaymentIntent
cancelledManually cancelled by payee or protocolCreate new PaymentIntent (if needed)
tip

The expired state is a terminal state — you cannot revive an expired PaymentIntent. Always create a new one with a fresh expires_at window. The same applies to failed and cancelled.


Key Behaviors

  • Idempotency: Creating a PaymentIntent with the same id is idempotent. Always generate a unique UUIDv7 per attempt.
  • Expiry: Intents are short-lived (typically 5–15 minutes). After expires_at, the QR code becomes invalid and no further transitions are possible.
  • QR Generation: The API returns a QRCharge reference with a scan_url. Render this URL as a QR code image for the human payer.
  • Double-spend protection: Only one QRCharge can be associated with a PaymentIntent. Once qr_generated is reached, the QR data is fixed.
  • Metadata: Use metadata to attach internal identifiers (session IDs, file hashes, order numbers) for reconciliation.
warning

Do not rely on polling alone for critical payment confirmation. Webhooks are the primary delivery mechanism; polling is a fallback for environments where webhooks cannot reach the agent (e.g., local development).

Webhook Events

EventDescription
payment_intent.succeededPayment completed successfully — deliver the service
payment_intent.expiredThe intent expired before payment was completed
payment_intent.cancelledThe intent was manually cancelled
payment_intent.failedThe intent failed (rejected, insufficient funds, etc.)

Retry Logic Summary

| Failure Point | Retry Strategy | Max Attempts | Human Notification | |---------|------|------|--------|----| | API 401 (auth) | Rotate key, retry | 1 | "Connection issue, please try later" | | API 409 (idempotency) | New idempotency key | 1 | Silent retry | | API 429 (rate-limit) | Wait Retry-After, backoff | 3 | Silent retry | | API 5xx (server error) | Exponential backoff (1s, 2s, 4s, 8s) | 3 | After all fail | | QR timeout (expired) | New PaymentIntent | 3 | "QR expired, want a new one?" | | Webhook delivery | Exponential backoff (1m, 5m, 30m, 2h, 6h) | 5 | N/A (server-side) | | Service delivery failure | Refund + apologize | 1 | "Full refund initiated" |


Next Steps

  • One-Time Pay — Simplified instant payment (no QR polling)
  • Refund — Process a refund against a completed payment
  • Void Service — Cancel a payment intent before settlement
  • x402 Semantics — Auto-pay and HTTP 402 flow