SSettlaDocs

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

POST/v1/webhooks

Register 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:

GET/v1/webhooks
GET/v1/webhooks/:id
PATCH/v1/webhooks/:id
DELETE/v1/webhooks/:id

Events

Event payloads carry only a redacted public view — public IDs and types, never secrets, raw addresses, or PII. livemode distinguishes test from live.

EventFired when
identity.createdA new identity is registered.
alias.registeredAn alias is attached to an identity.
alias.verifiedAn alias passes proof-of-control.
alias.releasedAn alias is released (cooldown set).
endpoint.addedAn endpoint is registered.
endpoint.verifiedAn endpoint passes verification.
endpoint.removedAn endpoint is revoked.
consent.grantedA consent grant becomes active.
consent.revokedA consent grant is revoked.

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

Reject deliveries whose timestamp is outside a tolerance window (e.g. 5 minutes) to defend against replay, and always compare signatures with a constant-time function (timingSafeEqual / compare_digest). Respond 2xx quickly; failed deliveries are retried with exponential backoff.

Note

All event payloads use public IDs only and contain no money amounts as balances — consistent with the no-money-movement posture.