Developer Documentation
EmailQA API
Render emails on real email clients — real Outlook on Windows, real iPhones and Android devices, not simulations — from your CI pipeline, campaign platform, or scripts. Upload HTML, trigger renders, and get back light and dark mode screenshots plus shareable preview links.
API access is available on the Business plan. See pricing. All endpoints are served from https://emailqa.live.
Authentication
Create an API key in Settings → API Keys and send it as a Bearer token on every request. Keys are shown once at creation and stored hashed — if you lose one, revoke it and create another.
curl https://emailqa.live/api/v1/projects \
-H "Authorization: Bearer eq_your_api_key"Keys carry scopes: read (fetch projects, results, comments) and write (create projects, upload HTML, trigger and cancel renders, post comments). Keys can optionally expire, and can be revoked at any time from Settings.
Quick start
Three requests: create a project with your HTML, trigger a render, poll for the screenshots.
# 1. Create a project with your email HTML (max 5MB)
curl -X POST https://emailqa.live/api/v1/projects \
-H "Authorization: Bearer eq_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Spring campaign", "html": "<!DOCTYPE html>..."}'
# -> { "data": { "slug": "n3j6ip1vb8kj", "previewUrl": "...", ... } }
# 2. Trigger renders (omit "clients" for the full fleet)
curl -X POST https://emailqa.live/api/v1/projects/n3j6ip1vb8kj/renders \
-H "Authorization: Bearer eq_your_api_key" \
-H "Content-Type: application/json" \
-d '{"clients": ["outlook-desktop", "gmail-ios"]}'
# -> 202 { "data": { "id": "rj_G6kHhvruMlPO6ibM", "status": "queued", ... } }
# 3. Poll until completed (or subscribe to the render.completed webhook)
curl https://emailqa.live/api/v1/renders/rj_G6kHhvruMlPO6ibM \
-H "Authorization: Bearer eq_your_api_key"
# -> { "data": { "status": "completed", "results": [ { "client": "...",
# "status": "success", "screenshotUrl": "https://..." } ] } }A full-fleet render typically completes in 10–15 minutes (mobile devices are the slowest lane); single desktop clients in about a minute. Re-submitting identical HTML returns cached screenshots instantly.
Render clients
/api/v1/clientsThe live client catalog. No authentication required. Use the id values in render requests. Clients under maintenance are listed but temporarily rejected by the render endpoint.
{
"data": [
{ "id": "outlook-desktop", "label": "Outlook Windows (Classic)",
"category": "Desktop", "os": "Windows", "darkMode": true, "status": "available" },
{ "id": "apple-mail", "label": "Apple Mail (Mac)",
"category": "Desktop", "os": "Mac", "darkMode": true, "status": "available" },
{ "id": "gmail-ios", "label": "Gmail (iOS)",
"category": "Mobile", "os": "iOS", "darkMode": true, "status": "available" },
... 8 more
]
}Clients with darkMode: true return separate light and dark screenshots automatically (each counts as one preview). The Gmail web clients return a single screenshot because Gmail strips dark-mode CSS from emails.
Projects
A project holds one email and its version history, comments, and renders.
/api/v1/projectsList your projects. Query params: archived (true/false), limit, offset.
/api/v1/projectsCreate a project. Body fields: name (required), description, html (creates version 1; max 5MB), isPrivate. API-created projects are private by default — pass isPrivate: false to make the preview link viewable by anyone who has it (useful for sharing with reviewers who don’t have accounts).
{
"data": {
"id": "…", "name": "Spring campaign", "slug": "n3j6ip1vb8kj",
"description": null, "isPrivate": true, "createdAt": "…",
"previewUrl": "https://emailqa.live/preview/n3j6ip1vb8kj"
}
}/api/v1/projects/{slug}Project details: versions, tags, folder, comment counts, preview URL.
/api/v1/projects/{slug}Update name, description, isPrivate, or isArchived.
/api/v1/projects/{slug}Delete a project and everything in it. Owner only; requires the delete scope.
Versions
/api/v1/projects/{slug}/versionsList a project’s version history.
/api/v1/projects/{slug}/versionsUpload a new HTML version. Body: { "html": "..." } (max 5MB). Returns the new version number and a preview URL pinned to it.
{
"data": {
"id": "…", "versionNumber": 3, "createdAt": "…",
"previewUrl": "https://emailqa.live/preview/n3j6ip1vb8kj?version=3"
}
}Triggering renders
/api/v1/projects/{slug}/rendersRender a version on real email clients. Body fields (both optional): version (defaults to the latest) and clients (array of client ids; defaults to the full available fleet). Returns 202 Accepted with a job:
{
"data": {
"id": "rj_G6kHhvruMlPO6ibM",
"status": "queued",
"cached": false,
"projectSlug": "n3j6ip1vb8kj",
"versionNumber": 2,
"requestedClients": ["gmail-web"],
"queuePosition": 0,
"estimatedWaitSeconds": 59,
"results": [],
"createdAt": "2026-07-17T12:10:09.063Z",
"completedAt": null
},
"monthlyPreviews": { "used": 4, "limit": 1000, "remaining": 996 }
}If identical HTML was rendered recently, the response is 200 OK with cached: true, status: "completed", and the screenshots included immediately — no fleet time, results in about a second.
Every response includes monthlyPreviews so you always know where your quota stands. One preview = one successful screenshot; a dark-mode-capable client produces two per render.
Render jobs
/api/v1/renders/{id}Job status and results. While the job runs, results stream in per client (completedCount / totalCount show progress). Job status is one of queued, processing, completed, error, cancelled.
{
"data": {
"id": "rj_G6kHhvruMlPO6ibM",
"status": "completed",
"results": [
{
"client": "gmail-web",
"status": "success",
"screenshotUrl": "https://…s3.us-east-2.amazonaws.com/…png?X-Amz-…",
"screenshotUrlExpiresInSeconds": 3600
}
],
"createdAt": "2026-07-17T12:10:09.063Z",
"completedAt": "2026-07-17T12:11:07.297Z"
}
}Two things to design for:
- Partial results are normal.Each client succeeds or fails independently — a job can complete with 18 screenshots and 2 per-client errors. Check each result’s
status; treaterrorstrings as opaque text. Failed clients never consume previews. - Screenshot URLs expire after 1 hour. They’re signed links — download the images or re-fetch the job for fresh URLs.
/api/v1/renders/{id}Cancel a queued or in-progress job. Finished jobs return 409.
Webhooks
Instead of polling, subscribe to events from your EmailQA account (webhooks can be scoped to a project, a team, or all your projects). The event for API renders is render.completed; other events include comment.created, version.uploaded, and approval.given.
POST <your webhook URL>
X-EmailQA-Event: render.completed
X-EmailQA-Delivery: <uuid>
X-EmailQA-Timestamp: <ISO timestamp>
X-EmailQA-Signature: sha256=<hex HMAC of the raw body>
{
"event": "render.completed",
"timestamp": "2026-07-17T12:11:07.000Z",
"data": {
"render": {
"id": "rj_G6kHhvruMlPO6ibM",
"status": "completed",
"cached": false,
"project": { "slug": "n3j6ip1vb8kj", "name": "Spring campaign" },
"versionNumber": 2,
"requestedClients": ["gmail-web"],
"results": [ { "client": "gmail-web", "status": "success" } ]
}
}
}Webhook payloads deliberately contain no screenshot URLs — on receipt, call GET /api/v1/renders/{id} for fresh signed links.
Verify signatures: if your webhook has a secret, the X-EmailQA-Signature header is sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with your webhook secret. Compute it over the exact bytes received and compare with a constant-time comparison.
Errors
Errors are JSON: { "error": "message" }, sometimes with extra context (invalid client requests include validClients; quota errors include monthlyPreviews).
| Status | Meaning |
|---|---|
| 400 | Invalid request — missing fields, unknown client ids, invalid HTML |
| 401 | Missing, invalid, or expired API key |
| 402 | Plan limit reached (monthly preview quota, render entitlement) |
| 403 | Key lacks the required scope, no access to this project, or API access not enabled |
| 404 | Not found (or not yours — existence is not revealed) |
| 409 | Conflict — e.g. cancelling a finished job |
| 413 | HTML larger than 5MB |
| 429 | Rate limited — see Rate limits below |
| 5xx | Something failed on our side; safe to retry with backoff |
Rate limits & quotas
| Limit | Value |
|---|---|
| Monthly previews | 1,000 included per month (1 preview = 1 successful screenshot) |
| Concurrent render jobs | 2 |
| Render jobs | 30 per hour |
| Project creation | 30 per hour |
| Version uploads | 60 per hour |
| HTML size | 5MB per upload |
| Screenshot URL lifetime | 1 hour (re-fetch the job for fresh links) |
- Failed client renders never count against your previews.
- Cached results count as previews but return in about a second.
- Renders of the same HTML are deduplicated — render only the clients you need per commit (e.g. just Outlook) and save full-fleet runs for release candidates.
- Need more than the included quota? Get in touch — higher volumes are available.
Comments
/api/v1/projects/{slug}/commentsList review comments on a version (defaults to latest). Query params:
version,status(open/resolved). Includes author, assignee, pin position, and reply counts — useful for syncing review state into your own tools./api/v1/projects/{slug}/commentsPost a comment. Body:
content(required),type(pin/box/general, default general),pinX/pinY(percentage coordinates for pins),versionNumber.