Skip to content

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.

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.

bash
GET https://retidal.com/api/projects/{projectId}/email/providers
POST https://retidal.com/api/projects/{projectId}/email/providers

GET 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
}
Create an aliyun channel
{
"name": "Aliyun channel",
"kind": "aliyun",
"fromAddress": "noreply@yourdomain.com",
"config": {
"accessKeyId": "...",
"accessKeySecret": "...",
"accountName": "...",
"regionId": "cn-hangzhou"
}
}
namestringrequired

1–120 characters.

kindstringrequired

resend | aliyun | log for a real configuration. sendgrid | mailgun | cf_email validate but send nothing (see warning above).

fromAddressstringrequired

Must be a valid email address.

replyToAddressstring

Optional, nullable.

configobject

Driver-specific credential map. resend{ apiKey }. aliyun{ accessKeyId, accessKeySecret, accountName, regionId }. log{}. Secret fields are encrypted at rest (AES-256-GCM) and never echoed back.

statusstring

active | paused | disabled. Default active.

rateLimitPerSecondinteger

1–100. Default 10. This is the only real send-rate limiter — there is no per-project daily quota beyond what you set here.

dailyLimitinteger

Optional cap on total sends per day, nullable.

transactionalReservePercentinteger

0–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.

marketingDailyLimitinteger

Optional 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.

GET /api/projects/{projectId}/email/providers/{id}/quota

Read-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.

GET /api/projects/{projectId}/email/domains
POST /api/projects/{projectId}/email/domains { "domain": "mail.yourdomain.com" }
GET /api/projects/{projectId}/email/domains/{domainId}/overview
POST /api/projects/{projectId}/email/domains/{id}/check

Domain 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.

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.

GET /api/projects/{projectId}/email/providers/{providerId}/overview

A 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.

GET /api/projects/{projectId}/email/routing
POST /api/projects/{projectId}/email/routing

Routing rules pick which channel handles a send by category (transactional vs marketing) when a template or trigger doesn’t pin a providerId explicitly.