Deliveries (Digital Goods)
The Deliveries capability enables merchants to fulfill digital goods after payment is confirmed. Unlike physical shipping, digital goods delivery has unique requirements: time-bounded access, tamper verification, single-use consumption, and optional end-to-end encryption.
This capability extends the ItPay Protocol's payment lifecycle with a dedicated fulfillment layer inspired by three real-world patterns:
- Stripe Fulfillment — Stripe has no built-in delivery API. Merchants listen for
checkout.session.completedand fulfill themselves via webhooks. Idempotency is critical: the same webhook event must not trigger duplicate delivery. This spec embeds that pattern natively. - AWS S3 Pre-signed URLs — SigV4 HMAC-SHA256 signing with expiry embedded in query parameters. Industry standard for secure download delivery. Max 7-day validity.
- CloudFront Signed URLs — RSA-SHA1 signed URLs with canned or custom policies (IP restrictions, time windows, path wildcards).
Swimlane Diagram — Digital Goods Delivery Flow
┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐
│ Human │ │ Agent │ │ ItPay │ │ Channel │
│ (Payer) │ │ (Buyer) │ │ Protocol │ │ (Alipay) │
└────┬─────┘ └──────┬───────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ (1) Payment │ │ │
│ Complete │ │ │
│ ◄────────────────│◄─────────────────│◄───────────────│
│ "Payment of │ │ │
│ $29.99 for │ │ │
│ Premium Report │ │ │
│ succeeded!" │ │ │
│ │ │ │
│ │ (2) POST /v1/deliveries │
│ │ { type: "signed_url", │
│ │ payment_intent_id: "pi_...", │
│ │ source_url: "https://..." } │
│ │ ─────────────────▶ │
│ │ │ │
│ │ │ (3) pending ──>│
│ │ │ ↓ │
│ │ │ (4) preparing │
│ │ │ ↓ │
│ │ │ (5) encrypting │
│ │ │ ┌─ Encrypt ───┤
│ │ │ │ X25519 ECDH│
│ │ │ │ HKDF key │
│ │ │ │ derivation │
│ │ │ │XChaCha20- │
│ │ │ │Poly1305 │
│ │ │ └─────────────┤
│ │ │ ↓ │
│ │ │ (6) ready ────>│
│ │ │ │
│ (7) Access URL │ │ │
│ ◄────────────────│◄─────────────────│ │
│ "Your report is │ │ │
│ ready to │ │ │
│ download! │ │ │
│ [Download Link]│ │ │
│ Expires in 1hr"│ │ │
│ │ │ │
│ (8) Download │ │ │
│ ─── HTTP GET ─────────────────────▶│ │
│ to access URL │ │ │
│ │ │ │
│ │ │ (9) delivered │
│ │ │ ↓ │
│ │ │ (10) receipt │
│ │ │ ┌─ Sign ─────┤
│ │ │ │ HMAC-SHA256│
│ │ │ │ delivery || │
│ │ │ │ checksum || │
│ │ │ │ timestamp │
│ │ │ └─────────────┤
│ │ │ │
│ (11) Decrypt & │ │ │
│ Verify Receipt │ │ │
│ ─────────────────▶ │ │
│ "Download │ │ │
│ complete. │ │ │
│ File verified │ │ │
│ ✓ Checksum OK │ │ │
│ ✓ Signature OK"│ │ │
│ │ │ │
Revocation on Refund
┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐
│ Human │ │ Agent │ │ ItPay │ │ Channel │
│ (Payer) │ │ (Buyer) │ │ Protocol │ │ (Alipay) │
└────┬─────┘ └──────┬───────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ "I'd like a │ │ │
│ refund for │ │ │
│ order ORD-7890"│ │ │
│ ─────────────────▶ │ │
│ │ │ │
│ │ (A) POST /v1/deliveries/:id/revoke
│ │ { reason: "order_cancelled" } │
│ │ ─────────────────▶ │
│ │ │ │
│ │ │ (B) revoked ──>│
│ │ │ │
│ │ │ (C) Access URL │
│ │ │ invalidated │
│ │ │ ── 410 Gone │
│ │ │ │
│ │ (D) 200 OK │ │
│ │ { status: │ │
│ │ "revoked" } │ │
│ │ ◄────────────────│ │
│ "Access has │ │ │
│ been revoked. │ │ │
│ You can no │ │ │
│ longer │ │ │
│ download the │ │ │
│ file." │ │ │
│ ◄────────────────│ │ │
│ │ │ │
Delivery States Lifecycle
┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐
│ Human │ │ Agent │ │ ItPay │ │ Channel │
│ (Payer) │ │ (Buyer) │ │ Protocol │ │ (Alipay) │
└────┬─────┘ └──────┬───────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ │ │ ┌─ pending ────┤
│ │ │ │ (created) │
│ │ │ └──────┬───────┤
│ │ │ │ │
│ │ │ ▼ │
│ │ │ ┌─ preparing ──┤
│ │ │ │ (signing) │
│ │ │ └──────┬───────┤
│ │ │ │ │
│ │ │ ▼ │
│ │ │ ┌─ encrypting ─┤
│ │ │ │(E2E encrypt) │
│ │ │ └──────┬───────┤
│ │ │ │ │
│ │ │ ▼ │
│ │ │ ┌── ready ─────┤
│ │ │ │ (URL avail) │
│ │ │ └───┬────┬─────┤
│ │ │ │ │ │
│ │ │ ┌──┘ └──┐ │
│ │ │ │ │ │
│ │ │ ▼ ▼ │
│ │ │ ┌──────┐ ┌───┐ │
│ │ │ │deliv'd│ │ex-│ │
│ │ │ │ │ │pired│ │
│ │ │ └──┬───┘ └───┘ │
│ │ │ │ │
│ │ │ ▼ │
│ │ │ ┌─ verified ── │
│ │ │ │ (confirmed) │
│ │ │ └──────────── │
│ │ │ │
│ │ │ ┌─ failed ──── │
│ │ │ │ (any state) │
│ │ │ └──────────── │
Detailed Interaction Trace
Step 1: Payment Succeeds
A PaymentIntent transitions to succeeded status. The protocol checks KYC level, verifies sufficient funds, and confirms payment through the channel.
Webhook:
POST https://merchant.example.com/webhooks/itpay
Content-Type: application/json
ItPay-Signature: t=1716801000,v1=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
{
"type": "payment_intent.succeeded",
"data": {
"id": "pi_01J7Z5E6F7G8H9I0J1K2L3M4N",
"amount": { "value": 2999, "currency": "USD" },
"status": "succeeded",
"channel": "alipay",
"metadata": {
"order_id": "ORD-7890",
"customer_email": "user@example.com"
}
},
"created_at": "2026-05-27T09:10:00Z"
}
Human dialog (payment success):
"Payment of $29.99 for Premium Market Report completed successfully via Alipay! Your report will be available for download shortly."
Step 2: Agent Creates Delivery
The merchant agent receives the payment success webhook and calls POST /v1/deliveries to fulfill the digital goods.
HTTP Request — Signed URL:
POST /v1/deliveries
Content-Type: application/json
Authorization: Bearer api_ke...n{
"payment_intent_id": "pi_01J7Z5E6F7G8H9I0J1K2L3M4N",
"type": "signed_url",
"goods": {
"type": "application/pdf",
"checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"ttl_seconds": 3600,
"source_url": "https://merchant-cdn.example.com/reports/market-q2-2026.pdf"
},
"metadata": {
"order_id": "ORD-7890"
},
"idempotency_key": "idem_01J8B3C4D5E6F7G8H9I0J1K2L"
}
Error handling — invalid payment intent:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": {
"code": "invalid_payment_intent",
"message": "PaymentIntent not found or not in a completed state",
"details": {
"payment_intent_id": "pi_01J7Z5E6F7G8H9I0J1K2L3M4N",
"current_status": "requires_payment_method"
}
}
}
Human dialog (error):
"The payment isn't complete yet — it still needs a payment method. Can't create the delivery until the transaction settles."
Step 3: Creating the Delivery (Encrypted Payload)
For sensitive digital goods requiring end-to-end encryption, the agent specifies type: "encrypted_payload" and provides their X25519 public key.
HTTP Request — Encrypted Payload:
POST /v1/deliveries
Content-Type: application/json
Authorization: Bearer api_ke...n{
"payment_intent_id": "pi_01J7Z5E6F7G8H9I0J1K2L3M4N",
"type": "encrypted_payload",
"goods": {
"type": "application/pdf",
"checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"ttl_seconds": 86400,
"source_url": "https://merchant-cdn.example.com/reports/market-q2-2026.pdf"
},
"encryption": {
"algorithm": "xchacha20-poly1305",
"public_key": "5CjHmU1P2Q3R4S5T6U7V8W9X0Y1Z2A3B4C5D6E7F8G9H0I="
},
"metadata": {
"order_id": "ORD-7890"
}
}
Step 4: ItPay Processes — Encrypting State
ItPay fetches the source content, generates an ephemeral X25519 key pair, performs ECDH key exchange, derives an encryption key via HKDF, and encrypts the payload with XChaCha20-Poly1305.
Internal encryption process (server-side):
1. Generate ephemeral X25519 key pair: (ephemeral_sk, ephemeral_pk)
2. ECDH: shared_secret = X25519(ephemeral_sk, merchant_pk)
3. HKDF-SHA256: enc_key = HKDF(shared_secret, salt=random(16), info="itpay-delivery-v1")
4. Generate random 192-bit nonce
5. AEAD encrypt: ciphertext = XChaCha20-Poly1305_Encrypt(enc_key, nonce, plaintext, aad=delivery_id)
6. Store: { ephemeral_pk, nonce, ciphertext }
HTTP Response (201 Created — pending):
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "del_01J8A2B3C4D5E6F7G8H9I0J1K",
"payment_intent_id": "pi_01J7Z5E6F7G8H9I0J1K2L3M4N",
"type": "encrypted_payload",
"status": "pending",
"goods": {
"type": "application/pdf",
"checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"ttl_seconds": 86400
},
"metadata": {
"order_id": "ORD-7890"
},
"created_at": "2026-05-27T09:10:00Z",
"updated_at": "2026-05-27T09:10:00Z"
}
Step 5: Pre-Signed URL Generation
After preparation completes, the delivery transitions to ready state. A pre-signed URL is generated for secure download.
Signature formula:
token_payload = {
"sub": "del_01J8A2B3C4D5E6F7G8H9I0J1K",
"exp": "2026-05-28T09:10:00Z",
"chk": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"nonce": "7c8a9b0d1e2f3a4b5c6d7e8f9a0b1c2d"
}
signature = HMAC-SHA256(
delivery_id || expires_at || goods.checksum || nonce,
gateway_signing_key
)
Webhook (delivery.ready):
POST https://merchant.example.com/webhooks/itpay
Content-Type: application/json
ItPay-Signature: t=1716801000,v1=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
{
"type": "delivery.ready",
"data": {
"id": "del_01J8A2B3C4D5E6F7G8H9I0J1K",
"payment_intent_id": "pi_01J7Z5E6F7G8H9I0J1K2L3M4N",
"status": "ready",
"goods": {
"type": "application/pdf",
"checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"access": {
"url": "https://delivery.itpay.io/v1/access/del_01J8A2B3C4D5E6F7G8H9I0J1K?token=eyJzdWIiOiJkZWxf...&expires=2026-05-28T09:10:00Z&sig=a1b2c3d4...",
"expires_at": "2026-05-28T09:10:00Z",
"method": "GET"
}
},
"created_at": "2026-05-27T09:11:30Z"
}
Step 6: Payer Accesses the Delivery
The human asks their agent for the download. The agent retrieves the payer-facing access URL.
HTTP Request (payer-facing):
GET /v1/deliveries/del_01J8A2B3C4D5E6F7G8H9I0J1K/access
Authorization: Bearer api_ke...n
HTTP Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_url": "https://delivery.itpay.io/v1/access/del_01J8A2B3C4D5E6F7G8H9I0J1K?token=eyJzdWIiOiJkZWxf...&expires=2026-05-28T09:10:00Z&sig=a1b2c3d4...",
"expires_at": "2026-05-28T09:10:00Z",
"goods": {
"type": "application/pdf",
"checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"encryption": {
"algorithm": "xchacha20-poly1305",
"ephemeral_public_key": "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6="
}
}
Human dialog (ready for download):
"Your Premium Market Report is ready! Here's your download link: Download
• File type: PDF document • Size: ~4.2 MB • Link expires: 2026-05-28 09:10 UTC (1 hour) • This link can only be used once"
Step 7: Download and Decryption
The human (or their agent) downloads the encrypted content and decrypts it using their private key.
Download (HTTP GET on access URL):
GET https://delivery.itpay.io/v1/access/del_01J8A2B3C4D5E6F7G8H9I0J1K?token=eyJzdWIiOiJkZWxf...&expires=2026-05-28T09:10:00Z&sig=a1b2c3d4...
Response: 200 OK
Content-Type: application/octet-stream
Content-Length: 4194304
[ciphertext bytes]
Error handling — expired link:
HTTP/1.1 410 Gone
Content-Type: application/json
{
"error": {
"code": "delivery_expired",
"message": "This delivery's access URL has expired. A new delivery must be created.",
"details": {
"expires_at": "2026-05-28T09:10:00Z",
"current_time": "2026-05-28T09:15:00Z"
}
}
}
Error handling — already consumed:
HTTP/1.1 410 Gone
Content-Type: application/json
{
"error": {
"code": "delivery_already_consumed",
"message": "This single-use URL has already been downloaded.",
"details": {
"consumed_at": "2026-05-27T09:12:00Z",
"status": "delivered"
}
}
}
Human dialog (error — expired):
"The download link has expired. Links are valid for 1 hour. Please contact support for a new delivery link."
Client-side decryption (for encrypted_payload type):
import os
import hashlib
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives.ciphers.aead import XChaCha20Poly1305
def decrypt_delivery(ciphertext, ephemeral_public_key_b64, merchant_private_key):
# 1. Decode ephemeral public key
ephemeral_pk = X25519PublicKey.from_public_bytes(
base64.b64decode(ephemeral_public_key_b64)
)
# 2. ECDH key exchange
shared_secret = merchant_private_key.exchange(ephemeral_pk)
# 3. Derive encryption key
enc_key = HKDF(
algorithm=hashlib.sha256,
length=32,
salt=None,
info=b"itpay-delivery-v1"
).derive(shared_secret)
# 4. Decrypt (first 24 bytes = nonce, rest = ciphertext + tag)
nonce = ciphertext[:24]
ct = ciphertext[24:]
aead = XChaCha20Poly1305(enc_key)
plaintext = aead.decrypt(nonce, ct, None)
return plaintext
Human dialog (successful decrypt):
"Download complete! File successfully decrypted and integrity verified: • SHA-256 checksum: ✓ matches • File: market-q2-2026.pdf • Size: 4,194,304 bytes • Ready to open with your PDF viewer"
Step 8: Receipt Verification
After successful download, ItPay generates a cryptographic receipt proving delivery occurred.
Receipt generation:
receipt = HMAC-SHA256(
delivery_id || goods.checksum || delivered_at,
merchant_secret
)
Where:
delivery_id=del_01J8A2B3C4D5E6F7G8H9I0J1Kgoods.checksum=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855delivered_at=2026-05-27T09:15:05Zmerchant_secret=<shared secret established at onboarding>
Webhook (delivery.delivered):
POST https://merchant.example.com/webhooks/itpay
Content-Type: application/json
ItPay-Signature: t=1716801000,v1=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
{
"type": "delivery.delivered",
"data": {
"id": "del_01J8A2B3C4D5E6F7G8H9I0J1K",
"payment_intent_id": "pi_01J7Z5E6F7G8H9I0J1K2L3M4N",
"status": "delivered",
"goods": {
"type": "application/pdf",
"checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"receipt": {
"signature": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b",
"algorithm": "hmac-sha256",
"delivered_at": "2026-05-27T09:15:05Z"
}
},
"created_at": "2026-05-27T09:15:05Z"
}
Receipt verification (merchant-side):
import hmac, hashlib
def verify_receipt(delivery_id, checksum, delivered_at, merchant_secret, received_signature):
message = delivery_id + checksum + delivered_at
expected = hmac.new(
merchant_secret.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, received_signature)
# Usage
is_valid = verify_receipt(
delivery_id="del_01J8A2B3C4D5E6F7G8H9I0J1K",
checksum="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
delivered_at="2026-05-27T09:15:05Z",
merchant_secret="sk_test_abc123",
received_signature="a1b2c3d4..."
)
Human dialog (receipt):
"Delivery verified and confirmed. Receipt signature: a1b2c3d4... (HMAC-SHA256). This serves as proof of delivery for your records."
Step 9: Revocation on Refund
When a refund is processed, associated deliveries must be revoked to prevent further access.
HTTP Request (revoke):
POST /v1/deliveries/del_01J8A2B3C4D5E6F7G8H9I0J1K/revoke
Authorization: Bearer api_ke...n
Content-Type: application/json
{
"reason": "order_cancelled"
}
HTTP Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "del_01J8A2B3C4D5E6F7G8H9I0J1K",
"status": "revoked",
"revoked_at": "2026-05-27T09:20:00Z",
"reason": "order_cancelled"
}
Subsequent access attempt (returns 410 Gone):
GET https://delivery.itpay.io/v1/access/del_01J8A2B3C4D5E6F7G8H9I0J1K?token=eyJzdWIiOiJkZWxf...&sig=a1b2c3d4...
HTTP/1.1 410 Gone
Content-Type: application/json
{
"error": {
"code": "delivery_revoked",
"message": "This delivery has been revoked due to: order_cancelled",
"details": {
"revoked_at": "2026-05-27T09:20:00Z"
}
}
}
Webhook (delivery.revoked):
{
"type": "delivery.revoked",
"data": {
"id": "del_01J8A2B3C4D5E6F7G8H9I0J1K",
"payment_intent_id": "pi_01J7Z5E6F7G8H9I0J1K2L3M4N",
"status": "revoked",
"reason": "order_cancelled"
},
"created_at": "2026-05-27T09:20:00Z"
}
Human dialog (revocation):
"The refund has been processed and your access to the Premium Market Report has been revoked. You will no longer be able to download the file. A receipt of revocation has been logged."
Idempotency — Preventing Duplicate Deliveries
If a network issue causes a retry of the delivery creation request, the idempotency key prevents double fulfillment.
Retry scenario:
POST /v1/deliveries
Content-Type: application/json
Authorization: Bearer api_ke...n
Idempotency-Key: idem_01J8B3C4D5E6F7G8H9I0J1K2L
{
"payment_intent_id": "pi_01J7Z5E6F7G8H9I0J1K2L3M4N",
"type": "signed_url",
...
}
Response (cached — same as first request):
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "del_01J8A2B3C4D5E6F7G8H9I0J1K",
"status": "ready",
"access": {
"url": "https://delivery.itpay.io/v1/access/del_01J8A2B3C4D5E6F7G8H9I0J1K?...",
"expires_at": "2026-05-28T09:10:00Z",
"method": "GET"
},
...
}
Error — duplicate key with different parameters:
HTTP/1.1 409 Conflict
Content-Type: application/json
{
"error": {
"code": "duplicate_idempotency",
"message": "Idempotency key reused with different request parameters",
"details": {
"idempotency_key": "idem_01J8B3C4D5E6F7G8H9I0J1K2L",
"original_request": {
"payment_intent_id": "pi_01J7Z5E6F7G8H9I0J1K2L3M4N",
"type": "signed_url"
}
}
}
}
Human dialog (idempotency notice):
"The delivery was already created (idempotency key matched). No duplicate will be made. Your download link is still valid."
Endpoint
| Method | Endpoint | Description |
|---|---|---|
POST | /v1/deliveries | Create a new delivery for a completed payment |
GET | /v1/deliveries/:id | Retrieve a delivery and its current status |
GET | /v1/deliveries/:id/access | Retrieve the access URL (payer-facing) |
POST | /v1/deliveries/:id/revoke | Revoke a delivery before it is consumed |
GET | /v1/deliveries | List deliveries filtered by payment_intent_id or status |
1. The Delivery Object
A Delivery represents a single fulfillment operation. Each delivery is associated with exactly one PaymentIntent and produces one access URL or encrypted payload.
Fields
| Field | Type | Description |
|---|---|---|
id | string (prefixed del_) | Unique delivery identifier |
payment_intent_id | string (prefixed pi_) | The PaymentIntent this delivery fulfills |
type | enum | Delivery mechanism: signed_url, encrypted_payload, or callback |
status | enum | Current delivery status (see lifecycle below) |
goods | object | Description of the digital goods being delivered |
goods.type | string | MIME type or content category (e.g., application/pdf, ebook, audio/mpeg) |
goods.checksum | string (hex) | SHA-256 hash of the content for integrity verification |
goods.ttl_seconds | number | Maximum time in seconds the access URL is valid (max 604,800 = 7 days) |
access | object | Access URL and metadata (populated after preparation) |
access.url | string (URL) | The time-bounded access URL or download link |
access.expires_at | string (ISO 8601) | Timestamp when the access URL expires |
access.method | string | HTTP method required (GET or POST with form) |
encryption | object | (Optional) Encryption parameters for E2E-protected deliveries |
encryption.algorithm | string | Encryption algorithm (e.g., xchacha20-poly1305) |
encryption.key_derivation | string | Key derivation scheme (e.g., x25519-hkdf) |
encryption.ephemeral_public_key | string (base64) | Ephemeral public key for key exchange |
receipt | object | Delivery receipt for proof-of-fulfillment |
receipt.signature | string (hex) | HMAC-SHA256 signature (see Delivery Receipt section) |
receipt.algorithm | string | Signing algorithm (hmac-sha256) |
receipt.delivered_at | string (ISO 8601) | When the delivery was fulfilled |
callback_url | string (URL) | (Optional) Merchant callback URL for callback type deliveries |
metadata | object | Arbitrary key-value data (max 4 KB) |
created_at | string (ISO 8601) | When the delivery was created |
updated_at | string (ISO 8601) | When the delivery was last updated |
Full Delivery Object Example
{
"id": "del_01J8A2B3C4D5E6F7G8H9I0J1K",
"payment_intent_id": "pi_01J7Z5E6F7G8H9I0J1K2L3M4N",
"type": "signed_url",
"status": "delivered",
"goods": {
"type": "application/pdf",
"checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"ttl_seconds": 3600
},
"access": {
"url": "https://delivery.itpay.io/v1/access/del_01J8A2B3C4D5E6F7G8H9I0J1K?token=***&expires=2026-05-28T09:10:00Z",
"expires_at": "2026-05-28T09:10:00Z",
"method": "GET"
},
"receipt": {
"signature": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b",
"algorithm": "hmac-sha256",
"delivered_at": "2026-05-27T09:15:05Z"
},
"metadata": {
"order_id": "ORD-7890",
"customer_email": "user@example.com"
},
"created_at": "2026-05-27T09:10:00Z",
"updated_at": "2026-05-27T09:15:05Z"
}
2. Create Delivery
Creates a new delivery for a completed PaymentIntent. The PaymentIntent must be in succeeded or completed status.
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
payment_intent_id | string (prefixed pi_) | yes | The PaymentIntent to fulfill |
type | enum | yes | signed_url, encrypted_payload, or callback |
goods | object | yes | Description of the digital goods |
goods.type | string | yes | MIME type or content category |
goods.checksum | string (hex) | yes | SHA-256 hash of the content |
goods.ttl_seconds | number | yes | Access URL validity in seconds (1–604,800) |
goods.source_url | string (URL) | no | Merchant-hosted URL of the goods (for gateway-side signing) |
encryption | object | no | Encryption parameters for E2E delivery |
encryption.algorithm | string | conditional | Required if type=encrypted_payload: xchacha20-poly1305 |
encryption.public_key | string (base64) | conditional | Required if type=encrypted_payload: merchant's X25519 public key |
callback_url | string (URL) | no | Merchant callback URL for callback type deliveries |
idempotency_key | string | no | Idempotency key (see Idempotency section) |
metadata | object | no | Arbitrary key-value data (max 4 KB) |
Request Example — Signed URL
POST /v1/deliveries
Content-Type: application/json
Authorization: Bearer *** "payment_intent_id": "pi_01J7Z5E6F7G8H9I0J1K2L3M4N",
"type": "signed_url",
"goods": {
"type": "application/pdf",
"checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"ttl_seconds": 3600,
"source_url": "https://merchant-cdn.example.com/reports/market-q2-2026.pdf"
},
"metadata": {
"order_id": "ORD-7890"
},
"idempotency_key": "idem_01J8B3C4D5E6F7G8H9I0J1K2L"
}
Request Example — Encrypted Payload
POST /v1/deliveries
Content-Type: application/json
Authorization: Bearer *** "payment_intent_id": "pi_01J7Z5E6F7G8H9I0J1K2L3M4N",
"type": "encrypted_payload",
"goods": {
"type": "application/pdf",
"checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"ttl_seconds": 86400,
"source_url": "https://merchant-cdn.example.com/reports/market-q2-2026.pdf"
},
"encryption": {
"algorithm": "xchacha20-poly1305",
"public_key": "5CjHmU1P2Q3R4S5T6U7V8W9X0Y1Z2A3B4C5D6E7F8G9H0I="
},
"metadata": {
"order_id": "ORD-7890"
}
}
Request Example — Callback (Stripe Pattern)
POST /v1/deliveries
Content-Type: application/json
Authorization: Bearer *** "payment_intent_id": "pi_01J7Z5E6F7G8H9I0J1K2L3M4N",
"type": "callback",
"goods": {
"type": "application/pdf",
"checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"ttl_seconds": 86400
},
"callback_url": "https://merchant.example.com/webhooks/delivery",
"metadata": {
"order_id": "ORD-7890"
}
}
Response Example — 201 Created
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "del_01J8A2B3C4D5E6F7G8H9I0J1K",
"payment_intent_id": "pi_01J7Z5E6F7G8H9I0J1K2L3M4N",
"type": "signed_url",
"status": "pending",
"goods": {
"type": "application/pdf",
"checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"ttl_seconds": 3600
},
"metadata": {
"order_id": "ORD-7890"
},
"created_at": "2026-05-27T09:10:00Z",
"updated_at": "2026-05-27T09:10:00Z"
}
3. Delivery Types
The protocol supports three delivery mechanisms, each suited to different trust and infrastructure models.
3.1. signed_url (Recommended)
The most common delivery type. The gateway generates a time-bounded, single-use download URL. The merchant provides a source_url pointing to the goods; the gateway signs it and returns a scoped access URL.
Pattern reference: This follows AWS S3 SigV4 pre-signed URLs with HMAC-SHA256 signing and query-parameter-embedded expiry. The token includes:
- Delivery ID (
del_xxx) - Expiry timestamp (ISO 8601)
- HMAC-SHA256 signature over
delivery_id + expires_at + goods.checksum
The resulting URL is tamper-evident: any modification to the query parameters invalidates the signature.
{
"access": {
"url": "https://delivery.itpay.io/v1/access/del_01J8A2B3C4D5E6F7G8H9I0J1K?token=***&expires=2026-05-28T09:10:00Z&sig=a1b2c3d4...",
"expires_at": "2026-05-28T09:10:00Z",
"method": "GET"
}
}
Single-use enforcement: Once the access URL is consumed (HTTP 200 on download), the delivery transitions to delivered status. Subsequent attempts return HTTP 410 Gone.
3.2. encrypted_payload
End-to-end encrypted inline delivery. The merchant provides their X25519 public key. The gateway:
- Generates an ephemeral X25519 key pair
- Performs ECDH key exchange to derive a shared secret
- Derives an encryption key via HKDF
- Encrypts the payload with XChaCha20-Poly1305
- Stores the ciphertext and ephemeral public key
The payer retrieves the ciphertext and ephemeral public key. Only the merchant's corresponding private key can decrypt the content.
Pattern reference: This follows modern E2E encryption conventions (Signal Protocol, Age encryption). The ciphertext is authenticated (AEAD) and resistant to tampering.
{
"type": "encrypted_payload",
"access": {
"url": "https://delivery.itpay.io/v1/access/del_01J8A2B3C4D5E6F7G8H9I0J1K",
"method": "GET"
},
"encryption": {
"algorithm": "xchacha20-poly1305",
"key_derivation": "x25519-hkdf",
"ephemeral_public_key": "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6="
}
}
3.3. callback
The gateway calls the merchant's callback_url with full delivery instructions, and the merchant fulfills the goods themselves.
Pattern reference: This is the Stripe fulfillment model. Stripe has no built-in delivery API — merchants listen for webhook events and implement delivery in their own infrastructure. This type provides the same pattern as a first-class protocol capability.
The gateway sends an authenticated POST request to callback_url with the following payload:
POST https://merchant.example.com/webhooks/delivery
Content-Type: application/json
ItPay-Signature: t=1716801000,v1=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
{
"type": "delivery.request",
"delivery_id": "del_01J8A2B3C4D5E6F7G8H9I0J1K",
"payment_intent_id": "pi_01J7Z5E6F7G8H9I0J1K2L3M4N",
"goods": {
"type": "application/pdf",
"checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"payer": {
"agent_id": "agent_cli_a1b2c3d4",
"human_id": "user_abc_789"
},
"created_at": "2026-05-27T09:10:00Z"
}
The merchant is expected to:
- Verify the
ItPay-Signatureheader (HMAC-SHA256 with the shared secret) - Fulfill the goods (email a download link, provision a license key, etc.)
- Return HTTP 200 with a confirmation or the actual access URL
HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "fulfilled",
"access_url": "https://merchant.example.com/download/ORD-7890?token=*** "expires_at": "2026-06-03T09:10:00Z"
}
Idempotency: The gateway includes a unique delivery_id in every callback. If the merchant receives a duplicate (e.g., due to retry), it should detect the duplicate via the delivery_id and return the previous response without re-fulfilling.
4. Delivery Lifecycle
Each delivery follows a state machine with the following states:
┌──────────┐
│ pending │
└────┬─────┘
│
▼
┌───────────┐
│ preparing │
└────┬──────┘
│
▼
┌───────┐
│ ready │
└───┬───┘
│
┌───┴────┐
│ │
▼ ▼
┌─────────┐ ┌───────────┐
│delivered│ │ expired │
└────┬────┘ └───────────┘
│
▼
┌───────────┐
│ confirmed │
└───────────┘
Any state ──revoked──▶ revoked
State Transitions
| From | To | Trigger |
|---|---|---|
pending | preparing | Gateway begins signing or encrypting the goods |
preparing | ready | Signed URL or encrypted payload is ready |
ready | delivered | Access URL is consumed (HTTP 200, or callback confirmed) |
delivered | confirmed | Payer or merchant explicitly confirms receipt |
ready | expired | expires_at reached without consumption |
delivered | expired | Receipt confirmation window passes without confirmation |
| any | revoked | Delivery is manually revoked before consumption |
State Descriptions
| State | Description |
|---|---|
pending | Delivery created but not yet processed |
preparing | Gateway is generating the signed URL or encrypting the payload |
ready | Access URL is available and can be consumed |
delivered | Access URL has been consumed (download started or completed) |
confirmed | Payer has confirmed receipt of goods |
expired | Access URL validity has expired |
revoked | Delivery was manually revoked by the merchant or gateway |
Retrieving Delivery Status
GET /v1/deliveries/del_01J8A2B3C4D5E6F7G8H9I0J1K
Authorization: Bearer ***
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "del_01J8A2B3C4D5E6F7G8H9I0J1K",
"status": "ready",
"access": {
"url": "https://delivery.itpay.io/v1/access/del_01J8A2B3C4D5E6F7G8H9I0J1K?token=*** "expires_at": "2026-05-28T09:10:00Z",
"method": "GET"
},
"updated_at": "2026-05-27T09:11:30Z"
}
Payer-Facing Access
The payer retrieves the access URL through a separate endpoint, which does not expose gateway-internal fields:
GET /v1/deliveries/del_01J8A2B3C4D5E6F7G8H9I0J1K/access
Authorization: Bearer ***
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_url": "https://delivery.itpay.io/v1/access/del_01J8A2B3C4D5E6F7G8H9I0J1K?token=*** "expires_at": "2026-05-28T09:10:00Z",
"goods": {
"type": "application/pdf",
"checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"encryption": {
"algorithm": "xchacha20-poly1305",
"ephemeral_public_key": "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6="
}
}
Revoking a Delivery
POST /v1/deliveries/del_01J8A2B3C4D5E6F7G8H9I0J1K/revoke
Authorization: Bearer ***
Content-Type: application/json
{
"reason": "order_cancelled"
}
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "del_01J8A2B3C4D5E6F7G8H9I0J1K",
"status": "revoked",
"updated_at": "2026-05-27T09:20:00Z"
}
5. Delivery Receipt
After a delivery is fulfilled (status reaches delivered), the gateway generates a cryptographic receipt. The receipt provides provable proof that the delivery occurred and is bound to the specific goods.
Receipt Formula
receipt = HMAC-SHA256(
delivery_id || goods.checksum || delivered_at,
merchant_secret
)
Where:
delivery_id— The delivery'sidfield (e.g.,del_01J8A...)goods.checksum— SHA-256 hash of the delivered content (hex string)delivered_at— ISO 8601 timestamp of the delivery eventmerchant_secret— Shared HMAC secret between the merchant and gateway, established at onboarding
Receipt Object Example
{
"receipt": {
"signature": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b",
"algorithm": "hmac-sha256",
"delivered_at": "2026-05-27T09:15:05Z"
}
}
Verification
The merchant (or a third-party auditor) can verify a receipt by recomputing the HMAC with the shared secret:
import hmac, hashlib
def verify_receipt(delivery_id, checksum, delivered_at, merchant_secret, received_signature):
message = delivery_id + checksum + delivered_at
expected = hmac.new(
merchant_secret.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, received_signature)
Receipts are useful for:
- Dispute resolution: Proving goods were delivered
- Audit trails: Verifiable fulfillment logs
- Regulatory compliance: Some jurisdictions require proof of digital delivery
6. Security Considerations
6.1. Signed URL Security
This pattern follows AWS S3 SigV4 pre-signed URLs with additional protections.
Signature scheme:
signature = HMAC-SHA256(
delivery_id || expires_at || goods.checksum || nonce,
gateway_signing_key
)
Token structure (base64url-encoded JSON):
{
"sub": "del_01J8A2B3C4D5E6F7G8H9I0J1K",
"exp": "2026-05-28T09:10:00Z",
"chk": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"nonce": "7c8a9b0d1e2f3a4b5c6d7e8f9a0b1c2d"
}
Short TTL: The protocol enforces a maximum ttl_seconds of 604,800 (7 days). For sensitive goods, merchants should use shorter windows (1 hour or less).
Single-use tracking: Each signed URL can be consumed at most once. The gateway maintains a consumption ledger keyed by delivery_id. After successful consumption, subsequent requests receive HTTP 410 Gone with the delivery status set to delivered.
6.2. E2E Encryption Security (X25519 + XChaCha20-Poly1305)
For encrypted_payload deliveries, the protocol uses:
| Component | Scheme | Purpose |
|---|---|---|
| Key exchange | X25519 ECDH | Shared secret derivation |
| Key derivation | HKDF-SHA256 | Expand shared secret into encryption key |
| Encryption | XChaCha20-Poly1305 | Authenticated encryption (AEAD) |
| Nonce | 192-bit random | Unique per encryption operation |
Flow:
- Merchant provides their X25519 public key during delivery creation
- Gateway generates an ephemeral X25519 key pair per delivery
- ECDH produces a shared secret:
shared_secret = X25519(ephemeral_sk, merchant_pk) - HKDF-SHA256 derives a 256-bit encryption key from the shared secret
- XChaCha20-Poly1305 encrypts the payload with a random 192-bit nonce
- The ephemeral public key and nonce are stored alongside the ciphertext
Security properties:
- Forward secrecy: Each delivery uses a fresh ephemeral key pair
- Authenticated encryption: XChaCha20-Poly1305 provides both confidentiality and integrity
- No plaintext storage: The gateway never holds the merchant's private key
6.3. Idempotency (Stripe Pattern)
The delivery creation endpoint supports idempotency to prevent duplicate fulfillment — critical when network issues cause retries.
Pattern reference: This follows the Stripe idempotency model. The client sends an Idempotency-Key header or idempotency_key field on every POST request. The gateway:
- Checks if the key has been used before
- If yes: returns the cached response (same HTTP status and body)
- If no: processes the request normally and caches the result
POST /v1/deliveries
Content-Type: application/json
Authorization: Bearer ***
Idempotency-Key: idem_01J8B3C4D5E6F7G8H9I0J1K2L
{
"payment_intent_id": "pi_01J7Z5E6F7G8H9I0J1K2L3M4N",
"type": "signed_url",
"goods": {
"type": "application/pdf",
"checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"ttl_seconds": 3600
}
}
Idempotency key expiration: Idempotency keys are valid for 24 hours. After that, the same key can be reused for a new request.
Idempotency for callbacks: When a callback delivery is retried, the gateway includes the same delivery_id in the callback payload. The merchant's webhook handler must check for duplicate delivery_id values and return the previous response without re-fulfilling.
6.4. Access Logging
All access to delivery URLs is logged with:
| Field | Description |
|---|---|
delivery_id | Which delivery was accessed |
ip_address | Payer's IP address |
user_agent | Payer's user agent string |
timestamp | When the access occurred |
success | Whether the download completed (HTTP 200) |
bytes_served | Number of bytes transferred |
Logs are retained for 90 days and are accessible to the merchant via the API.
6.5. Rate Limiting
| Limit | Scope | Window |
|---|---|---|
| 100 delivery creations | Per merchant | 1 minute |
| 10 access URL retrievals | Per delivery | 1 minute |
| 5 callback retries | Per delivery | 10 minutes |
Exceeding these limits returns HTTP 429 Too Many Requests.
7. Webhook Events
| Event | Description |
|---|---|
delivery.ready | Delivery is prepared and access URL is available |
delivery.delivered | Access URL was consumed successfully |
delivery.confirmed | Payer confirmed receipt |
delivery.expired | Access URL expired without consumption |
delivery.revoked | Delivery was manually revoked |
delivery.failed | Preparation failed (e.g., source URL unreachable) |
Webhook Payload Example
{
"type": "delivery.delivered",
"data": {
"id": "del_01J8A2B3C4D5E6F7G8H9I0J1K",
"payment_intent_id": "pi_01J7Z5E6F7G8H9I0J1K2L3M4N",
"status": "delivered",
"goods": {
"type": "application/pdf",
"checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"receipt": {
"signature": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b",
"algorithm": "hmac-sha256",
"delivered_at": "2026-05-27T09:15:05Z"
}
},
"created_at": "2026-05-27T09:15:05Z"
}
8. Error Codes
| Status | Code | Description |
|---|---|---|
| 400 | invalid_payment_intent | PaymentIntent not found or not in a completed state |
| 400 | invalid_goods | Goods specification is invalid or missing required fields |
| 400 | ttl_exceeds_maximum | ttl_seconds exceeds 604,800 (7 days) |
| 400 | invalid_encryption | Encryption parameters are malformed or incompatible |
| 400 | checksum_mismatch | Goods checksum does not match the source content (if verifiable) |
| 409 | duplicate_idempotency | Idempotency key reused with different request parameters |
| 410 | delivery_expired | Access URL has expired |
| 410 | delivery_revoked | Delivery was revoked |
| 410 | delivery_already_consumed | Single-use URL has already been downloaded |
| 422 | unsupported_type | The requested delivery type is not supported by the gateway |
| 429 | rate_limit_exceeded | Too many requests |
9. Natural Language Examples
"Deliver the premium report PDF to the buyer after payment completes."
"Generate a signed download link that expires in 1 hour."
"Encrypt the file with the merchant's public key before delivery."
"Send a fulfillment callback to the merchant's webhook URL."
"Revoke the download link for order ORD-7890 — the customer requested a refund."
"Verify the delivery receipt:
a1b2c3d4e5f6...for deliverydel_01J8A...."
10. Next Steps
- One-Time Pay — Create completed payments to deliver against
- Subscribe Pay — Set up recurring deliveries
- Refund — Revoke deliveries on refund
- Webhooks — Listen for delivery lifecycle events