DevelopersREST API reference

REST API reference

Copy page

REST API reference for Workflow Transactional Email: endpoints for requests, layouts, senders, secrets, history and stats, with access levels.

All paths are relative to https://shopify.workflow-transactional-email.app. Every endpoint except the index needs Authorization: Bearer fak_... - see Authentication and API keys.

Responses are JSON. Errors use the same shape throughout:

json
{ "error": "Invalid or missing API key" }

Index and identity

Method Path Level Purpose
GET /api/v1 none API index. Confirms the API is up and reports the current release
GET /api/v1/me read Check a key works and see its access level

The index needs no key, so it is a safe health check to point a monitor at.

Email layouts

Method Path Level Purpose
GET /api/v1/email-templates read List email layouts. q searches name, subject and description; category=transactional or category=marketing lists one email type
POST /api/v1/email-templates write Create a layout
GET /api/v1/email-templates/:id read One layout with its design or attachments, its footer texts and the list of its languages
PUT / PATCH /api/v1/email-templates/:id write Change a layout. Only the fields you send change
DELETE /api/v1/email-templates/:id write Delete a layout with its translations and versions
GET /api/v1/layout-schema read How to write a layout: the design format, every block's defaults, the Liquid you can use, limits and an example

The list accepts page (default 1), limit (default 25, max 100) and q, which searches name, subject and description. There is no filter by type over the API; read category on each layout instead.

A layout has these fields:

Field Notes
name, description Name required, at most 200 characters; description at most 2000
bodyType visual (default) or text. Fixed once created
defaultLocale The language of the layout's own content, en unless set
category transactional (default) or marketing. See below
subject, previewText Liquid allowed. Subject required
bodyDesign Visual layouts: { blocks, global?, header?, footer?, testVariables? }. Send the complete design when changing it; the HTML body is rendered from it
body Text layouts: the HTML body
attachments Text layouts: [{ filename, fileKey, sendAsLink? }] for uploaded files (see files below) or [{ filename, url }] for a file fetched over https when the email is sent
marketingTexts The footer texts of a marketing layout, or null. See below

Liquid in the subject, preview text, body and every block text must parse, and every fileKey must belong to your shop, or the write is refused with a 400 that names the field. Read GET /api/v1/layout-schema before writing a visual design: it is generated from the builder itself, so it cannot drift.

Every write through this API is kept in the layout's version history, exactly like a save in the app.

Transactional and marketing layouts

category says what a layout is for. A transactional layout is sent to every recipient, unchanged. A marketing layout skips recipients who unsubscribed (a step whose To recipients all unsubscribed is not sent and shows as SKIPPED in history), goes out as one message per To recipient with its own unsubscribe link (at most 20 per step), and ends with the unsubscribe sentence and your company details. {{ unsubscribe_url }} and {{ unsubscribe_link }} place the link yourself; in transactional layouts both are empty. The merchant side of this is in Marketing emails and unsubscribes.

marketingTexts is the footer wording a marketing layout uses instead of the shop-wide texts from Settings:

json
{
  "marketingTexts": {
    "unsubscribeText": "You get this because you shop with us. {{ unsubscribe_link }}",
    "unsubscribeLinkLabel": "Unsubscribe",
    "companyDetails": "Example GmbH, Musterstrasse 1, 10115 Berlin"
  }
}

Every field is optional. A set field replaces the Settings text for that field; the others stay as they are. null (the default) means the Settings texts for the email's language. Limits: unsubscribeText and companyDetails 2000 characters, unsubscribeLinkLabel 100. An unsubscribeText must contain {{ unsubscribe_link }} or {{ unsubscribe_url }}, otherwise the write is refused. The field is stored for any layout but only sent with marketing ones.

Deleting a layout is never blocked. If Shopify Flow steps still point at it, the response carries a warning naming how many; those steps fail until they use another layout.

Translations

Method Path Level Purpose
GET /api/v1/email-templates/:id/translations read Every language of a layout with its content
GET /api/v1/email-templates/:id/translations/:locale read One language
PUT /api/v1/email-templates/:id/translations/:locale write Create or replace one language
DELETE /api/v1/email-templates/:id/translations/:locale write Remove one language. Sends in it fall back to the layout
POST /api/v1/email-templates/:id/translations/:locale/promote write Make that language the layout's main language

:locale is a language code such as de, fr or pt-br; case and separator do not matter. Up to 30 languages per layout. The Shopify Flow step's Language field picks one at send time: exact locale, then the base language, then a regional variant, else the layout itself.

A PUT takes the full translated content: subject (required), previewText, and bodyDesign (visual) or body (text). Keep Liquid tags and URLs unchanged. Two fields are specific to a translation:

Field Text layouts Meaning
attachments yes A list (same shape as the layout's) = this language's own attachments, [] = none in this language. null = send the layout's attachments. Omitted = keep the current choice; a new language uses the layout's. Visual layouts carry their attachments as blocks in each language's bodyDesign and refuse this field
marketingTexts any This language's footer texts on top of the layout's, field by field. null = the layout's. Omitted = keep the current choice

A translation is returned with the same two fields: attachments is null while it sends the layout's, and marketingTexts is null while it uses the layout's.

promote swaps the layout and the translation: the layout takes the translation's content and locale, and the content it had becomes the translation for the language it used to be in. Attachments and footer texts trade places too, so every language keeps sending what it sent before. The response reports promoted, previousMainLanguage and the layout. It is one version, so it can be undone.

Versions

Method Path Level Purpose
GET /api/v1/email-templates/:id/versions read Saved versions, newest first. The app keeps the newest 25
GET /api/v1/email-templates/:id/versions/:version read The full snapshot of one version: layout and all translations
POST /api/v1/email-templates/:id/versions/:version/restore write Put that version back. Recorded as a new version
GET /api/v1/email-templates/:id/versions/github?page= read Every version kept in the connected GitHub repository, 20 per page
GET /api/v1/email-templates/:id/versions/github/:sha read The layout as it was at one commit
POST /api/v1/email-templates/:id/versions/github/:sha/restore write Put a GitHub version back, checked like a save

The GitHub endpoints answer 409 while no repository is connected on the Developer page. See Layout version history and Keep unlimited layout history on GitHub.

Email senders

Method Path Level Purpose
GET /api/v1/smtp-configs read List SMTP senders
GET /api/v1/senders read List connected mailboxes (Microsoft 365, Google)

SMTP senders report hasUsername and hasPassword rather than the credential itself - both halves are withheld, since a username is as much a part of the credential as the password. A sender stays identifiable by its name, host and from-address. Connected mailboxes report a partially masked address such as so***@example.com and never their sign-in tokens.

Read-only. Connect and edit senders in the app.

Secrets

Method Path Level Purpose
GET /api/v1/secrets read List secret names and descriptions

Returns hasValue, never the value. There is no endpoint that reads a secret back. Create and update secrets in the app.

Uploaded files

Method Path Level Purpose
GET /api/v1/files read List uploaded logos, images and attachments
DELETE /api/v1/files write Delete one, by key in the body

List accepts page, limit (max 100) and q to search by filename.

Add ?usage=true and each file also reports inUse plus a usedBy array naming the layouts and presets referencing it. That is how you find everything safe to clear out in a single request. It scans your layouts and presets, so it is opt-in rather than the default.

Uploading is not exposed here - add files through the media picker in the app.

HTTP requests

Method Path Level Purpose
GET /api/v1/http-requests read List HTTP requests
POST /api/v1/http-requests write Create one
GET /api/v1/http-requests/:id read Get one
PUT / PATCH /api/v1/http-requests/:id write Update one
DELETE /api/v1/http-requests/:id write Delete one
POST /api/v1/http-requests/:id/test execute Run it against the real target

List accepts the same page, limit and q parameters as email layouts.

The target url must be http:// or https://. Other schemes are rejected when you save.

Literal credentials written into a header or body are masked in every response. Values referenced as {{ secrets.KEY }} are returned as written, because the reference itself is not sensitive.

History and statistics

Covers both email sends and HTTP requests.

Method Path Level Purpose
GET /api/v1/history read List executions
GET /api/v1/history/:id read Get one execution with request and response data
GET /api/v1/stats read Totals and success rate

History accepts page, limit (max 100), actionType, status (PENDING, SUCCESS, FAILED, SKIPPED), and actionConfigId to filter to a single layout or request.

SKIPPED is a marketing email whose recipients had all unsubscribed: nothing was sent, no quota was used, and it does not count towards the success rate.

actionType must be exactly EMAIL or HTTP_REQUEST. An unrecognised value is ignored rather than rejected, so a typo returns everything instead of erroring.

Stats accepts days (default 30, max 365).

Status codes

Code Meaning
200 Success
201 Created
400 Malformed request or failed validation. The error names the field, for example subject: required
401 Missing, malformed or revoked API key
403 Key is valid but its level is too low for this endpoint
404 Not found, or belongs to a different shop
405 Wrong method for this path
409 Refused because the record is still in use (file deletion), or GitHub is not connected (GitHub versions)
429 Rate limited - see below
502 The request ran but the third-party target failed

A record belonging to another shop returns 404 rather than 403, so the API never confirms that an id exists elsewhere.

Rate limits

Two independent budgets, both per key:

  • 300 requests per 60 seconds across all endpoints.
  • 60 execute calls per hour on top of that, covering POST /api/v1/http-requests/:id/test.

Exceeding either returns 429 with an explanatory error. The execute budget is deliberately tight: a runaway loop firing live requests at a payment provider is a much worse failure than a slow script.

Budgets are per key, not per store, so one integration cannot exhaust another's allowance.