API reference
Everything common to the whole API: the base URL, bearer auth, key scopes, idempotency, pagination, and errors. The resource pages that follow assume you have read this.
Looking for the dashboard guide? See Dashboard overview.
The API is REST over JSON at https://api.emayler.com, with all resource paths under /v1. Request and response bodies are JSON unless an endpoint says otherwise (attachment upload is multipart, the raw message endpoint returns message/rfc822, and the event stream is SSE).
Authenticate with a bearer token on every request:
Authorization: Bearer emk_live_...Create keys with POST /v1/api_keys. Keys can also be created in the dashboard; see API keys. The full token is shown exactly once at creation; Emayler stores only a SHA-256 hash. List responses show the first 12 characters as a prefix so you can tell keys apart. Deleting a key revokes it immediately, and requests with a revoked key get a 401.
Scopes are deliberately coarse. There are two:
| Scope | Allows |
|---|---|
read | GET requests only |
write | Every method, including GET |
There are no per-resource scopes. A scope that looks enforced but is not is worse than none, so the model stays simple: a write key can do anything your organisation can do.
Every create and send endpoint expects an Idempotency-Key header. Generate one value per logical operation (a UUID is fine) and reuse the same value when you retry. Retries with the same key return the original result instead of creating a second domain, mailbox, or, worst case, a second copy of an email to your customer.
curl -X POST https://api.emayler.com/v1/mailboxes \
-H "Authorization: Bearer $EMAYLER_API_KEY" \
-H "Idempotency-Key: 9b2f8c7e-..." \
-H "Content-Type: application/json" \
-d '{"address": "billing@example.com"}'List endpoints paginate with cursors only; there is no offset parameter. Pass limit (endpoint-specific bounds, for threads 1 to 100 with a default of 50) and follow the next_cursor field in the response until it comes back empty:
curl "https://api.emayler.com/v1/threads?mailbox_id=mbx_...&limit=50&cursor=eyJ..." \
-H "Authorization: Bearer $EMAYLER_API_KEY"Errors are RFC 9457 problem details, returned as application/problem+json. Every error carries a stable machine-readable code you can branch on, plus human text and, where useful, an errors extension with actionable fields.
{
"type": "https://api.emayler.com/problems/domain-not-verified",
"code": "domain_not_verified",
"status": 409,
"title": "Domain not verified",
"detail": "example.com is missing required DNS records.",
"errors": {
"missing_records": ["MX", "DKIM"]
}
}Fix messages are actionable: you will not see "verification failed"; you will see "MX for example.com points to aspmx.l.google.com. Add this MX record: 10 mx1.emayler.com".
Rate-limited endpoints return 429. The manual DNS check endpoint is the one you are most likely to hit: one check per domain per 30 seconds, with retry_after_seconds in the body and a Retry-After header. Honour both.
const res = await fetch("https://api.emayler.com/v1/domains", {
headers: { Authorization: `Bearer ${process.env.EMAYLER_API_KEY}` },
});
if (!res.ok) {
const problem = await res.json();
throw new Error(`${problem.code}: ${problem.detail}`);
}
const { domains } = await res.json();
console.log(domains.map((d) => `${d.name}: ${d.status}`));Same API, three surfaces
There is no private admin surface with extra powers, so the dashboard, the SDK, and the MCP server all call the same endpoints.