Routing & events
Webhooks
Settla sends an HTTPS POST to your endpoint whenever a subscribed event happens — an alias is verified, an endpoint is added, consent is granted. Every delivery is signed; verify the signature before trusting the body.
Create a webhook
/v1/webhooksRegister an HTTPS URL and the events you care about. Scope: webhooks:write. The signing secret (whsec_…) is returned once and stored encrypted; keep it safe.
curl https://api.settla.network/v1/webhooks \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/settla/webhooks",
"enabled_events": ["alias.verified", "endpoint.added", "consent.granted"],
"description": "Production receiver"
}'
# signing secret (whsec_...) is returned exactly ONCE
# => { "id": "whk_...", "signing_secret": "whsec_3a...e1", ... }Manage webhooks:
/v1/webhooks/v1/webhooks/:id/v1/webhooks/:id/v1/webhooks/:idEvents
Event payloads carry only a redacted public view — public IDs and types, never secrets, raw addresses, or PII. livemode distinguishes test from live.
Delivery format
POST /settla/webhooks HTTP/1.1
Settla-Signature: t=1727654400,v1=8c4f...e2
Content-Type: application/json
{
"id": "evt_9k2...",
"type": "alias.verified",
"created": 1727654400,
"livemode": false,
"data": {
"identity": "idn_2x9...",
"alias": { "public_id": "als_...", "type": "username" }
}
}Verifying the signature
Each delivery carries a Settla-Signature header:
Settla-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "t.<rawBody>")>To verify: split the header into the timestamp t and signature v1, recompute the HMAC-SHA256 of "<t>.<rawBody>" using your signing secret, and compare in constant time. Use the raw request body bytes — not a re-serialized object — or the hash will not match.
import crypto from "node:crypto";
// Settla-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "t.<rawBody>")>
export function verifySettlaSignature(
rawBody: string,
header: string,
secret: string,
toleranceSec = 300,
): boolean {
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=") as [string, string]),
);
const t = Number(parts.t);
const sig = parts.v1;
if (!t || !sig) return false;
// Reject stale timestamps (replay protection).
if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
// Constant-time compare.
return (
expected.length === sig.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))
);
}Heads up
timingSafeEqual / compare_digest). Respond 2xx quickly; failed deliveries are retried with exponential backoff.Note