Skip to main content
Webhooks allow you to receive real-time notifications when events occur in your escrow transactions or identity verification sessions. This guide explains how to set up and use webhooks.

Overview

Webhooks are HTTP callbacks that notify your application when escrow, contract, or identity verification events occur. Instead of polling the API, you can receive instant notifications when:
  • Escrow status changes (pending → paid → delivered → completed → cancelled)
  • Escrows are cancelled
  • A contract is signed by the seller or buyer
  • Pre-account KYC status changes or links to a DHMAD user account
  • Any status update occurs

Setting Up Webhooks

Step 1: Create Webhook Endpoint

Create an HTTP endpoint in your application that can receive POST requests:

Step 2: Configure Webhook in Dashboard

  1. Log into the Developer Dashboard
  2. Navigate to the “Webhooks” section
  3. Click “Create Webhook”
  4. Enter your webhook URL (must be HTTPS in production)
  5. The webhook will automatically subscribe to escrow.status.updated events
You can create a maximum of 2 webhooks per developer account. Choose your endpoints carefully.
In development mode, HTTP URLs are allowed. In production, only HTTPS URLs are accepted.

Webhook Events

You can subscribe to the following events:

escrow.status.updated

Triggered when an escrow status changes (pending → paid → delivered → completed → cancelled)

contract.signed

Triggered when the escrow contract is signed by the seller or the buyer. Each signature triggers one event; data.contract.signedBy indicates who signed (“seller” or “buyer”).

escrow.cancellation.requested

Triggered when the buyer requests cancellation of a paid or delivered escrow. The escrow status does not change yet. Sellers cancel directly on dhmad.tn without this event.

escrow.cancellation.rejected

Triggered when the other party rejects a pending cancellation request. The escrow status remains unchanged.

escrow.cancellation.accepted

Triggered when the other party accepts a cancellation request. The escrow is refunded and its status changes to cancelled. An escrow.status.updated event is also fired.

escrow.updated

Triggered when escrow details are updated: title, amount, estimated delivery time, or contract terms (via the DHMAD dashboard or API). Use this to keep your app in sync when users edit these fields in DHMAD instead of in your app.

escrow.deposit_proof.rejected

Triggered when a guest instant-escrow payment proof is rejected: either an admin rejected it, or DHMAD auto-rejected it because the escrow was cancelled while the proof was still pending. Check data.depositProof.reason and data.escrow.status: if the escrow is still pending, the buyer can usually submit a new proof from the same checkout link; if the escrow is cancelled, treat the payment attempt as closed. Subscribe if you use guest checkout for instant escrows.

escrow.delivery.rejected

Triggered when the buyer rejects the seller’s delivery. The escrow status changes from delivered back to paid so the seller can fix issues and deliver again. Includes data.deliveryRejection.reason when the buyer provided feedback. An escrow.status.updated event (deliveredpaid) is also fired.

escrow.proof.*

Triggered during the Quick Escrow with Proof lifecycle (escrow.proof.required, .submitted, .correction_requested, .accepted, .accepted_by_timeout, .needs_review, .expired). Payload includes proof with phase, deadlines, and time-limited viewUrl on attachments.

escrow.funds.added

Triggered when the buyer adds funds to a paid or delivered escrow (checkout add_funds or wallet API). Includes data.adjustment with the added amount and fee deltas. Escrow amount / amountTnd in the payload reflect the new totals.

escrow.funds.refunded

Triggered when the seller refunds funds to the buyer (checkout refund_funds or wallet API). Includes data.adjustment. When the full remaining balance is refunded, adjustment.cancelled is true and an escrow.status.updated event to cancelled is also fired.

escrow.release_request.created

Progressive release: a payout request was created for one billing period and the amount reserved. The client has been notified and has until approvalDeadline to respond.

escrow.release_request.approved

The client approved a payout request. escrow.release_request.released follows once the ledger settles.

escrow.release_request.auto_approved

The client’s review window elapsed with no response, so the period was approved automatically.

escrow.release_request.disputed

The client rejected a payout request. A dispute scoped to that period is open; the amount stays reserved and later periods keep running.

escrow.release_request.resolved

DHMAD resolved a disputed period. releaseRequest.awardedTnd is what went to the seller; any remainder returned to the escrow balance.

escrow.release_request.released

Funds for this period reached the seller’s DHMAD balance. The escrow stays open with a reduced balance.

escrow.balance.exhausted

A progressive escrow has no unreserved balance left. Pause tracking and prompt the client to top up with add_funds — the escrow is not closed.

identity.verification.updated

Triggered when a pre-account identity verification kyc_status becomes approved or rejected (Didit result, admin review, or sandbox auto-approve). Not sent while Didit returns an ambiguous pending result awaiting admin review.

identity.verification.linked

Triggered when the user registers on DHMAD with the same email and approved KYC is attached to their account. Includes linked_user_id.

Event Payload

All webhook events share the same top-level structure: id, type, timestamp, and data. The data object depends on the event type.

escrow.status.updated

On progressive-release escrows, data.escrow also includes the full face + TND progressive snapshot. On terminal completed / cancelled, float fields are emptied so partners do not need to hard-settle locally:
Live amount / amountTnd may still show leftover on the document after progressive close; trust remainingAmount* = 0 and refundedTotal* for the unused return.

Charges

A charge reports through the same event, with two extra fields on data.escrow:
A paid charge fires paid and then completed. Credit your user from merchantReference, which is the reference you sent at creation, and treat the operation as idempotent — both events may arrive, and a delivery may be retried. receiptEmail is whoever paid; it is for receipts only and is often not the person being credited.

escrow.updated

Sent when the escrow’s title, amount, feeBearer, contract terms, or other pending fields are updated (via the dashboard or API). The payload includes which fields changed and the current escrow snapshot; when contract terms were updated, data.contract is also present.
When contract terms are updated, data also includes a contract object with id, terms, and optional language. The escrow object has the same shape as in the example above.

escrow.deposit_proof.rejected

Sent when a guest instant-escrow payment proof is rejected—by an admin (optional reason in depositProof.reason) or automatically when the escrow is cancelled while a proof was still pending (reason explains that the escrow was cancelled before review). Always inspect data.escrow.status: only when the escrow remains pending should you expect the buyer to upload another proof from the same checkout session.

contract.signed

Sent when the seller or the buyer signs the contract (one event per signature).

escrow.cancellation.requested

Sent when the buyer requests cancellation. The same payload structure applies to escrow.cancellation.rejected and escrow.cancellation.accepted (with the corresponding type and cancellationRequest.status).

escrow.delivery.rejected

Sent when the buyer rejects delivery. The escrow moves from delivered back to paid. Subscribe to this event to read the buyer’s optional feedback in data.deliveryRejection.reason. An escrow.status.updated event is also emitted.
If the buyer did not provide a reason, deliveryRejection.reason is omitted from the payload.

Quick Escrow with Proof events

Subscribe to these when you create escrows with fulfillmentPolicy.type: "purchase_proof_required". See the Quick Escrow with Proof guide. Proof payloads include a proof object with phase, deadlines, submissions (with time-limited viewUrl per attachment), latestSubmission, and checkoutActions for the next hosted step. You can also poll GET /api/v1/escrows/:id/proof.

escrow.funds.added / escrow.funds.refunded

Sent when the buyer adds funds or the seller refunds funds on a paid/delivered escrow (hosted checkout add_funds / refund_funds, or the equivalent wallet APIs). The escrow object reflects the updated commercial and ledger amounts.
For escrow.funds.refunded, adjustment.type is "refund_funds" and performedByRole is "seller". When a full refund cancels the escrow, adjustment.cancelled is true and an escrow.status.updated event (→ cancelled) is also emitted.

Progressive release events

Sent on progressive-release escrows, which pay out one slice per billing period. Every payload carries data.releaseRequest (with amountTnd, amountFace, currency, and fxRateUsed) plus the escrow’s live face + TND reservation snapshot, so you can reconcile without inventing FX or a follow-up GET.

escrow.release_request.created

escrow.release_request.released

decisionSource: "timeout" means the client never responded and the period auto-released. Surface this distinctly in your UI — it is not the same as an explicit approval.

Identity verification events

Subscribe to these when you use pre-account KYC.

identity.verification.updated

Sent when kyc_status becomes approved or rejected.

identity.verification.linked

Sent when the user creates a DHMAD account and KYC is attached.

Payload Fields

Webhook Headers

Each webhook request includes the following headers:
To protect against replay attacks, always validate X-Webhook-Timestamp — reject any delivery where the timestamp is more than 5 minutes in the past. Track X-Webhook-Id values to discard duplicate deliveries.

Verifying Webhook Signatures

Always verify webhook signatures to ensure requests are from DHMAD and haven’t been tampered with.

Getting Your Webhook Secret

The webhook secret is generated automatically when you create a webhook. The secret is only shown once at creation and again if you regenerate it. It is never returned by the list or get endpoints. Copy and store the secret immediately when it is displayed. If you lose it, use the “Regenerate Secret” action in your dashboard to get a new one (the old secret will stop working).
Store your webhook secret securely. Never commit it to version control or expose it publicly.

Signature Verification Examples

Best Practices

Verify Signatures

Always verify webhook signatures before processing. This ensures the request is from DHMAD and hasn’t been tampered with.

Respond Quickly

Return 200 OK within 5 seconds. Process the webhook asynchronously if needed.

Idempotency

Handle duplicate events gracefully. Use the event id to track processed events.

Error Handling

Log errors and implement retry logic for failed processing. Don’t fail the webhook response.

HTTPS Only

Use HTTPS endpoints in production. HTTP is only allowed in development.

Monitor Failures

Track webhook delivery failures in your dashboard and set up alerts.

Webhook Delivery

Retry Logic

If your endpoint doesn’t return a 200 OK status within 10 seconds, DHMAD will retry the webhook:
  • Retries: Up to 3 attempts
  • Backoff: Exponential backoff (1s, 2s, 4s)
  • Client Errors (4xx): Not retried (fix your endpoint)
  • Server Errors (5xx): Retried

Delivery Status

You can monitor webhook delivery status in your dashboard:
  • Last Triggered: Timestamp of last webhook delivery
  • Last Response Status: HTTP status code from your endpoint
  • Failure Count: Number of consecutive failures
If a webhook fails repeatedly, consider checking your endpoint and fixing any issues. High failure counts may indicate problems with your webhook handler.

Example: Complete Webhook Handler

Here’s a complete example of a webhook handler with signature verification and async processing:

Testing Webhooks

Using ngrok (Local Development)

To test webhooks locally, use ngrok to expose your local server:

Testing Checklist

  • Webhook endpoint returns 200 OK
  • Signature verification works correctly
  • Event processing handles all status transitions
  • Duplicate events are handled idempotently
  • Errors are logged but don’t fail the response
  • Webhook works in production (HTTPS)

Troubleshooting

Webhook Not Receiving Events

  1. Check Webhook Status: Ensure the webhook is active in your dashboard
  2. Verify URL: Make sure the URL is correct and accessible
  3. Check HTTPS: In production, ensure your endpoint uses HTTPS
  4. Review Logs: Check your server logs for incoming requests
  5. Test Endpoint: Manually POST to your endpoint to verify it works

Invalid Signature Errors

  1. Verify Secret: Ensure you’re using the correct webhook secret
  2. Check Payload Format: Make sure you’re stringifying the payload correctly
  3. Header Name: Verify you’re reading X-Webhook-Signature (case-sensitive)

High Failure Count

  1. Response Time: Ensure your endpoint responds within 10 seconds
  2. Status Code: Return 200 OK for successful processing
  3. Error Handling: Don’t throw errors that cause 500 responses
  4. Network Issues: Check for firewall or network problems

Webhook Management

Viewing Webhooks

In your dashboard, you can:
  • View all your webhooks
  • See delivery status and failure counts
  • Check last triggered timestamp
  • View last response status

Updating Webhooks

You can update webhook URLs and toggle active/inactive status from the dashboard.

Deleting Webhooks

Delete webhooks you no longer need. Remember, you can only have 2 webhooks maximum.
Webhooks are delivered for escrows where your associated user account is the seller or buyer, or for escrows created through your developer account via the API (tracked by createdByDeveloper). This means if you create escrows with a sellerEmail, you’ll still receive webhook notifications for those escrows.
Never expose your webhook secret. Treat it like a password and store it securely using environment variables or secret management services.