---
title: "REST API reference - Workflow Transactional Email"
description: "Every endpoint in the Workflow Transactional Email REST API: HTTP requests, email layouts, senders, secrets, history and stats, with the access level each one needs."
canonical: "https://docs.workflow-transactional-email.app/developer-rest-api"
---

# REST API reference

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](https://docs.workflow-transactional-email.app/developer-authentication.md).

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.

## HTTP requests

| Method | Path | Level | Purpose |
| --- | --- | --- | --- |
| GET | `/api/v1/http-requests` | read | List HTTP request actions |
| 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 `page` (default 1), `limit` (default 25, max 100) and `q` to search by name.

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.

> [!WARNING]
> **The test endpoint sends a real request**
> `POST /api/v1/http-requests/:id/test` calls the actual third-party URL with the actual credentials. It is not a dry run.
>
> It is recorded in history as a test, so it stays out of your statistics and does not consume plan quota. It is still a real outbound call, which is why it sits behind the execute level and has its own hourly budget.
>
> Requests aimed at internal or private network addresses are refused at send time, whatever the saved URL says.

## Email layouts and senders

| Method | Path | Level | Purpose |
| --- | --- | --- | --- |
| GET | `/api/v1/email-templates` | read | List email layouts |
| GET | `/api/v1/email-templates/:id` | read | Get one layout, including its rendered body |
| GET | `/api/v1/smtp-configs` | read | List SMTP senders |
| GET | `/api/v1/senders` | read | List connected mailboxes (Microsoft 365, Google) |

Layouts accept the same `page`, `limit` and `q` parameters as HTTP requests.

SMTP configurations 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 configuration stays identifiable by its name, host and from-address. Connected mailboxes report a partially masked address such as `so***@example.com` and never their OAuth tokens.

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

## History and statistics

| Method | Path | Level | Purpose |
| --- | --- | --- | --- |
| GET | `/api/v1/history` | read | List executed actions |
| 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` (`HTTP_REQUEST` or `EMAIL`), `status`, and `actionConfigId` to filter to a single action.

Stats accepts `days` (default 30, max 365).

> [!NOTE]
> **History responses are PII-masked, but that is not a guarantee**
> An action's stored request and response often contain customer details - most commonly a recipient address that came from a Flow variable. Before an execution leaves the server, values that look like personal data are replaced with `***`: email addresses, phone numbers, card numbers, and fields named after a person. Error messages are masked the same way, because a failed send frequently quotes the recipient.
>
> The masking works on field names and value patterns, so personal data sitting inside a free-text field - a message body, an order note - can still come through. Treat history responses as potentially containing customer data and store them accordingly.

## Status codes

| Code | Meaning |
| --- | --- |
| 200 | Success |
| 201 | Created |
| 400 | Malformed request or failed validation |
| 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 |
| 429 | Rate limited - see below |
| 502 | The action 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.
