API Reference
API Reference
GitHub
Getting Started

API Conventions

The two API surfaces, how each authenticates, and the response shape

Read this before any other API page. Two surfaces exist, they authenticate differently, and the response shape is the same everywhere.

Two Surfaces

SurfacePrefixAuthenticationProject
Product/api/v1/...Authorization: Bearer myk_... or the session cookieImplied by the key, else the X-Mailyard-Project-Id header
Console’s own/app/api/...Session cookie mailyard_sessionX-Mailyard-Project-Id header

They are split by what an operation is, not by who calls it.

/api/v1 carries the product: sending, templates, campaigns, domains, everything an integration could plausibly want. The console calls it too, with its cookie - which is why there is no second copy of these routes for the dashboard.

/api/v1/admin is the same surface one segment deeper: users, plans, identity providers, the shared SMTP pool, platform settings. Standing an installation up is exactly the kind of thing an operator wants to script, so it is not hidden behind the browser.

/app/api carries what cannot be used remotely: signing in, passkey and 2FA ceremonies, the OIDC round-trip, session management, the live event stream. An API key is not accepted there, and would have nothing to do with it.

Accepting the cookie on /api/v1 costs nothing in safety: the session cookie is SameSite=Strict, so a browser never sends it cross-site, and a mutating request carrying it from an origin that is not Mailyard’s own is refused. It also means cookie auth works only from Mailyard’s own origin - a third-party browser application still needs a key.

Project is a header, not a path segment

There is no /projects/current/ path prefix. A session addresses the active project through the X-Mailyard-Project-Id header, falling back to ?project_id= and then, only if you are a member of exactly one project, to that one. Belonging to no project is an ordinary state and nothing is created for a new account, so a route that needs a project answers 400 naming the header rather than inventing one.

An API key is bound to exactly one project, so the header is unnecessary with a key, and a mismatching one is rejected with 403 rather than ignored.

# An API key: no header needed, the key names its project
curl http://localhost:3000/api/v1/emails \
  -H "Authorization: Bearer myk_..."

# The same surface with a session: cookie plus project header
curl http://localhost:3000/api/v1/emails \
  -b cookies.txt \
  -H "X-Mailyard-Project-Id: 81af718e-f0ae-4780-a0d7-9f05b34dabcc"

# The console's own: signing in is not something a key can do
curl -c cookies.txt -X POST http://localhost:3000/app/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@example.com","password":"your-password"}'

Getting cookies.txt

Every console example on these pages passes -b cookies.txt. That file is a curl cookie jar, and you create it by logging in with -c:

curl -c cookies.txt -X POST http://localhost:3000/app/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@example.com","password":"your-password"}'

-c writes the jar, -b reads it. After that one call, every later -b cookies.txt request is authenticated until the session expires.

The login response contains no token

The reply body is {"user": {...}} and nothing else. The session JWT is delivered only as the mailyard_session cookie, which is HttpOnly - there is no token field to copy out of the JSON. This is why the console examples use a cookie jar rather than an Authorization header.

A bearer header does work, but the token has to come from the cookie itself:

JWT=$(awk '/mailyard_session/{print $7}' cookies.txt)
curl http://localhost:3000/api/v1/templates -H "Authorization: Bearer $JWT"

Prefer the jar. For anything non-interactive, prefer an API key on /api/v1 instead - it is the surface built for it.

If the account has two-factor authentication on, login returns 401 with {"requires_2fa": true} until you resend the same request with a totp_code field.

Response Shape

Responses are bare keyed JSON. There is no success flag and no data envelope:

{
    "templates": [
        {
            "id": "...",
            "name": "welcome"
        }
    ]
}

Single objects use a singular key:

{
    "template": {
        "id": "...",
        "name": "welcome"
    }
}

Errors carry an error string, and validation failures add a fields array:

{
    "error": "Name is required",
    "fields": [
        {
            "field": "name",
            "rule": "required",
            "message": "Name is required"
        }
    ]
}

The HTTP status is the authority: 200/201 success, 400 invalid input, 401 unauthenticated, 403 the credential lacks the permission, 404 missing (or in another project), 409 conflict, 429 rate limit or quota, 503 maintenance mode.

Identifiers

Every id is a string UUID generated by the application, not an auto-incrementing integer. Examples in these pages that show "id": 1 are stale - treat ids as opaque strings.

Cross-Project Access

A resource belonging to another project is reported as missing, not forbidden. GET /api/v1/templates/{id} for a template in a project you cannot see returns 404, never 403, so the API does not confirm that the id exists.

HTTP Methods

Partial updates use PATCH. PUT is reserved for full replacement and is used only where a resource genuinely has no partial form (stylesheets, languages, template localizations).

OpenAPI Description

The machine surface describes itself:

mailyard export-api-spec --out openapi.yaml

The document is generated from the types the binary was built with. Feed the file to Postman or a client generator, or read it rendered at API Reference , which the same instance serves behind the docs sign-in.

--surface selects which one it describes: api for /api/v1 (the default, and the one a client generator wants), app for the console’s own routes, all for both.

The document is generated from the response types the handlers return, so its field descriptions cannot drift from the code. A route nobody described, or a description whose route was renamed, fails the build.