Skip to content

CSV import and export

From django-crm 1.10.0, leads, contacts and tickets import from a CSV, and leads, contacts, accounts, deals, tickets and invoices export to one. Both directions work from the web app, the phone app and the API.

Import: preview, then commit

All three importers work the same way. You pick a file, the CRM checks every row and shows you what it would create and which rows are wrong, and nothing is written until you commit. Commit re-checks the whole file from scratch and is all or nothing: if any row is invalid, no row is created.

  • Web app: the Import button on the Leads, Contacts and Tickets lists.
  • Phone app: Import from CSV on the same three lists.
  • API: a preview and a commit endpoint per entity, both multipart/form-data with the file under the field name file.
POST /api/leads/import/preview/
POST /api/leads/import/commit/
POST /api/contacts/import/preview/
POST /api/contacts/import/commit/
POST /api/cases/import/preview/
POST /api/cases/import/commit/

Limits and who can import

  • 5 MB and 5,000 data rows per file, measured against the bytes actually read. Split a bigger file.
  • UTF-8 only, with or without a byte-order mark. Anything else is refused with one clear message asking you to re-save the file, rather than importing garbled text.
  • Admins, or members with sales access. Anyone else gets 403, although they can still create records one at a time. A mass-create surface is deliberately narrower than a single create.

Headers

Entity Required Notes
Leads first_name, last_name (each row needs one of them) Most fields the lead form takes, plus assigned_emails, team_names and tags. Each row is validated by the same serializer as a lead created by hand.
Contacts first_name, last_name Optional: email, phone, organization, title, department, do_not_call, linkedin_url, address fields, description, account_name, assigned_emails, team_names, tags.
Tickets name, status, priority Optional: description, case_type, account_name, contact_emails, assigned_emails, team_names, tags, closed_on.

Multiple values in one cell (assigned_emails, team_names, tags, contact_emails) are separated by ;. An unknown header fails the whole file rather than silently dropping a column.

What a row error looks like

{ "row": 3, "field": "email", "message": "'not-an-email' is not a valid email" }

row is 1-based against the data rows, so the first row under the header is row 1.

You can take the errors away as a file to fix in your spreadsheet: Download errors in the web app, and from django-crm 1.11.0 Save errors as CSV in the phone app, which asks where to keep the file. Each row of it names the row number, the field and the reason. The phone app can also save the importer's CSV template the same way.

Matching and duplicates

Every reference (an account name, an assignee's email, a team, a contact's email) is resolved against your organisation only, so a crafted file cannot attach a record to another tenant's data. An unresolved reference is a row error, not a silent skip. Tags are the one exception: an unknown tag is created at commit.

  • Leads: a repeated email, in the file or already in your org, is an error.
  • Contacts: a repeated email or phone number is an error. A repeated full name is only an error when the row has neither an email nor a phone to tell the two people apart.
  • Tickets: a repeated ticket name is an error. The ticket importer does not create contacts; contact_emails must name contacts you already have.

Accounts, deals, tasks and invoices have no importer. Create them through the API.

The older lead upload

POST /api/leads/upload/ (file under leads_file) still exists for scripts written against it. It is deprecated: it now runs the same validation and the same all-or-nothing write as the lead commit endpoint, synchronously, so a 200 means every row was created and a bad row fails the file with a 400 that names it. Prefer the preview and commit pair for anything new.

Export

Six lists export to CSV: leads, contacts, accounts, deals, tickets and invoices.

  • Web app: the Export button on each of those lists.
  • Phone app: the export action on the same six lists, which saves the file where you choose.
  • API: GET on the list's export path, with the same query string the list takes.
GET /api/leads/export/
GET /api/contacts/export/
GET /api/accounts/export/
GET /api/opportunities/export/
GET /api/cases/export/
GET /api/invoices/export/

What you get:

  • What the list shows you, all of it. The export runs the same query as the list for the same filters, so a filtered list exports filtered, and every page of it rather than the page on screen.
  • Only what you are allowed to see. Any member of the org can export, and each person gets exactly the rows the list would show them. A member who sees only their own deals exports only their own deals.
  • Safe to open in a spreadsheet. A text cell starting with =, +, -, @, a tab or a carriage return is written with a leading ', so a value somebody typed into a web form cannot run as a formula when a colleague opens the file. Numbers are left alone.
  • Opens cleanly in Excel. The file is UTF-8 with a byte-order mark and is streamed, so a large export does not time out.

For an API token, export needs the same read scope as the list it exports. A malformed filter value (an id that is not an id, a day that does not exist) is a 400 naming the parameter, returned before any of the file is sent. A saved view holds the same filters, so its query string exports the same rows.

The full export reference, with every column of each file, is CSV exports in the CRM docs.

See also

  • Webhooks: being told about changes rather than exporting them.
  • Custom fields: how org-defined fields work on the API.
  • Errors: the standard error response shape.