Void Service
The Void Service capability cancels in-flight payment intents, subscriptions, installs, or cumulative billing records. Voiding is the protocol-level mechanism for cancellation before settlement — it prevents further state transitions and, in the case of payment intents, triggers an automatic refund if funds have already been captured.
Swimlane: Void an In-Flight Payment Intent (Happy Path)
This swimlane shows the simplest case: an Agent creates a PaymentIntent (status: pending), the human changes their mind before any payment is made, and the Agent voids it.
┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐
│ Human │ │ Agent │ │ ItPay │ │ Channel │
│ (Payer) │ │ (Buyer) │ │ Protocol │ │ (Alipay) │
└────┬─────┘ └──────┬───────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ "I need to buy │ │ │
│ the Pro plan" │ │ │
│─────────────────>│ │ │
│ │ │ │
│ │ POST /v1/ │
│ │ payment_intents │
│ │ {amount:{...}, │
│ │ service:"pro_plan"} │
│ │─────────────────>│ │
│ │ │ │
│ │ 201 Created │ │
│ │ {id:"pi_01J7..", │
│ │ status:"pending"} │
│ │<─────────────────│ │
│ │ │ │
│ "Here's a QR │ │ │
│ code for ¥69 — │ │ │
│ scan to pay" │ │ │
│<─────────────────│ │ │
│ │ │ │
│ "Actually, I │ │ │
│ changed my │ │ │
│ mind — cancel │ │ │
│ this payment" │ │ │
│─────────────────>│ │ │
│ │ │ │
│ │ "Cancelling the │ │
│ │ pending │ │
│ │ payment. No │ │
│ │ charges will │ │
│ │ be made. │ │
│ │ Confirm?" │ │
│<─────────────────│ │ │
│ │ │ │
│ "Yes, cancel it" │ │ │
│─────────────────>│ │ │
│ │ │ │
│ │ POST /v1/voids │
│ │ { │
│ │ "target_type": │
│ │ "payment_intent", │
│ │ "target_id": │
│ │ "pi_01J7XZ1A2B3C4D5...", │
│ │ "reason": │
│ │ "user_cancelled", │
│ │ "description": │
│ │ "User changed mind │
│ │ before scanning QR" │
│ │ } │
│ │─────────────────>│ │
│ │ │ │
│ │ │ Validate │
│ │ │ intent: │
│ │ │ status= │
│ │ │ "pending" │
│ │ │ → voidable │
│ │ │ │
│ │ │ Cancel │
│ │ │ PaymentIntent│
│ │ │ status: │
│ │ │ "voided" │
│ │ │ Invalidate │
│ │ │ QR code │
│ │ │ │
│ │ 201 Created │ │
│ │ { │
│ │ "id":"void_01J7Z2A3B4...", │
│ │ "target_type": │
│ │ "payment_intent", │
│ │ "target_id": │
│ │ "pi_01J7XZ1A2B3...", │
│ │ "status":"voided", │
│ │ "auto_refund":false, │
│ │ "auto_refund_id":null │
│ │ } │
│ │<─────────────────│ │
│ │ │ │
│ │ Webhook: │ │
│ │ void.completed │ │
│ │<─────────────────│ │
│ │ │ │
│ │ "Done! The │ │
│ │ payment has │ │
│ │ been cancelled. │ │
│ │ No charges │ │
│ │ were made." │ │
│<─────────────────│ │ │
│ │ │ │
Swimlane: Cannot Void a Succeeded Payment (Redirect to Refund)
This swimlane covers the edge case where an Agent tries to void a completed (succeeded) payment. The protocol rejects the void with a clear error and redirects to the refund endpoint.
┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐
│ Human │ │ Agent │ │ ItPay │ │ Channel │
│ (Payer) │ │ (Buyer) │ │ Protocol │ │ (Alipay) │
└────┬─────┘ └──────┬───────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ "I want to │ │ │
│ cancel the │ │ │
│ payment I just │ │ │
│ made" │ │ │
│─────────────────>│ │ │
│ │ │ │
│ │ POST /v1/voids │
│ │ { │
│ │ "target_type": │
│ │ "payment_intent", │
│ │ "target_id": │
│ │ "pi_01J7XZ9K8J7...", │
│ │ "reason": │
│ │ "user_cancelled" │
│ │ } │
│ │─────────────────>│ │
│ │ │ │
│ │ │ Check │
│ │ │ PaymentIntent│
│ │ │ status: │
│ │ │ "succeeded" │
│ │ │ ✗ NOT VOIDABLE
│ │ │ │
│ │ 409 Conflict │ │
│ │ { │
│ │ "error":{ │
│ │ "code": │
│ │ "target_not_voidable", │
│ │ "message": │
│ │ "PaymentIntent pi_01J7XZ... │
│ │ has status 'succeeded' │
│ │ and cannot be voided. │
│ │ Use /v1/refunds for │
│ │ completed payments.", │
│ │ "details":{ │
│ │ "current_status": │
│ │ "succeeded", │
│ │ "suggested_action": │
│ │ "use_refund_endpoint" │
│ │ } │
│ │ } │
│ │ } │
│ │<─────────────────│ │
│ │ │ │
│ │ "This payment │ │
│ │ was already │ │
│ │ completed and │ │
│ │ can't be │ │
│ │ voided. Would │ │
│ │ you like me to │ │
│ │ process a │ │
│ │ refund instead?"│ │
│<─────────────────│ │ │
│ │ │ │
│ "Yes, refund it" │ │ │
│─────────────────>│ │ │
│ │ │ │
│ │ POST /v1/refunds │
│ │ {payment_intent:..., │
│ │ amount:{...}, │
│ │ reason:"customer_request"} │
│ │─────────────────>│ │
│ │ │ │
│ │ (continues │ │
│ │ in refund │ │
│ │ swimlane...) │ │
│ │ │ │
Swimlane: Void an In-Flight Intent That Was Already Captured (Auto-Refund)
This swimlane shows the case where a PaymentIntent has already been captured (status: captured) but not yet marked as succeeded. The void triggers an automatic refund.
┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐
│ Human │ │ Agent │ │ ItPay │ │ Channel │
│ (Payer) │ │ (Buyer) │ │ Protocol │ │ (Alipay) │
└────┬─────┘ └──────┬───────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ "Cancel the │ │ │
│ payment — I │ │ │
│ already scanned │ │ │
│ but the service │ │ │
│ is broken" │ │ │
│─────────────────>│ │ │
│ │ │ │
│ │ POST /v1/voids │
│ │ { │
│ │ "target_type": │
│ │ "payment_intent", │
│ │ "target_id": │
│ │ "pi_01J7Y2B3C4...", │
│ │ "reason": │
│ │ "service_issue", │
│ │ "description": │
│ │ "Service broken after │
│ │ payment capture" │
│ │ } │
│ │─────────────────>│ │
│ │ │ │
│ │ │ Check │
│ │ │ intent: │
│ │ │ status= │
│ │ │ "captured" │
│ │ │ → trigger │
│ │ │ auto-refund│
│ │ │ │
│ │ │ POST refund │
│ │ │ to channel │
│ │ │──────────────>│
│ │ │ │
│ │ │ │ Refund
│ │ │ │ processed
│ │ │<──────────────│
│ │ │ │
│ │ │ Mark intent │
│ │ │ voided │
│ │ │ Create │
│ │ │ refund │
│ │ │ record │
│ │ │ │
│ │ 201 Created │ │
│ │ { │
│ │ "id":"void_01J7Z3A4B5...", │
│ │ "target_type": │
│ │ "payment_intent", │
│ │ "status":"voided", │
│ │ "auto_refund":true, │
│ │ "auto_refund_id": │
│ │ "ref_01J7Z3A4B5...", │
│ │ "amount_refunded":{ │
│ │ "value":699, │
│ │ "currency":"CNY" │
│ │ } │
│ │ } │
│ │<─────────────────│ │
│ │ │ │
│ │ "The payment │ │
│ │ was captured │ │
│ │ before you │ │
│ │ cancelled, so │ │
│ │ an automatic │ │
│ │ refund of ¥6.99 │ │
│ │ has been │ │
│ │ initiated. It │ │
│ │ should arrive │ │
│ │ in 3–5 days." │ │
│<─────────────────│ │ │
│ │ │ │
Swimlane: Void a Subscription / Install (Cascading)
This swimlane shows voiding a subscription (cancels future renewals) and voiding an install (uninstalls service + cascades to cancel all associated subscriptions).
Scenario A: Void a Subscription
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Human │ │ Agent │ │ ItPay │
│ (Payer) │ │ (Buyer) │ │ Protocol │
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ │ │
│ "Cancel my Pro │ │
│ subscription — │ │
│ I don't need it │ │
│ anymore" │ │
│─────────────────>│ │
│ │ │
│ │ "Cancelling │
│ │ your Pro plan │
│ │ subscription. │
│ │ You'll keep │
│ │ access until │
│ │ the end of the │
│ │ billing period. │
│ │ No further │
│ │ charges. │
│ │ Confirm?" │
│<─────────────────│ │
│ │ │
│ "Yes, cancel" │ │
│─────────────────>│ │
│ │ │
│ │ POST /v1/voids │
│ │ { │
│ │ "target_type":│
│ │ "subscription",│
│ │ "target_id": │
│ │ "sub_01J7Y6A..│
│ │ "reason": │
│ │ "user_requested│
│ │ _cancellation"│
│ │ } │
│ │─────────────────>│
│ │ │
│ │ │ Validate │
│ │ │ subscription │
│ │ │ status: │
│ │ │ "active" │
│ │ │ → voidable │
│ │ │ │
│ │ │ Mark sub │
│ │ │ as cancelled │
│ │ │ status: │
│ │ │ "voided" │
│ │ │ Stop renewal │
│ │ │ schedule │
│ │ │ │
│ │ 201 Created │ │
│ │ { │
│ │ "status": │
│ │ "voided", │
│ │ "auto_refund":│
│ │ false, │
│ │ "ended_at": │
│ │ "2026-06-27.. │
│ │ } │
│ │<─────────────────│
│ │ │
│ │ "Done! Your Pro │
│ │ subscription │
│ │ will end on │
│ │ June 27. No │
│ │ further charges.│
│ │ Need anything │
│ │ else?" │
│<─────────────────│ │
│ │ │
Scenario B: Void an Install (Cascading Uninstall)
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Human │ │ Agent │ │ ItPay │
│ (Payer) │ │ (Buyer) │ │ Protocol │
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ │ │
│ "Uninstall the │ │
│ Smart Summary │ │
│ service — the │ │
│ developer │ │
│ deprecated it" │ │
│─────────────────>│ │
│ │ │
│ │ "Uninstalling │
│ │ Smart Summary │
│ │ will also │
│ │ cancel your │
│ │ associated Pro │
│ │ subscription. │
│ │ Confirm?" │
│<─────────────────│ │
│ │ │
│ "Yes, uninstall" │ │
│─────────────────>│ │
│ │ │
│ │ POST /v1/voids │
│ │ { │
│ │ "target_type":│
│ │ "install", │
│ │ "target_id": │
│ │ "inst_01J7Y2..│
│ │ "reason": │
│ │ "service_ │
│ │ deprecated" │
│ │ } │
│ │─────────────────>│
│ │ │
│ │ │ Find install │
│ │ │ Resolve │
│ │ │ associated │
│ │ │ subscriptions│
│ │ │ → sub_01J7Y6 │
│ │ │ │
│ │ │ Void install │
│ │ │ Void sub │
│ │ │ (cascade) │
│ │ │ │
│ │ 201 Created │ │
│ │ { │
│ │ "status": │
│ │ "voided", │
│ │ "cancelled_ │
│ │ subscriptions":│
│ │ ["sub_01J7Y6..│
│ │ } │
│ │<─────────────────│
│ │ │
│ │ "Smart Summary │
│ │ has been │
│ │ uninstalled. │
│ │ Your Pro │
│ │ subscription │
│ │ has been │
│ │ cancelled as │
│ │ well. No │
│ │ further │
│ │ charges." │
│<─────────────────│ │
│ │ │
Swimlane: Idempotent Void (No Double Void)
This swimlane shows that voiding an already-voided object returns the existing void record rather than an error — the operation is idempotent.
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Human │ │ Agent │ │ ItPay │
│ (Payer) │ │ (Buyer) │ │ Protocol │
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ │ │
│ │ FIRST CALL │
│ │─────────────────>│
│ │ POST /v1/voids │
│ │ {target_type: │
│ │ "payment_intent│
│ │ target_id: │
│ │ "pi_01J7XZ1.." │
│ │─────────────────>│
│ │ │
│ │ │ status= │
│ │ │ "pending" │
│ │ │ → void │
│ │ │ │
│ │ 201 Created │ │
│ │ {id:"void_...", │ │
│ │ status:"voided"│ │
│ │<─────────────────│ │
│ │ │ │
│ │ │ │
│ │ SECOND CALL │ │
│ │ (same target) │ │
│ │─────────────────>│ │
│ │ POST /v1/voids │ │
│ │ {target_type: │ │
│ │ "payment_intent│ │
│ │ target_id: │ │
│ │ "pi_01J7XZ1.." │ │
│ │─────────────────>│ │
│ │ │ │
│ │ │ status= │
│ │ │ "voided" │
│ │ │ → idempotent │
│ │ │ return │
│ │ │ existing │
│ │ │ │
│ │ 200 OK │ │
│ │ {id:"void_...", │ │
│ │ status:"voided", │
│ │ note: "Already voided" │
│ │ } │
│ │<─────────────────│ │
│ │ │ │
Endpoint
| Method | Endpoint | Description |
|---|---|---|
POST | /v1/voids | Void a payment intent, subscription, install, or cumulative record |
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
target_type | string | yes | Type of object to void — payment_intent, subscription, install, or cumulative_record |
target_id | string | yes | ID of the object to void |
reason | string | no | Brief reason for voiding (max 256 chars) |
description | string | no | Detailed explanation (max 1,024 chars) |
Valid target_type Values
| target_type | Target ID Format | Description |
|---|---|---|
payment_intent | pi_* | Cancel a pending or generated payment intent. Auto-refunds if already captured. |
subscription | sub_* | Cancel an active or past-due subscription. No further renewals. |
install | inst_* | Uninstall a service installation. Cancels associated active subscriptions. |
cumulative_record | rec_* | Void a cumulative usage record. Adjusts the running total. |
Request Example — Void a Payment Intent
POST /v1/voids
Content-Type: application/json
Authorization: Bearer ***
{
"target_type": "payment_intent",
"target_id": "pi_01J7XZ1A2B3C4D5E6F7G8H9IK",
"reason": "user_cancelled",
"description": "User cancelled the payment before scanning the QR code"
}
Response Example
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "void_01J7Z2A3B4C5D6E7F8G9H0I1J",
"target_type": "payment_intent",
"target_id": "pi_01J7XZ1A2B3C4D5E6F7G8H9IK",
"status": "voided",
"reason": "user_cancelled",
"description": "User cancelled the payment before scanning the QR code",
"auto_refund": false,
"auto_refund_id": null,
"created_at": "2026-05-27T09:20:00Z",
"updated_at": "2026-05-27T09:20:00Z"
}
Request Example — Void a Subscription
POST /v1/voids
Content-Type: application/json
Authorization: Bearer ***
{
"target_type": "subscription",
"target_id": "sub_01J7Y6A7B8C9D0E1F2G3H4I5J",
"reason": "user_requested_cancellation",
"description": "User no longer needs the Pro plan"
}
Response Example
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "void_01J7Z2B4C5D6E7F8G9H0I1J2K",
"target_type": "subscription",
"target_id": "sub_01J7Y6A7B8C9D0E1F2G3H4I5J",
"status": "voided",
"reason": "user_requested_cancellation",
"description": "User no longer needs the Pro plan",
"auto_refund": false,
"auto_refund_id": null,
"created_at": "2026-05-27T09:25:00Z",
"updated_at": "2026-05-27T09:25:00Z"
}
Request Example — Void an Install
POST /v1/voids
Content-Type: application/json
Authorization: Bearer ***
{
"target_type": "install",
"target_id": "inst_01J7Y2A3B4C5D6E7F8G9H0I1J",
"reason": "service_deprecated",
"description": "Smart Summary service has been deprecated — uninstalling all associated agents"
}
Response Example
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "void_01J7Z2C5D6E7F8G9H0I1J2K3L",
"target_type": "install",
"target_id": "inst_01J7Y2A3B4C5D6E7F8G9H0I1J",
"status": "voided",
"reason": "service_deprecated",
"description": "Smart Summary service has been deprecated — uninstalling all associated agents",
"auto_refund": false,
"auto_refund_id": null,
"cancelled_subscriptions": ["sub_01J7Y6A7B8C9D0E1F2G3H4I5J"],
"created_at": "2026-05-27T09:30:00Z",
"updated_at": "2026-05-27T09:30:00Z"
}
State Machine
┌─────────────┐
│ requested │
└──────┬──────┘
│ validation and processing
▼
┌─────────────┐
│ voided │
└──────┬──────┘
│
┌────┴────┐
│ │
▼ ▼
(terminal) ┌────────┐
│ failed │
└────────┘
Transitions
| From | To | Trigger |
|---|---|---|
requested | voided | Void request is validated and processed |
requested | failed | Void validation failed (object already in terminal state, not found, or not allowed) |
Auto-Refund Behavior
When voiding a payment_intent, the protocol checks the current status:
| PaymentIntent Status | Void Behavior |
|---|---|
pending | Intent is cancelled. No refund needed (no funds captured). |
qr_generated | Intent is cancelled. QR code invalidated. No refund needed. |
scanning | Intent is cancelled. No funds captured yet. |
authorized | Intent is cancelled. Authorization is released. No refund needed. |
captured | Intent is voided AND an automatic refund is initiated. auto_refund is true and auto_refund_id contains the refund ID. |
succeeded | Cannot be voided — use the Refund endpoint instead. |
Key Behaviors
- Cascading void for installs: Voiding an
installautomatically cancels all associated active subscriptions for that installation. Thecancelled_subscriptionsfield lists the affected subscription IDs. - No double void — idempotent: Once an object is voided, subsequent void requests return the existing void record with status
voided(200 OK, not an error). - Void vs. Refund: Void is for in-flight objects (before settlement). For completed payments, use the Refund endpoint.
- Cumulative record voiding: Voiding a cumulative record deducts its usage from the running total and recalculates the billing amount.
Webhook Events
| Event | Description |
|---|---|
void.completed | Void was processed successfully |
void.failed | Void request failed |
Error Codes
| Code | Description |
|---|---|
target_not_found | The specified target_id does not exist |
target_not_voidable | The target is in a state that cannot be voided |
target_already_voided | The target has already been voided |
invalid_target_type | The target_type is not a valid enum value |
void_invalid_reason | Reason is too long or contains invalid characters |
Next Steps
- Refund — Process refunds on completed payments
- Subscribe Pay — Create subscriptions that can be voided
- Install — Set up installations