Depa LogoDocsv1
Webhooks

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.

TopicWhat it covers
accountAccounts created, blocked and unblocked; bank account updates; trades; completed payouts.
identificationKYC and KYB verification results.
fiat_paymentBank transfers in and out, settlements and refunds. Also card events when card_payment has no URLs.
card_paymentCard payments and card payment links. Optional.
blockchain_paymentOn-chain deposits and payouts.
blockchain_walletWhitelisted payout wallets created.
open_banking_transaction_completedOpen banking payments completed.
daily_settlementDaily card settlement totals.
ticketSupport tickets created, updated or answered.
transaction_monitoringCompliance cases and flags that are shared with you, and services blocked by them.
manual_reviewManual 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=6d4e28e469c5e3d740c83a54b3879fcfd2b8344697924d5e89d81d26ad5df19f
  • t: 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:

  1. Read the raw request body, before any JSON parsing. Re-serialising the JSON changes the bytes and breaks the signature.
  2. Compute HMAC-SHA256 over t + "." + raw_body with your secret, as lowercase hex.
  3. Compare it to v1 in constant time.
  4. Reject requests whose t is 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)
end

5. Responding and Retries

  • Respond with 200 or 201. Depa treats any other status, including other 2xx codes such as 202 or 204, 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 partial and 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_fiat sends fiat_payment_received.
  • POST /sandbox/accounts/{account_id}/simulate_incoming_crypto sends blockchain_payment_received.

Use the delivery logs to check what was sent and what your endpoint answered.

On this page

🍪 We do not track your behaviour or use any cookie on this site.