Escrow Payments
The Escrow Payments capability adds a trust-minimized payment flow where funds are authorized and held by the protocol until conditions are met. Inspired by Stripe's capture_method=manual, crypto 2-of-3 multisig, PayPal's dispute lifecycle, and HTLC hash-time-lock contracts, escrow payments enable safe transactions between mutually untrusting parties.
Interaction Traces & Swimlane Diagrams
1. Full Escrow Flow (Happy Path)
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Payer │ │ ItPay │ │ Payee │
│ (Buyer) │ │ Escrow │ │ (Seller) │
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ │ │
│ Agree on deal │ │
│ (off-chain) │ │
│◄─────────────────│──────────────────>│
│ │ │
│ Initiate │ │
│ escrow │ │
│─────────────────>│ │
│ │ POST /v1/ │
│ │ payment-intents │
│ │ {type: "escrow", │
│ │ payer, payee, │
│ │ amount, desc} │
│ │ │
│ │── Authorize funds │
│ │ on payment ch. │
│ │ │
│ 201 + status: │ │
│ "authorized" │ │
│<─────────────────│ │
│ │ │
│ [Funds locked] │ │
│ │ │
│ Confirm │ │
│ delivery │ │
│ (off-chain) │ │
│◄─────────────────│──────────────────>│
│ │ │
│ Authorize │ │
│ release │ │
│─────────────────>│ │
│ │ POST /v1/ │
│ │ payment-intents/ │
│ │ {id}/release │
│ │ {signed_by: │
│ │ [payer, payee]} │
│ │ │
│ │── Verify 2-of-3 │
│ │ signatures │
│ │── Settle funds │
│ │ to payee │
│ │ │
│ 200 + status: │ │
│ "released" │ │
│<─────────────────│ │
│ │ Webhook: │
│ │ escrow.released │
│ │──────────────────>│
Escrow Creation Request:
POST /v1/payment-intents
Content-Type: application/json
Authorization: Bearer *** "type": "escrow",
"amount": { "value": 500000, "currency": "CNY" },
"payer": { "agent_id": "agent_buyer_a1b2c3", "human_id": "user_buyer_001" },
"payee": { "agent_id": "agent_seller_d4e5f6", "human_id": "user_seller_002" },
"arbiter": { "agent_id": "agent_platform_g7h8i9" },
"time_lock_seconds": 604800,
"description": "Escrow for domain name transfer — example.com"
}
Creation Response:
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "pi_esc_01J7Z8A1B2C3D4E5F6G7H8I9J",
"type": "escrow",
"amount": { "value": 500000, "currency": "CNY" },
"status": "authorized",
"payer": { "agent_id": "agent_buyer_a1b2c3", "human_id": "user_buyer_001" },
"payee": { "agent_id": "agent_seller_d4e5f6", "human_id": "user_seller_002" },
"arbiter": { "agent_id": "agent_platform_g7h8i9" },
"time_lock_seconds": 604800,
"time_lock_expires_at": "2026-06-03T09:30:00Z",
"held_amount": { "value": 500000, "currency": "CNY" },
"created_at": "2026-05-27T09:30:00Z",
"updated_at": "2026-05-27T09:30:00Z"
}
Status Polling:
GET /v1/payment-intents/pi_esc_01J7Z8A1B2C3D4E5F6G7H8I9J
Authorization: Bearer *** 200 OK
Content-Type: application/json
{
"id": "pi_esc_01J7Z8A1B2C3D4E5F6G7H8I9J",
"status": "held",
"held_amount": { "value": 500000, "currency": "CNY" },
"time_lock_expires_at": "2026-06-03T09:30:00Z",
"updated_at": "2026-05-27T09:31:00Z"
}
Release Request:
POST /v1/payment-intents/pi_esc_01J7Z8A1B2C3D4E5F6G7H8I9J/release
Content-Type: application/json
Authorization: Bearer *** "signed_by": ["agent_buyer_a1b2c3", "agent_seller_d4e5f6"]
}
Release Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "pi_esc_01J7Z8A1B2C3D4E5F6G7H8I9J",
"status": "released",
"released_at": "2026-05-28T14:00:00Z"
}
2. Dispute & Resolution Flow
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Payer │ │ ItPay │ │ Payee │
│ (Buyer) │ │ Escrow │ │ (Seller) │
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ │ │
│ [Funds held] │ │
│ │ │
│ Goods not │ │
│ delivered │ │
│ │ │
│ File dispute │ │
│─────────────────>│ │
│ │ POST /v1/ │
│ │ payment-intents/ │
│ │ {id}/dispute │
│ │ {reason, │
│ │ description, │
│ │ evidence} │
│ │ │
│ │── Set status to │
│ │ "disputed" │
│ │── Notify payee │
│ │ │
│ 201 + dsp_xxx │ │
│ status: inquiry │ │
│<─────────────────│ │
│ │ Webhook: │
│ │ escrow.dispute. │
│ │ created │
│ │──────────────────>│
│ │ │
│ ░░░ INQUIRY STAGE (72h) ░░░ │
│ │ │
│ Submit evidence │ │
│─────────────────>│ │
│ │ │
│ │ │ Submit evidence
│ │ │<─────────────────
│ │ │
│ ░░░ EVIDENCE STAGE ░░░ │
│ │ │
│ │ Arbiter reviews │
│ │ (3rd party) │
│ │ │
│ │── RESOLUTION: │
│ │ Release to │
│ │ seller (full) │
│ │ OR │
│ │ Refund to buyer │
│ │ OR │
│ │ Partial release │
│ │ │
│ [See resolution │ │
│ swimlanes │ │
│ below] │ │
Dispute Filing Request:
POST /v1/payment-intents/pi_esc_01J7Z8A1B2C3D4E5F6G7H8I9J/dispute
Content-Type: application/json
Authorization: Bearer *** "raised_by": "agent_buyer_a1b2c3",
"reason": "domain_not_transferred",
"description": "Seller has not delivered domain name access 48 hours after payment hold.",
"evidence": [
{
"type": "chat_log",
"url": "https://storage.example.com/evidence/chat_abc123.pdf",
"description": "Chat history showing seller acknowledged receipt"
}
]
}
Dispute Response:
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "dsp_01J7Z9K1A2B3C4D5E6F7G8H9I0",
"payment_intent_id": "pi_esc_01J7Z8A1B2C3D4E5F6G7H8I9J",
"status": "inquiry",
"raised_by": "agent_buyer_a1b2c3",
"reason": "domain_not_transferred",
"evidence_submitted": 1,
"created_at": "2026-05-28T14:30:00Z"
}
Evidence Submission:
POST /v1/disputes/dsp_01J7Z9K1A2B3C4D5E6F7G8H9I0/evidence
Content-Type: application/json
Authorization: Bearer *** "submitted_by": "agent_seller_d4e5f6",
"items": [
{
"type": "screenshot",
"url": "https://storage.example.com/evidence/screenshot_transfer.png",
"description": "Screenshot of domain transfer confirmation page"
},
{
"type": "contract",
"url": "https://storage.example.com/evidence/agreement_signed.pdf",
"description": "Signed service agreement"
}
]
}
3. Resolution Paths
Path A: Release to Seller (Payer at Fault)
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Payer │ │ ItPay │ │ Payee │
│ (Buyer) │ │ Escrow │ │ (Seller) │
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ │ │
│ Arbiter rules │ │
│ in favor of │ │
│ seller │ │
│ │ │
│ │ POST /v1/ │
│ │ payment-intents/ │
│ │ {id}/resolve │
│ │ {ruling: │
│ │ "release"} │
│ │ │
│ │── Transfer held │
│ │ funds to payee │
│ │ │
│ Webhook: │ │
│ escrow.dispute │ │
│ .resolved │ │
│<─────────────────│──────────────────>│
│ │ │
│ Funds released │ │ Funds received
│ to seller │ │ ✓
│ │ │
Resolution — Release to Seller:
POST /v1/payment-intents/pi_esc_01J7Z8A1B2C3D4E5F6G7H8I9J/resolve
Content-Type: application/json
Authorization: Bearer *** "resolved_by": "agent_platform_g7h8i9",
"ruling": "release",
"notes": "Seller provided evidence of domain transfer. Payer unable to refute."
}
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "pi_esc_01J7Z8A1B2C3D4E5F6G7H8I9J",
"status": "released",
"resolution": {
"ruling": "release",
"resolved_by": "agent_platform_g7h8i9",
"released_at": "2026-05-30T10:00:00Z",
"notes": "Seller provided evidence of domain transfer. Payer unable to refute."
}
}
Path B: Partial Release
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Payer │ │ ItPay │ │ Payee │
│ (Buyer) │ │ Escrow │ │ (Seller) │
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ │ │
│ Arbiter rules │ │
│ partial: 60/40 │ │
│ │ │
│ │ POST /v1/ │
│ │ payment-intents/ │
│ │ {id}/resolve │
│ │ {ruling: │
│ │ "partial", │
│ │ payer_refund: │
│ │ 200000, │
│ │ payee_release: │
│ │ 300000} │
│ │ │
│ │── Refund 40% │
│ │ to payer │
│ 40% refunded │── Release 60% │
│ 200000 CNY │ to payee │ 300000 CNY
│<─────────────────│──────────────────>│ received
│ │ │
Resolution — Partial Release:
POST /v1/payment-intents/pi_esc_01J7Z8A1B2C3D4E5F6G7H8I9J/resolve
Content-Type: application/json
Authorization: Bearer *** "resolved_by": "agent_platform_g7h8i9",
"ruling": "partial",
"payer_refund": {
"value": 200000,
"currency": "CNY"
},
"payee_release": {
"value": 300000,
"currency": "CNY"
},
"notes": "Domain delivered 4 days late. Payer compensated 40%."
}
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "pi_esc_01J7Z8A1B2C3D4E5F6G7H8I9J",
"status": "partially_resolved",
"resolution": {
"ruling": "partial",
"resolved_by": "agent_platform_g7h8i9",
"payer_refund": { "value": 200000, "currency": "CNY" },
"payee_release": { "value": 300000, "currency": "CNY" },
"refunded_at": "2026-05-30T11:00:00Z",
"released_at": "2026-05-30T11:00:00Z",
"notes": "Domain delivered 4 days late. Payer compensated 40%."
}
}
Path C: Full Refund (Payee at Fault)
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Payer │ │ ItPay │ │ Payee │
│ (Buyer) │ │ Escrow │ │ (Seller) │
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ │ │
│ Arbiter rules │ │
│ in favor of │ │
│ buyer │ │
│ │ │
│ │ POST /v1/ │
│ │ payment-intents/ │
│ │ {id}/resolve │
│ │ {ruling: │
│ │ "refund"} │
│ │ │
│ │── Release hold │
│ │── Return funds │
│ │ to payer │
│ │ │
│ Full refund │ │
│ 500000 CNY │ │
│<─────────────────│ │
│ │ │
Resolution — Full Refund:
POST /v1/payment-intents/pi_esc_01J7Z8A1B2C3D4E5F6G7H8I9J/resolve
Content-Type: application/json
Authorization: Bearer *** "resolved_by": "agent_platform_g7h8i9",
"ruling": "refund",
"notes": "Seller failed to provide domain access. Full refund ordered."
}
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "pi_esc_01J7Z8A1B2C3D4E5F6G7H8I9J",
"status": "refunded",
"resolution": {
"ruling": "refund",
"resolved_by": "agent_platform_g7h8i9",
"refunded_at": "2026-05-30T12:00:00Z",
"notes": "Seller failed to provide domain access. Full refund ordered."
}
}
4. Time-Lock Auto-Release Flow
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Payer │ │ ItPay │ │ Payee │
│ (Buyer) │ │ Escrow │ │ (Seller) │
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ │ │
│ [Funds held] │ │
│ │ │
│ No dispute │ │
│ raised │ │
│ │ │
│ ░░░ Time Lock Running ░░░ │
│ time_lock_expires_at: │
│ 2026-06-03T09:30:00Z │
│ │ │
│ │ ⏰ Timer fires │
│ │ │
│ │── Check: no │
│ │ dispute active │
│ │── Auto-release │
│ │ to payee │
│ │── Webhook: │
│ │ escrow.auto_ │
│ │ released │
│ │ │
│ Auto-released │ │
│ (N days later) │ │ Funds received
│ │ │ ✓
Payer-side dialog (before auto-release):
{
"dialog": {
"from": "escrow_bot",
"to": "user_buyer_001",
"message": "🔔 Escrow pi_esc_01J7Z8A1B2C3D4E5F6G7H8I9J will auto-release to seller in 3 days (2026-06-03T09:30:00Z). Raise a dispute now if the deal is incomplete.",
"actions": [
{ "label": "File Dispute", "action": "POST /v1/payment-intents/{id}/dispute" },
{ "label": "Authorize Release", "action": "POST /v1/payment-intents/{id}/release" }
]
}
}
Payee-side dialog (after auto-release):
{
"dialog": {
"from": "escrow_bot",
"to": "user_seller_002",
"message": "✅ Escrow pi_esc_01J7Z8A1B2C3D4E5F6G7H8I9J has been auto-released. 500,000 CNY has been settled to your account.",
"metadata": {
"auto_released_at": "2026-06-03T09:30:00Z",
"held_duration_days": 7
}
}
}
5. Human-Side Dialog Scenarios
Scenario A: Successful Deal
Payer: "I've received the domain access. Release the payment."
Agent (Payer): POST /v1/payment-intents/{id}/release signed_by: [payer]
Agent (Payee): POST /v1/payment-intents/{id}/release signed_by: [payee]
ItPay: → status: "released", funds settled to payee
Dialog (Payer): "✅ 500,000 CNY released to seller. Transaction complete."
Dialog (Payee): "✅ Escrow released. 500,000 CNY received."
Scenario B: Dispute → Refund
Payer: "The seller never delivered. I want my money back."
Agent (Payer): POST /v1/payment-intents/{id}/dispute
reason: "domain_not_transferred"
ItPay: → status: "disputed", 72h inquiry window
→ Notifies payee
Payee: "I delivered it. Here's the proof."
Agent (Payee): POST evidence (screenshots)
Arbiter reviews → ruling: "refund"
Dialog (Payer): "✅ Dispute resolved. Full refund of 500,000 CNY issued."
Dialog (Payee): "❌ Dispute lost. Funds returned to buyer."
Scenario C: Dispute → Partial Release
Payer: "Domain was delivered 4 days late. I want a discount."
Agent (Payer): POST dispute with reason: "partial_delivery"
Both parties submit evidence
Arbiter reviews → ruling: "partial"
→ 60% (300,000 CNY) to seller, 40% (200,000 CNY) refunded to buyer
Dialog (Payer): "✅ Partial resolution: 200,000 CNY refunded (40%)."
Dialog (Payee): "✅ Partial resolution: 300,000 CNY received (60%)."
Endpoints
| Method | Endpoint | Description |
|---|---|---|
POST | /v1/payment-intents | Create an escrow PaymentIntent (type: escrow) |
POST | /v1/payment-intents/{id}/release | Release held funds to the payee |
POST | /v1/payment-intents/{id}/dispute | Payer opens a dispute |
POST | /v1/payment-intents/{id}/resolve | Arbiter resolves a dispute (release or refund) |
Escrow PaymentIntent Fields
| Field | Type | Required | Description |
|---|---|---|---|
type | string | yes | Must be "escrow" |
amount | Money | yes | Amount to authorize and hold |
payer | Party | yes | Paying agent and optional human |
payee | Party | yes | Receiving agent and optional human |
arbiter | Party | no | Dispute resolution party (2-of-3 majority). Defaults to the platform operator if omitted. |
time_lock_seconds | number | no | Lock period in seconds before auto-release (HTLC-inspired). Default: 604800 (7 days). |
description | string | yes | What this escrow is for |
metadata | object | no | Arbitrary key-value data (max 4 KB) |
Party Object
| Field | Type | Description |
|---|---|---|
agent_id | string | Agent identifier |
human_id | string | Optional human user identifier |
Request Example
POST /v1/payment-intents
Content-Type: application/json
Authorization: Bearer ***
{
"type": "escrow",
"amount": {
"value": 500000,
"currency": "CNY"
},
"payer": {
"agent_id": "agent_buyer_a1b2c3",
"human_id": "user_buyer_001"
},
"payee": {
"agent_id": "agent_seller_d4e5f6",
"human_id": "user_seller_002"
},
"arbiter": {
"agent_id": "agent_platform_g7h8i9"
},
"time_lock_seconds": 604800,
"description": "Escrow for domain name transfer — example.com",
"metadata": {
"deal_id": "deal_abc_789"
}
}
Response Example
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "pi_esc_01J7Z8A1B2C3D4E5F6G7H8I9J",
"type": "escrow",
"amount": {
"value": 500000,
"currency": "CNY"
},
"status": "authorized",
"payer": {
"agent_id": "agent_buyer_a1b2c3",
"human_id": "user_buyer_001"
},
"payee": {
"agent_id": "agent_seller_d4e5f6",
"human_id": "user_seller_002"
},
"arbiter": {
"agent_id": "agent_platform_g7h8i9"
},
"time_lock_seconds": 604800,
"time_lock_expires_at": "2026-06-03T09:30:00Z",
"held_amount": {
"value": 500000,
"currency": "CNY"
},
"created_at": "2026-05-27T09:30:00Z",
"updated_at": "2026-05-27T09:30:00Z"
}
Release Example
POST /v1/payment-intents/pi_esc_01J7Z8A1B2C3D4E5F6G7H8I9J/release
Content-Type: application/json
Authorization: Bearer ***
{
"signed_by": ["agent_buyer_a1b2c3", "agent_seller_d4e5f6"]
}
Both payer and payee must authorize release (2-of-3 multisig pattern). At minimum, payer and payee signatures are required for the normal (non-disputed) path.
Response Example
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "pi_esc_01J7Z8A1B2C3D4E5F6G7H8I9J",
"status": "released",
"released_at": "2026-05-28T14:00:00Z"
}
Dispute Example
POST /v1/payment-intents/pi_esc_01J7Z8A1B2C3D4E5F6G7H8I9J/dispute
Content-Type: application/json
Authorization: Bearer ***
{
"raised_by": "agent_buyer_a1b2c3",
"reason": "domain_not_transferred",
"description": "Seller has not delivered domain name access 48 hours after payment hold.",
"evidence": [
{
"type": "chat_log",
"url": "https://storage.example.com/evidence/chat_abc123.pdf",
"description": "Chat history showing seller acknowledged receipt"
}
]
}
Response Example
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "dsp_01J7Z9K1A2B3C4D5E6F7G8H9I0",
"payment_intent_id": "pi_esc_01J7Z8A1B2C3D4E5F6G7H8I9J",
"status": "inquiry",
"raised_by": "agent_buyer_a1b2c3",
"reason": "domain_not_transferred",
"evidence_submitted": 1,
"created_at": "2026-05-28T14:30:00Z"
}
State Machine
┌──────────────┐
│ authorized │
└──────┬───────┘
│
┌───────────┴───────────┐
│ │
▼ ▼
┌──────────┐ ┌───────────┐
│ held │ │ disputed │
└────┬─────┘ └─────┬─────┘
│ │
▼ ▼
┌─────────┐ ┌────────────┐
│ released│ │ resolution │
└────┬─────┘ └─────┬──────┘
│ ┌─────┴─────┐
▼ │ │
┌─────────┐ ▼ ▼
│ settled │ ┌─────────┐ ┌──────────┐
└─────────┘ │released │ │ refunded │
│ →settled│ └──────────┘
└─────────┘
Any state ──time_lock_expires_at──▶ auto-released ──▶ settled
Transitions
| From | To | Trigger |
|---|---|---|
authorized | held | Funds successfully authorized on the payment channel |
held | released | Payer and payee mutually sign release |
held | disputed | Payer or payee raises a dispute |
released | settled | Funds settled to payee by the payment channel |
disputed | resolution | Arbiter initiates resolution |
resolution | released → settled | Arbiter rules in favor of payee |
resolution | refunded | Arbiter rules in favor of payer |
| any | auto-released → settled | time_lock_expires_at reached (HTLC-inspired) |
Dispute Resolution Lifecycle
The dispute lifecycle follows a PayPal-inspired three-stage process:
INQUIRY ──▶ EVIDENCE ──▶ RESOLUTION
| Stage | Description |
|---|---|
| INQUIRY | Dispute opened. Both parties can submit initial statements. 72-hour window for evidence gathering. |
| EVIDENCE | Each party submits supporting documents (max 10 items each). Arbiter may request additional evidence. |
| RESOLUTION | Arbiter reviews evidence and issues a decision: release funds to payee or refund to payer. |
Evidence Types
| Type | Description | Max Size |
|---|---|---|
chat_log | Conversation transcript | 10 MB |
receipt | Payment or delivery receipt | 5 MB |
contract | Signed agreement | 10 MB |
screenshot | Screen capture | 5 MB |
other | Other supporting documents | 10 MB |
Key Behaviors
- Three-actor model: The escrow involves three roles — payer, payee, and arbiter. The arbiter defaults to the platform operator but can be specified explicitly.
- 2-of-3 signature model: Normal fund release requires payer + payee consent. Dispute resolution requires only the arbiter's decision. The protocol enforces that at minimum two of three actors must agree on any state change.
- Time-lock auto-release (HTLC-inspired): If
time_lock_secondselapses without dispute, funds auto-release to the payee. This prevents indefinite fund locking. - Cancel on authorization failure: If the payment channel rejects the authorization (insufficient funds, channel error), the escrow transitions to
failedimmediately. - Partial release not supported: Escrow funds are released or refunded as a whole. For partial release scenarios, use multiple escrow PaymentIntents.
- Dispute time limits: All disputes must be raised within the escrow lock period or within 30 days of
held, whichever is sooner.
Webhook Events
| Event | Description |
|---|---|
escrow.held | Funds authorized and held |
escrow.released | Funds released to payee |
escrow.settled | Funds settled |
escrow.dispute.created | Dispute opened |
escrow.dispute.resolved | Dispute resolved (release or refund) |
escrow.auto_released | Funds auto-released by time-lock expiry |
escrow.refunded | Funds returned to payer (dispute or cancellation) |
Error Codes
| Code | Description |
|---|---|
escrow_not_held | Cannot release/dispute an escrow not in held state |
escrow_already_disputed | A dispute is already active on this escrow |
escrow_already_resolved | Escrow has already been resolved |
unauthorized_signer | One of the required signers is not a party to the escrow |
insufficient_signatures | Release requires at minimum payer + payee signatures |
dispute_time_expired | Dispute window has closed |
arbiter_required | No arbiter is configured and platform default is not available |
Next Steps
- Refunds & Revocations — Refund and revoke access post-escrow
- Void Service — Cancel a payment before it settles
- Security Model — Protocol security and actor verification