Webhooks

Receive real-time POST callbacks for every significant sending and protection event in your account

What Webhooks Are

Superkabe webhooks are outbound HTTP POST callbacks fired from our infrastructure to a URL you control whenever something material happens in your account — a mailbox enters quarantine, a domain trips a DNSBL, a campaign launches, a lead replies, a bounce threshold is breached. Each event is delivered as a JSON envelope, signed with HMAC-SHA256 over the raw body, and retried on failure with exponential backoff.

Webhooks are the right primitive when you want to mirror Superkabe's state into another system in near-real-time: a CRM, a data warehouse, an internal alerting bus, or a Slack incoming webhook URL. For pull-based access, use the REST API instead.

Delivery Guarantees

At-least-once delivery — Idempotency on the (org, event_id) pair lets you safely dedupe
HMAC-SHA256 signing — Stripe-compatible scheme, timestamped to prevent replay
6-attempt retry schedule — Spans 24+ hours with exponential backoff
Auto-disable on chronic failure — 5 consecutive dead-letters pauses the endpoint and emails the org
Manual replay — Any past delivery can be re-fired from the dashboard
Per-event subscriptions — Subscribe to specific events or all events on one endpoint

Event Types

The full taxonomy of emittable events. Subscribe to specific events when you create an endpoint, or leave the events list empty to receive everything.

Lead Events

EventFires when
lead.createdA new lead is ingested via Clay, API, or CSV upload
lead.validatedValidation pipeline finishes (syntax, MX, disposable, MillionVerifier)
lead.health_changedA lead transitions between GREEN / YELLOW / RED classifications
lead.repliedA reply is detected and the lead is removed from sequence

Campaign Events

EventFires when
campaign.launchedA campaign transitions from DRAFT to ACTIVE
campaign.pausedA campaign is paused — manually or by the protection layer
campaign.completedAll leads in the campaign have finished their sequence

Mailbox / Healing Events

EventFires when
mailbox.pausedA mailbox trips a bounce / connectivity / DNS threshold and is auto-paused
mailbox.entered_quarantineHealing phase 2 — DNS verification cycle begins
mailbox.entered_restricted_sendHealing phase 3 — conservative warmup volume resumes
mailbox.entered_warm_recoveryHealing phase 4 — graduated volume ramp
mailbox.healedHealing phase 5 — mailbox returns to HEALTHY and is re-added to campaigns

Domain Events

EventFires when
domain.dnsbl_listedThe domain appears on one of the monitored DNS blocklists
domain.dnsbl_clearedA previously listed domain is no longer present on any blocklist
domain.dns_failedSPF, DKIM, or DMARC verification fails for the sending domain

Send / Engagement Events

EventFires when
email.sentA message is dispatched through the sequencer
email.bouncedA hard or soft bounce is recorded against a sent message
email.openedAn open is detected via the tracking pixel
email.clickedA tracked link in a sent message is clicked
reply.receivedAn inbound reply is matched to a sent thread

Payload Schema

Every webhook ships the same envelope. The data field carries the event-specific body.

{
  "id": "d1f4...",                        // delivery id (X-Superkabe-Delivery-Id)
  "event": "mailbox.paused",              // event type
  "event_id": "e7c2...",                  // dedupe key (X-Superkabe-Event-Id)
  "organization_id": "org_8f1a...",       // your org
  "timestamp": "2026-04-29T14:22:18.412Z",// ISO-8601 emit time
  "data": { /* event-specific */ }
}

Sample: mailbox.paused

{
  "id": "d1f4a8e0-...",
  "event": "mailbox.paused",
  "event_id": "e7c2b3d4-...",
  "organization_id": "org_8f1a...",
  "timestamp": "2026-04-29T14:22:18.412Z",
  "data": {
    "mailbox_id": "mb_2k9...",
    "email": "outreach@acme-sales.com",
    "domain": "acme-sales.com",
    "reason": "bounce_rate_breached",
    "bounce_rate_24h": 0.087,
    "threshold": 0.05,
    "previous_state": "HEALTHY",
    "new_state": "PAUSED",
    "correlated_root_cause": "mailbox"
  }
}

Sample: lead.health_changed

{
  "id": "d4ab...",
  "event": "lead.health_changed",
  "event_id": "ec1a...",
  "organization_id": "org_8f1a...",
  "timestamp": "2026-04-29T14:23:01.118Z",
  "data": {
    "lead_id": "ld_91f...",
    "email": "jane@prospect.io",
    "previous_health": "GREEN",
    "new_health": "YELLOW",
    "score": 62,
    "reason": "domain_health_degraded"
  }
}

Sample: email.bounced

{
  "id": "d92c...",
  "event": "email.bounced",
  "event_id": "ee44...",
  "organization_id": "org_8f1a...",
  "timestamp": "2026-04-29T14:25:44.002Z",
  "data": {
    "send_id": "snd_7a2...",
    "lead_id": "ld_91f...",
    "campaign_id": "cmp_4b1...",
    "mailbox_id": "mb_2k9...",
    "bounce_type": "hard",
    "smtp_code": "550",
    "smtp_message": "5.1.1 The email account that you tried to reach does not exist"
  }
}

Every request includes these headers:

  • X-Superkabe-Event — event type, e.g. mailbox.paused
  • X-Superkabe-Event-Id — stable id for dedupe across retries
  • X-Superkabe-Delivery-Id — unique per delivery attempt batch
  • X-Superkabe-Signaturet=<ts>,v1=<hex> HMAC-SHA256
  • User-AgentSuperkabe-Webhooks/1.0

Signature Verification

Every webhook is signed with HMAC-SHA256 using the endpoint's signing secret. The X-Superkabe-Signature header takes the form:

X-Superkabe-Signature: t=1714400538,v1=5257a869e7eccf...

To verify: take the timestamp t, concatenate t.<raw_body>, compute HMAC-SHA256 with your signing secret, and timing-safe compare to v1. Reject any request where the timestamp is older than 5 minutes — that defeats replay attacks.

Security Warning

Signature verification is mandatory in production. Without it, anyone who learns your endpoint URL can forge events. Always verify against the raw request body — not a re-serialized JSON object — because any whitespace difference will break the HMAC.

Node.js (Express)

import crypto from 'crypto';
import express from 'express';

const app = express();
const SECRET = process.env.SUPERKABE_WEBHOOK_SECRET!; // whsec_...

// IMPORTANT: capture the raw body, not the parsed JSON.
app.post(
    '/webhooks/superkabe',
    express.raw({ type: 'application/json' }),
    (req, res) => {
        const header = req.header('X-Superkabe-Signature') || '';
        const parts = Object.fromEntries(
            header.split(',').map(p => p.split('='))
        ) as { t?: string; v1?: string };

        if (!parts.t || !parts.v1) return res.status(400).send('bad signature');

        const ageSec = Math.floor(Date.now() / 1000) - Number(parts.t);
        if (ageSec > 300) return res.status(400).send('signature too old');

        const signed = `${parts.t}.${req.body.toString('utf-8')}`;
        const expected = crypto.createHmac('sha256', SECRET).update(signed).digest('hex');

        const ok = crypto.timingSafeEqual(
            Buffer.from(expected, 'hex'),
            Buffer.from(parts.v1, 'hex')
        );
        if (!ok) return res.status(401).send('invalid signature');

        const event = JSON.parse(req.body.toString('utf-8'));
        // ... handle event.event, event.data ...
        res.status(200).send('ok');
    }
);

Python (Flask)

import hmac, hashlib, os, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["SUPERKABE_WEBHOOK_SECRET"].encode()  # whsec_...

@app.post("/webhooks/superkabe")
def superkabe():
    header = request.headers.get("X-Superkabe-Signature", "")
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1:
        abort(400)

    if time.time() - int(t) > 300:
        abort(400)  # too old

    raw = request.get_data()  # raw bytes — do NOT use request.json
    signed = f"{t}.".encode() + raw
    expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(expected, v1):
        abort(401)

    event = request.get_json()
    # ... handle event["event"], event["data"] ...
    return "", 200

Go (net/http)

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

var secret = []byte(os.Getenv("SUPERKABE_WEBHOOK_SECRET")) // whsec_...

func handler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)

    var t, v1 string
    for _, part := range strings.Split(r.Header.Get("X-Superkabe-Signature"), ",") {
        kv := strings.SplitN(part, "=", 2)
        if len(kv) != 2 { continue }
        switch kv[0] {
        case "t": t = kv[1]
        case "v1": v1 = kv[1]
        }
    }
    if t == "" || v1 == "" {
        http.Error(w, "bad signature", 400); return
    }

    ts, _ := strconv.ParseInt(t, 10, 64)
    if time.Now().Unix()-ts > 300 {
        http.Error(w, "signature too old", 400); return
    }

    mac := hmac.New(sha256.New, secret)
    mac.Write([]byte(t + "."))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))

    if !hmac.Equal([]byte(expected), []byte(v1)) {
        http.Error(w, "invalid signature", 401); return
    }
    // ... json.Unmarshal(body, &event); handle ...
    w.WriteHeader(200)
}

Retry Behavior

A delivery attempt fails when your endpoint returns a non-2xx response, times out (15-second cap), or the connection drops. Failed deliveries are retried up to 6 attempts total on the following exponential schedule, measured from the prior failure:

AttemptDelay after previous failureCumulative time
1immediate0s
230 seconds~30s
32 minutes~2.5m
410 minutes~12.5m
51 hour~1h 12m
66 hours~7h 12m
final24 hours~31h 12m → dead-letter

After the final attempt, the delivery is marked dead_letter. It is preserved in your delivery log and can be replayed manually at any time.

Idempotency Tip

Because retries can fire successfully after your endpoint already processed the first attempt (e.g. you returned 500 but actually persisted the event), you should dedupe on event_id. The same logical event keeps the same event_id across all retries.

Auto-Disable

If 5 consecutive deliveries dead-letter, Superkabe automatically disables the endpoint. No further events are dispatched until you manually re-enable it. The consecutive-failure counter is reset to zero on any successful delivery, so a healthy endpoint will not trip this rule.

When auto-disable fires, Superkabe sends two notifications:

  • An in-app notification visible to every member of your organization
  • A transactional email (sent via Resend) to every owner and admin on the org, with the endpoint name, URL, disable reason, and a one-click re-enable link

Re-enabling the endpoint resumes future deliveries. To re-fire events that arrived during the outage, replay them from the delivery log.

Replaying Deliveries

Every delivery — successful, failed, or dead-lettered — is preserved in the delivery log at /dashboard/integrations/webhooks. Each row shows the event type, attempt count, response code, response body (truncated to 4 KB), duration, and timestamps.

Click Replay on any row to re-enqueue the delivery. The full 6-attempt schedule is reset, so a replay can itself retry on failure. Replays are useful for backfilling events your endpoint missed during planned downtime, debugging signature verification, or testing handler changes against a real payload.

Setting Up an Endpoint

1. Create the endpoint in the dashboard

Navigate to Integrations → Webhooks and click Add Endpoint.

https://app.superkabe.com/dashboard/integrations/webhooks

2. Configure

  • URL — public HTTPS URL that will receive POSTs (HTTP is rejected)
  • Name — human label for the delivery log and alert emails
  • Events — pick specific event types, or leave blank to subscribe to everything
  • Providergeneric for HMAC-signed JSON, or slack for a Slack incoming webhook URL (payload is reshaped into Slack's blocks format and the signature is omitted)

3. Copy the signing secret

On creation, Superkabe generates a 256-bit secret prefixed whsec_. The full value is shown once. Store it in your secrets manager — it is required to verify signatures.

whsec_REPLACE_WITH_YOUR_GENERATED_SECRET

4. Verify with a test event

The endpoint detail page has a Send test event button that fires a synthetic email.sent payload. Confirm it lands at your endpoint and that signature verification passes before you wire up production traffic.

Quick Reference

  • Method: POST
  • Content-Type: application/json
  • Timeout: 15 seconds per attempt
  • Max attempts: 6
  • Auto-disable threshold: 5 consecutive dead-letters
  • Signature scheme: HMAC-SHA256, Stripe-compatible (t=<ts>,v1=<hex>)
  • Replay window enforcement: reject signatures older than 5 minutes