POST /api/v1/sequences/{sequenceId}/versions/{versionId}/publish
Freezes the draft, dates it, and switches the sequence's pointer to it. This is the only moment
the graph is checked, and the only moment it has consequences.
What a refused publication means
Every defect answers400, names itself, and names the nodes involved — all of them at once,
so you fix in one pass instead of publishing again to discover the next one.
| Defect | What it means | How to fix it |
|---|---|---|
| empty | The version has no node at all | Add at least one node |
| no entry | Every node has something pointing at it — there is no way in | Remove the wire that closes the loop at the top |
| multiple entries | Two nodes have nothing pointing at them | Wire one behind the other; a journey has one door |
| cycle | The wires come back on themselves — a contact would loop forever | Remove the wire that goes backwards |
| orphan | A node is unreachable from the entry | Wire it in, or delete it |
| dangling branch | A branch leads nowhere | Wire its end, or drop the branch |
| ambiguous branch | A node has two outgoing wires with no way to choose | Keep one wire |
| edge out of graph | A wire points at a node of another version | Delete that wire and rewire inside this version |
| empty send | A send_email node has no subject or no content | PATCH the node with a subject and HTML |
| incomplete action | An add_tag / remove_tag node has no tag | PATCH the node with a tagId |
| end with successor | An end_sequence node has a node after it | Delete what follows: nothing runs after the end |
| invalid webhook | A webhook node has no URL, or one the policy refuses | PATCH the node with an https:// public URL |
400 too: a published version is a snapshot, republishing
it in place would rewrite what contacts are walking. Use rollback instead.
Authorization
| Role in the organization | This endpoint |
|---|---|
OWNER | Allowed |
ADMIN | Allowed |
MEMBER | Refused — 403 |
Example
bash
curl -X POST -H "x-api-key: $AGENTMAIL_API_KEY" \
"https://www.agentsmail.io/api/v1/sequences/$SEQUENCE_ID/versions/$VERSION_ID/publish"Response
200 OK— the call succeeded.401 Unauthorized— missing or invalid key, or its owner left the organization.403 Forbidden— the key's owner is amemberof the organization, not anadmin.404 Not Found— no such resource, or it belongs to another organization. The API never confirms that an id exists to a caller who has no right to it.429 Too Many Requests— over 120 requests in a minute for this key.
Response fields
| Field | Type | Description |
|---|---|---|
success | boolean | Indicates if the operation was successful |
data.id | string | Unique identifier for the version |
data.sequenceId | string | The sequence it belongs to |
data.version | number | Its rank, starting at 1 |
data.status | string | draft or published |
data.publishedAt | string | null |
data.triggerType | string | list_joined or tag_added |
data.triggerTargetId | string | The list or tag that enrols contacts |
data.createdAt | string | ISO creation date |
data.updatedAt | string | ISO date of the last change |
Example response
json
{
"success": true,
"data": {
"id": "4d19c0a2",
"sequenceId": "9b3f2d10",
"version": 1,
"status": "subscribed",
"publishedAt": "2026-08-23T10:22:05.000Z",
"triggerType": "list_joined",
"triggerTargetId": "0b7c51e8",
"createdAt": "2026-08-23T10:22:05.000Z",
"updatedAt": "2026-08-23T10:22:05.000Z"
}
}Agent recipe
Build the graph freely, then read the refusal: validation happens at publish time, not on every edit. A400 here is the graph being refused, not a bug.
js
const publish = await fetch(
`${base}/sequences/${sequenceId}/versions/${versionId}/publish`,
{method: 'POST', headers}
)
if (publish.status === 400) {
const {error, details} = await publish.json()
// `details` names the steps at fault — fix the draft and publish again
return repairDraft(details ?? error)
}
// Nothing runs until the sequence itself is active
await fetch(`${base}/sequences/${sequenceId}`, {
method: 'PATCH',
headers: {...headers, 'content-type': 'application/json'},
body: JSON.stringify({status: 'active'}),
})