SendOps can hand you your organization as a set of files, and take one back.
That's two endpoints — `GET /v1/export` and `POST /v1/import` — and one format
between them, called a **bundle**.

A bundle is `sendops.json` plus every file it names, travelling as one unit. It
covers everything you author: templates, segments, Lists, topics, attributes,
activity properties, workflows and the images your templates reference.


  [`sendops push`](/templates/keeping-a-copy-in-git) is the GitHub Action that
  builds a bundle from a directory and posts it here for you — with a dry-run
  check on pull requests and an apply on merge. This page is what it's doing
  underneath, and what to use if you'd rather call it yourself.


## What a bundle looks like


```
sendops.json
templates/
├── welcome.html
└── receipt.html
audience/
├── all-contacts.sendql
├── lists.json
├── topics.json
├── attributes.json
├── activity-properties.json
└── workflows/
    └── onboarding.flow
static/
└── logo.png
```


`sendops.json` is the index: it names every template, segment, List, topic,
attribute, activity property and workflow, and where each one's file lives.
Everything else is the content, in the format it was authored in — Handlebars for
templates, [SendQL](/audience/segment-syntax) for segments, the
[`.flow` language](/workflows/flow-reference) for workflows, JSON for the
schema kinds. Images sit at whatever path your templates
[reference them by](/edge/asset-library).

## In the dashboard

**Settings → Export & import** does both ends without a terminal. **Download zip**
or **Download JSON** hands you the bundle; the import side takes a file, shows you
the plan it would apply — with a **prune** toggle that re-plans as you flip it —
and only writes when you confirm.

## Exporting

`GET /v1/export` returns the bundle. Ask for `Accept: application/zip` and you get
a zip archive whose root is `sendops.json` — a directory a CI job can unzip, or
zip back up and push unchanged. Ask for anything else and you get a JSON form
with every file inlined, which is easier if you're scripting against it.

**Exports are deterministic.** Two exports of an unchanged organization are
byte-identical: sorted throughout, fixed archive timestamps, workflows rendered
through the canonical formatter. You can export on a schedule and commit the
result without producing a diff every night.

Any single *view* permission is enough to call it. A section your credential can't
read is **left out and named**, rather than failing the whole call — so a
templates-only key gets a real, importable bundle of its templates plus the list
of what it wasn't allowed to see. That matters: "this organization has no
segments" and "you weren't allowed to look at the segments" are different facts,
and the response distinguishes them.

## Importing

`POST /v1/import` applies a bundle — the same shape export produces. Send it as a
zip, or as the JSON form.

Every object goes in through the same code the dashboard uses. Segments are
evaluated at save time, templates are deployed to SES, versions are snapshotted
with who did it. An import is not a side door.

### The plan comes first

**`dry_run` defaults to `true`.** A call that doesn't say otherwise returns the
plan and writes nothing.

That default is deliberate, because this endpoint is reached by pipelines and a
misconfigured job should print a plan rather than rewrite an organization.

The plan lists every object the bundle touches and what would happen to it —
create, update, unchanged, archive — along with totals, and any problem found
anywhere in the bundle positioned by file, line and column.

Pass `dry_run=false` to apply it.


  If any object in the bundle fails validation, the response carries the full plan
  marked not applicable and **nothing is written — not even the objects that were
  fine**. A bundle describes one intended state, so applying half of it would
  leave you somewhere nobody chose.


**Re-importing an unchanged bundle does nothing and says so.** Every object comes
back *unchanged* and no writes happen. That's worth knowing: importing is never a
risky thing to try.

### The order things are applied in

Attributes → activity properties → Lists → segments → topics → images →
templates → workflows.

That's also the order the plan lists them in, and it isn't arbitrary: each step's
validation needs the rows the previous step created. A segment rule can't be
checked before the attributes it references exist. Images come before templates
because a template is deployed with its image URLs already embedded.

A workflow **created** by an import arrives as a **draft** — an import never
starts a journey on its own. A workflow that already exists and is active stays
active, and the import is an ordinary edit to it.

### Pruning

`prune` defaults to `false`. With it on, objects your organization has and the
bundle omits are cleaned up: segments, Lists, topics and workflows are
**archived**; attributes and activity properties are **deleted**.

Three things keep that from being a foot-gun:

- **Pruning only applies to the sections your bundle actually declares.** A
  templates-only push never archives your audience.
- **An attribute a remaining segment or workflow still references is skipped**,
  not deleted, and the response names what blocked it.
- **Images are never pruned.** Mail you've already delivered renders from the URLs
  they back.


  Prune assumes the bundle is the complete picture. If people are also creating
  segments and templates in the dashboard, prune will archive their work on the
  next push.


### If an apply stops part-way

The kinds are written in their own transactions, so a failure mid-import leaves
the earlier kinds written. The response says so plainly: it names the last kind
that completed and what stopped it. Kinds before that point are applied; kinds
after it are not. Fix the problem and re-run — the parts that already landed come
back as *unchanged*.

## Permissions

Export needs any one **view** permission, and quietly omits what your credential
can't read.

Import needs the **manage** permission for each kind the bundle *contains*. A
bundle of templates alone only needs the templates one; a missing permission is
refused by name, so you know exactly which one to add to the key. Create keys
under **Settings → API keys** — a key that can deploy templates and nothing else
is a good key.

## What's next?

- [Keeping a copy in git](/templates/keeping-a-copy-in-git) — `sendops push`, and the pull-request recipe.
- [Version history & restore](/templates/version-history-and-restore) — undo a single bad edit without a bundle.
- [Template Management](/templates/template-management) — how templates are authored and deployed.
- [Importing from SES](/templates/importing-from-ses) — bringing existing SES templates in.