Webhooks

Receive signed HTTP callbacks for email lifecycle events.

Base URL: https://api.ranla.ai

Manage endpoints in the dashboard (Webhooks) or via the API / SDK / CLI below.


Authentication

Authorization: Bearer rnl_…

Dashboard session cookies also work for browser requests to /api/webhooks.


Supported events

Event When fired
email.received An inbound email arrived on a receive-enabled domain; retrieve full content from /received-emails/{id}
email.sent Ranla accepted the message for delivery
email.delivered The message was delivered to the recipient mailbox provider
email.delivery_delayed Delivery was delayed or temporarily held
email.bounced Hard/soft bounce; recipient is auto-suppressed
email.complained Recipient marked the message as spam/complaint
email.opened Open pixel loaded when open tracking is on
email.clicked Tracked link clicked when click tracking is on
email.failed Delivery failed; permanent SMTP failures also auto-suppress the recipient
email.suppressed Send blocked because a recipient is on the suppression list
email.scheduled Email accepted with scheduled_at
contact.unsubscribed Recipient completed a managed unsubscribe. data.scope is all (global suppression) or category (product/newsletter opt-out only)
automation.started An active automation matched an incoming event and a run was created
automation.step_completed One automation step completed successfully
automation.completed A run finished all steps (or ended early on a false condition)
automation.failed A run failed (send failure, timeout, validation, etc.)

Open/click tracking are domain settings (open_tracking / click_tracking) configured per domain.


Create endpoint

POST /webhooks

{
  "url": "https://yourapp.com/webhooks/supersendtx",
  "events": ["email.delivered", "email.bounced"]
}

events is optional — defaults to all supported events.

200 — Created

{
  "webhook": {
    "id": "clx…",
    "url": "https://yourapp.com/webhooks/supersendtx",
    "events": ["email.delivered", "email.bounced"],
    "enabled": true,
    "created_at": "2026-07-25T12:00:00.000Z",
    "updated_at": "2026-07-25T12:00:00.000Z"
  },
  "secret": "whsec_…"
}

Copy secret immediately — it is only returned once. Use it to verify incoming webhook signatures.


List endpoints

GET /webhooks

Cursor pagination: limit, cursor{ webhooks, has_more, next_cursor }.


Retrieve endpoint

GET /webhooks/{id}

{
  "webhook": {
    "id": "clx…",
    "url": "https://yourapp.com/webhooks/supersendtx",
    "events": ["email.delivered", "email.bounced"],
    "enabled": true,
    "created_at": "2026-07-25T12:00:00.000Z",
    "updated_at": "2026-07-25T12:00:00.000Z"
  }
}

Update endpoint

PATCH /webhooks/{id}

{
  "url": "https://yourapp.com/new-path",
  "events": ["email.bounced"],
  "enabled": false
}

All fields are optional.


Delete endpoint

DELETE /webhooks/{id}

{ "ok": true }

Test sink

POST /emails/test (full-scope API key)

{ "event": "email.bounced", "email_id": "msg_…", "deliver": true }

Builds a webhook payload (optionally for an existing email) and, when deliver is not false, enqueues delivery to your subscribed endpoints. Useful in CI.


Incoming webhook payload

Ranla POSTs JSON to your endpoint URL when a subscribed event occurs.

Email event example

{
  "type": "email.received",
  "created_at": "2026-07-25T12:05:00.000Z",
  "data": {
    "email_id": "inb_abc123…",
    "to": ["[email protected]"],
    "from": "Sender <[email protected]>",
    "subject": "Hello",
    "status": "received",
    "rcpt_to": "[email protected]",
    "cc": [],
    "message_id": "<[email protected]>",
    "in_reply_to": null,
    "references": [],
    "spam_status": "NotSpam",
    "attachment_count": 1,
    "attachments": [
      {
        "filename": "invoice.pdf",
        "content_type": "application/pdf",
        "size": 48291
      }
    ],
    "received_at": "2026-07-25T12:04:59.000Z"
  }
}

For inbound events, attachment metadata is included but attachment bytes are not — fetch /received-emails/{id} for full bodies and base64 attachment data.

For bounces, type is email.bounced, status is bounced, and bounce_reason may contain a provider reason string.

Automation event example

{
  "type": "automation.step_completed",
  "created_at": "2026-07-26T12:05:00.000Z",
  "data": {
    "automation_id": "a1b2c3",
    "automation_name": "Welcome flow",
    "run_id": "r1s2t3",
    "status": "running",
    "event_name": "user.created",
    "step_index": 0,
    "step_type": "send_email",
    "error": null
  }
}

Contact unsubscribe examples

Global unsubscribe (scope: "all") adds the address to suppressions and blocks every send:

{
  "type": "contact.unsubscribed",
  "created_at": "2026-08-02T12:05:00.000Z",
  "data": {
    "email": "[email protected]",
    "source": "unsubscribe",
    "scope": "all",
    "category": null,
    "category_label": null,
    "suppression_id": "sup_…",
    "category_opt_out_id": null,
    "email_id": "msg_…"
  }
}

Category opt-out (scope: "category") stops product or newsletter mail only — transactional messages still send:

{
  "type": "contact.unsubscribed",
  "created_at": "2026-08-02T12:05:00.000Z",
  "data": {
    "email": "[email protected]",
    "source": "unsubscribe",
    "scope": "category",
    "category": "product",
    "category_label": "product emails",
    "suppression_id": null,
    "category_opt_out_id": "opt_…",
    "email_id": "msg_…"
  }
}

Check data.scope (and data.category when scope is category) before treating the address as fully suppressed.


Signature verification

Each delivery includes:

SuperSendTX-Signature: t=1721908800,v1=abc123…

SDK helper (preferred)

import { Ranla, verifyWebhookSignature } from '@supersend/ranla'

const ok = verifyWebhookSignature(secret, rawBody, request.headers.get('supersendtx-signature'))
// or:
const client = new Ranla(process.env.RANLA_API_KEY!)
client.webhooks.verify(secret, rawBody, header)

Verify by:

  1. Read the raw request body as a string (before JSON parsing).
  2. Parse t (Unix timestamp) and v1 (hex HMAC) from the header.
  3. Reject if t is more than 5 minutes from your server clock.
  4. Compute HMAC-SHA256(secret, "${t}.${rawBody}") and compare to v1 (constant-time).

Return 2xx quickly from your handler. Failed deliveries are retried by Bull at 0s, 5s, 30s, 2m, 2m (persisted in Redis via the supersendtx:webhook-delivery queue). Run the worker:webhooks process alongside the app.


SDK

import { Ranla } from '@supersend/ranla'

const client = new Ranla(process.env.RANLA_API_KEY!)

const { webhook, secret } = await client.webhooks.create({
  url: 'https://yourapp.com/webhooks/supersendtx',
})

const { webhooks } = await client.webhooks.list()
const one = await client.webhooks.get(webhooks[0].id)
await client.webhooks.update(one.webhook.id, { enabled: false })
await client.webhooks.delete(webhooks[0].id)

CLI

supersendtx webhooks list
supersendtx webhooks create --url https://yourapp.com/hooks
supersendtx webhooks delete --id clx…

Requires RANLA_API_KEY or --api-key.


OpenAPI

Machine-readable spec: openapi/supersendtx.yaml