BETA
Skip to content

Webhooks ​

Receive real-time notifications when email events occur. Krafter Mail sends signed HTTP POST requests to your configured endpoint for delivery, bounce, open, click, complaint, and failure events.

Base URL: https://app.krafter.dev/api/v1

All webhook management endpoints live under /api/v1/mail/webhooks/* and require the mail:webhooks scope. Signing, retry policy, and auto-disable rules are shared across the platform — see Webhook delivery.

INFO

The legacy scope name webhooks:manage is still accepted on these endpoints for older keys, but the canonical name surfaced in the dashboard is mail:webhooks. New keys should use mail:webhooks.

Create Webhook ​

Registers a new webhook endpoint. The signing secret is generated server-side and returned only on creation and when rotated. Store it securely — you need it to verify webhook signatures.

POST /mail/webhooks

Required scope: mail:webhooks

Request Body ​

FieldTypeRequiredDescription
urlstringYesThe HTTPS URL that will receive webhook events.
eventsstring[]YesThe event types to subscribe to (see Available Events).
enabledbooleanNoWhether the webhook is active. Defaults to true.

Available Events ​

The dispatcher emits 7 mail events:

EventTriggered when
email.sentSend was accepted by the upstream provider (Amazon SES).
email.deliveredDelivery confirmed by the recipient's server.
email.openedRecipient opened the email.
email.clickedRecipient clicked a tracked link.
email.bouncedEmail bounced (hard or soft).
email.complainedRecipient marked the email as spam.
email.failedSend failed (post-retry, non-retryable).

Example Request ​

bash
curl -X POST https://app.krafter.dev/api/v1/mail/webhooks \
  -H "Authorization: Bearer kr_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.yourapp.com/webhooks/email",
    "events": ["email.delivered", "email.bounced", "email.opened", "email.clicked"]
  }'

Example Response ​

json
// 201 Created
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "url": "https://api.yourapp.com/webhooks/email",
    "events": ["email.delivered", "email.bounced", "email.opened", "email.clicked"],
    "enabled": true,
    "secret": "k9j8h7g6f5e4d3c2b1a0z9y8x7w6v5u4ABCDEFGhijklmn",
    "failure_count": 0,
    "last_triggered_at": null,
    "created_at": "2026-01-15T10:00:00Z",
    "updated_at": "2026-01-15T10:00:00Z"
  }
}

WARNING

The secret is returned only on creation and when rotated. Save it immediately and store it securely — Krafter cannot show it again.


List Webhooks ​

Returns every webhook owned by the authenticated team.

GET /mail/webhooks

Required scope: mail:webhooks

Example Request ​

bash
curl https://app.krafter.dev/api/v1/mail/webhooks \
  -H "Authorization: Bearer kr_live_abc123def456"

Example Response ​

json
{
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "url": "https://api.yourapp.com/webhooks/email",
      "events": ["email.delivered", "email.bounced", "email.opened", "email.clicked"],
      "enabled": true,
      "failure_count": 0,
      "last_triggered_at": "2026-01-15T11:42:18Z",
      "created_at": "2026-01-15T10:00:00Z",
      "updated_at": "2026-01-15T10:00:00Z"
    }
  ]
}

The secret field is omitted from list and detail responses. Use Rotate Secret to obtain a new value.


Get Webhook ​

Returns a single webhook by ID.

GET /mail/webhooks/:id

Required scope: mail:webhooks

Example Request ​

bash
curl https://app.krafter.dev/api/v1/mail/webhooks/550e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer kr_live_abc123def456"

Example Response ​

json
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "url": "https://api.yourapp.com/webhooks/email",
    "events": ["email.delivered", "email.bounced", "email.opened", "email.clicked"],
    "enabled": true,
    "failure_count": 0,
    "last_triggered_at": "2026-01-15T11:42:18Z",
    "created_at": "2026-01-15T10:00:00Z",
    "updated_at": "2026-01-15T10:00:00Z"
  }
}

Update Webhook ​

Updates an existing webhook. Only the fields you include in the request body are changed.

PATCH /mail/webhooks/:id

Required scope: mail:webhooks

Request Body ​

FieldTypeDescription
urlstringThe HTTPS URL to receive events.
eventsstring[]Event types to subscribe to.
enabledbooleanWhether the webhook is active.

Example Request ​

bash
curl -X PATCH https://app.krafter.dev/api/v1/mail/webhooks/550e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer kr_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "events": ["email.delivered", "email.bounced", "email.opened", "email.clicked", "email.complained"]
  }'

Example Response ​

json
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "url": "https://api.yourapp.com/webhooks/email",
    "events": ["email.delivered", "email.bounced", "email.opened", "email.clicked", "email.complained"],
    "enabled": true,
    "failure_count": 0,
    "last_triggered_at": "2026-01-15T11:42:18Z",
    "created_at": "2026-01-15T10:00:00Z",
    "updated_at": "2026-01-15T12:00:00Z"
  }
}

Delete Webhook ​

Permanently deletes a webhook.

DELETE /mail/webhooks/:id

Required scope: mail:webhooks

Example Request ​

bash
curl -X DELETE https://app.krafter.dev/api/v1/mail/webhooks/550e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer kr_live_abc123def456"

Example Response ​

204 No Content

Test Webhook ​

Sends a synthetic email.delivered event to the webhook URL to verify connectivity. The delivery includes the standard webhook headers plus x-krafter-test: true so receivers can ignore tests entirely.

POST /mail/webhooks/:id/test

Required scope: mail:webhooks

Example Request ​

bash
curl -X POST https://app.krafter.dev/api/v1/mail/webhooks/550e8400-e29b-41d4-a716-446655440000/test \
  -H "Authorization: Bearer kr_live_abc123def456"

Success Response ​

json
// 200 OK
{
  "data": {
    "webhook_id": "550e8400-e29b-41d4-a716-446655440000",
    "test_sent": true,
    "status_code": 200,
    "message": "Test event delivered to https://api.yourapp.com/webhooks/email"
  }
}

Failure Response ​

If the test delivery fails to reach your endpoint, the response is 502 Bad Gateway with a nested error object:

json
// 502 Bad Gateway
{
  "error": {
    "message": "Test event delivery failed",
    "detail": "timeout"
  }
}

INFO

This nested {error: {message, detail}} shape is unique to this endpoint. Every other Mail endpoint follows one of the four error shapes documented in Error Handling.


Rotate Secret ​

Generates a new signing secret. The old secret is invalidated immediately. The new secret is returned in the response — store it securely.

POST /mail/webhooks/:id/rotate-secret

Required scope: mail:webhooks

Example Request ​

bash
curl -X POST https://app.krafter.dev/api/v1/mail/webhooks/550e8400-e29b-41d4-a716-446655440000/rotate-secret \
  -H "Authorization: Bearer kr_live_abc123def456"

Example Response ​

json
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "url": "https://api.yourapp.com/webhooks/email",
    "events": ["email.delivered", "email.bounced"],
    "enabled": true,
    "secret": "n3w5ecr3tk3y1a2b3c4d5e6f7g8h9i0jABCDEFGhijklmn",
    "failure_count": 0,
    "last_triggered_at": "2026-01-15T11:42:18Z",
    "created_at": "2026-01-15T10:00:00Z",
    "updated_at": "2026-01-15T13:00:00Z"
  }
}

Webhook Delivery ​

When an email event occurs, Krafter Mail sends an HTTP POST request to your endpoint. The body is signed with HMAC-SHA256 over the raw bytes of the request body using your webhook secret.

Request Headers ​

Every delivery (real and test) carries these request headers (HTTP header names are case-insensitive; Krafter emits them in lowercase):

HeaderDescription
content-typeapplication/json
x-krafter-eventThe event name, e.g. email.delivered.
x-krafter-signaturet=<unix>,v1=<hex> — HMAC-SHA256 of <t>.<body>; see Verifying signatures, keyed with the webhook secret.
x-krafter-webhook-idUUID generated fresh on every delivery attempt. Receivers should dedupe on this.
x-krafter-testtrue — present only on deliveries from POST /mail/webhooks/:id/test.

INFO

x-krafter-webhook-id is regenerated on each retry: the same logical event arrives with a different x-krafter-webhook-id on each attempt. Use it to deduplicate delivery attempts; correlate the same logical event by the payload.email_id in the body.

Payload Envelope ​

Every real delivery wraps the event-specific fields in this top-level envelope:

json
{
  "event": "email.delivered",
  "timestamp": "2026-01-15T10:30:02Z",
  "payload": {
    "email_id": "550e8400-e29b-41d4-a716-446655440000",
    "from": "billing@yourdomain.com",
    "to": ["alice@example.com"],
    "subject": "Your invoice is ready"
  }
}

payload.to is always an array (matching how the email was created). payload.email_id is the UUID of the underlying emails row.

INFO

The synthetic event from POST /mail/webhooks/:id/test uses the same envelope but with a placeholder email_id of "test_<uuid>" and the x-krafter-test: true header. Receivers that branch on x-krafter-test can ignore tests entirely.

Example Payloads ​

email.delivered

json
{
  "event": "email.delivered",
  "timestamp": "2026-01-15T10:30:02Z",
  "payload": {
    "email_id": "550e8400-e29b-41d4-a716-446655440000",
    "from": "billing@yourdomain.com",
    "to": ["alice@example.com"],
    "subject": "Your invoice is ready"
  }
}

email.opened

json
{
  "event": "email.opened",
  "timestamp": "2026-01-15T10:31:15Z",
  "payload": {
    "email_id": "550e8400-e29b-41d4-a716-446655440000",
    "to": ["alice@example.com"],
    "subject": "Your invoice is ready"
  }
}

email.clicked

json
{
  "event": "email.clicked",
  "timestamp": "2026-01-15T10:31:22Z",
  "payload": {
    "email_id": "550e8400-e29b-41d4-a716-446655440000",
    "to": ["alice@example.com"],
    "subject": "Your invoice is ready",
    "url": "https://app.example.com/invoices/1042"
  }
}

email.bounced

json
{
  "event": "email.bounced",
  "timestamp": "2026-01-15T10:30:30Z",
  "payload": {
    "email_id": "550e8400-e29b-41d4-a716-446655440000",
    "from": "billing@yourdomain.com",
    "to": ["alice@example.com"],
    "subject": "Your invoice is ready",
    "bounce_type": "Permanent"
  }
}

email.failed

json
{
  "event": "email.failed",
  "timestamp": "2026-01-15T10:30:00Z",
  "payload": {
    "email_id": "550e8400-e29b-41d4-a716-446655440000",
    "from": "billing@yourdomain.com",
    "to": ["alice@example.com"],
    "subject": "Your invoice is ready",
    "error": "Permanent SMTP failure"
  }
}

Retry Policy ​

Webhook deliveries are dispatched as Oban jobs with max_attempts: 5 and exponential back-off. After repeated consecutive failures the webhook is automatically disabled (enabled: false); re-enable it via Update Webhook. Successful (2xx) deliveries reset the consecutive-failure counter. Use x-krafter-webhook-id to deduplicate per-attempt — the header is fresh on every retry of the same logical event.


Signature Verification ​

Every delivery made with a webhook secret carries:

x-krafter-signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>

The timestamp is part of the signed payload, so a captured request stops verifying once t goes stale — and a replayer cannot edit t to buy time without invalidating v1.

Verify in three steps: split the header into t and v1, reject anything older than your tolerance (300 seconds recommended), then compare HMAC-SHA256(secret, "<t>." + raw_body) to v1 in constant time. raw_body must be the exact bytes received — re-serialising parsed JSON will not match.

Node.js, Elixir and Python implementations are on Verifying signatures, which is the one place this recipe lives for every Krafter service.

Best Practices ​

  • Always verify the signature before processing the event. Reject unsigned or mismatched requests.
  • Use raw bytes, not a re-serialized JSON. Re-encoding can change byte ordering and break the signature.
  • Respond with 2xx quickly. Process events asynchronously if needed.
  • Deduplicate per attempt on x-krafter-webhook-id (regenerated per retry).
  • Use HTTPS only. HTTP endpoints are rejected at create time.
  • Rotate secrets periodically with Rotate Secret.

Built by Krafter Studio