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
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
| Event | Fires when |
|---|---|
lead.created | A new lead is ingested via Clay, API, or CSV upload |
lead.validated | Validation pipeline finishes (syntax, MX, disposable, MillionVerifier) |
lead.health_changed | A lead transitions between GREEN / YELLOW / RED classifications |
lead.replied | A reply is detected and the lead is removed from sequence |
Campaign Events
| Event | Fires when |
|---|---|
campaign.launched | A campaign transitions from DRAFT to ACTIVE |
campaign.paused | A campaign is paused — manually or by the protection layer |
campaign.completed | All leads in the campaign have finished their sequence |
Mailbox / Healing Events
| Event | Fires when |
|---|---|
mailbox.paused | A mailbox trips a bounce / connectivity / DNS threshold and is auto-paused |
mailbox.entered_quarantine | Healing phase 2 — DNS verification cycle begins |
mailbox.entered_restricted_send | Healing phase 3 — conservative warmup volume resumes |
mailbox.entered_warm_recovery | Healing phase 4 — graduated volume ramp |
mailbox.healed | Healing phase 5 — mailbox returns to HEALTHY and is re-added to campaigns |
Domain Events
| Event | Fires when |
|---|---|
domain.dnsbl_listed | The domain appears on one of the monitored DNS blocklists |
domain.dnsbl_cleared | A previously listed domain is no longer present on any blocklist |
domain.dns_failed | SPF, DKIM, or DMARC verification fails for the sending domain |
Send / Engagement Events
| Event | Fires when |
|---|---|
email.sent | A message is dispatched through the sequencer |
email.bounced | A hard or soft bounce is recorded against a sent message |
email.opened | An open is detected via the tracking pixel |
email.clicked | A tracked link in a sent message is clicked |
reply.received | An 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.pausedX-Superkabe-Event-Id— stable id for dedupe across retriesX-Superkabe-Delivery-Id— unique per delivery attempt batchX-Superkabe-Signature—t=<ts>,v1=<hex>HMAC-SHA256User-Agent—Superkabe-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 "", 200Go (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:
| Attempt | Delay after previous failure | Cumulative time |
|---|---|---|
| 1 | immediate | 0s |
| 2 | 30 seconds | ~30s |
| 3 | 2 minutes | ~2.5m |
| 4 | 10 minutes | ~12.5m |
| 5 | 1 hour | ~1h 12m |
| 6 | 6 hours | ~7h 12m |
| final | 24 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/webhooks2. 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
- Provider —
genericfor HMAC-signed JSON, orslackfor 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_SECRET4. 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