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:
- Read the raw request body as a string (before JSON parsing).
- Parse
t(Unix timestamp) andv1(hex HMAC) from the header. - Reject if
tis more than 5 minutes from your server clock. - Compute
HMAC-SHA256(secret, "${t}.${rawBody}")and compare tov1(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