Docs

Webhook events.

One JSON POST per event to your own URL, for the events you tick in Settings, on the Webhooks tab. Every delivery is signed, written down before the first try, and tried again if your server does not take it.

The events

sent

An email left one of your inboxes. Announced within a minute of going out, once per email.

{
  "event": "sent",
  "email": "owner@shop.example",
  "campaign": "Woo speed UK",
  "mailbox": "rae@yourdomain.example",
  "step": "1",
  "subject": "Your checkout on Shop Example",
  "sent_at": "2026-09-13T09:14:02.000Z"
}

reply

A human answered. Out-of-office and bounces are not replies.

{
  "event": "reply",
  "domain": "shop.example",
  "person": "Rebecca",
  "text": "Sounds good, call me Thursday?",
  "campaign": "Woo speed UK",
  "mailbox": "rae@yourdomain.example",
  "reply_id": "0b5e8f7a-2c1d-4e3f-9a8b-7c6d5e4f3a2b",
  "link": "https://app.craftnudge.example/unibox?reply=0b5e8f7a-2c1d-4e3f-9a8b-7c6d5e4f3a2b"
}

bounce

The address rejected the email.

{
  "event": "bounce",
  "email": "nobody@shop.example",
  "campaign": "Woo speed UK",
  "mailbox": "rae@yourdomain.example",
  "code": "5.1.1",
  "category": "bad_address",
  "link": "https://app.craftnudge.example/unibox?reply=0b5e8f7a-2c1d-4e3f-9a8b-7c6d5e4f3a2b"
}

unsubscribe

Someone used the unsubscribe link. They are on your blocklist from this moment.

{
  "event": "unsubscribe",
  "email": "owner@shop.example",
  "campaign": "Woo speed UK"
}

interested

You marked a reply as interested. Sent each time it is marked, including again.

{
  "event": "interested",
  "email": "owner@shop.example",
  "link": "https://app.craftnudge.example/unibox?reply=0b5e8f7a-2c1d-4e3f-9a8b-7c6d5e4f3a2b"
}

interest_changed

You moved a reply to a different status. Not sent when the status did not change.

{
  "event": "interest_changed",
  "email": "owner@shop.example",
  "from": "lead",
  "to": "meeting_booked",
  "link": "https://app.craftnudge.example/unibox?reply=0b5e8f7a-2c1d-4e3f-9a8b-7c6d5e4f3a2b"
}

task_due

A lead reached a manual step, so a task is waiting for someone. Sent once per task.

{
  "event": "task_due",
  "email": "owner@shop.example",
  "campaign": "Woo speed UK",
  "kind": "linkedin",
  "text": "Connect on LinkedIn and mention the checkout speed."
}

Headers on every delivery

Content-Type

application/json

X-CraftNudge-Event

The event name, the same as the event field in the body.

X-CraftNudge-Delivery

The delivery id. A retry carries the same one, so you can tell it is one event.

X-CraftNudge-Timestamp

When this attempt was signed, in seconds since 1970.

X-CraftNudge-Signature

sha256= and the HMAC-SHA256 of the timestamp, a dot, and the raw body, with your signing secret.

Checking a delivery

An owner can see the signing secret on the Integrations page, and rotate it there. Work out the signature from the timestamp header and the raw body, compare it with the signature header, and refuse a timestamp more than five minutes from your own clock. Then nobody else can post to your URL as us, or replay a delivery older than that. A retry, or a copy sent again inside those five minutes, carries the same delivery id, so keep the ids you have handled and skip one you have seen.

import { createHmac, timingSafeEqual } from 'node:crypto'

// rawBody is the body exactly as it arrived, before any JSON parsing.
function isFromCraftNudge(secret, headers, rawBody) {
  const timestamp = headers['x-craftnudge-timestamp']
  const expected = Buffer.from('sha256=' + createHmac('sha256', secret).update(timestamp + '.' + rawBody).digest('hex'))
  const given = Buffer.from(headers['x-craftnudge-signature'] ?? '')
  const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300
  return fresh && given.length === expected.length && timingSafeEqual(given, expected)
}

Retries

A delivery that fails is tried again after a minute, five minutes, thirty minutes, two hours and twelve hours, then given up. Each retry carries the same delivery id and body, with a fresh timestamp and signature. The Integrations page lists every delivery and whether it arrived.

REST hooks for Zapier

With an API key, another system subscribes a URL to one event and gets the same signed, retried deliveries. A sample body for each event helps a Zap get built before any real event has happened. The three routes are in the API reference.

Your CRM

An owner can connect HubSpot or Pipedrive on the Integrations page with a token. Each reply, and each change of interest, is added as a note on the contact with that email, and the contact is made if it is not there. Nothing is read from your CRM.