Creating and editing drafts
The draft contract of a flow: creating a flow with its first draft, reading the draft back, replacing it whole with an expectedDraftId precondition, and validating a graph without saving it. Includes the response shapes, validation errors and advice, limits and error codes.
A flow's draft is the one version the API can write. You create it together with the flow, read it back to change it, replace it whole, and validate graphs against the same checks the dashboard applies when it saves. Publishing it stays with a person in the dashboard — see Flows.
Four endpoints, one shape: every draft travels as { "trigger": { ... } }, the graph nested inside the trigger. That shape is described node by node in Nodes, and the fastest way to get a correct one is a template payload.
Before you start
Get a Product API token with automation write access
Creating, replacing and validating need
PROJECT_AUTOMATION_WRITE; reading the draft needsPROJECT_AUTOMATION_READ. Scopes are chosen when the token is minted, under Project settings → API tokens — see Authentication.Know your project UID
Every call is scoped to one project, which owns the data sources, senders and segments the draft may reference.
Export credentials
export INSTASENT_PROJECT="proj_xxx" export INSTASENT_TOKEN="eyJhbGciOi..." export BASE="https://api.instasent.com/v1/project/$INSTASENT_PROJECT"
Creating a flow
Create a flow together with its first draft.
namestringrequiredThe flow's name in the dashboard, up to 128 characters.
descriptionstringAn internal note about the flow.
tagsstring[]Labels for organizing flows in the dashboard.
templatestringThe key of the template the flow is created from. Recorded once, at creation, and it marks the template as used. An unknown key is a 400.
draftobjectrequiredThe graph, as { "trigger": { ... } }. See Nodes.
curl -X POST "$BASE/flow" \
-H "Authorization: Bearer $INSTASENT_TOKEN" \
-H "Content-Type: application/json" \
-d @flow.jsonA successful call returns 201 Created:
{
"entity": {
"flowId": "66b1f2a4e5a6b7c8d9e0f1a2",
"draftId": "66b1f2a4e5a6b7c8d9e0f1b7",
"valid": false,
"errors": [
{
"path": "graph",
"message": "...",
"parameters": { "errorsCount": 1, "uids": ["6f1c0a9e-..."] }
}
],
"warnings": [
{ "rule": "no-purchase-exit", "message": "..." }
]
}
}flowIdstringThe new flow's id. The value every other flow endpoint takes, including the flow reports once it is published.
draftIdstringThe id of the draft just saved. Keep it: it is the expectedDraftId of your next replace.
validbooleanfalse means the draft cannot be published yet. errors says why.
errorsobject[]The validation errors, each with path, message and parameters. Empty when valid is true. See Validation errors.
warningsobject[]Advice on a graph that validates but probably misbehaves, each with rule and message. See Advice.
Reading the draft
Read the flow's draft in the same shape it is written.
The response is the same flowId, draftId, valid and errors, plus draft: the graph, with every node's uid kept so it can be edited and sent back whole. A flow with no draft answers 404.
A draft can change without you: a person may open the flow in the dashboard and save it, and every save produces a new draftId. Read before you replace.
Replacing the draft
Replace the whole draft.
There is no partial edit. Every save — yours or a person's in the dashboard — replaces the whole draft and creates a new draft version with its own id. The request carries the graph and the id of the draft you started from:
expectedDraftIdstringrequiredThe draftId you last read, or null when the flow has no draft. The field must be present even when it is null.
draftobjectrequiredThe complete graph, as { "trigger": { ... } }.
If the flow's current draft is not the one you name, nothing is saved and the call answers 409 Conflict: someone saved in between, and overwriting their change silently would lose it. Read the draft again, reapply your change on top of it, and send it with the new draftId.
flowchart TD
A[GET /flow/id/draft] --> B[Edit the graph, keep the uids]
B --> C[PUT /flow/id/draft with expectedDraftId]
C -->|200| D[Saved: keep the new draftId]
C -->|409| A
class D success
Keep the uid of every node you read. A node that keeps its uid keeps its identity across saves; omit it only on nodes you add.
A successful replace returns 200 OK with flowId, the new draftId, valid and errors. Like creation, an invalid draft is saved. Editing the draft never changes what is live: the published version keeps running until a person publishes again.
Two things refuse a replace besides a stale draft: an archived flow, whose draft can no longer change (400), and a second save of the same flow within a couple of seconds of the previous one (429, retry after a moment).
Validating without saving
Validate a graph against a flow without saving anything.
Send { "draft": { ... } } and get back valid, errors and warnings, from the same checks a save runs. Nothing is stored, so it is the way to iterate on a graph before replacing a draft with it. The flow in the path must exist: create the flow first, then validate the changes you plan to make to it.
{
"entity": {
"valid": true,
"errors": [],
"warnings": [
{ "rule": "send-right-after-trigger", "message": "..." }
]
}
}Validation errors
Validation covers the graph's structure, each node's required settings, and limits on the flow as a whole. Each entry carries path, message and parameters. Base your handling on path and parameters: message is written for the dashboard and can still contain {{placeholders}} that parameters fills in.
- Problems with the graph as a whole come as entries with
path: "graph": a node reachable from more than one place, more than 50 nodes, waits on the longest path adding up to more than 180 days, or nodes that reference a sender or data source from outside this project (theiruidsare inparameters). - Problems inside nodes come as a single
path: "graph"entry whoseparameterscarryerrorsCountand theuidsof the nodes in error. The API does not return the reason per node; the dashboard editor shows it on each node. Keeping theuidof the nodes you read is what lets you tell which ones failed; a node you add gets itsuidwhen the draft is saved, so read the draft back to map it.
The node problems you will meet most:
- A send with no sender, or no text. The most common, and the one every template payload has until you set its senders.
- A branch with the wrong paths: a missing path, a default path with a condition, the wrong number of fixed outcomes. See Nodes.
- A value out of range: a delay shorter or longer than its node allows. See Delays and waits.
Advice
warnings flags a graph that validates but probably does not do what its author wants. It never blocks a save and is never stored; it rides the creation response and the validate response, not a replace.
rule | What it flags |
|---|---|
send-right-after-trigger | The first step is a send. Put a smart delay first, unless the message confirms what the contact just did. |
no-purchase-exit | A checkout or product journey with no exit on ecommerce_order_create, so a buyer keeps getting reminders. |
too-many-sends | One path sends more than 3 messages. |
fixed-delay-before-send | A fixed delay directly before a marketing send, which can land at any hour of the night. |
no-compliance-policy | A send with no consent policy. See The send. |
Read every entry: each one describes a journey that will misbehave in front of contacts once it is published.
Limits
| Limit | Value |
|---|---|
| Flows never published, per project | 50 |
| Nodes in a graph | 50 |
| Total waits on the longest path | 180 days |
name | up to 128 characters |
The first limit exists so an automated caller cannot fill a project with drafts in a loop; it is checked before any other work, and only on creation. A flow counts towards it until it is published, and stops counting once a person publishes or deletes it.
Errors
| Status | When | What to do |
|---|---|---|
400 | The body is not a JSON object; draft is missing or has no trigger; the draft uses a node type the API does not accept (the message lists the allowed ones); template is unknown; expectedDraftId is absent; the flow is archived. | Fix the request. Retrying unchanged fails the same way. |
403 | The token lacks the scope, or the organization owner is not verified (code: "organization.verification_required"). | See Errors. |
404 | The flow does not exist in this project, or has no draft to read. | Check the id. |
409 | On replace: the draft changed since you read it. On create: the project is at its limit of never-published flows. | See below. |
422 | A value in the draft has the wrong shape, or it references a sender, data source or segment that does not exist in this project. | Read errors.fields.draft: it names the value and the expected shape. |
429 | The rate limit, or a second save of the same flow within a couple of seconds. | Back off and retry. |
The two 409s are told apart by their body. The draft limit is the only one with a machine-readable code at the top level:
{
"code": "flow-draft-limit-reached",
"message": "This project already has 50 flows that were never published, the most allowed. Publish or delete one before creating another; retrying will fail the same way.",
"limit": 50,
"count": 50
}Retrying it changes nothing until a flow is published or deleted in the dashboard, so report it rather than looping. The stale-draft 409 carries only a message; its remedy is always to read the draft again.
A 422 looks like the other field errors of the Product API:
{
"errors": {
"fields": {
"draft": ["A value in the draft has the wrong shape: ..."]
}
}
}The most common cause is an RCS message written as a bare string: an RCS text is { "text": { "text": "..." } }. Everything else — 401, 5xx — behaves as described in Errors.