DOCUMENTATION

Webhooks

Signed deliveries of an organization's governance events.

Events offered in the app

EventLabel
policy_exceptionPolicy exception requested
exception_decidedException approved or rejected
exception_evidence_requestedMore evidence requested on an exception
exception_evidence_addedEvidence added to an exception
policy_activatedPolicy version activated
investigation_createdInvestigation opened
investigation_resolvedInvestigation closed
receipt_createdAPI verification receipt recorded

An organization's owners and admins can register HTTPS endpoints that receive the organization's governance events as they are recorded by the server. Manage them in the app: Ledger & Policy → POLICY (organization mode) → WEBHOOKS.

Events

EventSent when the server records…
policy_exceptionan exception request on a verdict
exception_decidedan exception approved or rejected by an authorized second person
exception_evidence_requesteda reviewer asking for more evidence before deciding an exception
exception_evidence_addedthe requester adding that evidence, which returns the exception to pending
policy_activateda policy version activated (four-eyes)
investigation_createda shared investigation opened
investigation_resolveda shared investigation closed
receipt_createdan API verification recorded against the organization
pinga test delivery requested from the app

Events come only from server records (the append-only audit log and API verifications). XRPL-side changes a workstation detects — issuer flags, credential expiry, liquidity — are not server records and are not sent.

Request

POST with Content-Type: application/json and these headers:

HeaderValue
X-Noshashi-Eventthe event name
X-Noshashi-Deliverythe delivery id (unique; use it to de-duplicate retries)
X-Noshashi-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
User-AgentNOSHASHI-Webhooks/1

Body:

{
  "id": "delivery uuid",
  "event": "exception_decided",
  "created_at": "2026-09-24T01:00:00.000000+00:00",
  "organization_id": "uuid",
  "data": {
    "action": "exception.approved",
    "entity_type": "policy_exception",
    "entity_id": "uuid",
    "actor_account_id": "uuid",
    "occurred_at": "…",
    "state": { "status": "approved", "requested_by": "…", "decided_by": "…", "receipt_digest": "…", "policy_id": "…", "policy_version": 6, "policy_hash": "…" },
    "audit_id": 123
  }
}

Verifying the signature

The signature is HMAC-SHA256, keyed with the webhook's secret (whsec_…, shown once when the webhook is created), over the string "<t>.<raw request body>". Verify against the raw bytes you received — never a re-serialised object — and reject timestamps more than five minutes old.

// Node 18+ / Deno / Workers
async function verify(secret: string, header: string, rawBody: string) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
  const t = Number(parts.t);
  if (Math.abs(Date.now() / 1000 - t) > 300) return false;
  const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret),
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
  const mac = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${t}.${rawBody}`));
  const hex = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, "0")).join("");
  return timingSafeEqual(hex, parts.v1);
}

The same function, tested against the server's signing, is in src/lib/org/webhookSignature.ts.

Delivery and retries

Deliveries leave the database after the transaction that recorded the event commits; nothing is sent for an action that was rolled back. A 2xx response marks the delivery delivered. Anything else, a timeout (10 s) or no response is recorded as failed and retried, up to three attempts in all: the second about a minute after the first fails, the third about four minutes after the second. Each attempt carries a fresh timestamp and signature and the same delivery id. Delivery history is visible to owners and admins in the app.

Addresses

Only https:// to a public DNS name is accepted. IP literals, localhost, .local, .internal, .arpa and Supabase hosts are refused.

Generated from src/lib/org/webhooks.ts · docs/api/WEBHOOKS.md