Webhooks
Deplao POSTs events to your URL as soon as they happen. You don't need to keep a connection open; Deplao retries automatically when your server fails.
Register
bash
curl -X POST http://127.0.0.1:9900/api/v1/webhooks -H "Authorization: Bearer dpk_xxx" -H "Content-Type: application/json" -d '{"name":"CRM sync","url":"https://crm.example.com/deplao","events":["message.received","payment.received"]}'The response contains a secret of the form whsec_..., shown only once. Save it to verify signatures. Send a test: POST /api/v1/webhooks/{id}/test.
What Deplao sends
http
POST /deplao HTTP/1.1
Host: crm.example.com
Content-Type: application/json
User-Agent: Deplao-Webhook/1
X-Deplao-Event: message.received
X-Deplao-Delivery: dlv_3c9e5a0f6d2b4e7a9c1d8f20
X-Deplao-Timestamp: 1760412345
X-Deplao-Signature: v1=5d0c7f...e1
{"id":"evt_...","seq":1532,"type":"message.received","ts":1760412345678,"account_id":"0501234567","channel":"zalo","data":{...}}- Success means an HTTP 2xx within 10 seconds. Return 200 right away and process later (push the event onto a queue).
- Redirects (3xx) are not followed and count as failures. Register the final URL.
- Retries happen after 30 seconds, 2 minutes, 10 minutes, 1 hour and 6 hours (6 attempts in total); after that the delivery becomes
failed. Manual redelivery:POST /api/v1/webhook-deliveries/{id}/retry. - After 100 consecutive failures the webhook turns itself off. Re-enable it with
PATCH /api/v1/webhooks/{id}and{"active": true}. - With retries you may get duplicates and out-of-order deliveries. Deduplicate by the
X-Deplao-Deliveryheader orid, and order byseq. - A webhook uses the permissions of the key that created it: it only receives events for the accounts that key has been granted, and deliveries stop if the key is revoked.
- Private URLs (
localhost,192.168.x,10.x) are only allowed when the webhook is created from the machine running Deplao itself (not through a tunnel), or whenapi_allow_private_urlsis enabled.
Verifying the signature
The signature is v1= + hex(HMAC-SHA256(secret, timestamp + "." + raw body)). Always use the raw body (before JSON parsing), compare in constant time, and reject timestamps that are more than 5 minutes off.
Node.js (Express):
js
import express from 'express';
import crypto from 'node:crypto';
const SECRET = process.env.DEPLAO_WEBHOOK_SECRET;
const app = express();
app.post('/deplao', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.get('X-Deplao-Timestamp') || '0';
const sig = req.get('X-Deplao-Signature') || '';
const expected = 'v1=' + crypto.createHmac('sha256', SECRET).update(ts + '.' + req.body.toString('utf8')).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
const valid = sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!fresh || !valid) return res.status(401).end();
res.status(200).end(); // respond right away
const event = JSON.parse(req.body.toString('utf8'));
console.log(event.type, event.seq); // process later, e.g. push it onto a queue
});
app.listen(3000);Python (Flask):
python
import hashlib, hmac, os, time
from flask import Flask, abort, request
SECRET = os.environ["DEPLAO_WEBHOOK_SECRET"].encode()
app = Flask(__name__)
@app.post("/deplao")
def deplao():
ts = request.headers.get("X-Deplao-Timestamp", "0")
sig = request.headers.get("X-Deplao-Signature", "")
body = request.get_data() # raw bytes
expected = "v1=" + hmac.new(SECRET, ts.encode() + b"." + body, hashlib.sha256).hexdigest()
if abs(time.time() - int(ts)) > 300 or not hmac.compare_digest(sig, expected):
abort(401)
event = request.get_json()
print(event["type"], event["seq"])
return "", 200PHP:
php
<?php
$secret = getenv('DEPLAO_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_DEPLAO_TIMESTAMP'] ?? '0';
$sig = $_SERVER['HTTP_X_DEPLAO_SIGNATURE'] ?? '';
$expected = 'v1=' . hash_hmac('sha256', $ts . '.' . $body, $secret);
if (abs(time() - (int)$ts) > 300 || !hash_equals($expected, $sig)) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
http_response_code(200);
// handle $event['type'], $event['data']Cloudflare Worker (Web Crypto):
js
export default {
async fetch(request, env) {
const body = await request.text();
const ts = request.headers.get('X-Deplao-Timestamp') || '0';
const hex = (request.headers.get('X-Deplao-Signature') || '').replace('v1=', '');
const enc = new TextEncoder();
const key = await crypto.subtle.importKey('raw', enc.encode(env.DEPLAO_WEBHOOK_SECRET), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
const sig = new Uint8Array((hex.match(/../g) || []).map((h) => parseInt(h, 16)));
const ok = await crypto.subtle.verify('HMAC', key, sig, enc.encode(ts + '.' + body));
if (!ok || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return new Response('bad signature', { status: 401 });
const event = JSON.parse(body);
// write to a Queue / KV / D1, then process later
return new Response('ok');
},
};Didn't find your answer?
Message us on Telegram