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

GET/api/v1/clients

The 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.

GET/api/v1/projects

List your projects. Query params: archived (true/false), limit, offset.

POST/api/v1/projects

Create 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"
  }
}
GET/api/v1/projects/{slug}

Project details: versions, tags, folder, comment counts, preview URL.

PATCH/api/v1/projects/{slug}

Update name, description, isPrivate, or isArchived.

DELETE/api/v1/projects/{slug}

Delete a project and everything in it. Owner only; requires the delete scope.

Versions

GET/api/v1/projects/{slug}/versions

List a project’s version history.

POST/api/v1/projects/{slug}/versions

Upload 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

POST/api/v1/projects/{slug}/renders

Render 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

GET/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; treat error strings 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.
DELETE/api/v1/renders/{id}

Cancel a queued or in-progress job. Finished jobs return 409.

Comments

GET/api/v1/projects/{slug}/comments

List 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.

POST/api/v1/projects/{slug}/comments

Post a comment. Body: content (required), type (pin/box/general, default general), pinX/pinY (percentage coordinates for pins), versionNumber.

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).

StatusMeaning
400Invalid request — missing fields, unknown client ids, invalid HTML
401Missing, invalid, or expired API key
402Plan limit reached (monthly preview quota, render entitlement)
403Key lacks the required scope, no access to this project, or API access not enabled
404Not found (or not yours — existence is not revealed)
409Conflict — e.g. cancelling a finished job
413HTML larger than 5MB
429Rate limited — see Rate limits below
5xxSomething failed on our side; safe to retry with backoff

Rate limits & quotas

LimitValue
Monthly previews1,000 included per month (1 preview = 1 successful screenshot)
Concurrent render jobs2
Render jobs30 per hour
Project creation30 per hour
Version uploads60 per hour
HTML size5MB per upload
Screenshot URL lifetime1 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.

Ready to build?

API access is included with the Business plan.

View pricing