Forge Notifications
Open console

POST once.
We handle the rest.

A multi-tenant email API with idempotent sends, versioned templates, automatic retries and failover, and signed webhooks for delivery status.

Open console No setup on your end beyond a token.
POST /api/v1/notifications
curl -X POST "$BASE_URL/api/v1/notifications" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "channel": "email",
    "to": [{"email": "jane@example.com"}],
    "template_key": "order_confirmation",
    "variables": {"order_id": "12345"}
  }'

< 202 Accepted
{"data": {"id": "ntf_550e8400...", "status": "queued"}}

What happens after your POST

Accepted for delivery is not delivered. The final outcome arrives by webhook, or you can poll for it.

1

Your app

Sends the request

2

Validate

Checks the payload, renders any template

3

Queue

Persisted, status: queued

4

SMTP

Delivered via the tenant's transport, with failover

5

Webhook

Signed callback on sent or failed

6

Or poll

GET the notification for its status

Built for the parts that usually go wrong

Idempotent by design

Send the same Idempotency-Key twice and the original response replays. Network retries are safe by default.

Versioned templates

Publishing a template creates a new version rather than overwriting it. Preview any change with /render before it goes live.

Tenant isolation

One token, one tenant. No cross-tenant reads, ever.

Automatic failover

Five retries with backoff. Each attempt can move to the tenant's next configured transport.

Signed webhooks

HMAC-SHA256, at-least-once, retried on non-2xx. Dedupe on the event id.

Verifying webhooks takes five lines

Every callback carries an X-Signature header. Recompute the HMAC over the timestamp and raw body, compare in constant time, and reject anything older than five minutes.

verify-webhook.php
[$tPart, $vPart] = explode(',', $request->header('X-Signature'));
$timestamp = substr($tPart, 2);
$signature = substr($vPart, 3);
$expected  = hash_hmac('sha256', $timestamp.'.'.$request->getContent(), $secret);

abort_unless(hash_equals($expected, $signature), 401);
abort_if(abs(time() - (int) $timestamp) > 300, 401);

Ask the platform owner for a token, then send.

An API token and a verified sender address are all you need to get started.

Open console