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
- Log into the Developer Dashboard
- Navigate to the “Webhooks” section
- Click “Create Webhook”
- Enter your webhook URL (must be HTTPS in production)
- The webhook will automatically subscribe to
escrow.status.updatedevents
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 (delivered → paid)
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.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
escrow.updated
Sent when the escrow’s title, amount, estimated delivery days, or contract terms 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.
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 indepositProof.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 toescrow.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 fromdelivered 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.
deliveryRejection.reason is omitted from the payload.
Quick Escrow with Proof events
Subscribe to these when you create escrows withfulfillmentPolicy.type: "purchase_proof_required". See the Quick Escrow with Proof guide.
| Event | When |
|---|---|
escrow.proof.required | Buyer funded; seller must upload proof |
escrow.proof.submitted | Seller uploaded; buyer should review |
escrow.proof.correction_requested | Buyer requested better proof |
escrow.proof.accepted | Buyer accepted; delivery unlocked |
escrow.proof.accepted_by_timeout | Review deadline passed; delivery unlocked |
escrow.proof.needs_review | Escalated to DHMAD after max correction rounds |
escrow.proof.expired | No proof in time; escrow cancelled and buyer refunded |
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.
Identity verification events
Subscribe to these when you use pre-account KYC.identity.verification.updated
Sent whenkyc_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: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).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
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
- Check Webhook Status: Ensure the webhook is active in your dashboard
- Verify URL: Make sure the URL is correct and accessible
- Check HTTPS: In production, ensure your endpoint uses HTTPS
- Review Logs: Check your server logs for incoming requests
- Test Endpoint: Manually POST to your endpoint to verify it works
Invalid Signature Errors
- Verify Secret: Ensure you’re using the correct webhook secret
- Check Payload Format: Make sure you’re stringifying the payload correctly
- Header Name: Verify you’re reading
X-Webhook-Signature(case-sensitive)
High Failure Count
- Response Time: Ensure your endpoint responds within 10 seconds
- Status Code: Return 200 OK for successful processing
- Error Handling: Don’t throw errors that cause 500 responses
- 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.