Skip to main content

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.completed and 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_01J8A2B3C4D5E6F7G8H9I0J1K
  • goods.checksum = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
  • delivered_at = 2026-05-27T09:15:05Z
  • merchant_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

MethodEndpointDescription
POST/v1/deliveriesCreate a new delivery for a completed payment
GET/v1/deliveries/:idRetrieve a delivery and its current status
GET/v1/deliveries/:id/accessRetrieve the access URL (payer-facing)
POST/v1/deliveries/:id/revokeRevoke a delivery before it is consumed
GET/v1/deliveriesList 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

FieldTypeDescription
idstring (prefixed del_)Unique delivery identifier
payment_intent_idstring (prefixed pi_)The PaymentIntent this delivery fulfills
typeenumDelivery mechanism: signed_url, encrypted_payload, or callback
statusenumCurrent delivery status (see lifecycle below)
goodsobjectDescription of the digital goods being delivered
goods.typestringMIME type or content category (e.g., application/pdf, ebook, audio/mpeg)
goods.checksumstring (hex)SHA-256 hash of the content for integrity verification
goods.ttl_secondsnumberMaximum time in seconds the access URL is valid (max 604,800 = 7 days)
accessobjectAccess URL and metadata (populated after preparation)
access.urlstring (URL)The time-bounded access URL or download link
access.expires_atstring (ISO 8601)Timestamp when the access URL expires
access.methodstringHTTP method required (GET or POST with form)
encryptionobject(Optional) Encryption parameters for E2E-protected deliveries
encryption.algorithmstringEncryption algorithm (e.g., xchacha20-poly1305)
encryption.key_derivationstringKey derivation scheme (e.g., x25519-hkdf)
encryption.ephemeral_public_keystring (base64)Ephemeral public key for key exchange
receiptobjectDelivery receipt for proof-of-fulfillment
receipt.signaturestring (hex)HMAC-SHA256 signature (see Delivery Receipt section)
receipt.algorithmstringSigning algorithm (hmac-sha256)
receipt.delivered_atstring (ISO 8601)When the delivery was fulfilled
callback_urlstring (URL)(Optional) Merchant callback URL for callback type deliveries
metadataobjectArbitrary key-value data (max 4 KB)
created_atstring (ISO 8601)When the delivery was created
updated_atstring (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

FieldTypeRequiredDescription
payment_intent_idstring (prefixed pi_)yesThe PaymentIntent to fulfill
typeenumyessigned_url, encrypted_payload, or callback
goodsobjectyesDescription of the digital goods
goods.typestringyesMIME type or content category
goods.checksumstring (hex)yesSHA-256 hash of the content
goods.ttl_secondsnumberyesAccess URL validity in seconds (1–604,800)
goods.source_urlstring (URL)noMerchant-hosted URL of the goods (for gateway-side signing)
encryptionobjectnoEncryption parameters for E2E delivery
encryption.algorithmstringconditionalRequired if type=encrypted_payload: xchacha20-poly1305
encryption.public_keystring (base64)conditionalRequired if type=encrypted_payload: merchant's X25519 public key
callback_urlstring (URL)noMerchant callback URL for callback type deliveries
idempotency_keystringnoIdempotency key (see Idempotency section)
metadataobjectnoArbitrary 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.

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:

  1. Generates an ephemeral X25519 key pair
  2. Performs ECDH key exchange to derive a shared secret
  3. Derives an encryption key via HKDF
  4. Encrypts the payload with XChaCha20-Poly1305
  5. 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:

  1. Verify the ItPay-Signature header (HMAC-SHA256 with the shared secret)
  2. Fulfill the goods (email a download link, provision a license key, etc.)
  3. 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

FromToTrigger
pendingpreparingGateway begins signing or encrypting the goods
preparingreadySigned URL or encrypted payload is ready
readydeliveredAccess URL is consumed (HTTP 200, or callback confirmed)
deliveredconfirmedPayer or merchant explicitly confirms receipt
readyexpiredexpires_at reached without consumption
deliveredexpiredReceipt confirmation window passes without confirmation
anyrevokedDelivery is manually revoked before consumption

State Descriptions

StateDescription
pendingDelivery created but not yet processed
preparingGateway is generating the signed URL or encrypting the payload
readyAccess URL is available and can be consumed
deliveredAccess URL has been consumed (download started or completed)
confirmedPayer has confirmed receipt of goods
expiredAccess URL validity has expired
revokedDelivery 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's id field (e.g., del_01J8A...)
  • goods.checksum — SHA-256 hash of the delivered content (hex string)
  • delivered_at — ISO 8601 timestamp of the delivery event
  • merchant_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:

ComponentSchemePurpose
Key exchangeX25519 ECDHShared secret derivation
Key derivationHKDF-SHA256Expand shared secret into encryption key
EncryptionXChaCha20-Poly1305Authenticated encryption (AEAD)
Nonce192-bit randomUnique per encryption operation

Flow:

  1. Merchant provides their X25519 public key during delivery creation
  2. Gateway generates an ephemeral X25519 key pair per delivery
  3. ECDH produces a shared secret: shared_secret = X25519(ephemeral_sk, merchant_pk)
  4. HKDF-SHA256 derives a 256-bit encryption key from the shared secret
  5. XChaCha20-Poly1305 encrypts the payload with a random 192-bit nonce
  6. 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:

  1. Checks if the key has been used before
  2. If yes: returns the cached response (same HTTP status and body)
  3. 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:

FieldDescription
delivery_idWhich delivery was accessed
ip_addressPayer's IP address
user_agentPayer's user agent string
timestampWhen the access occurred
successWhether the download completed (HTTP 200)
bytes_servedNumber of bytes transferred

Logs are retained for 90 days and are accessible to the merchant via the API.

6.5. Rate Limiting

LimitScopeWindow
100 delivery creationsPer merchant1 minute
10 access URL retrievalsPer delivery1 minute
5 callback retriesPer delivery10 minutes

Exceeding these limits returns HTTP 429 Too Many Requests.


7. Webhook Events

EventDescription
delivery.readyDelivery is prepared and access URL is available
delivery.deliveredAccess URL was consumed successfully
delivery.confirmedPayer confirmed receipt
delivery.expiredAccess URL expired without consumption
delivery.revokedDelivery was manually revoked
delivery.failedPreparation 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

StatusCodeDescription
400invalid_payment_intentPaymentIntent not found or not in a completed state
400invalid_goodsGoods specification is invalid or missing required fields
400ttl_exceeds_maximumttl_seconds exceeds 604,800 (7 days)
400invalid_encryptionEncryption parameters are malformed or incompatible
400checksum_mismatchGoods checksum does not match the source content (if verifiable)
409duplicate_idempotencyIdempotency key reused with different request parameters
410delivery_expiredAccess URL has expired
410delivery_revokedDelivery was revoked
410delivery_already_consumedSingle-use URL has already been downloaded
422unsupported_typeThe requested delivery type is not supported by the gateway
429rate_limit_exceededToo 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 delivery del_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