Carillon docs

Import devices

CSV columns, upload requests, import reports, and duplicate handling.

If you are moving from another provider, or you hold tokens your app collected before Carillon, an import brings them in. The endpoint takes the file itself as the request body, not a JSON envelope around it.

Samples on this page show placeholders. Sign in to the dashboard in this browser and they fill in with your own organization, app and mobile key.

Moving from OneSignal? Follow the migration guide for export conversion and staged SDK rollout.

The file

A header row, then one device per line.

token,platform,environment,external_id,timezone_id,locale
a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90,ios,production,user-42,Europe/Paris,fr-FR
d4c3b2a1f6e50817293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90,android,,user-43,America/New_York,en-US
ColumnRequiredNotes
tokenYesExactly as the provider issued it
platformYesios or android
environmentNoiOS only, production or sandbox. Absent means production
external_idNoYour own identifier for the person
timezone_idNoAn IANA identifier such as Europe/Paris, never an offset
localeNoA BCP 47 tag

Unknown columns are ignored rather than refused, and listed in the report as ignored_columns. A misspelled timezone_id appears there.

Uploading

curl -X POST https://api.carillon.dev/v1/devices/import \-H "Authorization: Bearer carillon_sk_live_your_secret_key" \-H "Content-Type: text/csv" \--data-binary @devices.csv

The answer is 202 and the import has not been read yet:

{
  "id": "01937b40-0000-7000-8000-000000000000",
  "status": "pending",
  "counts": { "created": 0, "already_present": 0, "invalid": 0 },
  "errors": [],
  "errors_truncated": false,
  "ignored_columns": [],
  "failure": null,
  "created_at": "2026-08-20T09:30:00.000Z",
  "finished_at": null
}

A file too large for one request answers 413 import_too_large. Split it and import the parts in any order: a token already imported is skipped, so overlapping files are safe.

Reading the report

curl https://api.carillon.dev/v1/imports/01937b40-0000-7000-8000-000000000000 \-H "Authorization: Bearer carillon_sk_live_your_secret_key"
{
  "id": "01937b40-0000-7000-8000-000000000000",
  "status": "done",
  "counts": { "created": 18422, "already_present": 96, "invalid": 3 },
  "errors": [
    { "line": 214, "field": "platform", "message": "Expected ios or android." }
  ],
  "errors_truncated": false,
  "ignored_columns": ["email"],
  "failure": null,
  "created_at": "2026-08-20T09:30:00.000Z",
  "finished_at": "2026-08-20T09:30:41.902Z"
}

status: "failed" means the file could not be read at all, and failure says why. A file with rejected lines is done: the valid lines were imported and the invalid ones are listed, at most a thousand of them. counts.invalid is exact however many the report shows.

Duplicates and token validity

A token this app already has is left exactly as it was, and the CSV line is ignored. Re-running the same file therefore creates nothing, and an import is safe to retry. New tokens are added; changed fields and later opt-outs are not synchronized.

Only import currently opted-in subscriptions. New imported devices default to opted_in=true; CSV status or opt-out columns are ignored.

Tokens are not checked against a provider at import time. They are imported exactly as given, and the feedback loop strikes off the dead ones on the first campaign. Expect the first send after a large import to invalidate a slice of it.

Imported devices carry no permission

A CSV carries no permission state, so push_permission stays unknown on an imported device until the SDK sees it. Filtering an audience on push_permission therefore does not reach imported devices; source is the filter that separates the two populations.

Devices · Audiences · Import devices

On this page