Developers

Webhooks

Find out when something happens instead of asking repeatedly. Signed with HMAC-SHA256, retried three times, and idempotent if you use the delivery id.

Last updated August 20, 2026

Before you ship

Verify X-Webhook-Signature on every request, respond within 10 seconds, and de-duplicate on X-Webhook-ID. Those three together are the difference between a webhook handler that works and one that works until it does not.

Why webhooks instead of polling

Polling for changes means asking repeatedly and getting the same answer nearly every time. It is slow to notice anything, wasteful for both of us, and it hits rate limits at exactly the moment activity picks up — the moment you most want to know.

A webhook is the other direction: you give us a URL, and we POST to it when something happens. You find out in seconds, and you spend no requests waiting.

Events

Subscribe to any combination. Each subscription lists the events it wants.

EventFires when
LEAD_CREATEDA lead arrives — from the API, a form, or a connected app.
DEAL_STAGE_CHANGEDA deal moves between pipeline stages. Carries the previous and new stage.
LISTING_CREATEDA connected inventory app reports a new item.
LISTING_UPDATEDA connected inventory app reports a change to an existing item.
LISTING_SOLDA connected inventory app reports an item sold.

The LISTING_* events exist for connected inventory systems. If you do not run one, you will never see them — subscribing does no harm, it simply stays quiet.

Creating a subscription

Through the API, with a token:

curl -X POST https://app.getfullarc.com/api/v1/webhooks \
  -H "Authorization: Bearer fa_your_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.example.com/hooks/fullarc",
    "events": ["LEAD_CREATED", "DEAL_STAGE_CHANGED"],
    "description": "Production lead router"
  }'
The secret is shown once

The response contains a secret. It is returned on creation and never again — listing your subscriptions afterwards shows it masked. Store it somewhere durable before you close the terminal. If you lose it, delete the subscription and create a new one; there is no way for us to reveal it later, which is the property that makes it worth anything.

Your endpoint must be reachable over HTTPS and should answer quickly. See delivery and retries for what “quickly” means.

What we send

A POST with a JSON body, plus four headers that tell you what arrived and let you prove it came from us:

POST /hooks/fullarc HTTP/1.1
Content-Type: application/json
X-Webhook-Event: LEAD_CREATED
X-Webhook-ID: clx8f2k1000...
X-Webhook-Timestamp: 2026-08-20T14:22:03.518Z
X-Webhook-Signature: sha256=9f86d081884c7d659a2feaa0...
HeaderWhat it carries
X-Webhook-EventWhich event this is, so you can route without parsing the body first.
X-Webhook-IDUnique per delivery. Use it to make your handler idempotent — see retries.
X-Webhook-TimestampISO 8601, when we sent it.
X-Webhook-Signaturesha256= followed by the HMAC of the raw body, keyed with your subscription secret.

Verifying the signature

Verify every delivery before acting on it. Your endpoint is a public URL; anyone who finds it can POST to it. The signature is what separates a real event from someone else's JSON.

Compute HMAC-SHA256 over the raw request body — the exact bytes, before any JSON parsing — using your subscription secret as the key, and compare it to the header.

import crypto from 'node:crypto';

function verify(rawBody, signatureHeader, secret) {
  const expected =
    'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');

  // Constant-time compare. A plain === leaks how much of the signature
  // matched through timing, which is enough to forge one given patience.
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader || '');

  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Two things commonly go wrong here. Re-serialising the parsed JSON before hashing produces a different byte sequence and a signature that never matches — capture the raw body. And comparing with === works, right up until it is the thing someone attacks.

Delivery and retries

  • Answer within 10 seconds. We abort the request after that and count it as a failure. Acknowledge first, process afterwards — return 200 as soon as you have the payload safely queued, rather than doing the work inline.
  • Any 2xx is success. Anything else, or a timeout, is a failure.
  • Three attempts, with exponential backoff — roughly 2 seconds, then 4, then 8. After the third, the delivery is marked failed and we stop.
  • A disabled or deleted subscription is not retried. There is nothing to retry into.
Make your handler idempotent

Retries mean the same event can arrive more than once — including after your first attempt succeeded but the response was lost on the way back. Record X-Webhook-ID and ignore an id you have already processed. Without that, a single network hiccup becomes a duplicate deal in your system.

Deliveries and their outcomes are recorded, so a subscription that has been quietly failing is visible rather than something you find out about weeks later.

Managing subscriptions

# List (secrets masked)
curl https://app.getfullarc.com/api/v1/webhooks \
  -H "Authorization: Bearer fa_your_token_here"

# Pause one without losing its configuration
curl -X PATCH "https://app.getfullarc.com/api/v1/webhooks?id=clx..." \
  -H "Authorization: Bearer fa_your_token_here" \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}'

# Remove it
curl -X DELETE "https://app.getfullarc.com/api/v1/webhooks?id=clx..." \
  -H "Authorization: Bearer fa_your_token_here"

Disabling is the right move while you deploy a change to your handler. Deleting loses the secret along with the subscription, which means updating every consumer of it.

Getting help

Signature not matching, or an event you expected never arriving? Send the X-Webhook-ID and roughly when it should have fired to support@getfullarc.com and we can look up the delivery. See also the API documentation.