Architecture

Обновлено: Jun 22, 2026Раздел: General

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 Subscriber for the recipient email. If unsubscribedAt is set, send is skipped (409, status SKIPPED_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 templateId or inline subject + 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 of text or html required 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; Message and BOUNCED delivery 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.firstName or first token of name
  • ``unsubLink`` --- one-click unsubscribe URL (per-integration signing secret + API ``MAILOO_PUBLIC_APP_URL``)
  • ``affirmationText``, ``writeUrl``, ``streak`` --- from vars when 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.

📚