Skip to content

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

EventFires when
checkout.createdA checkout link is created
checkout.paidPayment detected on-chain — fires immediately, ideal for instant merchants
checkout.completedPayment confirmed, KYT passed, and settled to your balance (~1 min after checkout.paid)
checkout.refundedA checkout is fully or partially refunded
checkout.expiredA checkout reaches its expiry time unpaid
checkout.cancelledA checkout is cancelled via the API or dashboard
invoice.paidAn invoice is fully paid
invoice.sentAn invoice is activated and sent to the customer
payout.completedA payout to an external account is confirmed
payout.failedA payout fails
card.createdA card is issued to a cardholder you manage
card.transactionA 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"
  }
}
FieldDescription
reseller_feeAmount credited to the reseller. 0 when no reseller
platform_feeDPT's platform cut. 0 when no platform rate is set
reseller_idUUID of the reseller merchant, or null
referral_feeReferral commission deducted. 0 when no referral
referred_byUUID 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"
    }
  }
}
FieldDescription
hosted_urlDPT-hosted payment page URL
crypto_chainsDeposit addresses per chain. Empty array if method is "fiat"
fiatTazapay 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"
  }
}
FieldDescription
refund_amountAmount refunded in this transaction (gross, before gas)
refunded_amountRunning total refunded across all refund operations
refund_feeOn-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_amountAmount 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"
  }
}
FieldDescription
idCard UUID
cardholder_idUUID of the cardholder who owns the card
ctypevirtual or physical
statusCard 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"
  }
}
FieldDescription
idTransaction UUID — stable across all lifecycle events for this transaction
card_idUUID of the card used
amountAmount in 6dp micro-units. Negative for payments/withdrawals; positive for deposits/refunds
local_amountAmount in the transaction's local currency (2dp cents). Negative for payments/withdrawals
local_currencyISO 4217 currency code of the transaction (e.g. USD, EUR)
merchant_nameMerchant or counterparty name
typeTransaction type — see table below
statusTransaction status — see table below
created_atWhen the transaction record was first created

Transaction Types

typeDescription
card_paymentCard swipe or online payment — cardholder spent funds
card_refundMerchant refunded the cardholder — funds returned to the card
card_depositFunds loaded onto a physical (Wasabi) card
card_withdrawFunds pulled off a physical (Wasabi) card back to the wallet

Transaction Statuses

statusDescription
requested / pendingAuthorization hold placed — funds frozen, settlement expected
paidTransaction created by the card network (Rain virtual cards)
completedTransaction settled — funds debited or credited
declinedAuthorization was declined — hold released, no funds moved
reversedAuthorization 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

HeaderValue
Content-Typeapplication/json
X-DPT-EventEvent type, e.g. checkout.completed
X-DPT-Delivery-IdDelivery UUID (same as top-level id in the body)
X-DPT-Signaturesha256={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:

AttemptDelay after previous failure
1Immediate
25 minutes
330 minutes
42 hours
55 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

DPT Merchant API