2 Email routes on the FLAM API: PostHog triggers a drip, FLAM sends it; Resend delivery webhook (standard-webhooks signature).
Base URL https://api-staging.flam.fashion. Send Authorization: Bearer flam_sk_… on every call; a handful of routes are session-only and say so. How keys and roles work.
POST /api/toolkit/email/drip
PostHog triggers a drip, FLAM sends it
PostHog decides WHEN a lifecycle email goes out; this renders and sends WHAT. Before this, PostHog's own email step sent copies of the templates that had been retyped into its drag-and-drop builder — measured 3 Aug 2026, all three carried zero <img> tags, so every one arrived with no FLAM masthead. Now there is one rendering pipeline: the send lands in email_log with the exact HTML and the Resend delivery webhook updates that row, so a bounce is visible.
PUBLIC, authenticated by Authorization: Bearer <DRIP_WEBHOOK_SECRET> — a PostHog workflow HTTP destination cannot sign a body the way Dodo and Resend do, so the shared secret is what the caller can actually produce. Compared in constant time, and it fails CLOSED (503) when the secret is unset.
dedupeKey is REQUIRED and the caller names the unit, because only drip-day3 is once-per-person — develop-interrupted and look-waiting should send again the next time a develop dies. Use drip-day3:{{ person.id }} and develop-interrupted:{{ event.uuid }}. A repeat inserts nothing and answers 200 { sent: false, reason: 'ALREADY_SENT' } rather than 409, because PostHog retries non-2xx and a retry storm against a correctly-behaving endpoint is the failure mode.
template is an allowlist of the three lifecycle letters, never all of @flam/email: whoever holds this secret must not be able to post somebody a sign-in link.
Request body — application/json (required)
| Field | Type | Required | Notes |
|---|---|---|---|
template | "drip-day3" | "develop-interrupted" | "look-waiting" | yes | — |
email | string (email) | yes | — |
dedupeKey | string | yes | — |
name | string | no | drip-day3 opens with it; absent falls back to 'there'. |
{
"template": "drip-day3",
"email": "string",
"dedupeKey": "string",
"name": "string"
}Responses
| Status | Meaning |
|---|---|
200 | { sent: true, template, status } or { sent: false, reason: 'ALREADY_SENT' } |
400 | BAD_REQUEST — unknown template, bad address, or no dedupeKey |
401 | UNAUTHORIZED — wrong or missing bearer secret |
503 | DRIP_NOT_CONFIGURED — DRIP_WEBHOOK_SECRET is unset |
Call it
curl -X POST "https://api-staging.flam.fashion/api/toolkit/email/drip" \
-H "Authorization: Bearer $FLAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"template":"drip-day3","email":"string","dedupeKey":"string","name":"string"}'POST /api/toolkit/email/webhook/resend
Resend delivery webhook (standard-webhooks signature)
Delivery truth. email_log.status is written once at send time — 'sent' means Resend accepted it, not that a director read it — so without this a hard bounce is indistinguishable from a delivered mail. Public and unauthenticated, authenticated by the webhook-id / webhook-timestamp / webhook-signature headers (the same verifier the Dodo webhook uses), and it fails CLOSED when RESEND_WEBHOOK_SECRET is unset. Correlates on provider_id (Resend's message id, echoed back as data.email_id). email.bounced and email.complained also raise an alert; events that say nothing about arrival answer 200 and write nothing.
Request body — application/json (required)
{}Responses
| Status | Meaning |
|---|---|
200 | Recorded ({ ok, status }), unmatched ({ ok, matched:false }) or ignored ({ ok, ignored }) |
400 | UNPARSEABLE |
401 | BAD_SIGNATURE |
Call it
curl -X POST "https://api-staging.flam.fashion/api/toolkit/email/webhook/resend" \
-H "Authorization: Bearer $FLAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'