Inbound webhooks
Inbound webhooks are the opposite direction from webhooks: an external
system pushes events into your workspace. Create an endpoint at Developer → Inbound Webhooks
(/settings/inbound) and you get a unique URL of the form:
https://<api-origin>/api/inbound/<publicId>POST a JSON body (up to 1 MiB) to that URL. Each verified delivery is recorded in the endpoint's
delivery log and fires the inbound_webhook.received workflow event — so a workflow rule can react
to it, filter on the endpoint's declared payload fields, and template their values into actions.
Duplicate deliveries (same event id, or the same body when no event id is configured) are
acknowledged but recorded only once.
Verification schemes
Every endpoint declares how requests prove themselves:
| Scheme | How a request is verified |
| --- | --- |
| fiskwise_hmac | X-Webhook-Signature: t=<unix>,v1=<hex> — an HMAC-SHA256 over the timestamp and raw body (construction below). |
| stripe_hmac | The same t=,v1= construction, read from the Stripe-Signature header — point a Stripe webhook at the URL directly. |
| github_sha256 | X-Hub-Signature-256: sha256=<hex> — an HMAC-SHA256 over the raw body (GitHub's format). |
| basic | HTTP Basic auth — any username; the password is the endpoint secret. |
| none | No verification. Anyone who learns the URL can post events — prefer a signed scheme whenever the sender supports one. |
The endpoint secret is generated at creation and shown once. Rotating issues a new secret; the previous one keeps verifying for 24 hours so the sender can switch over without dropped deliveries.
The fiskwise_hmac construction
This is exactly the signature our outbound webhooks use, so Fiskwise-to-Fiskwise integrations need no new crypto:
- Take the current Unix time in seconds as
t. - Compute
v1= HMAC-SHA256, keyed by the endpoint secret, over`${t}.${rawBody}`— the exact raw bytes you send, not a re-serialized copy. - Send the header
X-Webhook-Signature: t=<t>,v1=<v1>.
import { createHmac } from 'node:crypto';
function signatureHeader(rawBody, secret) {
const t = Math.floor(Date.now() / 1000);
const v1 = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return `t=${t},v1=${v1}`;
}Timestamps more than 5 minutes from server time are rejected (replay protection). The
github_sha256 and basic schemes carry no timestamp — there, duplicate suppression by event id
is the replay protection, so configure an event id source (or bind a provider) for those senders.
Responses
| Status | Meaning |
| --- | --- |
| 200 | Received — also returned for duplicates, so senders can retry safely. |
| 401 | The signature or credentials failed verification. |
| 404 | Unknown or disabled endpoint (uniform — endpoint existence is never disclosed). |
| 413 | The body exceeds the 1 MiB cap. |
| 429 | Rate limited — back off and honor Retry-After. |
With the none scheme the URL is the only secret. Use it only for senders that cannot sign
requests, and rotate the endpoint (delete + recreate) if the URL leaks.