Gareth Wilson Gareth Wilson

Guide to Breakcold Webhooks: Features and Best Practices

Published


Breakcold webhooks send your CRM activity to other systems as it happens. When a lead is created or edited, a note is added, a task is completed, two records are linked, or a call recording or file is attached, Breakcold posts the change to an HTTPS endpoint you control. That is how a Breakcold workspace keeps a data warehouse, a billing system or an internal tool in step without polling the API.

This guide covers how Breakcold endpoints are created and signed, the events and their envelope, Breakcold's short retry policy, and the practices that keep a Breakcold receiver reliable.

What are Breakcold webhooks?

Breakcold calls them outbound webhooks. Each endpoint is an HTTPS URL, a set of events and an optional filter on object types. When a subscribed event happens in the workspace, Breakcold sends a JSON POST to the URL.

Each delivery is signed. X-Webhook-Signature carries sha256= followed by a hex HMAC-SHA256 of the X-Webhook-Timestamp value, a full stop and the raw request body, keyed with the endpoint's signing secret. Breakcold also names the event in X-Webhook-Event and gives each delivery an ID in X-Webhook-Delivery-Id.

Breakcold webhook features

FeatureDetails
ConfigurationSettings > Webhook > Send > New outbound webhook
FilteringChoose events, and optionally limit record events to specific object types
Payload{ "eventId", "timestamp", "workspaceId", "event", "data" }
Signature headerX-Webhook-Signature: sha256=<hex>
Timestamp headerX-Webhook-Timestamp, Unix seconds
SchemeHMAC-SHA256, hex digest
Signed content{X-Webhook-Timestamp}.{raw body}
Other headersX-Webhook-Event, X-Webhook-Delivery-Id, User-Agent: Breakcold-Webhooks/1.0
SecretOne per endpoint, copied when the endpoint is created
SuccessAny 2xx
Timeout10 seconds
RetriesUp to 3 attempts in total, retrying after about 5 seconds and then 30 seconds
Auto-disableAfter 50 consecutive delivery failures
Delivery logsKept for 30 days
TestingThe endpoint test button sends a signed webhook.test event

Breakcold publishes no source IP list and states no timestamp tolerance.

Common events

CategoryEventsFires when
Recordsrecord.created, record.updated, record.deletedA record is created, a field on it is updated, or it is deleted
Notesnote.created, note.updated, note.deletedA note is added to a record, edited or deleted
Taskstask.created, task.completedA task is linked to a record, or marked as completed
Relationsrelation.added, relation.removedA relation between two records is added or removed
Callscall.linked, call.unlinkedA call recording is linked to or unlinked from a record
Filesfile.uploaded, file.deletedA file is uploaded to or deleted from a record

Every event uses the same envelope. Here is Breakcold's record.created example:

{
  "eventId": "evt_abc123",
  "timestamp": "2026-03-19T12:00:00.000Z",
  "workspaceId": "workspace_id",
  "event": "record.created",
  "data": {
    "id": "record_id",
    "objectType": "person",
    "objectTypeName": "Person",
    "name": "Ada Lovelace",
    "fields": {
      "name": "Ada Lovelace",
      "email": "ada@example.com",
      "company": "Analytical Engines Ltd"
    },
    "createdAt": "2026-03-19T12:00:00.000Z",
    "updatedAt": "2026-03-19T12:00:00.000Z"
  }
}
  • event names the event, and the same value arrives in the X-Webhook-Event header.
  • eventId is what Breakcold calls the unique delivery event ID. The X-Webhook-Delivery-Id header is described the same way.
  • timestamp is when Breakcold built the payload, as an ISO 8601 string. The signed timestamp is the separate header in Unix seconds.
  • data depends on the event. Record events carry the record and its fields, note events carry recordId, noteId and the note text, and relation events carry sourceRecordId, targetRecordId and the fieldSlug that links them.

record.deleted carries only the record id and "deleted": true, and note.deleted only the IDs and the title, so keep your own copy of anything you need after a deletion.

Setting up Breakcold webhooks

  1. In Breakcold, open Settings, then Webhook, then Send.
  2. Click New outbound webhook.
  3. Enter your HTTPS endpoint URL.
  4. Select the events you want to receive.
  5. Optionally, filter by object type. Record events are then sent only for records of those types.
  6. Copy the signing secret and store it server-side, for example as BREAKCOLD_WEBHOOK_SECRET.
  7. Use the test button to send a webhook.test event. It is signed like a real delivery, so it checks your verification code as well as reachability.

If you use Hookdeck, create a source with the Breakcold source type, paste the endpoint's signing secret into it, and use the Hookdeck source URL as the endpoint URL. The source accepts POST only, which is what Breakcold sends.

Securing Breakcold webhooks

Read X-Webhook-Timestamp and X-Webhook-Signature. Strip the sha256= prefix from the signature. Compute an HMAC-SHA256 of the timestamp header, a . and the raw body, using the signing secret as the key, encode it as hex and compare it with the received digest in constant time.

const crypto = require('crypto');
const express = require('express');

function verifyBreakcoldSignature(rawBody, timestamp, signatureHeader, secret) {
  if (!timestamp || !signatureHeader || !secret) return false;

  const signature = signatureHeader.replace(/^sha256=/i, '');
  if (!/^[0-9a-fA-F]{64}$/.test(signature)) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(timestamp, 'latin1')
    .update('.')
    .update(rawBody)
    .digest('hex');

  const received = Buffer.from(signature.toLowerCase(), 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

app.post('/webhooks/breakcold', express.raw({ type: '*/*' }), (req, res) => {
  const ok = verifyBreakcoldSignature(
    req.body,
    req.get('x-webhook-timestamp'),
    req.get('x-webhook-signature'),
    process.env.BREAKCOLD_WEBHOOK_SECRET
  );
  if (!ok) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on X-Webhook-Delivery-Id, then route on event.event.
  res.sendStatus(200);
});

The same check in Python:

import hashlib
import hmac
import re

HEX_64 = re.compile(r"[0-9a-fA-F]{64}")


def verify_breakcold_signature(raw_body: bytes, timestamp, signature_header, secret: str) -> bool:
    if not timestamp or not signature_header or not secret:
        return False
    signature = re.sub(r"^sha256=", "", signature_header, count=1, flags=re.IGNORECASE)
    if not HEX_64.fullmatch(signature):
        return False

    signed = timestamp.encode("latin-1") + b"." + raw_body
    expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature.lower().encode("utf-8"), expected.encode("utf-8"))


# In FastAPI: raw_body = await request.body()
# verify_breakcold_signature(raw_body, request.headers.get("x-webhook-timestamp"),
#                            request.headers.get("x-webhook-signature"),
#                            os.environ["BREAKCOLD_WEBHOOK_SECRET"])

Breakcold's own Node sample builds "sha256=" + digest and passes it to timingSafeEqual directly. That function throws when the two buffers differ in length, so a truncated or malformed header raises an exception instead of returning false. The length check above avoids that.

The timestamp is signed, but Breakcold sets no tolerance, so the samples don't check its age. If you add a check, keep it optional: Hookdeck retries and replays resend the original X-Webhook-Timestamp, and a short window would reject them.

Hookdeck's Breakcold source type verifies the same HMAC over {X-Webhook-Timestamp}.{body} and reads the signature with or without its sha256= prefix. It applies no timestamp tolerance. A request that fails the check is marked as failed verification and is not delivered to your destinations.

Make Breakcold webhooks production-ready. Hookdeck Event Gateway verifies the X-Webhook-Signature header on every Breakcold delivery, stores each CRM event, and delivers it to your services with retries you control.

Breakcold webhook limitations and pain points

Three attempts in about half a minute

The Problem: A brief outage or deploy loses events for good.

Why It Happens: Breakcold makes up to three attempts in total, retrying after about 5 seconds and then about 30 seconds. A service that is down for a minute misses every attempt, and the event is not sent again.

Workarounds:

  • Keep the endpoint highly available, and return a 2xx as soon as verification passes.
  • Use Breakcold's delivery logs, kept for 30 days, to spot failed deliveries, and reconcile those records against the Breakcold API.

How Hookdeck Can Help: Hookdeck accepts the delivery straight away and stores it, then retries your service on a schedule you choose, so a deploy or outage on your side costs latency instead of data.

Endpoints switch off after 50 failures

The Problem: Events stop arriving altogether, and nothing in your own system says why.

Why It Happens: Breakcold disables an endpoint after 50 consecutive delivery failures, and only delivers to active endpoints. A broken handler, a bad deploy or an expired certificate can reach 50 quickly in a busy workspace.

Workarounds:

  • Alert on non-2xx responses from your webhook route.
  • Return 2xx for events you don't handle, so they never count as failures.
  • Check the endpoint's state in Breakcold after any incident.

How Hookdeck Can Help: With Hookdeck in front, Breakcold's deliveries are accepted by Hookdeck, so failures in your own service don't count toward Breakcold's limit.

Deletes carry almost nothing

The Problem: When a record is deleted, your handler can't tell what it was.

Why It Happens: record.deleted contains only the record id and "deleted": true, and note.deleted contains the IDs and title but not the content.

Workarounds:

  • Store the record type and key fields when you process record.created and record.updated, keyed by record id.
  • Look up the deleted ID in your own store rather than in Breakcold.

How Hookdeck Can Help: Hookdeck keeps the payloads it has received, so you can look up the last record.updated for a record when you investigate a deletion.

Best practices

Verify against the raw body

The signature covers the exact body bytes. Use express.raw({ type: '*/*' }) in Express, Buffer.from(await request.arrayBuffer()) in a Next.js route handler and await request.body() in FastAPI, and parse JSON only after verification.

Deduplicate on X-Webhook-Delivery-Id

Breakcold tells you to use the delivery ID for idempotency. Record it when an event has been processed and skip repeats. See our guide to webhook idempotency.

Acknowledge fast and process asynchronously

Breakcold times out after 10 seconds and gives up after three attempts. Verify, persist, return 200, and do the work afterwards. See why to process webhooks asynchronously.

Use the object type filter

If you only sync people or only companies, filter by object type on the endpoint instead of discarding events in code. Fewer deliveries mean fewer chances to fail.

Route on event, and ignore webhook.test

Switch on the body's event (or the X-Webhook-Event header) and acknowledge webhook.test and unknown events with a 2xx without processing them.

Conclusion

Breakcold signs every delivery with sha256= and a hex HMAC-SHA256 of the timestamp and raw body. Verify against the raw bytes with a length-checked comparison, deduplicate on X-Webhook-Delivery-Id, keep your own copy of records so deletions make sense, and answer quickly, because Breakcold makes only three attempts and disables an endpoint after 50 straight failures.

Hookdeck Event Gateway verifies every Breakcold signature and holds each CRM event until your service has processed it, so a short outage doesn't cost you data.

Get started with Hookdeck for free and handle Breakcold webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.