Imports API — Mimeo docs
Imports API
The CSV import, drivable end to end: upload the file, adjust the mapping, preview, commit, then poll for the report. It's the same import the web wizard runs — same mapping targets, same background job — so an agent's import and a human's can't behave differently.
POST /api/v1/imports upload a CSV (multipart)
GET /api/v1/imports recent imports
GET /api/v1/imports/:id status, mapping, preview, report
PATCH /api/v1/imports/:id mapping, posture, tags, suppression
POST /api/v1/imports/:id/commit run it
Nothing is written to anyone until commit. Up to that point an import is a staged file plus decisions, all of them revisable.
Upload
The one endpoint in the API that takes a file — multipart, under the field name file, and the first row must name the columns:
curl -X POST https://your-mimeo.com/api/v1/imports \
-H "Authorization: Bearer mm_your_token" \
-F "file=@subscribers.csv"
{
"import": {
"id": 12,
"status": "mapping",
"filename": "subscribers.csv",
"row_count": 4980,
"mapping": {
"Email": "core:email",
"First Name": "core:first_name",
"Company": "skip",
"Tags": "tag"
},
"update_posture": "fill_blanks",
"static_tags": [],
"suppress_all": false,
"mark_historical": true
},
"headers": ["Email", "First Name", "Company", "Tags"],
"sample_rows": [
{ "Email": "ada@example.com", "…": "…" }
],
"mapping_targets": [
"core:email",
"core:first_name",
"core:last_name",
"core:subscribed_on",
"field:company",
"field:plan",
"tag",
"unsubscribed",
"skip"
],
"preview": {
"sample": [
{ "email": "ada@example.com", "action": "update" }
],
"existing_count": 14,
"sample_size": 20
}
}
The mapping that comes back is a guess from the header names, nothing more. mapping_targets is the full legal vocabulary, with this install's own custom fields spelled out as field:<key> — map against that list rather than guessing keys. A field: key that doesn't exist yet is created at commit, which is right when it's deliberate and quietly wrong when it's a typo of a key that does exist.
Mapping and the decisions
Every column maps to one target:
| Target | Behavior |
|---|---|
core:email |
The match key — lowercased, and an existing person with that address is updated, never duplicated. Exactly one column must carry this before commit. |
core:first_name / core:last_name |
The person's name fields. |
core:subscribed_on |
The date they joined the list. Only an earlier date replaces a stored one, so re-running an import is idempotent and batch order doesn't matter. An unreadable cell leaves the stored date alone instead of failing the row. |
field:<key> |
A custom field. Multi-word values land as given. |
tag |
The cell becomes tags, split on ` |
unsubscribed |
A truthy value suppresses the person; a timestamp is preserved as the unsubscribe date. |
skip |
Ignore the column. |
Send corrections — and the other three decisions — in one PATCH:
curl -X PATCH https://your-mimeo.com/api/v1/imports/12 \
-H "Authorization: Bearer mm_your_token" \
-H "Content-Type: application/json" \
-d '{
"mapping": { "Email": "core:email", "First Name": "core:first_name",
"Company": "field:company", "Tags": "tag" },
"update_posture": "fill_blanks",
"static_tags": ["from-mailchimp-2026-08"],
"suppress_all": false,
"mark_historical": true
}'
update_posture— how incoming values apply to people who already exist.fill_blanks(the default) writes only where Mimeo has nothing;overwritemakes the CSV the authority.static_tags— applied to every row. This is provenance: tag the batch and you can find, filter or walk it back later.suppress_all— the whole file arrives already unsubscribed. For suppression lists — import those before the active list when migrating.mark_historical— defaults totrue, and you almost always want it. Applying a tag emits atag_addedevent, and events start flows, so a 20,000-row import would otherwise start 20,000 flow runs and mail all of them. With this on, the events the import emits are marked historical and no flow reacts to them — nothing has to be deactivated first. Set it tofalseonly when you want the import to trigger automations on purpose, such as a small batch of genuinely new signups who should receive the welcome journey. See backfilling without sending mail.
Tags are additive either way — an import never strips tags a person already has — and an import never resubscribes anyone.
Preview
Once an email column is mapped, every response carries a preview built from the sample rows: which would update an existing person, which would create a new one, and how many of the sampled addresses already exist. It's a sample, not a census — read it as "14 of the 20 sampled rows already exist", not as a promise about the other 4,960.
Commit and poll
curl -X POST https://your-mimeo.com/api/v1/imports/12/commit \
-H "Authorization: Bearer mm_your_token"
Commit refuses (422) while no column maps to core:email. It answers 202, runs in the background, and from that point the import is read-only — further PATCHes and second commits refuse. Poll:
GET /api/v1/imports/12
{ "import": {
"id": 12,
"status": "completed",
"stats": { "created": 4211, "updated": 612, "skipped": 157, "processed": 4980 },
"errors_sample": [ { "row": 88, "error": "invalid or missing email \"n/a\"" } ],
"completed_at": "2026-08-01T17:03:11Z" } }
status moves mapping → importing → completed (or failed, with error_message saying why). skipped rows are the ones that couldn't be written — a missing or malformed email, almost always — and errors_sample names each one by line number so the source file can be fixed. Row errors are per-row, not fatal: the rest of the file imports around them.
A failed import stopped partway; rows already written stayed written. That's what the provenance tag in static_tags was for.