Webhooks

Receive real-time HTTP callbacks whenever something interesting happens — opens, clicks, replies, meetings booked, deals won. All deliveries are signed with HMAC-SHA256.

Register an endpoint

Add a webhook URL in Settings → Webhooks, select the events you care about, and copy the signing secret. You can also create webhooks programmatically via the API (see /api/v1/outbound-webhooks).

Delivery & retry policy

  • Method: POST with Content-Type: application/json.
  • Timeout: 10 seconds. Respond with any 2xx status to acknowledge.
  • Retries: Up to 3 attempts on failure, with backoff [10s, 60s, 300s]. After the final failure the event is marked dead and visible in Settings → Webhooks.
  • Ordering: Not guaranteed. Use occurred_at to order events on your side.
  • De-dup: Every delivery carries an X-WarmDeals-Delivery-Id header. Store it and reject duplicates.

Signature verification

Each request includes a header X-WarmDeals-Signature containing the HMAC-SHA256 of the raw request body using your webhook secret (hex-encoded).

Python

python
import hmac, hashlib

def verify(secret: str, body: bytes, signature: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Node.js / TypeScript

typescript
import { createHmac, timingSafeEqual } from "node:crypto"

export function verify(secret: string, body: Buffer, signature: string): boolean {
  const expected = createHmac("sha256", secret).update(body).digest("hex")
  const a = Buffer.from(expected, "hex")
  const b = Buffer.from(signature, "hex")
  return a.length === b.length && timingSafeEqual(a, b)
}

Important: verify against the raw, unparsed request body. JSON parsing can change byte representation (key order, whitespace) and break the signature.

Event types

lead.found

A new lead/company was discovered (manually, via parsing, or via import).

json
{
  "event": "lead.found",
  "occurred_at": "2026-05-22T10:14:33Z",
  "data": {
    "id": "01HXY...",
    "name": "Acme Corp",
    "email": "ceo@acme.com",
    "source": "csv_import"
  }
}
email.opened

Recipient opened an outbound email (tracking pixel hit).

json
{
  "event": "email.opened",
  "occurred_at": "2026-05-22T10:17:01Z",
  "data": {
    "campaign_id": "01HXY...",
    "company_id": "01HXY...",
    "message_id": "01HXY...",
    "user_agent": "Mozilla/5.0 ..."
  }
}
email.clicked

Recipient clicked a tracked link inside an outbound email.

json
{
  "event": "email.clicked",
  "occurred_at": "2026-05-22T10:17:42Z",
  "data": {
    "campaign_id": "01HXY...",
    "company_id": "01HXY...",
    "url": "https://yourdomain.com/pricing"
  }
}
email.bounced

Outbound email hard-bounced. Lead is auto-suppressed.

json
{
  "event": "email.bounced",
  "occurred_at": "2026-05-22T10:18:00Z",
  "data": {
    "campaign_id": "01HXY...",
    "company_id": "01HXY...",
    "reason": "550 5.1.1 user unknown"
  }
}
campaign.started

A campaign has begun sending.

json
{
  "event": "campaign.started",
  "occurred_at": "2026-05-22T10:00:00Z",
  "data": { "campaign_id": "01HXY...", "total_recipients": 1240 }
}
campaign.finished

A campaign finished sending all messages.

json
{
  "event": "campaign.finished",
  "occurred_at": "2026-05-22T11:30:00Z",
  "data": {
    "campaign_id": "01HXY...",
    "sent": 1240,
    "delivered": 1198,
    "bounced": 42
  }
}
reply.received

An inbound reply landed in the unified inbox.

json
{
  "event": "reply.received",
  "occurred_at": "2026-05-22T10:30:12Z",
  "data": {
    "thread_id": "01HXY...",
    "from": "ceo@acme.com",
    "subject": "Re: Quick question",
    "snippet": "Sure — happy to chat tomorrow at 2pm.",
    "sentiment": "positive"
  }
}
meeting.booked

A meeting was scheduled via the public booking link.

json
{
  "event": "meeting.booked",
  "occurred_at": "2026-05-22T10:45:00Z",
  "data": {
    "meeting_id": "01HXY...",
    "company_id": "01HXY...",
    "start_at": "2026-05-25T14:00:00Z",
    "duration_minutes": 30
  }
}
deal.won

A deal advanced to a won stage.

json
{
  "event": "deal.won",
  "occurred_at": "2026-05-22T16:00:00Z",
  "data": {
    "deal_id": "01HXY...",
    "company_id": "01HXY...",
    "value": 12000,
    "currency": "USD"
  }
}
sequence.step_sent

A sequence step was sent to a recipient.

json
{
  "event": "sequence.step_sent",
  "occurred_at": "2026-05-22T09:00:00Z",
  "data": {
    "sequence_id": "01HXY...",
    "step_index": 2,
    "company_id": "01HXY...",
    "channel": "email"
  }
}
voice.completed

An outbound AI voice call finished. Transcript & disposition included.

json
{
  "event": "voice.completed",
  "occurred_at": "2026-05-22T11:12:00Z",
  "data": {
    "campaign_id": "01HXY...",
    "company_id": "01HXY...",
    "duration_seconds": 87,
    "disposition": "interested",
    "transcript_url": "https://warm.deals/transcripts/01HXY..."
  }
}
sms.delivered

SMS was successfully delivered (per carrier receipt).

json
{
  "event": "sms.delivered",
  "occurred_at": "2026-05-22T12:00:00Z",
  "data": {
    "campaign_id": "01HXY...",
    "company_id": "01HXY...",
    "provider_message_id": "SM..."
  }
}

Manage your webhooks

Add, pause, rotate secrets, and replay failed deliveries from Settings → Webhooks.