Skip to content

Recipients ​

A recipient is one physical address. The five standard fields are always required; anything else is a custom field, and the first recipient added to a list freezes which custom fields that list accepts — see Lists & recipients.

List all recipients ​

GET/api/v1/recipients
requires recipients:read

Every recipient in the account, across all lists. Cursor-paginated, newest first. Requires recipients:read.

Query parameters ​

ParameterTypeDefaultNotes
cursoruuid—An id from a previous page
limitinteger201–100
directionnext | prevnextPage direction
searchstring—≤200 characters. Case-insensitive substring match on the recipient name only
bash
curl -H "x-api-key: drse_your_api_key" \
  "https://api.utskick.dr.se/api/v1/recipients?limit=1&search=alice"
ts
const params = new URLSearchParams({ limit: "1", search: "alice" });
const res = await fetch(
  `https://api.utskick.dr.se/api/v1/recipients?${params}`,
  { headers: { "x-api-key": process.env.DRSE_API_KEY! } },
);
const { data, meta } = await res.json();

200 OK

json
{
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440004",
      "name": "Alice Andersson",
      "street": "1 First St",
      "postCode": "111 11",
      "postTown": "Stockholm",
      "country": "SE",
      "customFields": { "vip": { "label": "VIP", "value": "yes" } },
      "listId": "550e8400-e29b-41d4-a716-446655440001",
      "userId": "user_2abc",
      "createdAt": "2026-07-25T09:10:00.000Z",
      "updatedAt": "2026-07-25T09:10:00.000Z"
    }
  ],
  "meta": { "count": 250, "nextCursor": "550e8400-e29b-41d4-a716-446655440004", "prevCursor": null }
}

count respects search: it is the number of matching recipients, not the account total.

Failures ​

StatuscodeWhen
400validation_failedlimit out of range, cursor not a UUID, direction invalid, or search over 200 characters
401unauthorizedKey problem
403forbiddenKey lacks recipients:read

List the recipients of one list ​

GET/api/v1/recipients/by-list/:listId
requires recipients:read

Same shape and parameters as above, scoped to one list. Requires recipients:read.

bash
curl -H "x-api-key: drse_your_api_key" \
  "https://api.utskick.dr.se/api/v1/recipients/by-list/550e8400-e29b-41d4-a716-446655440001?limit=50"
ts
const listId = "550e8400-e29b-41d4-a716-446655440001";
const res = await fetch(
  `https://api.utskick.dr.se/api/v1/recipients/by-list/${listId}?limit=50`,
  { headers: { "x-api-key": process.env.DRSE_API_KEY! } },
);
const { data, meta } = await res.json();

200 OK — identical to the account-wide endpoint; meta.count is the number of matching recipients in this list.

Failures ​

StatuscodeWhen
400validation_failedlistId not a UUID, or an invalid query parameter
401unauthorizedKey problem
403forbiddenKey lacks recipients:read
404not_foundList not found — unknown id, or a list in another account

Add one recipient ​

POST/api/v1/recipients
requires recipients:create

Requires recipients:create. For more than a handful of rows use an import instead.

Body ​

FieldTypeRequiredNotes
listIduuidyesTarget list, must be yours
namestringyes1–200 characters
streetstringyes1–255 characters
postCodestringyes1–20 characters
postTownstringyes1–100 characters
countrystringyes1–100 characters
customFieldsobjectnofieldKey → { label, value }. Max 16 entries. Keys are alphanumeric + underscore, ≤64 chars; values ≤512 chars
customFieldPoliciesobjectnofieldKey → { kind, value? } where kind is skip, fail or default. Only honoured on the first add to an unlocked list, where it defines the schema. Ignored afterwards
bash
curl -X POST https://api.utskick.dr.se/api/v1/recipients \
  -H "x-api-key: drse_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "listId": "550e8400-e29b-41d4-a716-446655440001",
    "name": "Alice Andersson",
    "street": "1 First St",
    "postCode": "111 11",
    "postTown": "Stockholm",
    "country": "SE",
    "customFields": { "vip": { "label": "VIP", "value": "yes" } }
  }'
ts
const res = await fetch("https://api.utskick.dr.se/api/v1/recipients", {
  method: "POST",
  headers: {
    "x-api-key": process.env.DRSE_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    listId: "550e8400-e29b-41d4-a716-446655440001",
    name: "Alice Andersson",
    street: "1 First St",
    postCode: "111 11",
    postTown: "Stockholm",
    country: "SE",
    customFields: { vip: { label: "VIP", value: "yes" } },
  }),
});
const body = await res.json();
if (res.status === 422) console.error(body.error.details.issues);

201 Created — the stored recipient, with any schema defaults filled in.

json
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440004",
    "name": "Alice Andersson",
    "street": "1 First St",
    "postCode": "111 11",
    "postTown": "Stockholm",
    "country": "SE",
    "customFields": { "vip": { "label": "VIP", "value": "yes" } },
    "listId": "550e8400-e29b-41d4-a716-446655440001",
    "userId": "user_2abc",
    "createdAt": "2026-07-25T09:10:00.000Z",
    "updatedAt": "2026-07-25T09:10:00.000Z"
  }
}

Failures ​

StatuscodeWhen
400validation_failedA required field is missing or over its length limit; a custom-field key is not alphanumeric; more than 16 custom fields
401unauthorizedKey problem
403forbiddenKey lacks recipients:create
404not_foundList not found
422recipient_validation_failedMessage Schema validation failed. details.issues explains: a custom field not in the locked schema (unexpected_field), or a schema field this recipient omits with no default (missing_field)

Empty means absent

A custom field sent as "" is treated as missing, exactly as an import treats an empty cell. If the locked schema has no default for it, the add is rejected rather than silently storing a blank.

Import recipients from a CSV file ​

POST/api/v1/recipients/import/csv
requires recipients:import

Bulk-import into an existing list. Requires recipients:import. multipart/form-data only.

Parts ​

PartTypeNotes
filefileThe CSV. Max 5 MB. Must look like a CSV by extension or MIME type
configJSON stringTarget list and column mapping, below

config ​

FieldTypeRequiredNotes
listIduuidyesTarget list, must be yours
columnMappingarrayyesOne entry per column you care about
countrySource"csv" | "fixed"yesRead country per row, or apply one value to all
fixedCountrystringwhen countrySource is fixedNon-empty

Each columnMapping entry:

FieldTypeNotes
csvHeaderstringThe column's header text (labelling only)
columnIndexinteger0-based position in the row. This is what actually selects the column
targetenumname, street, postCode, postTown, country, customField or skip
customFieldKeystringRequired when target is customField
customFieldLabelstringHuman label; defaults to the key
customFieldMissingValuePolicyobject{ kind, value? }. Only meaningful on the first import into an unlocked list

name, street, postCode and postTown must all be mapped. The header row is detected and skipped automatically.

bash
curl -X POST https://api.utskick.dr.se/api/v1/recipients/import/csv \
  -H "x-api-key: drse_your_api_key" \
  -F "file=@recipients.csv" \
  -F 'config={
    "listId": "550e8400-e29b-41d4-a716-446655440001",
    "countrySource": "fixed",
    "fixedCountry": "SE",
    "columnMapping": [
      { "csvHeader": "Name",      "columnIndex": 0, "target": "name" },
      { "csvHeader": "Street",    "columnIndex": 1, "target": "street" },
      { "csvHeader": "Post code", "columnIndex": 2, "target": "postCode" },
      { "csvHeader": "Post town", "columnIndex": 3, "target": "postTown" }
    ]
  }'
ts
import { readFile } from "node:fs/promises";

const form = new FormData();
form.append(
  "file",
  new File([await readFile("recipients.csv")], "recipients.csv", {
    type: "text/csv",
  }),
);
form.append(
  "config",
  JSON.stringify({
    listId: "550e8400-e29b-41d4-a716-446655440001",
    countrySource: "fixed",
    fixedCountry: "SE",
    columnMapping: [
      { csvHeader: "Name", columnIndex: 0, target: "name" },
      { csvHeader: "Street", columnIndex: 1, target: "street" },
      { csvHeader: "Post code", columnIndex: 2, target: "postCode" },
      { csvHeader: "Post town", columnIndex: 3, target: "postTown" },
    ],
  }),
);

const res = await fetch(
  "https://api.utskick.dr.se/api/v1/recipients/import/csv",
  { method: "POST", headers: { "x-api-key": process.env.DRSE_API_KEY! }, body: form },
);
const { data } = await res.json();
console.log(`${data.imported} imported, ${data.skipped} skipped`);

200 OK — note 200, not 201, because an import is a bulk action rather than the creation of one resource.

json
{ "data": { "imported": 247, "skipped": 3 } }
FieldNotes
importedRows written
skippedRows deliberately dropped — a required standard field was blank, or a custom field was missing with missingBehavior: "skip". Skipped rows are not errors

A fully-skipped import returns { "imported": 0, "skipped": N } with 200, and leaves an unlocked list unlocked.

Failures ​

StatuscodeWhen
400validation_failedCSV file is required; Invalid file mime or extension; Import configuration is required; Invalid import configuration JSON; or the config failed validation (details carries the field errors)
401unauthorizedKey problem
403forbiddenKey lacks recipients:import
404not_foundList not found
413payload_too_largeFile too large. Maximum 5MB.
422invalid_mappingInvalid column mapping — duplicate custom-field keys, a custom mapping with no key, or a key absent from the locked schema. details.issues names each
422recipient_validation_failedSchema validation failed — at least one row failed. Nothing is imported. details.issues lists the rows

Import recipients from an Excel file ​

POST/api/v1/recipients/import/excel
requires recipients:import

Identical to the CSV import, with three differences. Requires recipients:import.

CSVExcel
Size limit5 MB10 MB
Accepted.csv.xlsx, .xls
Missing-file messageCSV file is requiredExcel file is required

Only the first worksheet is read, and row 1 is treated as the header. columnIndex is the 0-based column position in that sheet.

bash
curl -X POST https://api.utskick.dr.se/api/v1/recipients/import/excel \
  -H "x-api-key: drse_your_api_key" \
  -F "file=@recipients.xlsx" \
  -F 'config={
    "listId": "550e8400-e29b-41d4-a716-446655440001",
    "countrySource": "csv",
    "columnMapping": [
      { "csvHeader": "Name",      "columnIndex": 0, "target": "name" },
      { "csvHeader": "Street",    "columnIndex": 1, "target": "street" },
      { "csvHeader": "Post code", "columnIndex": 2, "target": "postCode" },
      { "csvHeader": "Post town", "columnIndex": 3, "target": "postTown" },
      { "csvHeader": "Country",   "columnIndex": 4, "target": "country" }
    ]
  }'
ts
import { readFile } from "node:fs/promises";

const form = new FormData();
form.append(
  "file",
  new File([await readFile("recipients.xlsx")], "recipients.xlsx", {
    type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  }),
);
form.append(
  "config",
  JSON.stringify({
    listId: "550e8400-e29b-41d4-a716-446655440001",
    countrySource: "csv",
    columnMapping: [
      { csvHeader: "Name", columnIndex: 0, target: "name" },
      { csvHeader: "Street", columnIndex: 1, target: "street" },
      { csvHeader: "Post code", columnIndex: 2, target: "postCode" },
      { csvHeader: "Post town", columnIndex: 3, target: "postTown" },
      { csvHeader: "Country", columnIndex: 4, target: "country" },
    ],
  }),
);

const res = await fetch(
  "https://api.utskick.dr.se/api/v1/recipients/import/excel",
  { method: "POST", headers: { "x-api-key": process.env.DRSE_API_KEY! }, body: form },
);
const { data } = await res.json();

200 OK

json
{ "data": { "imported": 300, "skipped": 0 } }

Failures ​

Same as the CSV import, except 413 reports File too large. Maximum 10MB. and a missing part reports Excel file is required.

A deeper walkthrough of mapping, skipping and row numbering: Recipient files.