Providers
Configure sending channels (resend, aliyun), sending domains, and ESP bounce/complaint webhooks.
Email in Retidal is organized around channels (called providers in the API) — a channel is one sending
configuration: a driver (resend, aliyun, or the log no-op), a fromAddress, and rate limits. A project
can have more than one channel; templates and triggers pick a channel by providerId (or let the project’s
routing rules choose one).
Only two drivers actually send mail
The kind enum accepts six values — log, resend, sendgrid, mailgun, aliyun, cf_email — but
only resend and aliyun have a real delivery driver behind them. sendgrid, mailgun, and
cf_email are accepted by validation and silently fall back to the log driver (records the send,
delivers nothing) at send time. log is the intentional no-op safety net, not a placeholder for
the others. There is no SMTP driver.
Authentication
Section titled “Authentication”Every endpoint on this page is a console (Management API) route — authenticated with the
trackly_session session cookie from a logged-in console user, not a project API key. See
Authentication for the full session-vs-key model. Write operations (create/update a
channel, set a webhook secret, rotate a webhook ID) require project role EDITOR+; reads require any project
role.
List and create channels
Section titled “List and create channels”GET https://retidal.com/api/projects/{projectId}/email/providersPOST https://retidal.com/api/projects/{projectId}/email/providersGET never returns secret material — the response has a configConfigured boolean instead of the actual
config object.
{ "name": "Primary channel", "kind": "resend", "fromAddress": "noreply@yourdomain.com", "replyToAddress": "support@yourdomain.com", "config": { "apiKey": "re_..." }, "rateLimitPerSecond": 10, "dailyLimit": 10000}{ "name": "Aliyun channel", "kind": "aliyun", "fromAddress": "noreply@yourdomain.com", "config": { "accessKeyId": "...", "accessKeySecret": "...", "accountName": "...", "regionId": "cn-hangzhou" }}namestringrequired1–120 characters.
kindstringrequiredresend | aliyun | log for a real configuration. sendgrid | mailgun | cf_email validate but
send nothing (see warning above).
fromAddressstringrequiredMust be a valid email address.
replyToAddressstringOptional, nullable.
configobjectDriver-specific credential map. resend → { apiKey }. aliyun → { accessKeyId, accessKeySecret, accountName, regionId }.
log → {}. Secret fields are encrypted at rest (AES-256-GCM) and never echoed back.
statusstringactive | paused | disabled. Default active.
rateLimitPerSecondinteger1–100. Default 10. This is the only real send-rate limiter — there is no per-project daily quota beyond what you set here.
dailyLimitintegerOptional cap on total sends per day, nullable.
transactionalReservePercentinteger0–100, default 20. Percentage of the channel’s throughput reserved for transactional mail so a marketing campaign burst can’t starve password resets and receipts.
marketingDailyLimitintegerOptional separate daily cap that applies only to marketing-category sends on this channel, nullable.
PATCH /api/projects/{projectId}/email/providers/{id} does a partial update. Changing kind requires resubmitting the full
config for the new driver — a partial config on a kind change is rejected with 400.
Quota snapshot
Section titled “Quota snapshot”GET /api/projects/{projectId}/email/providers/{id}/quotaRead-only usage snapshot for the channel’s rate budget, plus a 24-hour aggregate of trigger sends skipped
for quota reasons (recentQuotaSkips). The snapshot can be null if the underlying rate-budget store is
unavailable — that’s a degraded response, not an error.
Sending domains
Section titled “Sending domains”GET /api/projects/{projectId}/email/domainsPOST /api/projects/{projectId}/email/domains { "domain": "mail.yourdomain.com" }GET /api/projects/{projectId}/email/domains/{domainId}/overviewPOST /api/projects/{projectId}/email/domains/{id}/checkDomain verification is not automated yet
POST /api/projects/{projectId}/email/domains/{id}/check currently always returns 409 — there is no automatic SPF/DKIM
verification. Check your sending domain’s DNS records directly in your ESP’s dashboard (Resend or Aliyun
console) instead of relying on this endpoint to confirm anything.
ESP bounce/complaint webhooks
Section titled “ESP bounce/complaint webhooks”Each channel exposes a webhook URL, keyed by a per-channel webhookId:
https://mhook.withretide.com/webhook/{resend|aliyun}/{webhookId}Register this URL in your ESP’s dashboard (Resend or Aliyun), then give Retidal the signing secret the ESP issues for it:
POST /api/projects/{projectId}/email/providers/{id}/webhook-secret { "webhookSecret": "whsec_..." }POST /api/projects/{projectId}/email/providers/{id}/webhook-secret only accepts webhookSecret (1–500 characters) and only works
for resend/aliyun channels — any other kind returns 400. It stores the secret without requiring you
to resend the send credentials.
To rotate the URL itself (for example, if you suspect the webhook URL leaked), rotate the webhookId:
POST /api/projects/{projectId}/email/providers/{id}/webhook/rotate → { "webhookId": "new-id" }Rotating the webhookId does not update your ESP
The old webhookId stops routing immediately. You must manually update the callback URL in your ESP’s
dashboard after rotating — there’s no automated push to Resend/Aliyun.
Once the secret is set, inbound bounce/complaint events are verified (Svix signature check) and
automatically written to the suppression list. Until the secret is
set, the mhook.withretide.com webhook receiver returns a server error (not an auth error) for every
incoming callback — a deliberate choice so the ESP’s own retry logic keeps trying instead of giving up on
what looks like an auth failure.
Provider overview
Section titled “Provider overview”GET /api/projects/{projectId}/email/providers/{providerId}/overviewA read-only dashboard payload: topology, recent send/delivery time series, and an approximate deliverability rate. The deliverability numbers here are aggregated across every sending domain attached to the channel, so if you share a domain across channels the rate is not channel-exclusive.
Routing
Section titled “Routing”GET /api/projects/{projectId}/email/routingPOST /api/projects/{projectId}/email/routingRouting rules pick which channel handles a send by category (transactional vs marketing) when a
template or trigger doesn’t pin a providerId explicitly.
Next steps
Section titled “Next steps”Build the HTML/text content and {{variable}} placeholders a channel will send.
Fire a template automatically when an event matches — with rate limits, quiet hours, and dedup.