Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/DataTalksClub/datamailer/llms.txt

Use this file to discover all available pages before exploring further.

POST /api/transactional/send is the single entry point for sending templated transactional email through Datamailer. The endpoint validates required template context variables before creating any database records, upserts the contact, checks hard-bounce and complaint suppression, and — if all checks pass — creates a TransactionalMessage record with status=queued and enqueues a payload to AWS SQS for delivery. Providing an idempotency_key makes the call safe to retry: duplicate requests return the existing message without re-queuing.

Authentication

All requests must include a Bearer token issued for the target client.
Authorization: Bearer <client-api-key>

Request

Method and path: POST /api/transactional/send Content-Type: application/json

Body Parameters

email
string
required
Recipient email address. Trimmed and validated. The contact is upserted by normalized (lowercased) email.
template_key
string
required
Key of the transactional email template to use. Must match an EmailTemplate record that has is_transactional=true and is_active=true for the authenticated client. Returns HTTP 404 if not found.
idempotency_key
string
Optional stable string that makes the request idempotent. If a TransactionalMessage already exists for this client with the same idempotency_key, the existing message is returned immediately without queuing a new send. Use a value that is unique per logical send event, such as registration-user-{user_id}.
context
object
Template variable dictionary. Keys must satisfy the template’s required_context list — missing required keys return a validation error before any record is created. Defaults to an empty object.
metadata
object
Arbitrary key-value metadata stored on the message record for tracing and auditing (e.g., source, request_id). Not used for rendering. Defaults to an empty object.

Send Flow

1

Validate payload

Parse and validate email, template_key, idempotency_key, context, and metadata. Return HTTP 400 on any field error.
2

Look up template

Fetch the active transactional template for this client by template_key. Return HTTP 404 if not found or not active.
3

Check idempotency

If idempotency_key is provided, query for an existing message with the same client + key. If found, return it immediately with idempotent_replay: true and skip all further steps.
4

Validate template context

Confirm that all keys listed in the template’s required_context are present in the provided context object. Return HTTP 400 if any are missing.
5

Upsert contact and check suppression

Create the contact if it does not exist, then check is_transactional_email_allowed. If the contact is hard-bounced or has filed a complaint, create a status=skipped message and return HTTP 409.
6

Create message and enqueue

Render the template subject, HTML body, and text body using the provided context. Create a TransactionalMessage with status=queued and enqueue the SQS payload on transaction commit.

Response

HTTP 202 — Queued successfully

message
object
idempotent_replay
boolean
true when an existing message was returned due to a matching idempotency_key. false for new messages.
enqueued
boolean
true when the message was placed on the SQS queue for delivery. false for idempotent replays and suppressed contacts.

HTTP 409 — Contact suppressed

Returned when the contact has a hard bounce or complaint on record. The message is still created with status=skipped for audit purposes, but it is not enqueued.
{
  "message": {
    "id": 17,
    "email": "bounced@example.com",
    "status": "skipped",
    "template_key": "registration-welcome",
    "idempotency_key": "registration-user-99",
    "created_at": "2024-09-01T10:05:00Z"
  },
  "idempotent_replay": false,
  "enqueued": false,
  "error": {
    "code": "transactional_suppressed",
    "message": "Contact is hard-suppressed for transactional email.",
    "reason": "hard_bounce"
  }
}
The error.reason field is one of hard_bounce, complaint, or suppressed (the fallback value when the cause is not a bounce or complaint).

HTTP 404 — Template not found

Returned when template_key does not match any active transactional template for the authenticated client.

Example

Request

curl -X POST https://datamailer.example.com/api/transactional/send \
  -H "Authorization: Bearer dm_dtccourses_demo_transactional_email_key" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "learner@example.com",
    "template_key": "registration-welcome",
    "idempotency_key": "registration-user-123",
    "context": {
      "name": "Learner",
      "course_name": "ML Zoomcamp"
    },
    "metadata": {
      "source": "registration"
    }
  }'

Response

{
  "message": {
    "id": 101,
    "email": "learner@example.com",
    "status": "queued",
    "template_key": "registration-welcome",
    "idempotency_key": "registration-user-123",
    "created_at": "2024-09-01T10:00:00Z"
  },
  "idempotent_replay": false,
  "enqueued": true
}

Idempotent replay (same idempotency_key sent again)

{
  "message": {
    "id": 101,
    "email": "learner@example.com",
    "status": "queued",
    "template_key": "registration-welcome",
    "idempotency_key": "registration-user-123",
    "created_at": "2024-09-01T10:00:00Z"
  },
  "idempotent_replay": true,
  "enqueued": false
}

Notes

Transactional sends bypass marketing subscription requirements — the contact does not need to be subscribed or verified to receive a transactional email. However, hard-bounced contacts and contacts that have filed spam complaints are always blocked, regardless of template or subscription state.
A suppressed contact returns HTTP 409, not HTTP 400. Your error-handling code must treat 409 separately from validation errors. A transactional_suppressed response is expected behavior, not a client mistake — log it and do not retry.
Sending requires SQS_TRANSACTIONAL_EMAIL_QUEUE_URL to be configured in the Datamailer environment. Without a valid queue URL, the message record is created but the on_commit SQS call will fail. In local development, use LocalStack to provide a compatible SQS endpoint.

Build docs developers (and LLMs) love