FLAM

Email

2 Email routes on the FLAM API: PostHog triggers a drip, FLAM sends it; Resend delivery webhook (standard-webhooks signature).

Base URL https://api.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 bodyapplication/json (required)

FieldTypeRequiredNotes
template"drip-day3" | "develop-interrupted" | "look-waiting"yes
emailstring (email)yes
dedupeKeystringyes
namestringnodrip-day3 opens with it; absent falls back to 'there'.
{
  "template": "drip-day3",
  "email": "string",
  "dedupeKey": "string",
  "name": "string"
}

Responses

StatusMeaning
200{ sent: true, template, status } or { sent: false, reason: 'ALREADY_SENT' }
400BAD_REQUEST — unknown template, bad address, or no dedupeKey
401UNAUTHORIZED — wrong or missing bearer secret
503DRIP_NOT_CONFIGURED — DRIP_WEBHOOK_SECRET is unset

Call it

curl -X POST "https://api.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 bodyapplication/json (required)

{}

Responses

StatusMeaning
200Recorded ({ ok, status }), unmatched ({ ok, matched:false }) or ignored ({ ok, ignored })
400UNPARSEABLE
401BAD_SIGNATURE

Call it

curl -X POST "https://api.flam.fashion/api/toolkit/email/webhook/resend" \
  -H "Authorization: Bearer $FLAM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'