Skip to main content

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

MethodEndpointDescription
POST/v1/voidsVoid a payment intent, subscription, install, or cumulative record

Request Fields

FieldTypeRequiredDescription
target_typestringyesType of object to void — payment_intent, subscription, install, or cumulative_record
target_idstringyesID of the object to void
reasonstringnoBrief reason for voiding (max 256 chars)
descriptionstringnoDetailed explanation (max 1,024 chars)

Valid target_type Values

target_typeTarget ID FormatDescription
payment_intentpi_*Cancel a pending or generated payment intent. Auto-refunds if already captured.
subscriptionsub_*Cancel an active or past-due subscription. No further renewals.
installinst_*Uninstall a service installation. Cancels associated active subscriptions.
cumulative_recordrec_*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

FromToTrigger
requestedvoidedVoid request is validated and processed
requestedfailedVoid 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 StatusVoid Behavior
pendingIntent is cancelled. No refund needed (no funds captured).
qr_generatedIntent is cancelled. QR code invalidated. No refund needed.
scanningIntent is cancelled. No funds captured yet.
authorizedIntent is cancelled. Authorization is released. No refund needed.
capturedIntent is voided AND an automatic refund is initiated. auto_refund is true and auto_refund_id contains the refund ID.
succeededCannot be voided — use the Refund endpoint instead.

Key Behaviors

  • Cascading void for installs: Voiding an install automatically cancels all associated active subscriptions for that installation. The cancelled_subscriptions field 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

EventDescription
void.completedVoid was processed successfully
void.failedVoid request failed

Error Codes

CodeDescription
target_not_foundThe specified target_id does not exist
target_not_voidableThe target is in a state that cannot be voided
target_already_voidedThe target has already been voided
invalid_target_typeThe target_type is not a valid enum value
void_invalid_reasonReason 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