# DPT Merchant API — Complete Reference # Generated: 2026-04-28T05:17:46.892Z # Source: https://docs.dpt.xyz # # This file contains the full DPT Merchant API documentation in a single # plain-text file for use as LLM context. Paste the contents below into # your AI assistant to get accurate, up-to-date API help. # # Production: https://api.dpt.xyz/v1 # Test: https://api-test.dpt.xyz/v1 # Auth: Authorization: Bearer dptb_ # Amounts: smallest currency unit (USDC: micro-units, USD: cents) # ───────────────────────────────────────────────────────────────────────────── # Merchant API — Overview The Merchant API lets your backend server interact with DPT programmatically using an **API key** instead of a user session. Use it to automate payments, manage customers, run payroll, and issue cards to your users. ## Base URLs | Environment | Base URL | |-------------|----------| | **Production** | `https://api.dpt.xyz` | | **Test** | `https://api-test.dpt.xyz` | Use the test environment for development and integration testing — it runs against a sandbox with no real funds. Switch to production when you're ready to go live. ## Authentication Every request must include your API key in the `Authorization` header: ```http Authorization: Bearer dptb_your_api_key_here ``` API keys are scoped to a single business. You do not need to pass a business ID in the URL — it is derived from the key itself. ### Getting an API Key 1. Go to **Dashboard → API Keys** 2. Click **New Key**, give it a name 3. Copy the key from the confirmation banner, or click **View** on any key to retrieve it later Keys are prefixed with `dptb_`. Store them securely (environment variables, secret managers). Never commit keys to source control. ### Revoking a Key From the dashboard, navigate to **API Keys** and click the revoke button next to any key. Revocation takes effect immediately. ## Request Format - All request bodies must be JSON - Set `Content-Type: application/json` on requests with a body ```http POST /v1/checkouts HTTP/1.1 Authorization: Bearer dptb_... Content-Type: application/json { "title": "Order #1234", "amount": 10000, "currency": "USDC" } ``` ## Response Format All responses are JSON. Successful responses return the created or fetched resource directly. Errors return: ```json { "error": "Human-readable error message" } ``` ## HTTP Status Codes | Code | Meaning | |------|---------| | `200` | Success with body | | `204` | Success, no body (DELETE, some POST) | | `400` | Bad request — check your parameters | | `401` | Missing or invalid API key | | `403` | API key does not have access to the resource | | `404` | Resource not found | | `422` | Validation error | | `500` | Server error | ## Amounts and Currencies All monetary amounts are in **the smallest unit** of the currency: - USDC / USDT: amounts are in micro-units (1 USDC = 1,000,000) - USD (fiat): amounts are in cents (1 USD = 100) Currency codes follow ISO 4217 for fiat and ticker symbols for crypto (e.g. `USDC`, `USDT`, `ETH`). ## Rate Limits - 1000 requests per minute per API key - 10 concurrent requests per API key Exceeding limits returns `429 Too Many Requests`. ## Available Resource Groups | Doc | Resources | |-----|-----------| | [Checkouts](./checkouts) | Payment links, refunds | | [Invoices](./invoices) | Invoice creation and lifecycle | | [Customers](./customers) | Customer records | | [Static Addresses](./static-addresses) | Permanent deposit addresses for any-amount crypto receive | | [Reseller](./reseller) | Create payments on behalf of other merchants, commission splits | | [Payroll](./payroll) | Payroll runs and disbursements | | [Cardholders](./cardholders) | Managed users and card issuance | | [Webhooks](./webhooks) | Event delivery and security | | [API Keys](./api-keys) | Key management | ─────────────────────────────────────────────────────────────────────────────── # Checkouts A checkout is a payment link you send to a customer. Webhooks fire at each stage of the checkout lifecycle: `checkout.created`, `checkout.paid` (payment detected on-chain — instant, ideal for POS), `checkout.completed` (confirmed + settled to your balance, ~1 min later), `checkout.refunded`, `checkout.cancelled`, and `checkout.expired`. ## Object `POST /v1/checkouts` and `GET /v1/checkouts/{id}` return the full checkout object with payment info: ```json { "id": "uuid", "merchant_id": "uuid", "title": "Order #1234", "description": "Premium plan — 1 year", "amount": 10000000, "currency": "USDC", "method": "any", "status": "active", "hosted_url": "https://pay.dpt.xyz/checkout/uuid", "reference": "order-1234", "customer_id": "uuid", "paid_amount": null, "paid_at": null, "payer_address": null, "refunded_amount": 0, "refund_status": null, "reseller_id": "uuid | null", "reseller_fee": 0, "platform_fee": 0, "expires_at": "2026-04-01T00:00:00Z", "created_at": "2026-03-16T10:00:00Z", "updated_at": "2026-03-16T10:00:00Z", "crypto_chains": [ { "chain": "Ethereum", "address": "0xabc...", "tokens": [ { "name": "USDC", "contract": "0x..." }, { "name": "USDT", "contract": "0x..." } ] } ], "fiat": { "session_url": "https://checkout.tazapay.com/...", "success_url": "https://your-site.com/success", "cancel_url": "https://your-site.com/cancel" } } ``` | Field | Description | |-------|-------------| | `hosted_url` | DPT-hosted payment page. Share this with your customer to let them pay via the DPT UI | | `payer_address` | Wallet address the customer paid from. Populated once the checkout is paid via crypto; `null` for fiat checkouts or unpaid checkouts. Used as the default refund destination | | `crypto_chains` | One entry per chain with the deposit address and supported tokens. Empty if `method` is `"fiat"` | | `fiat` | Tazapay-hosted fiat payment URL and redirect links. `null` if `method` is `"crypto"` | > `GET /v1/checkouts` (list) returns the base checkout object without `crypto_chains` or `fiat`. **Status values:** `active` | `paid` | `completed` | `partially_refunded` | `refunded` | `expired` | `cancelled` > `paid` = payment detected. `completed` = confirmed and settled to your balance. `partially_refunded` = at least one partial refund issued. `refunded` = fully refunded. **Refund status values:** `null` (not refunded) | `partially_refunded` | `refunded` --- ## Create Checkout ```http POST /v1/checkouts ``` ### Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `title` | string | ✓ | Short label shown on the payment page | | `description` | string | | Extended description | | `amount` | integer | | Amount in 6dp. Omit for open-amount checkouts | | `currency` | string | | `USDC`, `USDT`, `USD`, etc. Default: `USDC` | | `method` | string | | `crypto` (default), `fiat` *(coming soon)*, or `any` *(coming soon)*. | | `chains` | string[] | | Crypto chains to generate deposit addresses for (e.g. `["Ethereum", "Solana"]`). Crypto method only. | | `reference` | string | | Your own order/reference ID. Must be unique per merchant — submitting a duplicate `reference` returns a `400` error, making it a safe idempotency key | | `success_url` | string | | Redirect URL after successful payment (fiat method) | | `cancel_url` | string | | Redirect URL on cancellation (fiat method) | | `expires_at` | datetime | ✓ | ISO 8601 expiry time. Unpaid checkouts expire automatically | | `customer_id` | uuid | | Attach an existing customer | | `customer_name` | string | | Create/attach customer inline | | `customer_email` | string | | | | `customer_country` | string | | ISO 3166-1 alpha-3 (e.g. `SGP`, `USA`) | | `merchant_id` | uuid | | **Reseller only.** Create this checkout on behalf of another merchant. See [Reseller](./reseller) | ### Example ```bash curl -X POST https://api-test.dpt.xyz/v1/checkouts \ -H "Authorization: Bearer dptb_..." \ -H "Content-Type: application/json" \ -d '{ "title": "Order #1234", "amount": 50000000, "currency": "USDC", "customer_email": "alice@example.com" }' ``` ### Response `200` Returns the full [Checkout object](#object) including `hosted_url`, `crypto_chains`, and `fiat`. --- ## List Checkouts ```http GET /v1/checkouts ``` Returns an array of checkout objects ordered by creation date (newest first). --- ## Get Checkout ```http GET /v1/checkouts/{id} ``` --- ## Cancel Checkout ```http DELETE /v1/checkouts/{id} ``` Cancels an `active` checkout. Returns `204 No Content`. --- ## Refund Checkout ```http POST /v1/checkouts/{id}/refund ``` Requires `owner` or `admin` role. Can be called multiple times for partial refunds. Only checkouts with status `paid`, `completed`, or `partially_refunded` can be refunded. ### Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `amount` | integer | | Amount to refund in 6dp. Omit to refund the full remaining balance | | `reason` | string | | Optional reason memo, included in the webhook payload | | `receiver` | string | | Customer wallet address to send the refund to. **Crypto only**; ignored for fiat. If omitted, the refund is sent to the wallet the customer originally paid from. Required only if the payer address was not recorded (checkouts created before this feature was added) | | `gas_fee_payer` | string | | `"merchant"` to absorb the on-chain gas fee yourself. Omit (or any other value) to deduct gas from the refund amount (customer receives less). Crypto only; has no effect on fiat refunds | ### Gas fee (crypto checkouts) Every on-chain refund incurs a network gas fee denominated in the checkout currency (USDC/USDT). You control who absorbs it: | `gas_fee_payer` | Customer receives | Your balance debited | |---|---|---| | omitted (default) | `refund_amount − gas_fee` | `proportional share of refund_amount − gas_fee` | | `"merchant"` | `refund_amount` (full) | `proportional share of refund_amount + gas_fee` | The gas fee is calculated as: `network_base_fee + refund_amount × commission_rate`. If the refund amount is too small to cover the gas fee, the request is rejected with a `400` error. ### Fee handling When you receive a checkout payment, platform fees are held in a 7-day window before being distributed to resellers and DPT. This affects refunds: - **Within 7 days of payment** — fees are still held. A refund claws back a proportional share of the held fees alongside your balance, so you only pay back your net portion. Your stored `reseller_fee` and `platform_fee` on the checkout are reduced accordingly. - **After 7 days** (or once fees are released) — fees have already been distributed. The full refund amount is debited entirely from your available balance. Reseller and platform fees are not reversed. ### Partial refunds You may call this endpoint multiple times until `refunded_amount` reaches `paid_amount`. Each call advances `refund_status`: - First partial refund → `refund_status: "partially_refunded"`, `status` unchanged - Subsequent partial refunds → same - Final refund that reaches the full paid amount → `refund_status: "refunded"`, `status: "refunded"` ### Example — full refund, merchant pays gas (receiver auto-detected) ```bash curl -X POST https://api-test.dpt.xyz/v1/checkouts/uuid/refund \ -H "Authorization: Bearer dptb_..." \ -H "Content-Type: application/json" \ -d '{ "gas_fee_payer": "merchant" }' ``` ### Example — partial refund with explicit receiver ```bash curl -X POST https://api-test.dpt.xyz/v1/checkouts/uuid/refund \ -H "Authorization: Bearer dptb_..." \ -H "Content-Type: application/json" \ -d '{ "amount": 25000000, "receiver": "0xCustomerWalletAddress", "reason": "Partial cancellation" }' ``` ### Response **`200 OK`** — refund accepted. Returns the updated checkout status: ```json { "id": "uuid", "status": "partially_refunded", "refund_status": "partially_refunded", "refunded_amount": 25000000, "refund_fee": 500000, "remaining_amount": 75000000 } ``` | Field | Description | |-------|-------------| | `status` | Overall checkout status: `paid` / `completed` / `partially_refunded` / `refunded` | | `refund_status` | `partially_refunded` until the full paid amount is returned, then `refunded` | | `refunded_amount` | Cumulative amount refunded so far (in 6dp) | | `refund_fee` | On-chain gas fee charged for this refund (in 6dp). `0` for fiat checkouts | | `remaining_amount` | Amount still refundable after this refund (in 6dp). `0` when fully refunded | **`400 Bad Request`** — the refund cannot be processed. Body: ```json { "error": "reason" } ``` | Reason | Description | |--------|-------------| | `Cannot refund a checkout with status '...'` | Checkout is not in a refundable state | | `Checkout already fully refunded` | Nothing left to refund | | `Invalid refund amount. Max refundable: ...` | Amount is zero, negative, or exceeds the remaining balance | | `Refund amount too small to cover gas fee (gas: ...)` | Crypto only — refund is too small after gas deduction | | `Merchant balance insufficient to cover refund (required: ..., available: ...)` | Your available balance is too low to fund this refund | A `checkout.refunded` webhook fires after each successful refund. ─────────────────────────────────────────────────────────────────────────────── # Invoices Invoices are drafted, sent to customers, and paid via a checkout link. An invoice goes through a defined lifecycle: `draft → sent → paid | cancelled`. ## Object ```json { "id": "uuid", "merchant_id": "uuid", "customer_id": "uuid", "invoice_number": "INV-000042", "title": "March Consulting Invoice", "description": null, "status": "draft", "amount": 200000000, "currency": "USDC", "due_date": "2026-04-15", "line_items": [ { "description": "Consulting — March", "quantity": 1, "unit_price": 200000000, "total": 200000000 } ], "subtotal": 200000000, "tax_rate": 0, "tax_amount": 0, "recipient_note": null, "checkout_id": null, "paid_at": null, "paid_amount": null, "created_at": "2026-03-16T10:00:00Z", "updated_at": "2026-03-16T10:00:00Z" } ``` **Status values:** `draft` | `sent` | `paid` | `cancelled` --- ## Create Invoice ```http POST /v1/invoices ``` ### Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `title` | string | ✓ | Invoice title | | `customer_id` | uuid | | Attach existing customer | | `customer_name` | string | | Inline customer name (required if no `customer_id`) | | `customer_email` | string | | | | `customer_country` | string | | ISO 3166-1 alpha-3 (e.g. `SGP`, `USA`) | | `description` | string | | Extended description | | `currency` | string | | Default: `USDC` | | `line_items` | array | | Line items array. See below | | `amount` | integer | | Total amount in 6dp. Use instead of `line_items` for a simple fixed amount | | `due_date` | string | | ISO 8601 date `YYYY-MM-DD` | | `recipient_note` | string | | Note shown to the customer on the invoice | | `subtotal` | integer | | Pre-tax amount in 6dp. Triggers tax calculation | | `tax_rate` | integer | | Manual tax rate in basis points (0–10000). e.g. `1000` for 10%. Used with `subtotal` | | `merchant_id` | uuid | | **Reseller only.** Create this invoice on behalf of another merchant. See [Reseller](./reseller) | **Line item fields:** | Field | Type | Required | |-------|------|----------| | `description` | string | ✓ | | `quantity` | integer | ✓ | | `unit_price` | integer | ✓ | ### Example ```bash curl -X POST https://api-test.dpt.xyz/v1/invoices \ -H "Authorization: Bearer dptb_..." \ -H "Content-Type: application/json" \ -d '{ "title": "March Consulting Invoice", "customer_email": "billing@acme.com", "customer_name": "Acme Corp", "currency": "USDC", "due_date": "2026-04-15", "line_items": [ { "description": "Consulting — March", "quantity": 1, "unit_price": 200000000 } ] }' ``` --- ## List Invoices ```http GET /v1/invoices ``` --- ## Get Invoice ```http GET /v1/invoices/{id} ``` --- ## Activate Invoice (Send to Customer) Transitions from `draft` to `sent` and generates a hosted checkout link. ```http POST /v1/invoices/{id}/activate ``` ### Body (optional) | Field | Type | Description | |-------|------|-------------| | `success_url` | string | Redirect URL after successful payment | | `cancel_url` | string | Redirect URL on cancellation | ### Response `200` ```json { "invoice": { /* Invoice object */ }, "checkout": { /* Checkout object with hosted_url */ } } ``` --- ## Cancel Invoice ```http DELETE /v1/invoices/{id} ``` Only `draft` or `sent` invoices can be cancelled. Returns `204 No Content`. ─────────────────────────────────────────────────────────────────────────────── # Customers Customer records are reusable — attach them to checkouts and invoices instead of repeating name/email each time. ## Object ```json { "id": "uuid", "merchant_id": "uuid", "name": "Alice Smith", "email": "alice@example.com", "phone": "+1 415 000 0000", "country": "SGP", "notes": null, "created_at": "2026-03-16T10:00:00Z", "updated_at": "2026-03-16T10:00:00Z" } ``` --- ## Create Customer ```http POST /v1/customers ``` ### Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | ✓ | Full name | | `email` | string | | | | `phone` | string | | E.164 format recommended | | `country` | string | | ISO 3166-1 alpha-3 (e.g. `SGP`, `USA`) | | `notes` | string | | Internal notes | | `merchant_id` | uuid | | **Reseller only.** Create this customer under another merchant. See [Reseller](./reseller) | ### Example ```bash curl -X POST https://api-test.dpt.xyz/v1/customers \ -H "Authorization: Bearer dptb_..." \ -H "Content-Type: application/json" \ -d '{ "name": "Alice Smith", "email": "alice@example.com", "country": "SGP" }' ``` --- ## List Customers ```http GET /v1/customers ``` --- ## Get Customer ```http GET /v1/customers/{id} ``` --- ## Update Customer ```http PUT /v1/customers/{id} ``` ### Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | ✓ | Full name | | `email` | string | | | | `phone` | string | | E.164 format recommended | | `country` | string | | ISO 3166-1 alpha-3 (e.g. `SGP`, `USA`) | | `notes` | string | | Internal notes | --- ## Delete Customer ```http DELETE /v1/customers/{id} ``` Returns `204 No Content`. Existing checkouts and invoices referencing this customer are unaffected. ─────────────────────────────────────────────────────────────────────────────── # Payroll ::: warning Coming Soon The Payroll API is not yet available. We are actively working on it and will update this page when it launches. ::: Payroll will allow you to batch-disburse funds to any number of recipients in a single API call — ideal for contractor and employee payouts. Stay tuned for updates. ─────────────────────────────────────────────────────────────────────────────── # Cardholders Cardholders are managed users your business creates and controls. You can issue physical or virtual cards to them, fund their wallets, and monitor spending — all programmatically. ## Full Lifecycle ``` 1. Create cardholder 2. Complete KYC (via SumSub token) 3. Apply for a card (submits application to card provider) 4. Issue a card (once application is approved) 5. Fund the cardholder's wallet 6. Manage card — freeze/unfreeze, set limits, view secrets 7. Monitor transactions 8. Recall unused funds ``` --- ## Cardholder Object ```json { "id": "uuid", "merchant_id": "uuid", "name": "Bob Jones", "email": "bob@yourcompany.com", "username": "bob_abc123", "is_kyc_verified": false, "balances": [ { "currency": "USDC", "balance": 10000000, "rate": 1.0, "value": 10000000 } ], "created_at": "2026-03-16T10:00:00Z" } ``` `balances` is included in the list response. Each entry uses micro-units (divide by 1,000,000 for display). Only non-zero balances are returned. ## Card Object ```json { "id": "uuid", "user_id": "uuid", "catalogue_id": "uuid", "status": "active", "ctype": "virtual", "last4": "4242", "exp_month": "03", "exp_year": "2029", "spend_tx_max": 100000, "spend_daily_max": 500000, "spend_monthly_max": 2000000, "spend_online_enabled": true, "spend_present_enabled": false, "created_at": "2026-03-16T10:00:00Z" } ``` **Card status values:** `notActivated` | `active` | `locked` | `canceled` --- ## Step 1 — Create Cardholder ```http POST /v1/cardholders ``` ### Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | ✓ | Full name | | `email` | string | | Email address | ### Example ```bash curl -X POST https://api-test.dpt.xyz/v1/cardholders \ -H "Authorization: Bearer dptb_..." \ -H "Content-Type: application/json" \ -d '{ "name": "Bob Jones", "email": "bob@yourcompany.com" }' ``` ### Response `200` Returns the [Cardholder object](#cardholder-object). Note the `id` — you will need it for all subsequent calls. --- ## Step 2 — Start KYC Returns a SumSub SDK token. Embed the [SumSub Web SDK](https://docs.sumsub.com/docs/web-sdk) in your app and pass the token to collect the cardholder's identity documents. ```http POST /v1/cardholders/{id}/kyc ``` ### Response `200` ```json { "token": "sumsub_sdk_token_..." } ``` After the cardholder completes verification, `is_kyc_verified` becomes `true`. KYC status is updated automatically via webhook from SumSub — no polling needed. --- ## Step 3 — Apply for a Card Submit a card application to the card provider. The cardholder must be KYC-verified first. The application is reviewed and approved asynchronously — status updates arrive via webhook. ```http POST /v1/cardholders/{id}/cards/apply ``` ### Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `catalogue` | uuid | ✓ | Card product ID (get from dashboard) | | `account_purpose` | string | ✓ | Intended use of the card (e.g. `business_expenses`) | | `occupation` | string | ✓ | Cardholder's occupation | | `annual_salary` | string | ✓ | Annual salary band (e.g. `50000_100000`) | | `expected_monthly_volume` | string | ✓ | Expected monthly spend band (e.g. `1000_5000`) | ### Example ```bash curl -X POST https://api-test.dpt.xyz/v1/cardholders/CH_ID/cards/apply \ -H "Authorization: Bearer dptb_..." \ -H "Content-Type: application/json" \ -d '{ "catalogue": "CATALOGUE_UUID", "account_purpose": "business_expenses", "occupation": "software_engineer", "annual_salary": "50000_100000", "expected_monthly_volume": "1000_5000" }' ``` ### Response `200` Returns the card application object including `status`. Once `status` is `approved`, proceed to issue a card. --- ## Step 4 — Issue a Card The cardholder must have an approved card application before a card can be issued. ```http POST /v1/cardholders/{id}/cards ``` ### Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `catalogue` | uuid | ✓ | Card product ID — must match the approved application | | `type` | string | ✓ | `virtual` or `physical` | ### Example ```bash curl -X POST https://api-test.dpt.xyz/v1/cardholders/CH_ID/cards \ -H "Authorization: Bearer dptb_..." \ -H "Content-Type: application/json" \ -d '{ "catalogue": "CATALOGUE_UUID", "type": "virtual" }' ``` --- ## Step 5 — Fund Cardholder Transfer funds from your business wallet to the cardholder's wallet. ```http POST /v1/cardholders/{id}/fund ``` ### Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `amount` | integer | ✓ | Amount in smallest unit | | `currency` | string | ✓ | e.g. `USDC` | | `remark` | string | | Optional memo | ### Example ```bash curl -X POST https://api-test.dpt.xyz/v1/cardholders/CH_ID/fund \ -H "Authorization: Bearer dptb_..." \ -H "Content-Type: application/json" \ -d '{ "amount": 1000000000, "currency": "USDC" }' ``` Returns `204 No Content`. --- ## List Cardholders ```http GET /v1/cardholders ``` --- ## Get Cardholder ```http GET /v1/cardholders/{id} ``` --- ## Recall Funds Pull unused funds back from a cardholder's wallet to your business wallet. ```http POST /v1/cardholders/{id}/recall ``` Same body as [Fund Cardholder](#step-4--fund-cardholder). Returns `204 No Content`. --- ## List Cardholder Cards ```http GET /v1/cardholders/{id}/cards ``` --- ## Get Card ```http GET /v1/cardholders/{ch_id}/cards/{card_id} ``` --- ## Update Card (Freeze / Unfreeze) ```http PATCH /v1/cardholders/{ch_id}/cards/{card_id} ``` ### Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `status` | string | ✓ | `active` to unfreeze, `locked` to freeze | --- ## Update Card Spending Limits ```http PUT /v1/cardholders/{ch_id}/cards/{card_id}/limits ``` ### Body | Field | Type | Description | |-------|------|-------------| | `spend_tx_max` | integer | Max per-transaction amount (cents/micro-units) | | `spend_daily_max` | integer | Max daily spend | | `spend_monthly_max` | integer | Max monthly spend | | `spend_online_enabled` | bool | Allow online / card-not-present transactions | | `spend_present_enabled` | bool | Allow in-person / contactless transactions | --- ## Card Secrets & PIN — Encryption Model Sensitive card data (PAN, CVC, PIN) is **never returned in plaintext**. Instead, the API uses end-to-end encryption: you generate a one-time AES key, encrypt it with DPT's RSA public key to create a `session`, and we re-encrypt the card data with your AES key before returning it. Only your client can decrypt the response. ### How it works ``` 1. Client generates a random 32-byte AES key 2. Client RSA-OAEP encrypts base64(aesKey) with DPT's public key → session 3. Client sends { session } to the API 4. Server decrypts session → recovers aesKey 5. Server fetches card data from the card provider and re-encrypts it with aesKey 6. Client receives { iv, data } and decrypts with their aesKey ``` All encrypted payloads use **AES-256-GCM** with a 16-byte IV. The `data` field is `base64(ciphertext + 16-byte auth tag)`. ### DPT Public Key Use the key that matches your environment. Always use the **test key** against `api-test.dpt.xyz` and the **production key** against `api.dpt.xyz`. **Test** ``` -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtQZcsZCAT/fRiACkhLoJ l6gZM6YV5koykEYHUtdH5yRklfiKWnX0zMsGeCXvkS3kYBb+VzSe7VOo+9NgOFW9 IWoVi7n4FTWVbBzHX+fG/fvu7jkrdustVi64uYf7aNoh0F63w7Hp+Zgdlbpy67u5 /sb2tIKNHpgl7keS9G/mLzkIFkugNslNj7TeyFm35pqKwjPUUvymW36Bp1nB4ThI J11+cxIhp7Dzq8CWZT0ZwEnR2kmIKWX88aSoerLLnPRFm5kPIWITyimcdDkYV3FA ozBn8e7Xz/K6VenePWvdqI3UzX+F4beOUkLbi+wp4IySAM7nr8dPXefK5MVp75On PQIDAQAB -----END PUBLIC KEY----- ``` **Production** ``` -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkrpc2FGCmfGzVsA8+njm iVAS2j05f0/PvFaCDeYn7oLCzMMa+00PjfkurfT9ink99n2rRx1fr3m0GN0DQrsS T4M3PxU7vFB1cToSeSBTdrvIrI59fPQt7vOe9tqCJ/wZw/tG5pAfLoCCPJ0f0Q+B osuVEmdVWQZrZPtUx1dR2o2mHEQpwseRwUCWKc7MK0icU3AWw2+ItQsjXTNKq54l +nnz76xJsT/ycKiJHz160Xh+xb9tcQW5ASkqRzxF9QlJ4IeT/6kIG32H+DYF1HI0 DGuWxgHUGS+Kvvw8Axi+ItBHtHZSmCZq6jbI1ePrS01znVcw1KJhmoGTCgf32M1a CwIDAQAB -----END PUBLIC KEY----- ``` ### TypeScript helper Install zero dependencies — the examples below use only Node.js built-ins (`node:crypto`). ```typescript import * as crypto from "node:crypto"; interface EncryptedData { iv: string; // base64-encoded 16-byte IV data: string; // base64-encoded ciphertext with 16-byte GCM tag appended } /** * Generate a one-time AES-256 key and produce the `session` value required * by all card secrets / PIN endpoints. * * Keep `aesKey` in memory — you need it to decrypt the API response. * Never persist or log it. */ function createCardSession(dptPublicKeyPem: string): { aesKey: Buffer; session: string; } { const aesKey = crypto.randomBytes(32); // The RSA payload is the base64 representation of the raw key bytes const payload = Buffer.from(aesKey.toString("base64"), "utf8"); const encrypted = crypto.publicEncrypt( { key: dptPublicKeyPem, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha1", }, payload, ); return { aesKey, session: encrypted.toString("base64") }; } /** Decrypt an { iv, data } payload returned by the API. */ function cardDecrypt(edata: EncryptedData, aesKey: Buffer): string { const iv = Buffer.from(edata.iv, "base64"); const buf = Buffer.from(edata.data, "base64"); const tag = buf.subarray(buf.length - 16); const ciphertext = buf.subarray(0, buf.length - 16); const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv); decipher.setAuthTag(tag); return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8"); } /** Encrypt a plaintext string with your AES key (used for set PIN). */ function cardEncrypt(plaintext: string, aesKey: Buffer): EncryptedData { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv); const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]); const tag = cipher.getAuthTag(); return { iv: iv.toString("base64"), data: Buffer.concat([encrypted, tag]).toString("base64"), }; } /** * Format a PIN string into the PIN block expected by the API. * PIN must be 4–12 digits. * * Example: "1234" → "241234FFFFFFFFFF" */ function buildPinBlock(pin: string): string { if (!/^\d{4,12}$/.test(pin)) throw new Error("PIN must be 4–12 digits"); const lenHex = pin.length.toString(16); return "2" + lenHex + pin + "F".repeat(14 - pin.length); } ``` --- ## Get Card Secrets (PAN + CVC) Returns the full card number and CVC, encrypted with your AES key. ```http POST /v1/cardholders/{ch_id}/cards/{card_id}/secrets ``` ### Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `session` | string | ✓ | RSA-encrypted AES key — from `createCardSession()` | ### Response `200` ```json { "encryptedPan": { "iv": "base64...", "data": "base64..." }, "encryptedCvc": { "iv": "base64...", "data": "base64..." } } ``` ### TypeScript example ```typescript const DPT_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----`; const API_BASE = "https://api.dpt.xyz/v1"; const token = "dptb_..."; async function getCardSecrets(chId: string, cardId: string) { const { aesKey, session } = createCardSession(DPT_PUBLIC_KEY_PEM); const res = await fetch(`${API_BASE}/cardholders/${chId}/cards/${cardId}/secrets`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ session }), }); if (!res.ok) throw new Error(await res.text()); const { encryptedPan, encryptedCvc } = await res.json(); return { pan: cardDecrypt(encryptedPan, aesKey), cvc: cardDecrypt(encryptedCvc, aesKey), }; } // Usage const { pan, cvc } = await getCardSecrets("ch_id_here", "card_id_here"); console.log(pan); // "4111111111111234" console.log(cvc); // "123" ``` --- ## Get Card PIN Returns the current PIN, encrypted with your AES key. ```http GET /v1/cardholders/{ch_id}/cards/{card_id}/pin ``` ### Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `session` | string | ✓ | RSA-encrypted AES key — from `createCardSession()` | ### Response `200` ```json { "encryptedPin": { "iv": "base64...", "data": "base64..." } } ``` ### TypeScript example ```typescript async function getCardPin(chId: string, cardId: string) { const { aesKey, session } = createCardSession(DPT_PUBLIC_KEY_PEM); const res = await fetch(`${API_BASE}/cardholders/${chId}/cards/${cardId}/pin`, { method: "GET", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ session }), }); if (!res.ok) throw new Error(await res.text()); const { encryptedPin } = await res.json(); // Decrypt the PIN block and extract the digits const pinBlock = cardDecrypt(encryptedPin, aesKey); // Block format: "2{lenHex}{pin}{F...}" — e.g. "241234FFFFFFFFFF" const pinLen = parseInt(pinBlock[1], 16); return pinBlock.slice(2, 2 + pinLen); // "1234" } ``` --- ## Set Card PIN ```http PUT /v1/cardholders/{ch_id}/cards/{card_id}/pin ``` ### Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `session` | string | ✓ | RSA-encrypted AES key — from `createCardSession()` | | `encrypted_data` | object | ✓ | PIN block encrypted with your AES key | Returns `200 OK`. ### TypeScript example ```typescript async function setCardPin(chId: string, cardId: string, newPin: string) { const { aesKey, session } = createCardSession(DPT_PUBLIC_KEY_PEM); // Build and encrypt the PIN block const pinBlock = buildPinBlock(newPin); // e.g. "241234FFFFFFFFFF" const encrypted_data = cardEncrypt(pinBlock, aesKey); const res = await fetch(`${API_BASE}/cardholders/${chId}/cards/${cardId}/pin`, { method: "PUT", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ session, encrypted_data }), }); if (!res.ok) throw new Error(await res.text()); } // Usage await setCardPin("ch_id_here", "card_id_here", "1234"); ``` --- ## List Card Transactions ```http GET /v1/cardholders/{ch_id}/cards/{card_id}/transactions ``` Returns the most recent card transactions for this card. ### Response `200` ```json [ { "id": "uuid", "amount": -50000, "currency": "USDC", "description": "Starbucks #1234", "status": "completed", "created_at": "2026-03-16T09:00:00Z" } ] ``` ─────────────────────────────────────────────────────────────────────────────── # Static Addresses A static address is a **permanent crypto deposit address** tied to your merchant account. It never expires and accepts any amount of any supported token on its chain. Use it for websites, POS systems, partner integrations, or anywhere you want a stable receive address. Static addresses are created automatically for all supported chains when your merchant account is set up. --- ## Object ```json { "id": 1, "checkout_id": null, "merchant_id": "uuid", "chain": "ethereum", "address": "0xabc123...", "created_at": "2026-03-26T10:00:00Z" } ``` | Field | Description | |-------|-------------| | `id` | Internal record ID | | `checkout_id` | Always `null` for static addresses | | `merchant_id` | Your merchant UUID | | `chain` | Chain identifier: `ethereum`, `tron`, `bitcoin`, `polygon`, `bsc` | | `address` | The on-chain deposit address | | `created_at` | When the address was generated | --- ## List Static Addresses Returns all static deposit addresses for your merchant account. ```http GET /v1/static-addresses ``` ### Response `200` ```json [ { "id": 1, "checkout_id": null, "merchant_id": "uuid", "chain": "ethereum", "address": "0xabc123...", "created_at": "2026-03-26T10:00:00Z" }, { "id": 2, "checkout_id": null, "merchant_id": "uuid", "chain": "tron", "address": "TXyz...", "created_at": "2026-03-26T10:00:00Z" } ] ``` ### Example ```bash curl https://api-test.dpt.xyz/v1/static-addresses \ -H "Authorization: Bearer dptb_..." ``` --- ## Receiving Payments Any crypto transfer sent to a static address is automatically detected and credited to your merchant balance. No webhook configuration is required to receive funds — they are credited on-chain confirmation. A `deposit.received` webhook is fired for each incoming transfer: ```json { "event": "deposit.received", "data": { "merchant_id": "uuid", "amount": 10000000, "currency": "USDC", "txid": "0x..." } } ``` See [Webhooks](./webhooks) for setup and verification details. --- ## Supported Chains and Tokens | Chain | `chain` value | Accepted tokens | |-------|---------------|-----------------| | Ethereum | `ethereum` | USDC, USDT, ETH | | Tron | `tron` | USDT (TRC-20) | | Bitcoin | `bitcoin` | BTC | | Polygon | `polygon` | USDC, USDT | | BNB Chain | `bsc` | USDT (BEP-20) | --- ## vs. Checkout | | Static Address | Checkout | |--|----------------|----------| | Expires | Never | Optional TTL | | Amount | Any | Fixed or open | | Customer info | Not required | Optional | | Reusable | Yes, indefinitely | One payment | | Use case | POS, website, partners | E-commerce, invoicing | ─────────────────────────────────────────────────────────────────────────────── # Reseller The reseller feature lets a merchant (the **reseller**) create checkouts, invoices, and customers on behalf of other merchants using its own API key. When the payment is collected, the proceeds are split: the target merchant receives `amount − reseller_fee − platform_fee`, the reseller earns `reseller_fee`, and DPT retains `platform_fee`. This is useful for platforms, marketplaces, and aggregators that process payments for sub-merchants. --- ## How It Works 1. **Reseller creates a connection** to the target merchant via `POST /v1/reseller/connections`, setting their commission `rate` and optional `min_fee` / `max_fee` 2. The connection is **immediately active** — no approval needed from the target merchant 3. **Reseller creates checkouts/invoices/customers** by passing `merchant_id` in the request — DPT verifies the active connection 4. When payment arrives, DPT splits the funds: - `gross = amount × rate / 10000`, clamped to `[min_fee, max_fee]` if set - `platform_fee = amount × platform_rate / 10000` → DPT's direct cut of the transaction - `reseller_fee = gross − platform_fee` → what the reseller actually receives - `merchant_net = amount − gross` → credited to the target merchant 5. A `checkout.completed` webhook is sent to the target merchant's registered endpoint, including `reseller_fee`, `platform_fee`, and `reseller_id` 6. The **target merchant can revoke** the connection at any time via dashboard or API — the reseller immediately loses access --- ## Commission Rate The commission `rate` is expressed in **basis points** (bps), where 10000 bps = 100%. | `rate` | Commission | |--------|------------| | `0` | 0% (no fee) | | `100` | 1% | | `200` | 2% | | `500` | 5% | | `10000` | 100% | The rate is set **on the connection** and applies to all checkouts created through it. You can update it at any time via `PUT /v1/reseller/connections/{id}`. There is no per-checkout rate override. ### Fee Caps (optional) Set `min_fee` and/or `max_fee` (in **6dp** units, same as all other amounts in the API) to cap the commission: - `min_fee` — fee is at least this amount (floor). e.g. always charge at least $1.00 = `1000000` - `max_fee` — fee is at most this amount (ceiling). e.g. never charge more than $50.00 = `50000000` When both are set: `fee = clamp(amount × rate / 10000, min_fee, max_fee)` --- ## Connection Lifecycle ``` reseller merchant | | |-- POST /v1/reseller/connections --> | (status: active) | | |-- PUT /v1/reseller/connections/{id}->| (update rate / fee caps) | | |-- POST /v1/checkouts (merchant_id) ->| ✓ active connection required | | |-- DELETE /v1/reseller/connections/{id} (reseller removes) | | | DELETE ...reseller-connections/{id} --| (merchant revokes → status: revoked) ``` ### Connection Object ```json { "id": "uuid", "reseller_id": "uuid", "merchant_id": "uuid", "status": "active | revoked", "rate": 200, "min_fee": null, "max_fee": 50000000, "created_at": "2026-03-26T10:00:00Z", "updated_at": "2026-03-26T10:00:00Z" } ``` --- ## Reseller: Create or Update a Connection ```http POST /v1/reseller/connections ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `merchant_id` | uuid | ✓ | UUID of the merchant you want to resell for | | `rate` | integer | ✓ | Commission in basis points (0–10000) | | `min_fee` | integer | — | Minimum fee per transaction in 6dp (e.g. `1000000` = $1.00). Omit for no floor. | | `max_fee` | integer | — | Maximum fee per transaction in 6dp (e.g. `50000000` = $50.00). Omit for no ceiling. | Idempotent — if a connection to the same merchant already exists, it is updated with the new values and reactivated. ```bash curl -X POST https://api-test.dpt.xyz/v1/reseller/connections \ -H "Authorization: Bearer dptb_..." \ -H "Content-Type: application/json" \ -d '{ "merchant_id": "TARGET_MERCHANT_UUID", "rate": 200, "min_fee": 1000000, "max_fee": 50000000 }' ``` --- ## Reseller: Update a Connection ```http PUT /v1/reseller/connections/{connection_id} ``` Update the commission rate and/or fee caps on an existing connection by ID. Only the reseller can call this endpoint. | Field | Type | Required | Description | |-------|------|----------|-------------| | `rate` | integer | ✓ | New commission rate in basis points (0–10000) | | `min_fee` | integer | — | New minimum fee per transaction in 6dp. Pass `null` to remove the floor. | | `max_fee` | integer | — | New maximum fee per transaction in 6dp. Pass `null` to remove the ceiling. | The update takes effect immediately — the new rate applies to all checkouts created after the call. In-flight checkouts are unaffected. ```bash curl -X PUT https://api-test.dpt.xyz/v1/reseller/connections/CONN_ID \ -H "Authorization: Bearer dptb_..." \ -H "Content-Type: application/json" \ -d '{ "rate": 300, "min_fee": 1000000, "max_fee": null }' ``` Returns the updated connection object. --- ## Reseller: List Outgoing Connections ```http GET /v1/reseller/connections ``` Returns all outgoing connections for the authenticated reseller (all statuses). --- ## Reseller: Delete a Connection ```http DELETE /v1/reseller/connections/{connection_id} ``` Permanently removes the connection. The reseller can no longer create checkouts for that merchant. ```bash curl -X DELETE https://api-test.dpt.xyz/v1/reseller/connections/CONN_ID \ -H "Authorization: Bearer dptb_..." ``` --- ## Merchant: List Incoming Connections ```http GET /v1/merchants/{id}/reseller-connections/incoming ``` Returns all connections where this merchant is the target. Includes both `active` and `revoked` connections. ```bash curl https://api-test.dpt.xyz/v1/merchants/YOUR_ID/reseller-connections/incoming \ -H "Authorization: Bearer dptb_..." ``` Also available in the dashboard at **Settings → Resellers**. --- ## Merchant: Revoke a Connection ```http DELETE /v1/merchants/{id}/reseller-connections/{connection_id} ``` Sets status to `revoked`. The reseller immediately loses the ability to create checkouts for your account. The record is retained for audit purposes. ```bash curl -X DELETE https://api-test.dpt.xyz/v1/merchants/YOUR_ID/reseller-connections/CONN_ID \ -H "Authorization: Bearer dptb_..." ``` > **Note:** Both the reseller (`DELETE /v1/reseller/connections/{id}`) and the merchant (`DELETE /v1/merchants/{id}/reseller-connections/{id}`) can remove a connection. The reseller's delete permanently removes the record; the merchant's delete sets it to `revoked` for audit purposes. --- ## Creating a Checkout as a Reseller Add `merchant_id` (target merchant) to the standard [Create Checkout](./checkouts#create-checkout) request. The commission rate is taken from the connection. ```bash curl -X POST https://api-test.dpt.xyz/v1/checkouts \ -H "Authorization: Bearer dptb_YOUR_RESELLER_KEY" \ -H "Content-Type: application/json" \ -d '{ "merchant_id": "TARGET_MERCHANT_UUID", "title": "Order #5678", "amount": 50000000, "currency": "USDC" }' ``` The checkout is created under the target merchant. The reseller earns their connection rate when this checkout is paid. --- ## Creating an Invoice as a Reseller Add `merchant_id` to the standard [Create Invoice](./invoices#create-invoice) request. ```bash curl -X POST https://api-test.dpt.xyz/v1/invoices \ -H "Authorization: Bearer dptb_YOUR_RESELLER_KEY" \ -H "Content-Type: application/json" \ -d '{ "merchant_id": "TARGET_MERCHANT_UUID", "customer_email": "billing@acme.com", "customer_name": "Acme Corp", "currency": "USDC", "line_items": [ { "description": "Consulting — March", "quantity": 1, "unit_price": 200000000 } ] }' ``` --- ## Creating a Customer as a Reseller Add `merchant_id` to the standard [Create Customer](./customers#create-customer) request. The customer record is created under the target merchant. ```bash curl -X POST https://api-test.dpt.xyz/v1/customers \ -H "Authorization: Bearer dptb_YOUR_RESELLER_KEY" \ -H "Content-Type: application/json" \ -d '{ "merchant_id": "TARGET_MERCHANT_UUID", "name": "Alice Smith", "email": "alice@example.com" }' ``` --- ## Provision a Merchant (Reseller) ```http POST /v1/reseller/provision ``` Create a DPT user account, a merchant, and a reseller connection in a single call. Use this when your platform is onboarding a new sub-merchant and you want to handle the entire signup flow server-side without requiring the end user to go through the standard registration. > **Production only.** User accounts are shared across test and production — there is no separate test registry. Calling this endpoint creates a real Firebase account and a real DPT user. Always use `https://api.dpt.xyz` for this endpoint; `api-test.dpt.xyz` is not supported. > **KYB required.** The reseller merchant calling this endpoint must have a `kyb_status` of `approved`. Calls from merchants that have not completed KYB verification will be rejected with `403`. ### Request body **User fields** | Field | Type | Required | Description | |-------|------|----------|-------------| | `email` | string | ✓ | Email address for the new user account | | `password` | string | ✓ | Initial password (min 6 characters) | | `full_name` | string | — | User's display name | **Merchant fields** | Field | Type | Required | Description | |-------|------|----------|-------------| | `merchant_name` | string | ✓ | Public-facing merchant / brand name | | `legal_name` | string | ✓ | Legal entity name | | `country` | string | ✓ | ISO 3166-1 alpha-2 country code (e.g. `"US"`) | | `btype` | string | — | `"company"` (default) or `"individual"` | | `website` | string | — | Merchant website URL | **Reseller connection fields** | Field | Type | Required | Description | |-------|------|----------|-------------| | `rate` | integer | ✓ | Commission in basis points (0–10000). See [Commission Rate](#commission-rate) | | `min_fee` | integer | — | Minimum fee per transaction in 6dp. Omit for no floor | | `max_fee` | integer | — | Maximum fee per transaction in 6dp. Omit for no ceiling | ### Response ```json { "user_id": "uuid", "merchant": { "id": "uuid", "name": "Acme Corp", "legal_name": "Acme Corporation Ltd", "country": "US", "btype": "company", "kyb_status": "draft", "created_at": "2026-04-28T10:00:00Z" }, "connection_id": "uuid" } ``` | Field | Description | |-------|-------------| | `user_id` | UUID of the newly created DPT user | | `merchant` | Full merchant object for the newly created merchant | | `connection_id` | UUID of the reseller connection (active immediately) | ### Example ```bash curl -X POST https://api.dpt.xyz/v1/reseller/provision \ -H "Authorization: Bearer dptb_YOUR_RESELLER_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "owner@acme.com", "password": "s3cr3tPass", "full_name": "Alice Smith", "merchant_name": "Acme Corp", "legal_name": "Acme Corporation Ltd", "country": "US", "btype": "company", "rate": 200, "min_fee": 1000000, "max_fee": 50000000 }' ``` ### Error cases | HTTP | Reason | |------|--------| | `400` | Missing required fields, invalid country code, password too short | | `409` | Email already registered | | `403` | Reseller KYB not approved | --- ## Finding Your Merchant ID Your merchant ID is available in: - **Dashboard → Overview** — shown in the top-right of the page header (click to copy) - **Dashboard → Settings → General** — shown in the General section (click to copy) --- ## Webhook Fields When a reseller checkout is settled, the `checkout.completed` webhook includes additional fields: | Field | Description | |-------|-------------| | `reseller_fee` | Amount credited to the reseller (`gross − platform_fee`) | | `platform_fee` | DPT's direct cut (`amount × platform_rate / 10000`). `0` if platform rate is not set | | `reseller_id` | UUID of the reseller merchant | Merchant net = `amount − gross` (= `amount − reseller_fee − platform_fee`). See [Webhooks — checkout.completed](./webhooks#checkoutcompleted-payload) for the full payload example. ─────────────────────────────────────────────────────────────────────────────── # Webhooks Webhooks let DPT notify your server in real time when events happen in your business. Instead of polling the API, you register an endpoint and DPT sends an HTTP POST to it whenever an event fires. ## Setup 1. Go to **Dashboard → Webhooks** 2. Click **Add Webhook** 3. Enter your endpoint URL and select which events to subscribe to 4. After creation, copy the **signing secret** from the banner — it is also visible at any time in the webhook list via the eye icon --- ## Event Types | Event | Fires when | |-------|-----------| | `checkout.created` | A checkout link is created | | `checkout.paid` | Payment detected on-chain — fires immediately, ideal for instant merchants | | `checkout.completed` | Payment confirmed, KYT passed, and settled to your balance (~1 min after `checkout.paid`) | | `checkout.refunded` | A checkout is fully or partially refunded | | `checkout.expired` | A checkout reaches its expiry time unpaid | | `checkout.cancelled` | A checkout is cancelled via the API or dashboard | | `invoice.paid` | An invoice is fully paid | | `invoice.sent` | An invoice is activated and sent to the customer | | `payout.completed` | A payout to an external account is confirmed | | `payout.failed` | A payout fails | | `card.created` | A card is issued to a cardholder you manage | | `card.transaction` | A card transaction event (authorization, update, or settlement) | --- ## Payload All webhook deliveries share the same envelope: ```json { "id": "delivery-uuid", "event": "checkout.completed", "data": { "id": "uuid", "merchant_id": "uuid", "amount": 50000000, "currency": "USDC", "status": "completed" } } ``` The top-level `id` is the **delivery ID** — a stable UUID that stays the same across all retry attempts. Use it to deduplicate events on your end. The `data` object contains the full resource at the time of the event. --- ## `checkout.paid` Payload Fires as soon as the incoming transfer is detected on-chain. Because on-chain crypto transactions are practically irreversible, this event is safe to act on immediately for most use cases — especially **instant merchants** (in-person, POS, vending) where speed matters. Funds are not yet credited to your balance at this point — that happens with `checkout.completed` roughly 1 minute later. ```json { "id": "delivery-uuid", "event": "checkout.paid", "data": { "id": "uuid", "merchant_id": "uuid", "amount": 50000000, "currency": "USDC", "status": "paid" } } ``` **When to act on `checkout.paid`:** - In-person / POS payments where instant confirmation matters - Digital goods delivery where speed is expected - Showing a "payment received" status to your customer immediately --- ## `checkout.completed` Payload Fires approximately 1 minute after `checkout.paid`, once the payment is confirmed on-chain, passes KYT risk checks, and is **credited to your merchant balance**. Use this event to trigger accounting, payouts, or any flow that depends on settled funds. ```json { "id": "delivery-uuid", "event": "checkout.completed", "data": { "id": "uuid", "merchant_id": "uuid", "amount": 50000000, "currency": "USDC", "status": "completed", "reseller_fee": 1000000, "platform_fee": 100000, "reseller_id": "uuid | null", "referral_fee": 50000, "referred_by": "uuid | null" } } ``` | Field | Description | |-------|-------------| | `reseller_fee` | Amount credited to the reseller. `0` when no reseller | | `platform_fee` | DPT's platform cut. `0` when no platform rate is set | | `reseller_id` | UUID of the reseller merchant, or `null` | | `referral_fee` | Referral commission deducted. `0` when no referral | | `referred_by` | UUID of the referring merchant, or `null` | The merchant's net is `amount − reseller_fee − platform_fee − referral_fee`. --- ## `checkout.created` Payload Fires immediately after a checkout link is created, before any payment is made. The payload includes payment info so your server can act without making an extra API call. ```json { "id": "delivery-uuid", "event": "checkout.created", "data": { "id": "uuid", "merchant_id": "uuid", "hosted_url": "https://pay.dpt.xyz/checkout/uuid", "amount": 50000000, "currency": "USDC", "method": "any", "reference": "order-1234", "customer_id": "uuid", "reseller_id": "uuid | null", "expires_at": "2026-04-01T00:00:00Z | null", "status": "active", "crypto_chains": [ { "chain": "Ethereum", "address": "0xabc..." } ], "fiat": { "session_url": "https://checkout.tazapay.com/...", "success_url": "https://your-site.com/success", "cancel_url": "https://your-site.com/cancel" } } } ``` | Field | Description | |-------|-------------| | `hosted_url` | DPT-hosted payment page URL | | `crypto_chains` | Deposit addresses per chain. Empty array if `method` is `"fiat"` | | `fiat` | Tazapay session URL and redirect URLs. `null` if `method` is `"crypto"` | --- ## `checkout.refunded` Payload Fires after a full or partial refund is recorded. `status` will be `"refunded"` (fully refunded) or `"partially_refunded"`. ```json { "id": "delivery-uuid", "event": "checkout.refunded", "data": { "id": "uuid", "merchant_id": "uuid", "refund_amount": 10000000, "refunded_amount": 10000000, "refund_fee": 500000, "remaining_amount": 0, "currency": "USDC", "reason": "Customer request", "status": "refunded" } } ``` | Field | Description | |-------|-------------| | `refund_amount` | Amount refunded in this transaction (gross, before gas) | | `refunded_amount` | Running total refunded across all refund operations | | `refund_fee` | On-chain gas fee in 6dp. `0` for fiat refunds. When `gas_fee_payer` is `"merchant"` this is charged on top of `refund_amount`; otherwise it is deducted from it | | `remaining_amount` | Amount still refundable after this refund. `0` when fully refunded | | `status` | `"refunded"` when fully refunded, `"partially_refunded"` otherwise | --- ## `card.created` Payload Fires when a card is successfully issued to a cardholder managed by your merchant account. Use this event to record the card ID in your system or trigger onboarding flows. ```json { "event": "card.created", "data": { "id": "uuid", "cardholder_id": "uuid", "status": "active", "ctype": "virtual", "last4": "4242", "exp_month": "03", "exp_year": "2029", "created_at": "2026-04-20T10:00:00Z" } } ``` | Field | Description | |-------|-------------| | `id` | Card UUID | | `cardholder_id` | UUID of the cardholder who owns the card | | `ctype` | `virtual` or `physical` | | `status` | Card status at creation time — normally `active` for virtual cards | --- ## `card.transaction` Payload Fires at each stage of the card transaction lifecycle. Every event for the same underlying transaction shares the same `data.id`. ```json { "event": "card.transaction", "data": { "id": "uuid", "card_id": "uuid", "amount": -5000000, "local_amount": -500, "local_currency": "USD", "merchant_name": "Starbucks #1234", "type": "card_payment", "status": "completed", "created_at": "2026-04-20T10:00:00Z" } } ``` | Field | Description | |-------|-------------| | `id` | Transaction UUID — stable across all lifecycle events for this transaction | | `card_id` | UUID of the card used | | `amount` | Amount in 6dp micro-units. Negative for payments/withdrawals; positive for deposits/refunds | | `local_amount` | Amount in the transaction's local currency (2dp cents). Negative for payments/withdrawals | | `local_currency` | ISO 4217 currency code of the transaction (e.g. `USD`, `EUR`) | | `merchant_name` | Merchant or counterparty name | | `type` | Transaction type — see table below | | `status` | Transaction status — see table below | | `created_at` | When the transaction record was first created | ### Transaction Types | `type` | Description | |--------|-------------| | `card_payment` | Card swipe or online payment — cardholder spent funds | | `card_refund` | Merchant refunded the cardholder — funds returned to the card | | `card_deposit` | Funds loaded onto a physical (Wasabi) card | | `card_withdraw` | Funds pulled off a physical (Wasabi) card back to the wallet | ### Transaction Statuses | `status` | Description | |----------|-------------| | `requested` / `pending` | Authorization hold placed — funds frozen, settlement expected | | `paid` | Transaction created by the card network (Rain virtual cards) | | `completed` | Transaction settled — funds debited or credited | | `declined` | Authorization was declined — hold released, no funds moved | | `reversed` | Authorization was voided before settlement — hold released | ### Lifecycle A typical card payment fires these events in order: ``` card.transaction (status=requested) ← authorization hold placed card.transaction (status=completed) ← settled ``` A declined authorization: ``` card.transaction (status=requested) ← hold placed card.transaction (status=declined) ← declined, hold released ``` A voided authorization: ``` card.transaction (status=requested) ← hold placed card.transaction (status=reversed) ← voided, hold released ``` Deposit and withdrawal events fire a single `card.transaction` each: ``` card.transaction (type=card_deposit, status=completed) card.transaction (type=card_withdraw, status=completed) ``` --- ## Request Headers | Header | Value | |--------|-------| | `Content-Type` | `application/json` | | `X-DPT-Event` | Event type, e.g. `checkout.completed` | | `X-DPT-Delivery-Id` | Delivery UUID (same as top-level `id` in the body) | | `X-DPT-Signature` | `sha256={hex_signature}` | --- ## Verifying the Signature Every delivery is signed with HMAC-SHA256 using your webhook's signing secret. **Always verify the signature** before processing the event. ### Algorithm 1. Read the raw request body as a string (do not parse it first) 2. Compute `HMAC-SHA256(secret, raw_body)` and hex-encode the result 3. Compare with the value after `sha256=` in `X-DPT-Signature` 4. If they match, the payload is authentic ### Examples **Node.js** ```js const crypto = require('crypto'); function verifySignature(secret, rawBody, signatureHeader) { const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signatureHeader) ); } // Express handler app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['x-dpt-signature']; if (!verifySignature(process.env.WEBHOOK_SECRET, req.body, sig)) { return res.status(401).send('Invalid signature'); } const { event, data } = JSON.parse(req.body); // process event... res.sendStatus(200); }); ``` **Python** ```python import hmac, hashlib def verify_signature(secret: str, raw_body: bytes, signature_header: str) -> bool: expected = 'sha256=' + hmac.new( secret.encode(), raw_body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature_header) # Flask handler @app.route('/webhooks', methods=['POST']) def webhook(): sig = request.headers.get('X-DPT-Signature', '') if not verify_signature(os.environ['WEBHOOK_SECRET'], request.data, sig): abort(401) payload = request.get_json() event = payload['event'] data = payload['data'] # process event... return '', 200 ``` **Go** ```go func verifySignature(secret, rawBody, signatureHeader string) bool { mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(rawBody)) expected := "sha256=" + hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(signatureHeader)) } ``` --- ## Retry Policy If your endpoint returns a non-2xx HTTP status (or times out after 10 seconds), DPT retries the delivery with exponential backoff: | Attempt | Delay after previous failure | |---------|------------------------------| | 1 | Immediate | | 2 | 5 minutes | | 3 | 30 minutes | | 4 | 2 hours | | 5 | 5 hours | After 5 failed attempts, the delivery is marked **failed** and no further retries occur. You can view the full delivery history, including HTTP status codes and response bodies, in **Dashboard → Webhooks**. --- ## Delivery History Each event delivery is logged with: - **Status**: `pending`, `delivered`, `failed` - **Attempt number** - **HTTP response status** from your server - **Timestamp** Access the log from the dashboard by clicking on any webhook endpoint. --- ## Best Practices - **Return 2xx quickly** — do heavy processing asynchronously (queue the event and acknowledge immediately) - **Handle duplicates** — use the top-level `id` (delivery ID) to deduplicate retries; `data.id` is the resource ID, not the delivery ID - **Check the event type first** — ignore events your code doesn't handle - **Use HTTPS** — only register endpoints with valid TLS certificates - **Rotate secrets periodically** — delete and recreate the webhook to get a new secret