PUT
/sequences/{id}/graphPUT /sequences/{id}/graph
Replace the steps and edges of a sequence in one call. This is how a sequence is built or repaired over the API.
Request
curl -X PUT 'https://api.sendheron.com/api/v1/sequences/{id}/graph' \
-H 'Authorization: Bearer <YOUR_API_KEY>' \
-H 'Content-Type: application/json'Required scopes
The key must carry these:
- sequences:write
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | uuid | Yes | Sequence UUID. |
Body parameters
| Name | Type | Required | Description |
|---|---|---|---|
| steps | object[] | Yes | 1 to 100 steps, upserted by id. The nested fields are documented below. |
| steps[].ref | string | Yes | Client-local reference for this step, used by edges. Any short string, unique within the payload, e.g. welcome-email. |
| steps[].id | string | No | Existing step UUID to update in place, taken from GET /sequences/{id}/steps. Omit to create a new step. Steps that contacts have already passed through cannot be deleted, so repairs should carry existing ids. |
| steps[].type | string | Yes | One of TRIGGER, SEND_EMAIL, DELAY, CONDITION, ACTION. |
| steps[].config | object | No | Step configuration. The keys depend on type, and a key this step type does not read is REJECTED rather than stored: a config the worker cannot understand used to be accepted silently and simply never run. TRIGGER: triggerType (manual, contact_created, list_added, tag_added, event or webhook), plus tagId (UUID) for tag_added, listId (UUID) for list_added, eventName for event, and an optional source filter for contact_created. SEND_EMAIL: templateId (UUID). DELAY: duration (positive number) and unit (minutes, hours or days). A delay of one week is {"duration": 168, "unit": "hours"} or {"duration": 7, "unit": "days"}; there is no days key. CONDITION: field, operator, and value where the operator takes one. ACTION: actionType, plus tagId or tagName for tag actions, properties for property updates, webhookUrl (https) for webhooks, whose runtime rules are in the note below. Values are validated here; whether a step is COMPLETE is checked at activation, so a draft may still omit required keys. |
| steps[].x | integer | No | Canvas X position. |
| steps[].y | integer | No | Canvas Y position. |
| edges | object[] | Yes | Up to 200 edges, replaced wholesale rather than merged. The nested fields are documented below. |
| edges[].fromRef | string | Yes | Ref of the step this edge leaves. |
| edges[].toRef | string | Yes | Ref of the step this edge enters. |
| edges[].edgeType | string | No | One of default, true, false. default for linear flow; true and false only from CONDITION steps. |
| expectedUpdatedAt | ISO-8601 datetime | No | The sequence's updatedAt as you last read it. Supply it and a concurrent edit fails with 409 instead of being silently overwritten. |
| allowBranchRemoval | boolean | No | Confirm removing a true or false branch edge that a CONDITION step had before this write. Without it such a payload is refused with 400 branchRemovalNeedsConfirmation: a one-branch condition is a valid graph, meaning "stop here", so the dropped branch would otherwise take effect silently and contacts taking that outcome would just stop. Retargeting a branch, meaning the same edgeType pointed at a different destination, needs no flag. |
Responses
Returns 200 on success.
- 400
- The payload failed validation, or the request is not valid for the current state. The body's `description` names the field and says what was wrong with it in words, e.g. "domain: domain must be a bare hostname such as acme.com, with no scheme, path, or port". The `error` code is stable and safe to branch on; `description` is for the human reading the log.
- 401
- Missing or invalid API key.
- 403
- Valid key, but it does not carry the required scope. The `error` code is `apiKeys.insufficientScopes` and the `description` names both halves of the problem: "Missing required scope(s): X. This key holds: Y." Neither is a secret, since the required scopes are on this page and the held ones are your own credential, and a bare "Forbidden" costs a debugging pass to work out which of the two it was.
- 404
- No such record in this workspace. An id belonging to a different workspace returns this too, never a 403, because the API will not confirm that a record exists outside the workspace your key was issued in. Read it as "not yours or not there" rather than as "definitely gone".
- 409
- A duplicate, or an idempotency key reused with a different payload.
- 429
- Either rate-limit ceiling was exceeded.
- 400
- Invalid graph by the same rules activation enforces, an active sequence, an unknown ref, a type change on an existing step, deleting a step with history, a step config carrying a key its type does not read, or removing a wired condition branch without `allowBranchRemoval`.
- 409
- `expectedUpdatedAt` does not match, so somebody else changed the sequence since you read it.
Rate limits
100/min per key, counted against the organization's WRITE ceiling (400/min). Both windows are one minute. There is no hourly or daily quota.