Webhooks

Receive changes as they happen

Subscribe an HTTPS endpoint to exact events or resource wildcards, then verify every delivery before processing it.

Create a subscription

Register a public HTTPS endpoint with the events it should receive. You can subscribe to exact names such as customer.created, resource wildcards such asinvoice.*, or * for all events.

curl
curl --request POST \
  --url "https://quick-goldfinch-475.eu-west-1.convex.site/api/v1/webhooks" \
  --header "Authorization: Bearer $CRAFTIVO_API_KEY" \
  --header "X-Organization-Id: $CRAFTIVO_ORGANIZATION_ID" \
  --header "Idempotency-Key: webhook-setup-001" \
  --header "Content-Type: application/json" \
  --data '{
    "url": "https://example.com/webhooks/craftivo",
    "events": ["customer.created", "invoice.*"]
  }'
The response contains the signing secret exactly once. Store it before continuing. A later hard rotation returns a new secret without a grace period.

Delivery format

craftivo sends a JSON POST request. The delivery ID stays stable across retries, so it can also be used for deduplication.

Headers
Content-Type: application/json
X-Webhook-Event: customer.created
X-Webhook-Delivery: delivery_…
X-Webhook-Timestamp: 1786291200
X-Webhook-Signature: 7f1c…
Body
{
  "id": "event_…",
  "event": "customer.created",
  "createdAt": 1786291200000,
  "data": {
    "customer": {
      "id": "customer_…",
      "name": "Müller Haustechnik GmbH"
    }
  }
}

Verify the HMAC signature

Compute a hex HMAC-SHA256 over <timestamp>.<raw body>using the endpoint secret. Use the exact raw request body before JSON parsing, reject old timestamps, and compare equal-length buffers in constant time.

TypeScript / Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyCraftivoWebhook(input: {
  rawBody: string;
  timestamp: string;
  signature: string;
  secret: string;
}): boolean {
  const { rawBody, timestamp, signature, secret } = input;
  const timestampSeconds = Number(timestamp);

  if (
    !Number.isFinite(timestampSeconds) ||
    Math.abs(Date.now() / 1000 - timestampSeconds) > 300
  ) {
    return false;
  }

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

  return (
    received.length === expected.length &&
    timingSafeEqual(received, expected)
  );
}
Return a 2xx response only after the signature is valid and the event has been accepted for processing. Treat the delivery ID as idempotent because the same delivery may arrive more than once.

Retries and automatic disabling

Each request times out after 10 seconds. A non-2xx response, redirect, timeout, or network error is retried up to five times after 1 minute, 5 minutes, 15 minutes, 1 hour, and 6 hours.

After ten final delivery failures in a row, the endpoint is disabled. HTTP 410 disables it immediately. Disabled endpoints receive nothing until they are reactivated.