KoltrixKoltrix docs

Webhooks

Real-time delivery and list events delivered to an HTTPS endpoint you control. Every payload is signed with HMAC-SHA256 using a per-endpoint secret so you can prove the request came from us. Use webhooks to update your own database when a send completes, slack a channel when a hard bounce happens, or trigger downstream automation when a new subscriber joins a list.

Events

EventWhen it fires
message.sentThe receiving server accepted the message (250 OK)
message.deliveredReceiver returned a successful DSN or Delivered-To
message.bouncedHard bounce (5xx) — recipient auto-suppressed
message.openedTracking pixel was loaded
message.clickedA tracked link was clicked
message.complainedMailbox provider reported a spam complaint via FBL
message.unsubscribedRecipient hit the one-click unsubscribe link
list.subscriber.addedSubscriber transitioned to active on a list
list.subscriber.removedSubscriber unsubscribed, hard-bounced, or was deleted

When you create an endpoint without specifying events, we default to the seven message.* events. Pass an explicit array to subscribe to the list events too, or to scope the endpoint to a smaller set.

Add an endpoint

The fastest way is Settings → Webhooks → New endpoint in the dashboard. Pick the events, paste the HTTPS URL, copy the signing secret (shown once).

The secret looks like whsec_.... Store it as an env var on whichever service runs your handler — you'll need it to verify signatures.

Request shape

Every delivery is a POST with three headers and a JSON body:

POST /koltrix HTTP/1.1
Host: hooks.acme.com
Content-Type: application/json
X-Koltrix-Event: message.delivered
X-Koltrix-Signature: sha256=8f30b3...
User-Agent: Koltrix-Webhook/1.0
 
{
  "event":      "message.delivered",
  "timestamp":  1716220800,
  "message_id": "01HX9C8...",
  "to":         "[email protected]",
  "from":       "[email protected]",
  "subject":    "Your invoice"
}
HeaderDescription
X-Koltrix-EventThe event name (matches event in the body).
X-Koltrix-Signaturesha256= + hex HMAC of the raw request body.
User-AgentAlways Koltrix-Webhook/1.0.

The body fields vary by event:

  • message.* events include message_id, to, from, subject, plus event-specific fields (url on message.clicked, bounce_reason on message.bounced, etc.).
  • list.subscriber.* events include list_id, subscriber_id, email, and a reason.

Verify the signature

The signing scheme is HMAC-SHA256 over the raw request body using your endpoint's secret. The hex digest is prefixed with sha256= to match the header.

// Node 18+ — verify in your handler before trusting the payload.
import crypto from "node:crypto";
 
function verifyKoltrix(rawBody: string | Buffer, signature: string, secret: string) {
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  // timingSafeEqual avoids leaking via timing differences.
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
# Python equivalent.
import hmac, hashlib
 
def verify_koltrix(body: bytes, signature: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(),
        body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
// Go equivalent.
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)
 
func verifyKoltrix(body []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}

Use the raw bytes — not a re-serialised JSON object. If your framework parses JSON before your handler sees the body, you'll need a raw-body middleware (e.g. express.raw({ type: "application/json" })) or the signature won't match.

Retries

When your endpoint responds with a non-2xx status — or doesn't respond within 8 seconds — we record the failed status code and response body and retry with exponential backoff:

AttemptDelay after previousCumulative time
1(initial fire)0
21 minute1 min
35 minutes6 min
430 minutes36 min
52 hours2h 36m
612 hours14h 36m
724 hours38h 36m

After the 7th attempt the delivery is marked failed and stops retrying.

We recommend your handler:

  • Responds 200 OK the moment the payload is verified and processes asynchronously. Long-running handlers will time out and get retried, which means duplicate work.
  • Tolerates duplicates. A retry can land before the first response was written, so design your handler to be idempotent — usually by storing message_id (or message_id + event) in a dedupe table.
  • Verifies the signature before doing anything trust-sensitive. Bad signatures are dropped at our edge, but if a deployment leaks your endpoint URL you don't want an attacker firing fake events into your billing logic.

Inspecting deliveries

The dashboard shows every recent delivery under Settings → Webhooks → endpoint → Deliveries — status code, response body (first 2 KB), attempt count. Retries appear as additional rows so you can see the timeline.

Testing

Two options:

  1. Send test next to a webhook in the dashboard — fires a synthetic message.sent event with "_synthetic": true in the body so you can filter it out of analytics.
  2. Replay — pick any past delivery and click Replay to refire the exact same payload with a fresh signature. Useful for debugging a handler that rejected the first attempt.

Disabling an endpoint

Toggle Active off in the dashboard. New deliveries stop, but the endpoint, secret, and delivery history are kept so you can re-enable it later without losing context.