Using the APIWebhook events

Using the API

Webhook events

Signed webhooks for every change. Payload shape, delivery and retries, verifying the FIA-Signature header in Node.js, and the full event list.

Webhooks tell your systems the moment something changes in your account: an invoice is sent, a client pays, a credit note is created. Instead of polling the API, you give us an HTTPS URL and we POST a signed JSON event to it. This page is the reference for the payload, delivery, signatures and event types. To create and test endpoints, see Webhooks.

The payload

Every delivery is a POST with a JSON body in the same shape:

document.sent
{
  "id": "0193f3c0-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
  "type": "document.sent",
  "api_version": "v1",
  "created_at": "2027-01-04T10:15:00Z",
  "account_id": "0193f29d-4b5c-7d6e-8f90-1a2b3c4d5e6f",
  "business_id": "0193f29e-11aa-7c3b-8d2e-5a6b7c8d9e0f",
  "data": {
    "object": {
      "id": "0193f2b4-5a6b-7c8d-9e0f-1a2b3c4d5e6f",
      "type": "invoice",
      "number": "INV-2027-0001",
      "status": "sent",
      "total_minor": "125000",
      "currency": "USD"
    }
  }
}
idstring<uuid>required

The event id. It is the same on every retry of this event, so use it to ignore duplicates.

typestringrequired

The event type, such as document.sent. See the event list.

api_versionstringrequired

Always v1 for this API version. See Versioning.

created_atstring<date-time>required

When the change happened (RFC 3339, UTC).

account_idstring<uuid>required

The account the event belongs to.

business_idstring<uuid>

The business the event belongs to.

dataobjectrequired

Headers

HeaderValue
Content-Typeapplication/json
FIA-Signaturet=<unix seconds>,v1=<hex signature>
FIA-Event-IdThe event id, so you can dedupe before parsing the body
FIA-Event-TypeThe event type

The signature scheme and header names are proposed and may be finalised before launch.

Delivery

  • Each endpoint chooses its events. You only receive the types you subscribed to. An endpoint can also be limited to one business.
  • HTTPS only. Endpoint URLs must use https and may not point at private or internal network addresses. Redirects are not followed (proposed).
  • Acknowledge with any 2xx status, quickly. Return 200 as soon as you have stored the event, then do the work in the background. A delivery that times out counts as failed; the planned timeout is 10 seconds (proposed).
  • Retries. A failed delivery is retried with exponential backoff, about 8 attempts over roughly 24 hours (proposed). After the last attempt the delivery is marked dead and shown in the delivery log, where you can redeliver it by hand.
  • Same id on every retry. Store the ids you have processed and skip repeats.
  • Order is not guaranteed. A document.paid can arrive before the document.sent that preceded it. Use created_at, or fetch the current state from the API, rather than relying on arrival order.
  • Repeated failures. An endpoint that keeps failing can be disabled automatically (proposed: after 50 failed deliveries in a row). Re-enable it once it is fixed.

Delivery statuses in the log are pending, succeeded, failed and dead. You can see them in the app and with List webhook deliveries, and redeliver with Redeliver a webhook.

Verifying signatures

Every delivery is signed with your endpoint's signing secret, shown when you create the endpoint. Always verify the signature before trusting a delivery; anyone can send a POST to a public URL.

The signature is:

v1 = HMAC-SHA256(endpoint_secret, "<t>.<raw body>")

where t is the timestamp from the header and <raw body> is the exact bytes of the request body. To verify:

  1. Read FIA-Signature and split it into t and one or more v1 values.
  2. Reject the request if t is more than 5 minutes from your current time. This stops replays of old deliveries.
  3. Compute the HMAC-SHA256 of t, a full stop, and the raw body, using your secret, as lowercase hex.
  4. Compare it to each v1 value in constant time. Accept if any one matches.

Verify against the raw body. If your framework parses JSON first and you re-serialise it, the bytes change and the signature will not match.

Node.js

import crypto from "node:crypto";

const TOLERANCE_SECONDS = 5 * 60;

export function verifyFiaSignature(rawBody, header, secret) {
  if (!header) return false;
  const parts = header.split(",").map((part) => part.trim().split("="));
  const timestamp = Number(parts.find(([key]) => key === "t")?.[1]);
  const signatures = parts.filter(([key]) => key === "v1").map(([, value]) => value);
  if (!Number.isFinite(timestamp) || signatures.length === 0) return false;

  const age = Math.abs(Math.floor(Date.now() / 1000) - timestamp);
  if (age > TOLERANCE_SECONDS) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest("hex");
  const expectedBuffer = Buffer.from(expected, "hex");

  return signatures.some((signature) => {
    const given = Buffer.from(signature, "hex");
    return given.length === expectedBuffer.length && crypto.timingSafeEqual(given, expectedBuffer);
  });
}

Rotating the secret

When you rotate an endpoint's secret, we send both signatures for a grace period, as two v1 values: t=1767521700,v1=<old>,v1=<new>. The code above accepts a match on either, so you can deploy the new secret at your own pace. After the grace period, only the new signature is sent.

Event list

Event names are proposed and each maps to a state change in the product. Events for later releases are added to v1 when their feature ships.

Documents

EventWhenRelease
document.createdA document (invoice, estimate, credit note, receipt) is createdLaunch
document.updatedA document is changedLaunch
document.revisedA sent document gets a new revisionLaunch
document.sentA document is sent to the clientLaunch
document.viewedThe client opens the document (proposed)Launch
document.overdueAn invoice passes its due date unpaidLaunch
document.voidedA document is voidedLaunch
document.partially_paidA payment covers part of the balanceLaunch
document.paidThe balance reaches zeroLaunch
credit_note.createdA credit note is issued against an invoiceLaunch

Estimates

EventWhenRelease
estimate.acceptedThe client accepts an estimateLaunch
estimate.changes_requestedThe client asks for changesLaunch
estimate.convertedAn estimate is converted to an invoiceLaunch

Payments

EventWhenRelease
payment.recordedA person records a payment manuallyLaunch
payment.succeededA payment rail (such as Stripe) confirms a paymentLaunch
payment.refundedA payment is refundedLaunch
payment_proof.submittedA client clicks "I have paid" and submits a proofLaunch

Clients and items

EventWhenRelease
client.created, client.updatedA client is created or changedLaunch
item.created, item.updatedA saved item is created or changedLaunch

AI agent

EventWhenRelease
agent_run.completedAn agent run finishedLaunch
agent_run.failedAn agent run failedLaunch

Collections

EventWhenRelease
reminder.sentA reminder was sentJanuary 2027
late_fee.appliedA late fee was added to an invoiceJanuary 2027
late_fee.waivedA late fee was waivedJanuary 2027
recurring.document_createdA recurring schedule created a documentJanuary 2027
installment.dueA payment schedule instalment falls dueJanuary 2027
installment.paidAn instalment is paidJanuary 2027

Saved cards and contracts

EventWhenRelease
auto_charge.succeededAn automatic charge on a saved card succeededMarch 2027
auto_charge.failedAn automatic charge failedMarch 2027
saved_payment_method.removedA client removed a saved cardMarch 2027
contract.sentA contract was sent for signatureMarch 2027
contract.signedThe client signed a contractMarch 2027
contract.sealedA fully signed contract was sealedMarch 2027

Time, expenses, accounting and ledger

EventWhenRelease
time_entry.createdA time entry was loggedApril 2027
expense.createdAn expense was recordedApril 2027
accounting_sync.failedA push to QuickBooks or Xero failedMay 2027
journal_entry.postedA journal entry was postedJune to July 2027
accounting_period.lockedAn accounting period was lockedJune to July 2027

Good practice

  • Respond fast, work later. Store the event, return 200, then process it from a queue.
  • Be idempotent. Dedupe on id; the same event can arrive more than once.
  • Treat the payload as a hint. For decisions that matter, such as releasing goods when an invoice is paid, fetch the document from the API to confirm its current state.
  • Ignore unknown types and fields. New ones are added over time.
  • Keep the secret secret. Store it like an API key. If it leaks, rotate it.