PUT/sequences/{id}/graph

PUT /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.

View as Markdown

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

NameTypeRequiredDescription
iduuidYesSequence UUID.

Body parameters

NameTypeRequiredDescription
stepsobject[]Yes1 to 100 steps, upserted by id. The payload replaces the graph, so a step left out is deleted, which is refused once contacts have passed through it. The nested fields are documented below.
steps[].refstringYesClient-local reference for this step, used by edges. Any short string, unique within the payload, e.g. welcome-email.
steps[].idstringNoExisting 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[].typestringYesOne of TRIGGER, SEND_EMAIL, DELAY, CONDITION, ACTION.
steps[].configobjectNoStep configuration. The keys depend on type, and a key this step type does not read is REJECTED rather than stored, so a misspelled or misplaced key fails loudly instead of being ignored. 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: operator (equals, not_equals, contains, not_contains, starts_with, ends_with, is_set, is_not_set, greater_than, less_than, has_tag or not_has_tag), field and value. field is a contact column (email, firstName, lastName, status) or a custom property key, compared as text; greater_than and less_than compare as numbers. is_set and is_not_set take no value. has_tag and not_has_tag take the tag UUID as value and read no field, so it can be left out. ACTION: actionType (add_tag, remove_tag, update_property or send_webhook), plus tagId or tagName for add_tag and remove_tag, properties (object) for update_property, and webhookUrl (https) for send_webhook, 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[].xnumberNoCanvas X position.
steps[].ynumberNoCanvas Y position.
edgesobject[]YesUp to 200 edges, replaced wholesale rather than merged. The nested fields are documented below.
edges[].fromRefstringYesRef of the step this edge leaves.
edges[].toRefstringYesRef of the step this edge enters.
edges[].edgeTypestringNoOne of default, true, false. default for linear flow; true and false only from CONDITION steps.
expectedUpdatedAtISO-8601 datetimeNoThe sequence's updatedAt as you last read it. Supply it and a concurrent edit fails with 409 instead of being silently overwritten.
allowBranchRemovalbooleanNoConfirm removing a true or false branch edge that a CONDITION step had before this write. Without it such a payload is refused with 400 sequences.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.