Developer Platform
Docs chevron_right Developer Platform chevron_right Webhooks

Webhooks

Get an HTTPS POST the moment things happen — deliveries, opens, clicks, bounces, complaints, subscribes, and unsubscribes — instead of polling the analytics feed. Every delivery is signed so your receiver can prove it came from EmailFlow AI.

The event catalog

Subscribe each endpoint to exactly the events it cares about. Event names are frozen — they never change meaning; new events are only ever added.

EventFires when
email.deliveredA message is handed to the recipient's mail server.
email.failedA message could not be sent.
email.openedA recipient opens a tracked message.
email.clickedA recipient clicks a tracked link.
email.bouncedA message bounces (the metadata carries the bounce type).
email.complainedA recipient files a spam or abuse report.
contact.subscribedA contact subscribes to a list (bulk imports deliberately do not fire it).
contact.unsubscribedA contact unsubscribes.

The delivery payload

Every delivery is a JSON POST. id is unique per delivery — store it to deduplicate retries. data carries the campaign, automation, contact, and list identifiers when they apply, plus event-specific meta (clicked URL, bounce type, IP address, user agent, message id):

POST https://example.com/hooks/emailflow
X-EmailFlow-Signature: t=1718200000,v1=5257a869e7...

{
  "id": "64a91b2c7de31",
  "event": "email.clicked",
  "occurred_at": "2026-06-12T09:15:02+00:00",
  "data": {
    "campaign_uid": "ab12cd34ef",
    "subscriber_email": "alice@example.com",
    "list_uid": "cd34ef56ab",
    "meta": { "url": "https://acme.com/promo", "ip_address": "198.51.100.7" }
  }
}

Verifying signatures

When you create a subscription, the response includes a secret — shown exactly once at creation, never in any later response, so store it immediately. Every delivery then carries an X-EmailFlow-Signature header in the form t=<unix timestamp>,v1=<signature> where the signature is HMAC-SHA256(secret, t + "." + raw_body). To verify:

  1. Split the header into t and v1.
  2. Compute HMAC-SHA256(secret, t + "." + raw_body) over the raw request body (before any JSON parsing).
  3. Compare against v1 with a constant-time comparison.
  4. Reject if t is older than your tolerance (5 minutes is typical) to block replay attacks.
// Node.js — verify a delivery
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(header, rawBody, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSeconds) return false;
  const expected = createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');
  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

A complete, type-checked version of this receiver ships in the SDK cookbook.

Retries and auto-disable

A delivery succeeds when your endpoint answers any 2xx within the timeout. Anything else is retried up to 3 attempts with backoff of 30 seconds, 5 minutes, then 30 minutes. Respond fast — acknowledge first, process asynchronously.

When all attempts for a delivery fail, the subscription gains a failure strike; any success resets the counter. At 20 consecutive failed deliveries the subscription is automatically disabled, and you get an in-app notification plus an email. Fix your receiver, then re-enable from the UI or with PATCH {"active": true} — re-enabling clears the strike counter. Delivery logs are retained for 30 days.

Managing subscriptions

Manage subscriptions over the API (scope: webhooks) or in the app under Account → API → Webhooks — the UI includes a deliveries drawer with per-attempt status, response codes, and durations.

emailflow.ai/rui/account/api
The Webhooks tab on the Account API page showing a subscriptions table with endpoint URLs, subscribed events, status badges, and a recent-deliveries view with per-attempt response codes
Account → API → Webhooks — subscriptions with status, and per-delivery attempt logs.

Endpoint URLs must be HTTPS. Each subscription is limited to your account's data — events from other tenants can never reach your endpoint.

MethodEndpointDescription
GET/webhooksAll subscriptions on the account.
POST/webhooksCreate a subscription. Required: url (HTTPS), events[]. Returns 201 with the signing secret — shown once.
GET/webhooks/{uid}One subscription with status and failure counter.
PATCH/webhooks/{uid}Update url, events, or active. Setting active: true on an auto-disabled subscription re-enables it.
DELETE/webhooks/{uid}Delete the subscription and its delivery logs.
POST/webhooks/{uid}/testSend a signed synchronous ping event and return the live result: status, response code, duration.
GET/webhooks/{uid}/deliveriesPaginated delivery log, newest first. Filter with status=.
# Subscribe an endpoint to engagement events
curl -X POST https://emailflow.ai/api/v1/webhooks \
  -H "Authorization: Bearer efa_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/emailflow",
    "events": ["email.opened", "email.clicked", "email.bounced"]
  }'

# Response (201) — copy the secret now; it is never shown again
{
  "data": {
    "uid": "7bc2e91d4af52",
    "url": "https://example.com/hooks/emailflow",
    "events": ["email.opened", "email.clicked", "email.bounced"],
    "active": true,
    "secret": "b41c0f8a2e..."
  }
}

# Fire a test ping and see the live result
curl -X POST https://emailflow.ai/api/v1/webhooks/7bc2e91d4af52/test \
  -H "Authorization: Bearer efa_YOUR_KEY"