Skip to main content

Version history and editing

Every save of a flow keeps a version you can look at, compare and restore. This page covers:

  • updating a flow with PATCH (the version check, ignored read-only fields, the history fields),
  • the version history endpoints,
  • the edit lease the Flow Builder uses so that two people do not edit the same flow at once.

The JSON format of definition itself (nodes, output keys, layout) is described in the Flow format spec.

Updating a flow​

curl -X PATCH https://api.sendseven.com/api/v1/flows/FLOW_ID \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"version": 12,
"name": "Welcome flow",
"definition": { "...": "the whole definition" },
"revision_summary": "Added the support menu"
}'
  • version is the flow's current version from your last GET. You can send it as an If-Match: 12 header instead. When someone saved in the meantime the response is 409 with {"code": "flow_version_conflict"} and nothing is saved: load the flow again and reapply your change. Every successful save increments version.
  • Send the whole definition. definition is replaced, not merged.
  • Read-only fields are ignored. You can send back the document you got from GET /flows/{flow_id}: id, tenant_id, status, validation, run_count, timestamps and any other field that PATCH does not accept are ignored, never an error. Change status with POST /publish and POST /pause.
  • revision_summary (optional, at most 500 characters) is a short note shown in the version history.
  • revision_source (optional) says where the edit came from: canvas, json or ai_assistant. Requests made with an API key are always recorded as api, whatever you send. Any other value is a 422.

POST /api/v1/flows accepts revision_summary and revision_source too. The validation rules for definition (including the stricter checks for a live flow) are described in Triggers and the API.

Version history​

MethodPathScope
GET/api/v1/flows/{flow_id}/revisions?page=1&page_size=20flows:read
GET/api/v1/flows/{flow_id}/revisions/{revision_id}flows:read
GET/api/v1/flows/{flow_id}/revisions/{revision_id}/diff?against=currentflows:read
POST/api/v1/flows/{flow_id}/revisions/{revision_id}/restoreflows:write

Every save that changes the definition or a setting records a version. A save that changes nothing records nothing. Quick saves in a row by the same person from the builder canvas (or by the same API key) within 10 minutes are combined into one version, so autosave does not flood the history. Saves from the JSON view, the AI assistant and restores always get their own version. The newest 200 versions of a flow are kept.

List versions​

{
"items": [
{
"id": "6f1c0d3e-...",
"revision_number": 14,
"created_at": "2026-11-03T10:12:00Z",
"updated_at": "2026-11-03T10:19:30Z",
"author": { "user_id": "8a2e...", "name": "Dana Weber" },
"source": "canvas",
"summary": "3 steps changed",
"flow_version": 12,
"stats": { "added": 1, "changed": 2, "removed": 0 },
"restored_from_revision_id": null
}
],
"pagination": { "total": 14, "page": 1, "page_size": 20, "total_pages": 1, "has_next": false, "has_prev": false }
}

Newest first, page_size up to 100.

  • author is null for saves made with an API key. author.name is null once the person has left the workspace.
  • source is one of canvas, json, ai_assistant, ai_flow_builder (created by the AI Flow Builder), api or restore.
  • stats counts steps added, changed and removed compared with the version before. Moving a step on the canvas does not count as a change.
  • A flow created before version history existed starts with a version that holds the state of its first save after that, summary "Saved before version history started".

Read one version​

GET /revisions/{revision_id} returns the same fields plus definition and settings (the flow's other editable fields at that version, for example name, default_channel_chain and priority).

Credentials typed directly into an API Call step (the token, password or value of config.auth, and the values of headers such as Authorization or X-API-Key) are not stored in the history and read as ***. A value is kept only when it is exactly one variable such as {{secrets.NAME}} (optionally after Bearer or Basic ), which is one more reason to use flow secrets. Publishing also shows a warning for every credential typed directly into a step.

Compare two versions​

GET /revisions/{revision_id}/diff compares a version with:

  • against=current (default): the flow as it is now. This is what a restore would change.
  • against=previous: the version before it. This is what that save changed.
  • against=<revision_id>: another version of the same flow.
{
"revision_id": "6f1c0d3e-...",
"against": "current",
"nodes": {
"added": [{ "id": "support_menu", "label": "Support", "type": "send" }],
"removed": [],
"changed": [{ "id": "welcome", "label": "Welcome", "type": "send", "fields": ["config.messages"] }]
},
"edges": {
"added": [{ "from": "welcome", "to": "support_menu", "branch": "btn_support" }],
"removed": []
},
"settings_changed": ["name"],
"layout_changed": true
}

The diff names what changed, never the values. For a line-by-line view, fetch both versions and compare their definition yourself. The endpoint allows 60 requests per minute per user (429 above that).

Restore a version​

curl -X POST https://api.sendseven.com/api/v1/flows/FLOW_ID/revisions/REVISION_ID/restore \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "version": 15, "revision_summary": "Back to the October menu" }'

A restore is a normal save of that version's definition and settings, so everything that applies to PATCH applies here:

  • version (required) is the flow's current version; a stale one returns 409 flow_version_conflict,
  • the same validation, and on a published or paused flow the full publish checks (422 with "reason": "live_flow_invalid"),
  • the flow's status does not change,
  • contacts waiting on a step that the restored version does not have leave the flow. Preview this with POST /api/v1/flows/{flow_id}/edit-impact.

You can restore up to 10 times per minute per user and flow (429 with a Retry-After header above that). The response is the updated flow, the same as for PATCH. The restore itself becomes a new version with source: "restore" and restored_from_revision_id set. If the version holds a *** credential and the current flow has a value for the same field, that value is kept. If not, the restore fails with 422 and the path of the step, so you can enter the credential again or move it to a flow secret.

Edit lease​

The Flow Builder holds an edit lease while someone has a flow open, so others see who is editing and open it read-only. The lease is advisory: PATCH never checks it, and version remains what protects you from overwriting a newer save. You only need these endpoints if you build your own editor.

MethodPathScope
GET/api/v1/flows/{flow_id}/edit-lease?session_id=...flows:read
POST/api/v1/flows/{flow_id}/edit-leaseflows:write
DELETE/api/v1/flows/{flow_id}/edit-lease?session_id=...flows:write
POST/api/v1/flows/{flow_id}/edit-lease/take-overflows:write
POST/api/v1/flows/{flow_id}/edit-lease/requestflows:write

session_id identifies one editor window: a random id of 8 to 64 letters, digits or dashes (a UUID works) that you create when the editor opens. The same person in a second window is a second session. All write endpoints need a signed-in user; API keys get 403 with {"code": "token_not_allowed"}.

The lease looks like this:

{
"held": true,
"is_mine": false,
"holder": { "user_id": "8a2e...", "name": "Dana Weber", "since": "2026-11-03T10:12:00Z" },
"ttl_seconds": 60,
"heartbeat_seconds": 20
}
  • Acquire and keep it: POST /edit-lease with {"session_id": "..."}. Repeat it every heartbeat_seconds (20); the lease ends ttl_seconds (60) after the last call, for example when the window closes without releasing it. When another session holds it, the response is 409:

    { "detail": { "code": "flow_edit_lease_held", "message": "Someone else is editing this flow.",
    "holder": { "user_id": "8a2e...", "name": "Dana Weber", "since": "2026-11-03T10:12:00Z" } } }
  • Release: DELETE /edit-lease?session_id=... returns {"success": true, "id": "<flow_id>"}. It only releases your own session's lease and can be called more than once.

  • Take over: POST /edit-lease/take-over with {"session_id": "..."} gives you the lease whoever holds it. Up to 10 per minute per user and flow.

  • Ask to edit: POST /edit-lease/request with {"session_id": "..."} notifies the holder and returns 202 with {"success": true, "notified": true, "lease": {...}}. notified is false when nobody else holds the lease. One request per 30 seconds per flow, and a holder receives at most 10 per minute.

Rate limits return 429 with {"code": "...", "retry_after_seconds": N} and a Retry-After header. When the lease service is briefly unavailable the response is 503 with {"code": "flow_edit_lease_unavailable"}; keep editing and retry the heartbeat.

Real-time events​

The person concerned receives these events over the SendSeven WebSocket connection (only them, never the whole workspace):

{ "type": "flow.edit_lease_taken", "tenant_id": "...", "flow_id": "...", "by": { "user_id": "...", "name": "Sam Novak" }, "at": "2026-11-03T10:20:00Z" }
{ "type": "flow.edit_requested", "tenant_id": "...", "flow_id": "...", "from": { "user_id": "...", "name": "Sam Novak" }, "at": "2026-11-03T10:20:00Z" }

flow.edit_lease_taken goes to the previous holder (switch your editor to read-only). flow.edit_requested goes to the holder. Check tenant_id and flow_id: a user's connection receives events for every workspace they belong to.