Architecture
Transactional email (integrators)
Send one outbound email per recipient from a FORM or CONTACT_FORM integration using that integration's outboundMail SMTP. The call runs server-to-server with X-API-Key and scope ``transactional.send``.
This is not a stateless SMTP relay. Mailoo creates a full send lifecycle inside the integration: TransactionalSend, Message (OUTBOUND), optional Subscriber sync, open/click tracking, and optional delivery webhooks. See outbound-smtp{.interpreted-text role="doc"} for how this differs from exposing SMTP credentials to integrators.
Related: SMTP layer --- outbound-smtp{.interpreted-text role="doc"}; API key scopes --- ../../../api/authentication{.interpreted-text role="doc"}; production env --- ../../../deployment/transactional-email-production{.interpreted-text role="doc"}; unsubscribe tokens --- website-forms{.interpreted-text role="doc"}.
Architecture
- Trigger, not relay: Your backend calls Mailoo; Mailoo renders the message, persists records, and sends via ``integration.config.outboundMail`` (same SMTP as dashboard replies and welcome mail).
- Secrets: Use ``X-API-Key`` with ``transactional.send`` (or FULL) from your server or BFF only --- never in browser bundles.
- Idempotency: Every request requires ``idempotencyKey`` (max 256 chars, unique per integration). First success returns 201; replays return 200 with
idempotentReplay: true. - Subscribers: Mailoo upserts a
Subscriberfor the recipient email. IfunsubscribedAtis set, send is skipped (409, statusSKIPPED_UNSUBSCRIBED). - Delivery events: Optional outbound webhooks (
delivered,opened,clicked,unsubscribed,bounced) with HMAC signing and retries --- see Delivery webhooks below.
Send endpoint
``POST {baseUrl}/api/v1/webhooks/transactional/{projectUid}/{integrationId}/send``
Headers: Content-Type: application/json, X-API-Key (scope ``transactional.send`` or FULL)
Body (JSON):
- ``idempotencyKey`` (required) --- stable key for this logical send (e.g.
daily-2026-06-20-user123) - ``to`` (required) --- recipient email
- ``name`` (optional) --- display name; used for
{{User.name}}/{{firstName}} - ``templateId`` (optional) --- campaign id (dashboard campaign on this integration); use either
templateIdor inlinesubject+text/html - ``vars`` (optional) --- object of string/number/boolean Mustache variables (see below)
- ``subject``, ``text``, ``html`` (optional) --- inline content when not using
templateId; subject and at least one oftextorhtmlrequired for inline sends
Responses:
- 201 --- email sent (
status: SENT,idempotentReplay: false) - 200 --- idempotent replay of a prior send
- 409 --- recipient unsubscribed (
status: SKIPPED_UNSUBSCRIBED) - 502 --- SMTP send failed (
status: FAILED;MessageandBOUNCEDdelivery webhook may still be recorded) - 400 --- validation error
- 403 --- API key missing ``transactional.send``
- 404 --- project/integration not found or not FORM/CONTACT_FORM
Example (inline):
curl -X POST "https://api.mailoo.app/api/v1/webhooks/transactional/{projectUid}/{integrationId}/send" \
-H "Content-Type: application/json" \
-H "X-API-Key: mai_live_..." \
-d '{
"idempotencyKey": "daily-2026-06-20-user1",
"to": "user@example.com",
"name": "Jane Doe",
"subject": "Hi {{firstName}}",
"html": "<p>{{affirmationText}} <a href=\"{{writeUrl}}\">Write</a></p><p><a href=\"{{unsubLink}}\">Unsubscribe</a></p>",
"vars": {
"affirmationText": "You are enough.",
"writeUrl": "https://app.example.com/write"
}
}'
Example response (201):
{
"success": true,
"data": {
"sendId": "clx...",
"messageId": "clx...",
"status": "SENT",
"idempotentReplay": false
}
}
Mustache variables
Templates (campaign templateId or inline subject/text/html) are rendered with Mustache before SMTP send.
Built-in context:
- ``User.email``, ``User.name`` --- from
to/name - ``firstName`` --- from
vars.firstNameor first token ofname - ``unsubLink`` --- one-click unsubscribe URL (per-integration signing secret + API ``MAILOO_PUBLIC_APP_URL``)
- ``affirmationText``, ``writeUrl``, ``streak`` --- from
varswhen provided (common for retention/nudge flows)
Additional string/number/boolean keys in vars are merged into the context.
Open and click tracking
When HTML body is non-empty, Mailoo injects:
- A 1×1 open-tracking pixel
- Rewritten
https://(and//) links via click-tracking redirects
Public routes (no API key):
- ``GET /api/v1/public/transactional/open/{trackingToken}`` --- returns transparent GIF; records first open
- ``GET /api/v1/public/transactional/click/{trackingToken}?u={urlencodedTarget}`` --- records click, 302 redirect to target
Set ``MAILOO_PUBLIC_API_URL`` on the API host so pixel/link URLs point at your public API (see ../../../deployment/transactional-email-production{.interpreted-text role="doc"}).
Delivery webhooks
When configured, Mailoo POSTs JSON events to your notification endpoint.
Configuration (per integration):
integration.config.deliveryWebhook.url and .secret --- set in the dashboard (Delivery webhooks on the integration Setup tab) or via PATCH …/integrations/{id} with deliveryWebhook.
Events: delivered (after successful SMTP), opened, clicked, unsubscribed (after unsubscribe webhook), bounced (SMTP failure).
Payload shape (JSON):
{
"event": "delivered",
"integrationId": "...",
"messageId": "...",
"idempotencyKey": "...",
"recipientEmail": "user@example.com",
"timestamp": "2026-06-20T12:00:00.000Z",
"metadata": {}
}
Signing: Header ``X-Mailoo-Signature: t={unixSeconds},v1={hex}`` where v1 is HMAC-SHA256 of "{timestamp}.{rawBody}" using the webhook secret. Header ``X-Mailoo-Event`` repeats the enum name (e.g. DELIVERED).
Retries: Outbox worker (10s interval on the API process) retries failed deliveries up to 8 attempts with exponential backoff (max 5 minutes). Set ``DISABLE_DELIVERY_WEBHOOK_WORKER=1`` only for tests or if you run a separate processor.
Prerequisites
- FORM or CONTACT_FORM integration with ACTIVE status
- ``outboundMail`` configured (dashboard → Connection & settings → Outbound email)
- ``MAIL_SMTP_ENCRYPTION_KEY`` on the API when SMTP password or unsubscribe secret is stored encrypted
- RESTRICTED or FULL API key with ``transactional.send``
Unsubscribe signing
Each FORM/CONTACT_FORM integration stores an encrypted unsubscribe HMAC secret in integration.config. Mailoo generates {{unsubLink}} automatically in transactional templates. Owners can rotate the secret in the dashboard (invalidates links in already-sent mail). External backends that sign their own links need the rotated plaintext once, or should call ``POST /api/v1/webhooks/unsubscribe`` with an API key instead.