Business feature— included with the Business plan on Carbon Cloud. If you'd like to self-host this feature, you need a commercial license. Licensing →

Webhooks

Paid

Get an HTTP callback when a Carbon record is created, changed, or deleted.

A webhook sends an HTTP POST to a configured URL when a record in a subscribed table is inserted, updated, or deleted. Use webhooks to react to changes without polling the API.

Subscribable tables

Each webhook subscribes to one table. The following tables support subscriptions:

ModuleTables
Salescustomer, quote, salesOrder, salesRfq
PurchasingpurchaseOrder, supplier, supplierQuote
InvoicingsalesInvoice, purchaseInvoice
Productionjob
Inventoryreceipt
Itemsitem, makeMethod, methodMaterial, methodOperation, modelUpload, changeOrder, changeOrderAffectedItem
Usersemployee

Select at least one trigger operation: insert, update, or delete.

The payload

Carbon sends a JSON body to the configured URL. type identifies the operation, record contains the affected row, and old contains the previous row for updates.

json
{
  "type": "UPDATE",
  "table": "salesOrder",
  "companyId": "abc123",
  "eventId": "1042",
  "record": { "id": "...", "salesOrderId": "SO000042", "status": "To Ship and Invoice" },
  "old": { "id": "...", "salesOrderId": "SO000042", "status": "Draft" }
}

old appears only for updates. For deletes, record contains the last state of the deleted row. Carbon sets Content-Type: application/json and a Carbon-Signature header (see Verifying deliveries).

A bill of material or routing change arrives as a change to methodMaterial or methodOperation, with the makeMethodId of the version it belongs to. A version's status (Draft, Active, Archived) is on makeMethod. A change notice's release is an update of changeOrder from Implementation to Done.

eventId identifies one change. It remains stable across delivery retries and differs for each new change, including repeated updates to the same record. Use it as the idempotency key.

Configuration

Create and manage webhooks under Settings → Webhooks. A webhook has:

  • Name: a label, unique within your company.
  • Table: the one table to watch.
  • URL: where the POST is sent; must be an http or https URL that resolves to a public address. Carbon refuses to deliver to loopback, private-network, link-local and cloud metadata addresses.
  • Triggers: any of insert, update, delete (at least one).
  • Active: webhooks are on by default; switch off to pause delivery.
  • Signing secret: reveal or roll it from the webhook's edit drawer (see below).

Carbon tracks success and error counts and the latest timestamp for each outcome.

Delivery & reliability

Webhook delivery is asynchronous and normally occurs a few seconds after the database change.

Delivery is at least once. If the endpoint does not return a success status within 30 seconds, Carbon retries with exponential backoff before marking the change as failed. Design for these conditions:

  • Handle duplicates. A request can be retried after your service has processed it. De-duplicate on eventId, not type and record.id, because separate changes to the same record share the latter values.
  • Reconcile after outages. After retries are exhausted, Carbon drops the change and increments the error count. Query the API when complete event processing is required.

The success and error counters record one outcome per change, not per attempt, so a retried-then-delivered event counts once as a success.

Carbon does not follow redirects. A 3xx response is a failed attempt, so configure the final URL. A delivery to a refused address is not retried; it counts once as an error.

Verifying deliveries

Each webhook has its own signing secret (whsec_…). Every delivery carries a Carbon-Signature header. With the secret whsec_example, a body of {} sent at 1760000000 is signed:

Carbon-Signature: t=1760000000,v1=703d2abadc91281113b98bbc26dd6ec6ebd63072172c44b3635b575b1f1313f1

t is the time of the attempt in Unix seconds. v1 is the hex HMAC-SHA256 of `${t}.${body}`, keyed by the whole secret string, where body is the raw request body. To verify a delivery:

  1. Read the raw body before you parse it. Parsing and re-serialising JSON changes the bytes.
  2. Compute the HMAC and compare it with v1 in constant time.
  3. Reject a t more than five minutes from your clock, so a captured request cannot be replayed. Each retry is signed again with a new t.
ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyCarbonSignature(
  rawBody: string,
  header: string,
  secret: string,
  toleranceSeconds = 300
) {
  const parts = Object.fromEntries(
    header.split(",").map((part) => part.split("=", 2) as [string, string])
  );
  const timestamp = Number(parts.t);
  if (!Number.isFinite(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  const received = parts.v1 ?? "";
  return (
    received.length === expected.length &&
    timingSafeEqual(Buffer.from(received), Buffer.from(expected))
  );
}

Reveal the secret in the webhook's edit drawer under Settings → Webhooks. Roll secret replaces it, and the next delivery is signed with the new one, so update your endpoint at the same time. Revealing or rolling the secret requires permission to update settings.

NOTE

A delivery tells you a record changed. Treat its body as a notification, and fetch the record with an API key before you act on it.