POST /api/v1/series
Creates the series and its version 1 draft, in one transaction. Answers 201.
This is the route that POST /api/v1/sequences refuses: there, triggerType: "fixed_date" returns
400, because a series needs its audience and its start date in the same call. Here the trigger is
imposed — sending another triggerType in the body changes nothing.
Authorization
| Role | Access |
|---|---|
OWNER | Allowed |
ADMIN | Allowed |
MEMBER | Refused — 403 |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | 1 to 100 characters |
listId | string (uuid) | Yes | The list the series sends to |
startsAt | string (ISO 8601) | No | Day 1. Optional here — a series is created then scheduled — and required at publish |
audienceSegmentId | string (uuid) | No | Narrows the list |
audienceTagId | string (uuid) | No | Narrows it further |
json
{
"name": "March launch",
"listId": "9f8e7d6c-1111-4bbb-8ccc-ddddeeeeffff",
"startsAt": "2026-03-15T08:00:00.000Z",
"audienceSegmentId": null,
"audienceTagId": null
}Example
bash
curl -s -X POST 'https://www.agentsmail.io/api/v1/series' \
-H "x-api-key: $AGENTMAIL_API_KEY" \
-H 'content-type: application/json' \
-d '{
"name": "March launch",
"listId": "9f8e7d6c-1111-4bbb-8ccc-ddddeeeeffff",
"startsAt": "2026-03-15T08:00:00.000Z"
}'Response
| Code | When |
|---|---|
201 | The series and its version 1 draft exist |
400 | Invalid body, or startsAt is not a readable date |
401 | Missing or invalid key |
403 | The key's role is MEMBER |
Response fields
Same shape as list series, for the single object indata.
Example response
json
{
"success": true,
"data": {
"id": "7d7d7d7d-8888-4ccc-9ddd-eeee00001111",
"name": "March launch",
"status": "paused",
"startsAt": "2026-03-15T08:00:00.000Z",
"listId": "9f8e7d6c-1111-4bbb-8ccc-ddddeeeeffff",
"audienceSegmentId": null,
"audienceTagId": null,
"topicId": null,
"createdAt": "2026-03-01T10:00:00.000Z",
"updatedAt": "2026-03-01T10:00:00.000Z"
}
}Agent recipe
Creating the series is the first of four steps — on its own it sends nothing. The id you get back is a sequence id, and the steps are added through the Sequences API.js
const headers = {
'x-api-key': process.env.AGENTMAIL_API_KEY,
'content-type': 'application/json',
}
const api = 'https://www.agentsmail.io/api/v1'
// 1. The series, with its audience and its start date
const {data: series} = await fetch(`${api}/series`, {
method: 'POST',
headers,
body: JSON.stringify({name: 'March launch', listId, startsAt: '2026-03-15T08:00:00.000Z'}),
}).then((r) => r.json())
// 2. Its version 1 draft already exists — find it
const {data: versions} = await fetch(`${api}/sequences/${series.id}/versions`, {
headers,
}).then((r) => r.json())
const draft = versions.find((v) => v.status === 'draft')
// 3. The steps, on the Sequences API — in TWO calls each for a send.
// Creating a node only takes its `type` (plus `waitHours` on a wait, or
// `templateId` to start from a template): a new send step is born with the
// default content and no subject. The subject, the content and the send
// time are written by a PATCH on the node that comes back.
const nodes = `${api}/sequences/${series.id}/versions/${draft.id}/nodes`
const addNode = (body) =>
fetch(nodes, {method: 'POST', headers, body: JSON.stringify(body)})
.then((r) => r.json())
.then((r) => r.data)
const editNode = (id, body) =>
fetch(`${nodes}/${id}`, {method: 'PATCH', headers, body: JSON.stringify(body)})
.then((r) => r.json())
.then((r) => r.data)
// One PATCH per FAMILY: the subject and the content are one family, the send
// time is another, and sending two families in a single call is refused.
const teaser = await addNode({type: 'send_email'})
await editNode(teaser.id, {subject: 'Something is coming', content: html})
// The time of day, in minutes from midnight. Leave it out and the step goes
// out as soon as the wait before it has elapsed.
await editNode(teaser.id, {sendTimeMinutes: 540})
// A wait gives the DURATION between two sends.
await addNode({type: 'wait', waitHours: 72})
const launch = await addNode({type: 'send_email'})
await editNode(launch.id, {subject: 'It is here', content: html2})
await editNode(launch.id, {sendTimeMinutes: 540})
// 4. Publish. This is where a series without a start date is refused, by name.
await fetch(`${api}/sequences/${series.id}/versions/${draft.id}/publish`, {
method: 'POST',
headers,
}).then((r) => r.json())Notes
A new series ispaused. Activating it is a PATCH with {"status": "active"}, and it is refused
until a version is published.