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(theversioncheck, 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"
}'
versionis the flow's currentversionfrom your lastGET. You can send it as anIf-Match: 12header instead. When someone saved in the meantime the response is409with{"code": "flow_version_conflict"}and nothing is saved: load the flow again and reapply your change. Every successful save incrementsversion.- Send the whole
definition.definitionis 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 thatPATCHdoes not accept are ignored, never an error. ChangestatuswithPOST /publishandPOST /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,jsonorai_assistant. Requests made with an API key are always recorded asapi, whatever you send. Any other value is a422.
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
| Method | Path | Scope |
|---|---|---|
GET | /api/v1/flows/{flow_id}/revisions?page=1&page_size=20 | flows:read |
GET | /api/v1/flows/{flow_id}/revisions/{revision_id} | flows:read |
GET | /api/v1/flows/{flow_id}/revisions/{revision_id}/diff?against=current | flows:read |
POST | /api/v1/flows/{flow_id}/revisions/{revision_id}/restore | flows: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.
authorisnullfor saves made with an API key.author.nameisnullonce the person has left the workspace.sourceis one ofcanvas,json,ai_assistant,ai_flow_builder(created by the AI Flow Builder),apiorrestore.statscounts 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 returns409 flow_version_conflict,- the same validation, and on a published or paused flow the full publish checks (
422with"reason": "live_flow_invalid"), - the flow's
statusdoes 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.
| Method | Path | Scope |
|---|---|---|
GET | /api/v1/flows/{flow_id}/edit-lease?session_id=... | flows:read |
POST | /api/v1/flows/{flow_id}/edit-lease | flows:write |
DELETE | /api/v1/flows/{flow_id}/edit-lease?session_id=... | flows:write |
POST | /api/v1/flows/{flow_id}/edit-lease/take-over | flows:write |
POST | /api/v1/flows/{flow_id}/edit-lease/request | flows: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-leasewith{"session_id": "..."}. Repeat it everyheartbeat_seconds(20); the lease endsttl_seconds(60) after the last call, for example when the window closes without releasing it. When another session holds it, the response is409:{ "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-overwith{"session_id": "..."}gives you the lease whoever holds it. Up to 10 per minute per user and flow. -
Ask to edit:
POST /edit-lease/requestwith{"session_id": "..."}notifies the holder and returns202with{"success": true, "notified": true, "lease": {...}}.notifiedisfalsewhen 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.