EmailOnSteroids
openapi.json

Render an email in five calls

Rendering is asynchronous. Submitting returns immediately with status: "queued" and one preview per client and colour mode — up to 32. A whole matrix normally finishes in 20–60 seconds.

  1. Get a key

    Dashboard → API keys → Create. It is shown once and stored only as a hash, so copy it now — a lost key is rotated, never recovered.

  2. Submit the HTML

    The response comes back before anything has rendered. Keep data.id.

    Create a test
    curl -X POST https://app.emailonsteroids.io/v1/tests \
      -H "Authorization: Bearer eos_live_xxxxxxxx" \
      -H "Content-Type: application/json" \
      -d '{"subject":"Spring sale","html":"<!doctype html><html><body><h1>Hi</h1></body></html>"}'
  3. Poll until it is done

    About every two seconds. counts tells you how far along the matrix is without walking the previews array.

    Poll status
    curl https://app.emailonsteroids.io/v1/tests/6f1d2c4a-9b3e-4f27-8f0a-1c2b3d4e5f60 \
      -H "Authorization: Bearer eos_live_xxxxxxxx"
  4. Read a screenshot

    The URL in data.url is signed and lives for 600 seconds. Fetch it promptly; do not store it.

    Get one client's preview
    curl "https://app.emailonsteroids.io/v1/tests/6f1d2c4a-9b3e-4f27-8f0a-1c2b3d4e5f60/previews/outlook-365-win?mode=dark" \
      -H "Authorization: Bearer eos_live_xxxxxxxx"
  5. Share it with a human

    A public page, no login, gone at expiresAt. Send no body at all to accept the 7-day default.

    Create a share link
    curl -X POST https://app.emailonsteroids.io/v1/tests/6f1d2c4a-9b3e-4f27-8f0a-1c2b3d4e5f60/share \
      -H "Authorization: Bearer eos_live_xxxxxxxx" \
      -H "Content-Type: application/json" \
      -d '{"ttl":"24h"}'

Connect an agent

The MCP server speaks Streamable HTTP at https://app.emailonsteroids.io/mcp and authenticates with the same API key. It is stateless, so every request carries its own credential — there is no session to establish.

Claude Code — one command
claude mcp add --transport http emailonsteroids https://app.emailonsteroids.io/mcp \
  --header "Authorization: Bearer eos_live_xxxxxxxx"
Any MCP client — config block
{
  "mcpServers": {
    "emailonsteroids": {
      "type": "http",
      "url": "https://app.emailonsteroids.io/mcp",
      "headers": {
        "Authorization": "Bearer eos_live_xxxxxxxx"
      }
    }
  }
}

Then ask for something that exercises the whole loop:

Render this email across every client you can and show me the ones that break: <paste your HTML>

Authentication

An API key, sent as Authorization: Bearer eos_live_….

Create one in the dashboard under API keys. The key is shown once and stored only as a hash, so it cannot be recovered — rotate rather than recover. Every failure mode (no key, unknown key, revoked key) answers with the same 401, deliberately: distinguishing them would hand out a key-existence oracle.

The envelope

Every endpoint answers with the same four keys, on success and on failure alike. Branch on error.code, never on the HTTP status alone. Every response also carries an X-Request-Id header, echoed as meta.requestId — the one thing to quote in a support request.

Every response, success or failure
{
  "success": true,
  "data": {
    "id": "6f1d2c4a-9b3e-4f27-8f0a-1c2b3d4e5f60",
    "status": "queued"
  },
  "error": null,
  "meta": {
    "requestId": "req_9f3c1a7b2e04"
  }
}
A failure carries the same four keys
{
  "success": false,
  "data": null,
  "error": {
    "code": "validation_failed",
    "message": "Request body is invalid.",
    "details": [
      {
        "path": "html",
        "message": "HTML is required."
      }
    ]
  },
  "meta": {
    "requestId": "req_9f3c1a7b2e04"
  }
}

Rate limits

Limits are per API key and per surface:

  • POST /tests — 20 requests per 60s. It is the expensive verb: one call fans out to up to 32 renders.
  • every other endpoint — 240 requests per 60s, generous enough that polling every two seconds is never throttled.

Exceeding a limit returns 429 with error.code: "rate_limited" and a Retry-After header in seconds. Honour it rather than inventing a backoff.

Endpoints

POST

/v1/tests

Submit an email for rendering

Queues one render per (emulated client profile × colour mode) and returns before any of them has run. Omit profiles to render the whole matrix your plan allows — that is almost always what you want.

Parameters

html
Required. The complete email HTML. Max 2 MB.
subject
Shown in the preview chrome. Defaults to empty.
campaignId
UUID. Groups repeated runs of one campaign.
profiles
Client keys to render. An empty array is rejected, not ignored.

Responses

201
Queued, with one preview row per render.
422
validation_failed — error.details names the field.
429
rate_limited — honour the Retry-After header.
GET

/v1/tests

List your tests

Newest first, cursor-paginated. previews is omitted here — read a single test for the full matrix.

Parameters

limit
Page size. Out-of-range values are clamped, not rejected.
cursor
The meta.nextCursor from the previous page.

Responses

200
A page. meta.nextCursor is null on the last one.
GET

/v1/tests/{id}

Read one test and every client's status

The endpoint to poll, about every two seconds. A whole matrix normally completes in 20–60 seconds. counts is the per-status tally, so progress needs no reduce over the previews array.

Responses

200
The test, with its previews.
404
No such test for this account — never 403, so ids cannot be probed.
POST

/v1/tests/{id}/rerun

Render the same campaign again

Creates a **new** test from the stored HTML and returns it — the original run is untouched, so a link someone already has keeps showing what they were told to look at. Two calls are two runs; it costs the same budget as submitting a new email.

Responses

201
The new test. Its id differs from the one in the path.
404
No such test for this account.
GET

/v1/tests/{id}/events

Subscribe to progress (SSE)

For when polling is the wrong shape. Frames: snapshot once on connect, then preview.updated and test.updated as clients finish, then done. A timeout frame after ~290s means reconnect, not fail.

Responses

200
text/event-stream.
404
Checked before the stream opens, so a 404 is a 404.
GET

/v1/tests/{id}/previews/{client}

Read one client's screenshot

Returns a signed URL valid for 600 seconds, not image bytes. url is null while the client is still rendering and when it failed — read status and error before treating null as a bug.

Parameters

client
Profile key, e.g. outlook-365-win.
mode
light (default) or dark. Not every client has dark.

Responses

200
The preview and its signed URLs.
404
Unknown test, or this client never rendered in this mode.

MCP tools

Five tools over the same service layer as the REST endpoints above — a test created by an agent and one created by curl are the same row, queued and billed the same way.

list_client_profiles

Which emulated clients this key's plan can render, their colour modes, and each one's fidelity class.

Call it before narrowing profiles — an out-of-plan key is refused, not skipped.

create_test

Submit the HTML. Returns immediately with status 'queued'.

Never report a result from this call alone. Nothing has rendered yet.

get_test_status

Poll progress and read per-client results.

Every ~2s. One client failing does not fail the run — check each preview.

get_preview

One client's emulated screenshot as a short-lived signed URL.

The URL expires after 600s. Re-request it rather than storing it. The fidelity block says how defensible that client's emulation is.