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.
POST
/v1/tests/{id}/share
Mint a self-destructing public link
Creates a public page showing every preview. The slug is the credential: anyone holding the URL can see the previews until it expires. Each call mints a new link rather than returning an existing one.
Parameters
- ttl
- One of
24h, 7d, 30d. Default 7d. There is no permanent option. - expiresAt
- An exact ISO instant to expire at, overriding
ttl. At most 90 days out. - password
- Optional second factor on top of the URL. Stored only as a hash — it cannot be read back, so send it by another route.
Responses
- 201
- The new link, with its public
url and expiresAt. - 404
- No such test for this account.
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.
create_share_link
A public, expiring page for a human reviewer.
The URL needs no login. Hand it out deliberately.