Webhooks
Get notified when payments, identities, accounts and tickets change, and verify that each notification comes from Depa.
Depa sends webhooks to your servers when something changes: money arrives or leaves, an identity
is verified, an account is blocked, a support ticket gets a reply. Each webhook is an HTTPS POST
with a JSON body, signed with a secret only you and Depa know.
1. How Webhooks Are Organised
Webhooks are grouped into topics, and you register the URLs for each topic separately. A topic receives every event of its kind; see the event catalogue for the full list and their payloads.
| Topic | What it covers |
|---|---|
account | Accounts created, blocked and unblocked; bank account updates; trades; completed payouts. |
identification | KYC and KYB verification results. |
fiat_payment | Bank transfers in and out, settlements and refunds. Also card events when card_payment has no URLs. |
card_payment | Card payments and card payment links. Optional. |
blockchain_payment | On-chain deposits and payouts. |
blockchain_wallet | Whitelisted payout wallets created. |
open_banking_transaction_completed | Open banking payments completed. |
daily_settlement | Daily card settlement totals. |
ticket | Support tickets created, updated or answered. |
transaction_monitoring | Compliance cases and flags that are shared with you, and services blocked by them. |
manual_review | Manual review of payouts switched on. |
Webhook settings belong to your vault, so they cover every user and account in it.
2. Registering Your Endpoints
Set the URLs for each topic with PUT /users/update_webhook. Each topic
takes a list, so you can send the same webhook to more than one endpoint:
curl -X PUT https://api.sandbox.depa.finance/v1/users/update_webhook \
-H "Authorization: <your_token>" \
-H "Content-Type: application/json" \
-d '{
"webhook_url": {
"fiat_payment": ["https://api.yourcompany.com/depa/webhooks"],
"blockchain_payment": ["https://api.yourcompany.com/depa/webhooks"],
"identification": ["https://api.yourcompany.com/depa/webhooks"],
"account": ["https://api.yourcompany.com/depa/webhooks"]
}
}'Only the topics you send change; the others keep their URLs. Send an empty list to stop a topic.
Then fetch your settings to confirm the URLs and get your webhook secret, which you need to verify signatures:
curl https://api.sandbox.depa.finance/v1/users/settings \
-H "Authorization: <your_token>"{
"data": {
"webhook_url_list": {
"fiat_payment": ["https://api.yourcompany.com/depa/webhooks"],
"identification": ["https://api.yourcompany.com/depa/webhooks"]
},
"webhook_secret": "9kQ2vXbT7hR4mW8nZ1cF6yJ3pL5sD0aG2eU7iO4tB9xK1qV6wN8rM3zH5jC0fY2u"
}
}Keep the secret on your server
Anyone with the webhook secret can forge webhooks that pass verification. Store it like a password and never ship it to a browser or mobile app. Sandbox and production have different secrets.
3. What Depa Sends
Each webhook is a POST to every URL registered for its topic:
POST /depa/webhooks HTTP/1.1
Content-Type: application/json
Depasify-Signature: t=1772813114,v1=6d4e28e469c5e3d740c83a54b3879fcfd2b8344697924d5e89d81d26ad5df19f
{"data":{"event":"fiat_payment_received","attributes":{"fiat_payment_uuid":"7e2a9c4b-5d1f-4b8e-9a3c-6f0e2d8b1a74", …}}}The body wraps the event in data. A few failure events use error instead, with a message
explaining what went wrong:
{ "data": { "event": "fiat_payment_received", "attributes": { … } } }
{ "error": { "event": "fiat_payment_failed", "attributes": { …, "message": "…" } } }Read the event name from whichever key is present, for example
(body.data ?? body.error).event in JavaScript, and branch on it.
4. Verifying the Signature
Every webhook carries a Depasify-Signature header with two parts:
Depasify-Signature: t=1772813114,v1=6d4e28e469c5e3d740c83a54b3879fcfd2b8344697924d5e89d81d26ad5df19ft: when the request was signed, in Unix seconds.v1: the HMAC-SHA256 hex digest of"{t}.{raw request body}", keyed with your webhook secret.
To verify a webhook:
- Read the raw request body, before any JSON parsing. Re-serialising the JSON changes the bytes and breaks the signature.
- Compute HMAC-SHA256 over
t + "." + raw_bodywith your secret, as lowercase hex. - Compare it to
v1in constant time. - Reject requests whose
tis more than five minutes from your clock, to stop replays. Each delivery attempt is signed afresh, so retries pass this check.
Node.js / TypeScript
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 300;
export function verifyDepaSignature(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(
header.split(",").map((part) => {
const [key, ...rest] = part.split("=");
return [key.trim(), rest.join("=").trim()];
}),
);
const timestamp = Number(parts.t);
const received = parts.v1;
if (!timestamp || !received) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;
const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const a = Buffer.from(received, "hex");
const b = Buffer.from(expected, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}With Express, keep the raw body for this route:
app.post("/depa/webhooks", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body.toString("utf8");
if (!verifyDepaSignature(rawBody, req.get("Depasify-Signature") ?? "", process.env.DEPA_WEBHOOK_SECRET!)) {
return res.sendStatus(400);
}
res.sendStatus(200); // acknowledge first, then process
const body = JSON.parse(rawBody);
queue.add((body.data ?? body.error).event, body);
});Python
import hashlib
import hmac
import time
TOLERANCE_SECONDS = 300
def verify_depa_signature(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(part.strip().split("=", 1) for part in header.split(",") if "=" in part)
timestamp, received = parts.get("t"), parts.get("v1")
if not timestamp or not received:
return False
if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
return False
expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(received, expected)Ruby
require "openssl"
TOLERANCE_SECONDS = 300
def verify_depa_signature(raw_body, header, secret)
parts = header.split(",").to_h { |part| part.strip.split("=", 2) }
timestamp, received = parts["t"], parts["v1"]
return false unless timestamp && received
return false if (Time.now.to_i - timestamp.to_i).abs > TOLERANCE_SECONDS
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{raw_body}")
Rack::Utils.secure_compare(received, expected)
end5. Responding and Retries
- Respond with
200or201. Depa treats any other status, including other2xxcodes such as202or204, as a failed delivery. - Respond quickly. Verify the signature, store the event, return
200, and do the work in the background. - Failed deliveries are retried. If none of the topic's URLs accept a webhook, Depa tries again up to three more times, ten seconds apart. After that it stops; fix your endpoint and re-send it from the delivery log (below).
- Partial deliveries aren't retried. When a topic has several URLs and at least one accepts
the webhook, the delivery is marked
partialand the URLs that failed aren't retried.
Handling duplicates and ordering
Because of retries, and because you can re-send deliveries yourself, the same event can reach you
more than once, and events about the same payment can arrive out of order. Make your handler
idempotent: record which combination of event name, resource ID (for example
fiat_payment_uuid) and status you've already processed, and skip repeats. When the order
matters, fetch the resource from the API and act on its current state.
6. Delivery Logs
Every webhook Depa sends is logged with its payload, status, attempts and the last error your
server returned. List them with GET /webhook_logs, filtering by topic and
status, and search the payloads, for example by payment ID:
curl -G https://api.sandbox.depa.finance/v1/webhook_logs \
-H "Authorization: <your_token>" \
--data-urlencode "type=fiat_payment" \
--data-urlencode "status=failed"Send type and status together; requests without both currently return no results.
To send a delivery again, to the URLs currently configured for its topic:
curl -X POST https://api.sandbox.depa.finance/v1/webhook_logs/5d2c8e7a-1f4b-4a9e-b3c6-8e0d7f1a2b94/retry \
-H "Authorization: <your_token>"Delivery statuses are pending, processing, completed (every URL accepted it), partial
(some did) and failed (none did, after all retries).
7. Testing in the Sandbox
Point your topics at a public URL, such as a tunnel to your machine or a request inspector, then use the Sandbox endpoints to simulate money arriving. They create real sandbox payments and send the same webhooks as production:
POST /sandbox/accounts/{account_id}/simulate_incoming_fiatsendsfiat_payment_received.POST /sandbox/accounts/{account_id}/simulate_incoming_cryptosendsblockchain_payment_received.
Use the delivery logs to check what was sent and what your endpoint answered.