API Docs / Webhooks

Webhooks

Webhooks deliver real-time HTTP POST notifications to your server when events happen in Kerabie Mail. Available on the Premium plan. Configure endpoints under Webhooks in the sidebar.

Hosting partner? The partner_* events below use this exact same signing and retry mechanism — see the Hosting Partner API docs for the full event list and create endpoints from Partner Dashboard → Developer.

Overview

Endpoints

Add up to 10 HTTPS endpoints per account. Each endpoint subscribes to a subset of events.

Delivery

Payloads are delivered within seconds of the event. Retries run for up to 8 minutes on failure.

Security

Every request is signed with HMAC-SHA256. Verify the signature before processing.

Managing endpoints

Register and manage endpoints from Webhooks in the sidebar, or programmatically — these management calls use your browser/mobile session, not an API key (an API key with the webhooks scope can still be used for send/mailbox calls elsewhere, but managing webhook endpoints themselves isn't exposed to API keys today).

POST/webhooks
Register an endpoint (Premium plan). Returns the signing secret exactly once — GET /webhooks never returns it again.
{
  "url": "https://your-app.com/webhooks/kerabie",
  "events": ["email.received", "email.bounced"],
  "allowed_ips": null  // optional — restricts which destination IP this URL may resolve to
}
GET/webhooks
List your endpoints.
PATCH/webhooks/{id}
Toggle is_active (pauses delivery without deleting) or replace allowed_ips. url/events can't be changed — delete and re-create instead.
DELETE/webhooks/{id}
Permanently delete an endpoint (hard delete, unlike API keys).
POST/webhooks/{id}/rotate-secret
Issue a new signing secret, returned once. The old secret stops verifying immediately — any deliveries already in flight, signed with it, will fail your signature check.
POST/webhooks/{id}/test
Sends a sample email.received payload immediately, bypassing the queue.
GET/webhooks/{id}/deliveries?limit=50
Recent delivery attempts — id, event, status (pending/delivered/dead), attempts, response_status, created_at. Also shown under Webhooks → Deliveries in the dashboard.
POST/webhooks/{id}/deliveries/{delivery_id}/retry
Re-queues a dead or failed delivery for one fresh attempt — resets its attempt count. 400 if the delivery isn't currently dead/failed.
POST/webhooks/{id}/deliveries/{delivery_id}/replay
Re-sends any delivery regardless of status — unlike retry, which only works on dead/failed deliveries and overwrites that same row, this creates a brand-new delivery (with its own id, pointing back at the original via replayed_from_id) so the original attempt's history is never lost. Useful for backfilling a newly added endpoint, or just re-confirming your receiving side handles an event correctly.

Events

EventTriggered when
email.receivedAn email arrives in a mailbox (includes threading fields)
email.sentAn email is successfully sent
email.failedSending fails permanently (all retries exhausted)
email.openedA tracked email is opened by the recipient
email.clickedA tracked link inside a sent email is clicked (fires per click, not deduped)
email.bouncedA sent email hard-bounces
email.spam_reportedRecipient marks the email as spam
email.forwardedA forwarding rule delivers an email to another address
mailbox.createdA new mailbox is provisioned
mailbox.deletedA mailbox is deleted
mailbox.quota_reachedStorage exceeds 90% of the mailbox quota
domain.verifiedA custom domain passes DNS verification
domain.verification_failedDNS verification fails after 3 attempts
partner_mailbox.createdA hosting partner provisions a client mailbox
partner_mailbox.deletedA hosting partner deletes a client mailbox
partner_mailbox.renewedA mailbox's prepaid term is renewed
partner_mailbox.expiring_soonA paid mailbox's term ends in ~7 days
partner_domain.addedA hosting partner claims a client domain
partner_domain.verifiedA client domain passes DNS verification
partner_domain.verification_failedA client domain fails DNS verification
partner_pool.purchasedA mailbox-slot pool purchase completes
partner_addon.purchasedA domain-slot or storage add-on purchase completes
partner_trial.expiring_soonA hosting partner's trial ends in ~7 days

Payload structure

Every webhook POST has this envelope:

{
  "event": "email.received",
  "timestamp": "2026-05-17T10:00:00Z",
  "webhook_id": "wh_1042",
  "data": { ... event-specific fields ... }
}

email.received data

{
  "message_id": "<abc123@kerabie.email>",
  "mailbox": "john@yourdomain.com",
  "from": "sender@example.com",
  "subject": "Re: Project update",
  "received_at": "2026-05-17T10:00:00Z",

  // Threading — use these to group conversations
  "thread_id": "a3f1c29d8e7b4012",     // stable 16-char SHA-256 prefix
  "in_reply_to": "<prev@example.com>", // Message-ID this replies to (null if new thread)
  "references": "<root@x.com> <prev@x.com>",  // full thread chain
  "is_reply": true,

  // Content preview — use for AI auto-respond without a separate inbox call
  "preview": "Hey John, just following up on...",

  "has_attachments": false,
  "size_bytes": 4200
}

email.opened data

{
  "message_id": "<abc123@kerabie.email>",
  "opened_at": "2026-05-17T11:30:00Z",
  "location_country": "NG",
  "client": "Apple Mail / macOS"
}

email.clicked data

Fires once per click — a message with several tracked links clicked fires this once per click, not once per message.

{
  "message_id": "<abc123@kerabie.email>",
  "url": "https://yourdomain.com/receipts/4421",
  "clicked_at": "2026-05-17T11:32:00Z"
}

domain.verified data

{
  "domain": "yourdomain.com",
  "verified_at": "2026-05-17T10:05:00Z",
  "checks": { "mx": true, "spf": true, "dkim": true, "dmarc": true }
}

Conversation threading

The thread_id field is a stable 16-character identifier derived from the email's References and In-Reply-To headers (or the normalised subject for new threads). Use it to group webhook events into conversations in your database.

CRM example — group emails by thread

// Receive webhook
const { event, data } = req.body

if (event === 'email.received') {
  const thread = await db.threads.upsert({
    where:  { id: data.thread_id },
    create: { id: data.thread_id, subject: data.subject, mailbox: data.mailbox },
    update: { last_received_at: data.received_at },
  })

  await db.messages.create({
    data: {
      thread_id:   thread.id,
      message_id:  data.message_id,
      from:        data.from,
      preview:     data.preview,
      received_at: data.received_at,
      is_reply:    data.is_reply,
    }
  })
}

AI auto-respond example

if (event === 'email.received' && data.is_reply) {
  // Fetch thread history from your DB for context
  const history = await db.messages
    .findMany({ where: { thread_id: data.thread_id }, orderBy: { received_at: 'asc' } })

  // Ask AI to draft a reply using the preview + history
  const draft = await generateReply({ preview: data.preview, history })

  // Send via Kerabie API
  await fetch('https://api.kerabie.email/mail/send', {
    method: 'POST',
    headers: { Authorization: `Bearer ${API_KEY}` },
    body: JSON.stringify({
      from_email: data.mailbox,
      to: [data.from],
      subject: 'Re: ' + data.subject,
      body_html: draft,
    })
  })
}

Signature verification

Every webhook request includes an X-Kerabie-Signature header. Always verify this before processing the payload to prevent spoofed events.

The signature format is t={unix_timestamp},v1={hmac_hex}. The signed payload is {timestamp}.{json_body} (compact, no spaces). This prevents replay attacks — reject requests where t is more than 300 seconds old.

Node.js / TypeScript

import crypto from 'crypto'

function verifyWebhook(
  rawBody: Buffer,
  signatureHeader: string,
  secret: string
): boolean {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map(p => p.split('='))
  ) as Record<string, string>

  const timestamp = parts['t']
  const receivedSig = parts['v1']

  // Reject stale requests (> 5 minutes)
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false

  const sigPayload = `${timestamp}.${rawBody.toString()}`
  const expected = crypto
    .createHmac('sha256', secret)
    .update(sigPayload)
    .digest('hex')

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(receivedSig)
  )
}

Python

import hmac, hashlib, time

def verify_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
    parts = dict(p.split('=', 1) for p in signature_header.split(','))
    timestamp = parts.get('t', '')
    received  = parts.get('v1', '')

    if abs(time.time() - float(timestamp)) > 300:
        return False  # Stale

    sig_payload = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(secret.encode(), sig_payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, received)

Retry schedule

If your endpoint returns a non-2xx response or times out (10s), Kerabie retries with exponential backoff. After 5 failures the delivery is marked dead and no further retries occur. Dead deliveries appear in the Webhooks page's delivery log.

AttemptDelay after previous failureCumulative time
1Immediate0s
21 minute1 min
32 minutes3 min
44 minutes7 min
58 minutes15 min
Dead—15 min+

Your endpoint must respond within 10 seconds. For slow processing, respond immediately with 200 and handle the payload asynchronously.

Integration examples

email.received→Slack

Post new email to #sales channel with sender, subject, and preview

email.received→CRM

Attach email to contact record; group by thread_id for conversation view

email.received→AI auto-responder

Use preview + thread history to draft and send a reply via the API

email.received→Notion / Airtable

Log incoming leads to a database row

email.opened→Mobile push

"John opened your email" — close the feedback loop on important messages

email.bounced→Mailing list tool

Remove bounced addresses from future campaigns automatically

domain.verified→Onboarding flow

Trigger next onboarding step once DNS is confirmed

mailbox.quota_reached→Alert system

Notify admin before the mailbox stops receiving email