Skip to main content

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

MethodEndpointDescription
POST/v1/payment-intentsCreate an escrow PaymentIntent (type: escrow)
POST/v1/payment-intents/{id}/releaseRelease held funds to the payee
POST/v1/payment-intents/{id}/disputePayer opens a dispute
POST/v1/payment-intents/{id}/resolveArbiter resolves a dispute (release or refund)

Escrow PaymentIntent Fields

FieldTypeRequiredDescription
typestringyesMust be "escrow"
amountMoneyyesAmount to authorize and hold
payerPartyyesPaying agent and optional human
payeePartyyesReceiving agent and optional human
arbiterPartynoDispute resolution party (2-of-3 majority). Defaults to the platform operator if omitted.
time_lock_secondsnumbernoLock period in seconds before auto-release (HTLC-inspired). Default: 604800 (7 days).
descriptionstringyesWhat this escrow is for
metadataobjectnoArbitrary key-value data (max 4 KB)

Party Object

FieldTypeDescription
agent_idstringAgent identifier
human_idstringOptional 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

FromToTrigger
authorizedheldFunds successfully authorized on the payment channel
heldreleasedPayer and payee mutually sign release
helddisputedPayer or payee raises a dispute
releasedsettledFunds settled to payee by the payment channel
disputedresolutionArbiter initiates resolution
resolutionreleased → settledArbiter rules in favor of payee
resolutionrefundedArbiter rules in favor of payer
anyauto-released → settledtime_lock_expires_at reached (HTLC-inspired)

Dispute Resolution Lifecycle

The dispute lifecycle follows a PayPal-inspired three-stage process:

INQUIRY ──▶ EVIDENCE ──▶ RESOLUTION
StageDescription
INQUIRYDispute opened. Both parties can submit initial statements. 72-hour window for evidence gathering.
EVIDENCEEach party submits supporting documents (max 10 items each). Arbiter may request additional evidence.
RESOLUTIONArbiter reviews evidence and issues a decision: release funds to payee or refund to payer.

Evidence Types

TypeDescriptionMax Size
chat_logConversation transcript10 MB
receiptPayment or delivery receipt5 MB
contractSigned agreement10 MB
screenshotScreen capture5 MB
otherOther supporting documents10 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_seconds elapses 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 failed immediately.
  • 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

EventDescription
escrow.heldFunds authorized and held
escrow.releasedFunds released to payee
escrow.settledFunds settled
escrow.dispute.createdDispute opened
escrow.dispute.resolvedDispute resolved (release or refund)
escrow.auto_releasedFunds auto-released by time-lock expiry
escrow.refundedFunds returned to payer (dispute or cancellation)

Error Codes

CodeDescription
escrow_not_heldCannot release/dispute an escrow not in held state
escrow_already_disputedA dispute is already active on this escrow
escrow_already_resolvedEscrow has already been resolved
unauthorized_signerOne of the required signers is not a party to the escrow
insufficient_signaturesRelease requires at minimum payer + payee signatures
dispute_time_expiredDispute window has closed
arbiter_requiredNo arbiter is configured and platform default is not available

Next Steps