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
PaymentIntentvia 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
| Method | Endpoint | Description |
|---|---|---|
POST | /v1/payment-intents | Create a payment intent with QR code |
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
service_id | string (UUIDv7) | yes | The ServiceManifest id this intent is paying for |
type | string | yes | Payment type — must be "one_time" |
amount | Money | yes | Amount to charge in the payer's local currency |
amount.currency | string (ISO 4217) | yes | Currency code (e.g. CNY, USD, THB) |
amount.value | number | yes | Amount in minor units (e.g. cents) |
description | string | yes | Human-readable description of what this payment is for |
payer_channel | string | no | Channel the payer will use (e.g. alipay, wechat). If omitted, the service's default channel is used. |
return_url | string (URL) | no | URL to redirect the user to after payment completes |
metadata | object | no | Arbitrary key-value data (max 4 KB) |
Money Object
| Field | Type | Description |
|---|---|---|
currency | string (ISO 4217) | Currency code |
value | number | Amount in minor currency units (e.g. cents) |
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.
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 thescan_urlwhich must be rendered as a QR code image for the human payer.status— Tracks the lifecycle state. Starts aspendingand transitions throughqr_generated→scanning→authorized→captured→succeeded.settlement— Shows the converted amount in the payee's settlement currency, along with the exchange rate applied.payer/payee— Identifies the participating agents.human_idis populated only after the human authorizes payment.expires_at— Hard deadline (typically 15 minutes). After this time, no further state transitions are possible.
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."
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
| Scenario | Payer provides payer_channel | User's wallet detected | Routing Decision |
|---|---|---|---|
| Explicit channel | "alipay" | — | Route to Alipay adapter. Must be in service's accepted_channels. |
| Auto-detect | "auto" or omitted | Alipay | Route to channel matching detected wallet |
| Fallback | Omitted | Wallet not detected | Use service default_channel or return CHANNEL_UNAVAILABLE |
Channel Resolution (PaaS Layer)
When routed to a channel, the ItPay PaaS Layer:
- Loads the channel adapter matching
payer_channel - Checks channel credentials are valid (mchid + certificate for WeChat, app_id + private key for Alipay)
- Translates the ItPay PaymentIntent into the channel's native request format
- Calls the channel's QR creation endpoint (e.g.,
POST /v3/pay/transactions/nativefor WeChat,alipay.trade.precreatefor Alipay) - Returns the normalized QR response
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:
- Validates
payer_channelis in the service'saccepted_channels - Loads the channel adapter for Alipay
- Translates PaymentIntent into Alipay's native format (
alipay.trade.precreate) - 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
×tamp=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_at→expires_at: 15-minute windowstatus:pending
Agent-side behavior:
- Stores the
id(pi_...) andqr.scan_urlin session state - Sets up a polling interval (every 3 seconds) for
GET /v1/payment-intents/{id} - Registers a webhook handler for
payment_intent.*events - 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_urlas 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."
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: scanning → authorized
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:
- Reads
X-Webhook-Signatureheader - Computes HMAC-SHA256 of the raw body using the shared webhook secret
- Compares signatures (constant-time comparison)
- 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.
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.
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:
- Receives the
payment_intent.succeededwebhook (or detects via polling) - Looks up the
metadata.session_idto find the in-progress session - Executes the paid service (e.g., calls the AI summarization API)
- 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."
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.
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: captured → succeeded
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:
- PaymentIntent created, QR displayed
- Agent polls every 3 seconds for 15 minutes
- At
expires_at(09:15:05Z), the next poll returnsstatus: "expired" - Agent says: "The QR code has expired. Would you like me to generate a new one?"
- If human agrees, agent creates a new PaymentIntent (cannot reuse expired IDs)
- New QR code is displayed with a fresh 15-minute window
- 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.
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"
}
}
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
| From | To | Trigger |
|---|---|---|
pending | qr_generated | QR code has been rendered by the payment channel |
qr_generated | scanning | Payer's wallet app has scanned the QR code |
scanning | authorized | Payer has confirmed the payment in their wallet |
authorized | captured | Payee captures the authorized funds |
captured | succeeded | Settlement is confirmed on the payment channel |
| any | expired | expires_at timestamp is reached |
| any | cancelled | Payee or protocol cancels the intent |
Error States
| State | Meaning | Recovery |
|---|---|---|
failed | Payment rejected by channel or user | Create new PaymentIntent |
expired | QR code lifetime exceeded (15 min default) | Create new PaymentIntent |
cancelled | Manually cancelled by payee or protocol | Create new PaymentIntent (if needed) |
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
idis 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
QRChargereference with ascan_url. Render this URL as a QR code image for the human payer. - Double-spend protection: Only one
QRChargecan be associated with a PaymentIntent. Onceqr_generatedis reached, the QR data is fixed. - Metadata: Use
metadatato attach internal identifiers (session IDs, file hashes, order numbers) for reconciliation.
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
| Event | Description |
|---|---|
payment_intent.succeeded | Payment completed successfully — deliver the service |
payment_intent.expired | The intent expired before payment was completed |
payment_intent.cancelled | The intent was manually cancelled |
payment_intent.failed | The 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