# PUT /sequences/{id}/graph

Part of the SendHeron REST API. Base URL: `https://api.sendheron.com/api/v1`

Replace the steps and edges of a sequence in one call. This is how a sequence is built or repaired over the API.

```http
PUT https://api.sendheron.com/api/v1/sequences/{id}/graph
Authorization: Bearer <YOUR_API_KEY>
```

**Required scopes:** `sequences:write`

**Success status:** `200`

**Rate limit:** 100/min per key, counted against the organization's WRITE ceiling (400/min).

**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. |

**Endpoint-specific errors** (beyond the shared error set):

- `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.

> Only DRAFT and PAUSED sequences can be edited, so pause an active one first. The graph is validated with the same rules activation enforces, which means a shape that could not go live is refused here rather than accepted and rejected later. Each step's `config` is validated against its type on the way in as well, so a delay carrying a field name the worker does not read is refused on write instead of being stored as a step that silently never runs. Steps that contacts have already moved through are permanent history: they cannot be deleted, and their type cannot change. The 200 response carries the saved steps and edges plus a diff of what the write changed: steps created, updated and deleted, edges added and removed. Creating the sequence itself is still dashboard work, since there is no `POST /sequences`. A webhook `ACTION` step is the one config whose runtime behaviour is worth knowing before you point it at something. The URL must be https, and its host has to resolve to a public address: private, loopback, link-local and cloud metadata addresses are refused. That check runs when the connection is made rather than only when you save, so a hostname that resolves somewhere else later is refused then too. Redirects are never followed. A 3xx is recorded as the status it is and the step counts as done, even though nothing received the payload, so point the URL at its final destination rather than at something that forwards to it. The request is given 10 seconds. The response body is read by nothing: only the status code is recorded on the enrollment log, so there is nothing you can return that changes what the sequence does next.


---

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.
