Event triggers
Fire a template automatically when a reported event matches — with rate limits, quiet hours, dedup, and dynamic per-recipient attachments.
An event trigger is the only “event happened, send an email” mechanism in Retidal. There is no general-purpose “send this one email to this one address” API — for a one-off send to a specific person, use a campaign whose audience filter matches exactly one user.
Authentication for every endpoint on this page is the console session cookie — see Providers § Authentication. Writes need EDITOR+.
The event pipeline needs identify, not just track
A trigger matches on the identified user behind an event. If your application only calls the
event-ingestion endpoint and never calls
POST /api/v1/identify with that user’s email trait, the trigger log will
show every match skipped as no_identified_user (never identified) or no_email (identified, but no email
trait) — and you will get no error anywhere else. Identify the user (with traits.email set) before
the event you want to trigger on, not after.
Create a trigger
Section titled “Create a trigger”POST https://retidal.com/api/projects/{projectId}/email/triggers{ "name": "Welcome on sign-up", "eventName": "sign_up", "match": { "properties": { "plan": "pro" }, "userFilter": { "hasEmail": true, "locale": "zh-CN" } }, "templateId": "tpl_x", "providerId": "prov_x", "category": "transactional", "enabled": true, "maxSendsPerWindow": 1, "windowSeconds": 86400, "quietHoursStartHour": 22, "quietHoursEndHour": 8, "quietHoursMode": "defer", "dedupeWindowSeconds": 3600}namestringrequired1–120 characters.
eventNamestringrequired1–200 characters. Must match the eventName your application reports to
POST /api/v1/t exactly.
templateIdstringrequiredMust belong to the same project.
providerIdstringChannel to send from. Nullable — omit to let routing pick one by category.
identityIdstringA specific sending identity to send as, nullable.
categorystringtransactional | marketing. Default transactional. Marketing triggers are blocked until
compliance settings are configured.
enabledbooleanDefault true.
matchobjectSee below. additionalProperties is disallowed — an unrecognized key is a 400, not a silent no-op.
match has exactly three possible keys:
| Key | Purpose |
|---|---|
properties |
Exact-match against the triggering event’s own properties object. |
conditionTree |
A nested (2-level) any/all fact-rule tree for conditions properties alone can’t express. Nullable. |
userFilter |
Audience/segment filter — same shape as a campaign’s audienceFilter (hasEmail, locale, paidStatus, valueTier, queueKey, and more — see Campaigns § audienceFilter for the full field list). |
Guardrails
Section titled “Guardrails”| Field | Range | Default | Purpose |
|---|---|---|---|
maxSendsPerWindow |
1–100,000, nullable | unset | Cap sends to the same user within windowSeconds. |
windowSeconds |
1–31,536,000 | 86400 | Window the send cap above applies to. |
quietHoursStartHour / quietHoursEndHour |
0–23, nullable | unset | Hour range (project-local) to hold sends. Set both together or neither. |
quietHoursMode |
defer |
drop |
defer |
dedupeWindowSeconds |
0–31,536,000 | 3600 | Suppresses a second send for the same (user, trigger) pair inside this window. |
Guardrails are evaluated in this order — suppression list → rate cap → quiet hours → dedup — and the
trigger log’s skipReason names whichever one stopped the send.
Dynamic attachments
Section titled “Dynamic attachments”"dynamicAttachments": [ { "fromProperty": "invoice_url", "filename": "invoice.pdf" }]fromPropertystringrequired1–120 characters. The property name on the triggering event that holds a URL — read at send time, not stored ahead of time.
filenamestring1–200 characters. Falls back to a name inferred from the URL if omitted.
Up to 3 dynamicAttachments entries per trigger. This is how “send an invoice after payment” works end
to end: your application includes an invoice_url property on the payment_succeeded event it reports, the
trigger is configured with dynamicAttachments: [{ fromProperty: "invoice_url" }], and — critically — that
URL’s domain has to already be on the
dynamic-attachment allowlist, or the fetch fails
closed and the attachment silently doesn’t go out (the email itself still sends).
If fromProperty is missing from the event, or isn’t a string, that one attachment is marked
attachment_fetch_failed on the sent message — the rest of the email still sends.
Debugging why a trigger didn’t fire
Section titled “Debugging why a trigger didn’t fire”GET /api/projects/{projectId}/email/triggers/{id}/logReturns the latest 50 evaluation records with status/skipReason. Work through these in order:
- No log entries at all — the
eventNamedoesn’t match what’s actually being reported, or the trigger isenabled: false, or the event was never reported (check with the console analytics/events query). no_identified_user— the event’suserIdwas never passed toPOST /api/v1/identify.no_email— identified, but thetraits.emailfield was never set.no_match—properties/conditionTree/userFilterdidn’t match this event or this user.suppressed— the recipient is on the suppression list for a reason that blocks this category.capped— hitmaxSendsPerWindow.quiet— inside the quiet-hours window (deferred or dropped perquietHoursMode).deduped— insidededupeWindowSecondsof a previous send to the same user.
If none of those explain it, check the channel itself: GET /api/projects/{projectId}/email/providers to confirm the providerId
isn’t a log-driver channel (which “sends” but delivers nothing), and GET /api/projects/{projectId}/email/messages?limit=20 for the
actual send status of any message that did get created.