Require human approval for a workflow's sends
Turns the send-approval gate ON. While it is on, a run reaching a send
step parks awaiting review instead of sending, and a person approves,
skips or cancels each batch in the SendOps dashboard. On writes {id}
resolves as a UUID or, failing that, as the workflow's org-unique key.
One direction only. enabled: true succeeds. enabled: false is
refused with 403 and code send_approval_dashboard_only, on this API
and on the MCP surface alike: the gate is what stands between an
automated journey and unreviewed mail leaving your SES account, and a
credential that could clear it could send on its own authority. A person
turns it off in the dashboard, or nobody does. An omitted enabled is a
422 rather than a silent false.
Operational, not an edit: it does not bump the version, change the content hash, or re-validate the source.
Path parameters
id string required The workflow's UUID or, when the value does not parse as a UUID, its org-unique key. Unknown handles read as 404 not_found. Accepted on every workflow route, read and write alike.
Request body
Content type: application/json
enabled boolean required Must be true. false is refused with 403 (send_approval_dashboard_only) — no API or MCP credential may disable the gate.
Responses
Errors follow the RFC 7807 problem format — see the error reference.
id string<uuid> required name string required key string required The workflow's stable slug within the org.
description string optional status string required Lifecycle status: draft, active, paused, archived, or invalid.
invalid_reason string optional Present only when status is invalid.
require_send_approval boolean optional When true, sends are held for manual review instead of dispatching automatically.
send_mode string enum required shadow means the workflow is ACTIVE and enrolling real contacts but mailing nobody: every send, attribute write and list add is recorded on the run timeline instead of performed. A shadow workflow that looks broken is working as configured — POST /v1/workflows/{id}/go-live arms it for real.
One of: live, shadow
shadow_started_at string<date-time> optional When the current shadow began. Absent while live.
shadow_cohort_list_id string<uuid> optional The list bounding who may enrol while shadowing. Absent while live or when the shadow is unbounded.
shadow_cohort object optional That list resolved: which list, and how many people the rehearsal can reach. Absent while live or when the shadow is unbounded. The bare shadow_cohort_list_id stays beside it because it is the handle activate takes back.
current_version integer required The live definition version.
run_counts object required By-status run tally, e.g. active/completed → counts. Always present.
created_at string<date-time> required updated_at string<date-time> required source string optional The .flow program. Present only for a caller holding api.workflows.manage — the read scope withholds it — and on every response to a write, which returns what was stored.
content_hash string optional The optimistic-concurrency token. Pass it back as expected_content_hash on the next update and a concurrent edit is refused rather than overwritten. Present alongside source.
warnings array<string> optional The analyzer's non-fatal findings from the write that produced this row. A definition can be perfectly valid and still not do what somebody asked for, so these survive a successful save — read them.
diagnostics array<object> optional Every positioned finding, errors as well as warnings, from validating the source this call wrote. This is what makes a 201 with status: invalid actionable: the row was stored, and this says why it cannot run yet. Absent when none were gathered.
Each entry in diagnostics:
line integer | null optional column integer | null optional severity string required error blocks the definition from running; warning does not.
message string required requires array<object> optional Everything this definition still depends on, with each dependency's state. Present on the write responses and on the detail read (the polling surface); absent on the collection read. An empty array means nothing is outstanding — see V1Requirement.
Each entry in requires:
kind string enum required The namespace key is looked up in. Branch on this; never parse detail.
One of: template, segment, list, topic, identity, channel, attribute, activity_property, asset
key string required The identifier as the definition names it — a template name, a segment id or key, a from-address. Empty for a requirement about the ABSENCE of a reference: a send with no topic at all names no topic.
status string enum required ok — resolves and is usable now.
missing — nothing in this organization answers to that key. Correct
the reference, or create the thing.
not_ready — it exists, in a state that cannot be used yet. Usually
a wait, and retrying the identical request is the move. Treating this
as missing sends you hunting for a typo that is not there.
One of: ok, missing, not_ready
detail string required The state, in the words that say which state — deploy_state=pending, member_count=null (evaluation queued). For display and logs; do not parse it.
fix string required The call that clears this requirement, as an HTTP route where there is one and as a plain instruction where there is not (a template deploy is waited for, not called). Empty on an ok entry.
api.workflows.manage
(invalid_scope), or the request asked to DISABLE send approval,
which no API or MCP credential may do
(send_approval_dashboard_only).
application/problem+json validation_failed Problem. When the refusal is about a
reference — a segment key, template slug, topic or attribute name the
organization does not have — it additionally carries validation_code,
field, line/column, missing, candidates and next_step, so a
client can correct the call without a second round of guessing. See
ValidationProblem.
application/problem+json code is internal_error. The
request_id field can be quoted to SendOps support to investigate.
application/problem+json