Apply a sendops.json bundle, dry run by default
Applies a bundle — the same shape GET /v1/export produces — through
the ordinary authoring services. Every object goes through the method
the dashboard and MCP call, so optimistic concurrency, version snapshots
with actor identity, save-time segment evaluation and inline SES deploy
all apply.
dry_run defaults to true. A call that omits it returns the plan
and writes nothing. That default is deliberate: this route is reached by
pipelines, and a misconfigured job should print a plan rather than
rewrite an organization.
prune defaults to false. With prune=true, objects the org has
and the bundle omits are archived (segments, lists, topics, workflows)
or deleted (attributes, activity properties) — and an attribute a
remaining segment or workflow still references is skipped with
referenced_by naming what blocked it, never deleted. Pruning applies
only to sections the bundle actually declares: a templates-only push
never archives an audience. Images are never pruned, because mail
already delivered renders from the URLs they back.
Applicable or nothing. If any object fails validation the response
carries the full plan with applicable: false and every diagnostic
positioned by line and column, and NOTHING is written — not even the
objects that passed.
Idempotent. Applying the same bundle twice yields an all-unchanged
plan and writes nothing on the second run.
Apply order, which is also the order the plan lists objects in: attributes → activity properties → lists → segments → topics → images → templates → workflows. Each step's validation needs the previous step's rows; images precede templates because a template deploys with its image URLs embedded. A workflow created by an import lands as a draft; an existing active workflow updated by one stays active.
Partial results are reported honestly. The services open their own
transactions, so a failure part-way leaves the earlier kinds written.
applied_until names the last kind that completed and error says what
stopped it.
Body. Either a zip archive (Content-Type: application/zip, or any
body starting with the zip magic number) or the JSON form. The JSON form
may also carry dry_run and prune as top-level fields, for clients
building one document; the query parameter wins when both are given.
Scopes. Each kind the bundle CONTAINS needs that kind's .manage
scope (api.templates.manage, api.segments.manage,
api.lists.manage, api.topics.manage, api.attributes.manage,
api.activities.manage, api.workflows.manage, api.assets.manage).
A bundle of templates alone needs only api.templates.manage; a missing
scope is a 403 that names it.
Query parameters
dry_run boolean optional Plan only, writing nothing. Defaults to true — an unparseable value is a 422 rather than a silent fallback.
prune boolean optional Archive or delete objects the org has and the bundle omits, within the sections the bundle declares.
Request body
Content type: application/json
manifest object optional The sendops.json object. Wins over a files["sendops.json"] string when both are present.
files object optional images object optional Binary files, standard base64. A data: URL prefix is stripped.
dry_run boolean optional The query parameter's inline twin, for clients building one document. The query parameter wins when both are given.
prune boolean optional As dry_run — the query parameter wins.
Responses
Errors follow the RFC 7807 problem format — see the error reference.
200 even when
applicable is false: the plan is the answer to the request, and a
bundle that does not apply is a result to read, not a malformed
call.
application/json dry_run boolean required What actually happened. A dry run never has applied true.
plan object required Fields of plan:
applicable boolean required False when any object failed validation. Nothing is then applied, including the objects that passed.
prune boolean required Echoes the request's flag.
objects array<object> required Every object the bundle touches, in the order an apply writes them.
Each entry in objects:
kind string enum required One of: template, segment, list, topic, attribute, activity_property, workflow, image
key string required The identifier within the kind — a template name, a segment key, an image path, or activity.property for an activity property.
path string optional The bundle file this object came from. Absent for a prune, which is about an object the bundle does not contain.
action string enum required unchanged writes nothing at all — no version snapshot, no SES deploy, no segment re-evaluation — which is what makes a push safe to run on every commit.
One of: create, update, unchanged, archive, delete, skip
reason string optional Why a skip, and the basis of any action that is not obvious.
referenced_by array<string> optional What blocks a prune. Present only on a skip.
diagnostics array<object> required Validation failures on this object. Non-empty means the whole plan is inapplicable.
Each entry in diagnostics:
path string optional The bundle-relative file the finding is about.
line integer optional column integer optional message string required totals object required Objects per action. An action with no objects is absent rather than zero.
diagnostics array<object> required Bundle-level problems — a manifest warning, a declared file the bundle does not carry.
Each entry in diagnostics:
path string optional The bundle-relative file the finding is about.
line integer optional column integer optional message string required applied boolean required True only when every step completed.
objects array<object> required Per-object outcomes of an apply. Empty on a dry run.
Each entry in objects:
kind string required key string required action string required reason string optional Why the object was skipped, in prose — a scope the credential lacks, a prune that something still references. Absent for an object that was written.
referenced_by array<string> optional What blocked a prune. Only present on a skip.
deploy_state string enum optional The template's SES state after the write. pending on an update of an already-deployed template is correct: SES holds the previous body until the new one lands.
One of: pending, deployed, failed
member_count integer<int64> | null optional The segment's membership after the save-time evaluation, and null when that evaluation was queued instead. Null is not zero.
requires array<object> optional The outstanding-dependency block for kinds that have one.
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.
error string optional The failure that stopped the import at this object.
applied_until string optional The last kind that completed when an apply stopped early. The kinds before it are written; the kinds after it are not.
error string optional Why the apply stopped.
code: invalid_scope), or it is bound to the test environment and this operation is irreversible (code: test_environment_forbidden). Branch on code: the first is fixed by granting the scope, the second only by using a live credential. See the "Live and test credentials" section of the API description. 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 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