Skip to content

Recipient files ​

CSV and Excel files are loaded by describing your columns with a mapping. The same mapping model is used by the standalone import endpoints and by recipientsFile on campaign create.

Mapping is by column index ​

The platform does not guess from headers — you state which column position holds which field.

json
{
  "listId": "550e8400-e29b-41d4-a716-446655440001",
  "countrySource": "fixed",
  "fixedCountry": "SE",
  "columnMapping": [
    { "csvHeader": "Full name", "columnIndex": 0, "target": "name" },
    { "csvHeader": "Address",   "columnIndex": 1, "target": "street" },
    { "csvHeader": "Zip",       "columnIndex": 2, "target": "postCode" },
    { "csvHeader": "City",      "columnIndex": 3, "target": "postTown" },
    { "csvHeader": "Segment",   "columnIndex": 4, "target": "customField",
      "customFieldKey": "segment", "customFieldLabel": "Segment" },
    { "csvHeader": "Internal",  "columnIndex": 5, "target": "skip" }
  ]
}
FieldNotes
columnIndex0-based, and what actually selects the column
csvHeaderLabelling only. A mismatch with the real header changes nothing
targetname, street, postCode, postTown, country, customField, skip
customFieldKeyRequired when target is customField
customFieldLabelHuman label; defaults to the key

csvHeader is documentation, columnIndex is behaviour

If your export changes column order, updating csvHeader alone silently imports the wrong data into the wrong fields. columnIndex is what to keep correct.

name, street, postCode and postTown must all be mapped, or the config is rejected with 400. Unmentioned columns are ignored; skip only documents intent.

Split name columns ​

The addressee comes in one of two forms — one full-name column, or a first/last pair:

json
{ "csvHeader": "Förnamn",   "columnIndex": 0, "target": "firstName" },
{ "csvHeader": "Efternamn", "columnIndex": 1, "target": "lastName" }

A middleName column may sit between them. The parts join with a single space, first, middle, last regardless of column order — a file that leads with the surname column still prints "Anna Andersson". A row missing one part uses the others; empty parts never join as a stray space.

Each part is also kept on the recipient as it came, as the built-in fields fornamn, mellannamn and efternamn — so a letter's is "Garcia Alfageme", not a guess from the joined name. Every list may carry them, whatever custom fields it is locked to; a custom field of your own with the same key takes precedence.

Use name only for a column that holds the full addressee by itself, and do not combine it with firstName/lastName — the two forms are alternatives. The name requirement is satisfied by either.

Split street columns ​

street may also be mapped by several columns. SPAR exports — the standard source for purchased Swedish address lists — split the postal address over Utdelningsadress1, Utdelningsadress2 and CareOf, any of which may be empty on a given row. Map them all: non-empty parts join in column order, comma-separated, so a plain row prints "Storgatan 1" and a c/o row prints "c/o Eriksson, Storgatan 1". A row whose mapped street columns are all empty is skipped as missing its street.

Country: per row or fixed ​

countrySourceBehaviour
"csv"Read from the column mapped to country
"fixed"Apply fixedCountry to every row. fixedCountry must be non-empty

Use fixed for a single-country send — one less column to keep clean.

The header row is handled for you ​

Row 1 is treated as headers and never imported, for CSV and Excel alike. Do not skip it yourself with an offset. For Excel, only the first worksheet is read.

Row numbering in errors ​

details.issues[].row is a 1-based index over data rows, with the header excluded. Row 1 is the first row of real data — spreadsheet row 2.

json
{ "row": 3, "field": "vip", "issue": "missing_field", "message": "..." }

That is the 3rd data row, i.e. row 4 in your spreadsheet.

Skipped versus failed ​

The most important distinction when importing.

SkippedFailed
MeaningRow deliberately not importedRow is invalid
EffectOther rows still importNothing imports
Reported as{ "imported": N, "skipped": M }, 200422 with details.issues

A row is skipped when a required standard field is blank, or a custom field is missing with policy skip.

A row fails when it carries a custom field absent from the locked schema, or omits a schema field with policy fail and no default.

json
{ "data": { "imported": 247, "skipped": 3 } }

Always read skipped

A 200 with "imported": 0, "skipped": 500 is a successful response that imported nothing. Compare imported against the rows you expected and alert on the difference — a mis-set columnIndex typically presents exactly this way.

Validation happens before any write ​

Files are validated in full first, then inserted. A file with one bad row imports nothing — there is no partial state to reconcile, and you can safely correct and resend the whole file.

Importing into an existing list ​

Read the schema first:

ts
const res = await fetch(`${BASE}/api/v1/lists/${listId}/schema`, {
  headers: { "x-api-key": KEY },
});
const { data } = await res.json();

if (data.status === "locked") {
  const allowed = new Set(data.fields.map((f) => f.fieldKey));
  // Every customFieldKey you map must be in `allowed`.
}

Mapping a key the locked schema does not have is 422 invalid_mapping:

json
{
  "error": {
    "code": "invalid_mapping",
    "message": "Invalid column mapping",
    "details": {
      "issues": [
        { "row": 0, "field": "segment", "issue": "unexpected_field",
          "message": "Custom field key \"segment\" is not in the list schema" }
      ]
    }
  }
}

row: 0 marks a mapping-level problem rather than a data row.

Mapping issues also cover duplicate custom-field keys (duplicate_key) and a customField column with no key (missing_required).

Formats and limits ​

CSVExcel
Extensions.csv.xlsx, .xls
Size5 MB10 MB
EndpointPOST /api/v1/recipients/import/csvPOST /api/v1/recipients/import/excel

Over the limit is 413 with File too large. Maximum 5MB. or File too large. Maximum 10MB.. Split the file and import in batches — imports are additive.

The format is detected from the filename and MIME type. On campaign create, where that can be ambiguous, pass fileFormat: "csv" | "excel" explicitly. Otherwise:

Could not detect the recipients file format — pass fileFormat: "csv" or "excel" (400)

Import, or send directly? ​

Import then sendrecipientsFile on campaign create
EndpointsTwo callsOne call
ListYou name and reuse itCreated for you, named after the campaign
Fully-skipped file200, imported 0422 list_empty — a campaign needs a recipient
Best forAudiences you send to repeatedlyOne-off sends

Next ​