Skip to main content
Pre-release — contact your Fortary account team for access.
When something happens in your custody account — a transaction is confirmed, a deposit address is created — Fortary sends a signed HTTP POST to an HTTPS endpoint you control.

The delivery contract

  • At-least-once. The same event can be delivered more than once — deduplicate on the event id.
  • Unordered. A retried earlier event can arrive after a later one — order by the event’s date, never by arrival time.
  • Signed. Always verify the signature before trusting a payload.

Setting up a webhook

Webhooks can be managed in the Fortary web portal or through the API. When creating one you choose:
  • Entities — the entity or entities (your organization’s operating units in Fortary) whose events the webhook receives.
  • Event types — an explicit list of the event kinds to deliver. There are no wildcards; new kinds are opt-in.
  • Vault filter (optional) — restrict vault-scoped events to specific vaults.
  • Notification email — where Fortary sends operational notices, for example an automatic disable.
The endpoint URL must be HTTPS. The signing secret (whsec_…) is returned exactly once at creation — store it in your secret manager immediately. A lost secret cannot be retrieved, only rotated. An entity can be covered by at most 20 active webhooks.

Authorization

Webhook reads require the webhook:read scope; mutations require webhook:manage. Because changing a webhook’s URL redirects custody data, management is restricted to owner-level credentials. A webhook outside the credential’s access returns 404, never 403 — see Authorization.

Operations

OperationPathDescription
Create webhookPOST /v1/webhooksCreate a webhook (returns the signing secret once)
List webhooksGET /v1/webhooksList webhooks across accessible entities
Get webhookGET /v1/webhooks/{webhookId}Retrieve a webhook with its health counters
Update webhookPATCH /v1/webhooks/{webhookId}Update URL, event kinds, vault filter, status, or description
Rotate webhook secretPOST /v1/webhooks/{webhookId}/rotate-secretNew secret, returned once; 24-hour grace window
Delete webhookDELETE /v1/webhooks/{webhookId}Delete a webhook (history retained)
Ping webhookPOST /v1/webhooks/{webhookId}/pingSigned test POST with the live result inline
List webhook deliveriesGET /v1/webhooks/{webhookId}/deliveriesDelivery history with per-attempt detail
List webhook eventsGET /v1/webhook-eventsThe event log (31-day retention)
Get webhook eventGET /v1/webhook-events/{eventId}Event detail including how many webhooks matched
List event deliveriesGET /v1/webhook-events/{eventId}/deliveriesAll deliveries of one event
RedeliverPOST /v1/webhook-deliveries/{deliveryId}/redeliverRe-send a delivery’s event

Your first handler

Capture the raw body, verify the signature, acknowledge immediately, process asynchronously:
import { createServer } from "node:http";
import { verifyWebhookSignature } from "./verify-webhook-signature.js"; // from Verifying signatures

const secret = process.env.FORTARY_WEBHOOK_SECRET ?? ""; // whsec_…

createServer((req, res) => {
  const chunks: Buffer[] = [];
  req.on("data", (chunk) => chunks.push(chunk));
  req.on("end", () => {
    const rawBody = Buffer.concat(chunks).toString("utf8");
    const signature = req.headers["x-fortary-signature"];

    if (typeof signature !== "string" || !verifyWebhookSignature({ secret, signature, rawBody })) {
      res.writeHead(400).end();
      return;
    }

    res.writeHead(200).end(); // acknowledge before doing real work

    const event = JSON.parse(rawBody);
    // hand `event` to your queue / worker here — deduplicate on event.id
  });
}).listen(8080);
Respond with a 2xx within 10 seconds.

Guides