Skip to content

HTTP API

Live OpenAPI spec is served at /api-docs/openapi.json and a Swagger UI at /swagger-ui on every running Mailify instance. This page is the narrated overview; the JSON is the source of truth.

All routes under /mail/*, /templates/*, and /config require a Bearer JWT in the Authorization header. Acquire one via POST /auth/token by exchanging a long-lived API key.

Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...

Public routes: GET /health, POST /auth/token, GET /swagger-ui*, GET /api-docs/openapi.json.

Liveness probe. Returns 200 OK with a JSON body when the process is running. Does not check Postgres or SMTP — see Observability for how to add deeper probes.

Exchange an API key for a short-lived JWT.

Request

{ "api_key": "mky_abc123..." }

Response 200 OK

{
"token": "eyJ...",
"expires_at": "2026-04-23T14:00:00Z",
"issuer": "mailify"
}

Errors

  • 401 — invalid or unknown API key (argon2 verification failed).
  • 400 — malformed request body.

Enqueue a send using a registered template from TemplateRegistry.

Request

{
"template_id": "welcome",
"locale": "en",
"from": { "address": "no-reply@example.com", "name": "Example" },
"to": [{ "address": "alice@example.com", "name": "Alice" }],
"cc": [],
"bcc": [],
"reply_to": null,
"priority": "normal",
"vars": { "first_name": "Alice", "activation_url": "https://..." },
"headers": { "X-Campaign-Id": "welcome-v2" },
"subject_override": null,
"smtp_override": null
}

Response 202 Accepted

{ "job_id": "01J8KZ7...", "status": "pending" }
  • job_id is an apalis ULID (string). Use it with GET /mail/jobs/:id.
  • status is always "pending" at enqueue time — check the jobs endpoint for live state.

Send a one-shot email with caller-supplied raw HTML (no template registry lookup).

Request

{
"from": { "address": "no-reply@example.com", "name": "Example" },
"to": [{ "address": "alice@example.com" }],
"subject": "Hello",
"html": "<h1>Hi</h1>",
"text": "Hi",
"locale": "en",
"priority": "high",
"smtp_override": {
"host": "smtp.tenant.com",
"port": 587,
"tls": "starttls",
"username": "postmaster",
"password": "secret"
}
}

Same response shape as /mail/send. Custom HTML is still sent through the priority queue and the worker.

Look up the state of a previously enqueued job.

Response 200 OK

{
"id": "01J8KZ7...",
"status": "Done",
"attempts": 1,
"last_error": null,
"run_at": "2026-04-23T13:00:00Z",
"done_at": "2026-04-23T13:00:02Z"
}

status is one of: Pending, Scheduled, Running, Done, Failed, Killed (apalis state enum).

Errors

  • 404 — unknown id (or id mismatch — apalis uses ULIDs, not the caller’s UUID).

List every template id + locale currently loaded in TemplateRegistry.

Render a registered template with placeholder data — useful for design review without sending.

Same, with caller-supplied vars to inspect a specific render.

Dump the resolved config with secrets redacted. Useful when debugging “which env var actually took effect”.

priority on any send request controls scheduling weight. Lower weight runs earlier.

ValueWeight
critical0
high10
normal50 (default)
low100

Not built-in. Put Mailify behind your reverse proxy (nginx, Caddy, Traefik) or API gateway and rate-limit at the edge.

GET /api-docs/openapi.json

Pipe to a file:

Terminal window
make openapi
# writes ./openapi.json