API Reference
Contact Imports API
Import a CSV file of any size, millions of rows included, into a campaign or a contact list. The file goes straight to storage and is imported in the background.
An import takes three calls: create it (you get an upload URL per 32 MiB part of the file), upload the parts, and start it. Then follow its progress. Keys need contacts:write (and contacts:read to follow it), plus campaigns:write for a campaign or contact_lists:write for a list.
POST /v1/contact-importswith the target, the file's name and size, and its columns.PUTeach part's bytes to itsurl(a file up to 32 MiB has one part:upload_url). The URLs last two hours;POST /v1/contact-imports/:id/uploadanswers fresh ones for the parts still missing.POST /v1/contact-imports/:id/start.GET /v1/contact-imports/:idevery few seconds untilcompleted; thenGET /v1/contact-imports/:id/rejectsfor the rows that were not imported.
An active campaign starts calling while the file imports
People join the campaign as their rows land, so an active campaign calls the first ones while the rest of the file is still being read. A draft or paused campaign holds them until it starts. Numbers are checked as in the dashboard: only mobiles and landlines of Mexico, the United States and Canada are imported, people who asked not to be contacted never are, and a number written without its country code is read in phone_country or refused.
Endpoints
/v1/contact-importsCreate an importPOST/v1/contact-imports/:id/uploadRenew the upload URLsPOST/v1/contact-imports/:id/startStart an importGET/v1/contact-imports/:idGet an import's progressPOST/v1/contact-imports/:id/cancelCancel an importGET/v1/contact-imports/:id/rejectsDownload the refused rowsCreate an import
/v1/contact-importsStarts a background import of a CSV file into a campaign (its duplicates rule applies) or a contact list, and answers one signed upload URL per part of the file. PUT each part's bytes to its URL (upload_url is the first part's; a file up to 32 MiB has only that one), then call POST /v1/contact-imports/{id}/start. Every number is checked as in the dashboard: Mexico, the United States and Canada, a mobile or landline.
contacts:writeSupports Idempotency-KeyUpload each part with PUT <url>, the part's bytes (byte_start up to byte_end, exclusive) as the body and Content-Type: text/csv. A part uploaded again replaces itself.
The URLs are valid for two hours, and replaying this call with the same Idempotency-Key answers the same URLs: when an upload takes longer, POST /v1/contact-imports/{id}/upload answers fresh ones for the parts still missing.
The first row is the header. Values may be quoted, span lines and use "" for a quote; rows of empty cells are skipped. Row numbers count as a spreadsheet shows them (the header is row 1).
A number repeated in the file is imported once (its first row). For a campaign, a person already enrolled follows its duplicates rule: skip, update_only (the lead data is replaced) or re_enroll (enrolled again; a person who is being called or has a callback booked keeps their place, and only their lead data is replaced).
The campaign dials while the import runs: people of an active campaign are queued as their rows land; a draft or paused campaign holds them until it starts.
Request body (JSON)
| Name | Type | Description |
|---|---|---|
campaign_id | uuid | Import into this campaign (or send |
contact_list_id | uuid | Import into this list (or send |
file_nameRequired | string | The file's name, shown in the dashboard. Up to 255 characters. |
file_sizeRequired | integer | The file's exact size in bytes (it decides the parts: 32 MiB each). 1–1073741824. |
columnsRequired | object | Which column holds what. Only |
columns.phoneRequired | string | The phone number. A column of your file: its header text ( |
columns.name | string | Full name. A column of your file: its header text ( |
columns.first_name | string | First name. A column of your file: its header text ( |
columns.last_name | string | Last name. A column of your file: its header text ( |
columns.email | string | Email (an invalid one is dropped). A column of your file: its header text ( |
columns.company | string | Company (fills the contact's when empty). A column of your file: its header text ( |
columns.notes | string | Notes for the agent. A column of your file: its header text ( |
columns.external_id | string | Your id for the person, kept on the contact as |
columns.custom | object[] | Columns kept as custom fields: in the enrollment's lead data under |
phone_country | string | null | The country of numbers written without their country code: |
delimiter | string | null | The separator; detected from the file when omitted. One of: ,, ;, , |. |
encoding | string | null | The text encoding; detected when omitted (Excel's "CSV" in Spanish is windows-1252). One of: utf-8, windows-1252. |
source | string | Origin label for the enrollments (lowercase letters, digits, - and _, up to 40), and the key of Default: |
Example request
curl -X POST "https://www.nutalk.ai/v1/contact-imports" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Idempotency-Key: 11111111-2222-4333-8444-555555555555" \ -H "Content-Type: application/json" \ -d '{ "campaign_id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b", "file_name": "base-octubre.csv", "file_size": 148210544, "phone_country": "MX", "columns": { "phone": "Teléfono", "name": "Nombre", "email": "Correo", "custom": [ { "column": "Interés", "key": "interes" } ] }}'The import, awaiting its upload, with the URL of every part (valid for two hours).
{
"success": true,
"data": {
"id": "3c9a0f7e-5d21-4b8e-9a47-6f1e2d3c4b5a",
"status": "awaiting_upload",
"campaign_id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"contact_list_id": null,
"file_name": "base-octubre.csv",
"file_size": 148210544,
"progress": {
"bytes_processed": 0,
"percent": 0,
"estimated_total_rows": null
},
"rows": {
"processed": 0,
"added": 0,
"updated": 0,
"reenrolled": 0,
"duplicates": 0,
"rejected": 0
},
"rejected_by_reason": {},
"rejects_available": 0,
"contacts_created": 0,
"error": null,
"created_at": "2026-10-07T15:02:11.512Z",
"started_at": null,
"completed_at": null,
"upload_url": "https://api.nutalk.ai/storage/v1/object/upload/sign/contact-imports/…/part-0001.csv?token=…",
"upload": {
"method": "PUT",
"part_size": 33554432,
"expires_at": "2026-10-07T17:02:11.512Z",
"parts": [
{
"part": 1,
"url": "https://api.nutalk.ai/storage/v1/object/upload/sign/contact-imports/…/part-0001.csv?token=…",
"byte_start": 0,
"byte_end": 33554432
},
{
"part": 2,
"url": "https://api.nutalk.ai/storage/v1/object/upload/sign/contact-imports/…/part-0002.csv?token=…",
"byte_start": 33554432,
"byte_end": 67108864
}
]
}
}
}Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error | The body is not JSON, or a field is missing or invalid (details.errors lists each field). |
| 404 | not_found | No campaign or contact list with this id in your workspace (ids of other workspaces answer the same). |
| 403 | forbidden | The key lacks campaigns:write (a campaign, or a list that feeds an outbound campaign) or contact_lists:write (a list). |
| 409 | campaign_not_active | The campaign is not active and API import is off in its settings (turn it on to hold the people until it starts). |
| 409 | campaign_not_accepting | The campaign is completed or archived. |
Plus the errors every request can return (401, 403, 429, 500): Errors.
Renew the upload URLs
/v1/contact-imports/:id/uploadFresh upload URLs (valid for two hours) for the parts of the file not uploaded yet, or uploaded with another size. Parts already in place keep their bytes.
contacts:writePath parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The import id. |
Example request
curl -X POST "https://www.nutalk.ai/v1/contact-imports/3c9a0f7e-5d21-4b8e-9a47-6f1e2d3c4b5a/upload" \ -H "Authorization: Bearer YOUR_API_KEY"The import, awaiting its upload, with a URL for every part still missing.
{
"success": true,
"data": {
"id": "3c9a0f7e-5d21-4b8e-9a47-6f1e2d3c4b5a",
"status": "awaiting_upload",
"campaign_id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"contact_list_id": null,
"file_name": "base-octubre.csv",
"file_size": 148210544,
"progress": {
"bytes_processed": 0,
"percent": 0,
"estimated_total_rows": null
},
"rows": {
"processed": 0,
"added": 0,
"updated": 0,
"reenrolled": 0,
"duplicates": 0,
"rejected": 0
},
"rejected_by_reason": {},
"rejects_available": 0,
"contacts_created": 0,
"error": null,
"created_at": "2026-10-07T15:02:11.512Z",
"started_at": null,
"completed_at": null,
"upload_url": "https://api.nutalk.ai/storage/v1/object/upload/sign/contact-imports/…/part-0002.csv?token=…",
"upload": {
"method": "PUT",
"part_size": 33554432,
"expires_at": "2026-10-07T19:05:40.208Z",
"parts": [
{
"part": 2,
"url": "https://api.nutalk.ai/storage/v1/object/upload/sign/contact-imports/…/part-0002.csv?token=…",
"byte_start": 33554432,
"byte_end": 67108864
}
]
}
}
}Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | No import with this id in your workspace (ids of other workspaces answer the same). |
| 409 | already_uploaded | The import already started (or was cancelled): its file needs no upload. |
Plus the errors every request can return (401, 403, 429, 500): Errors.
Start an import
/v1/contact-imports/:id/startCall it once every part is uploaded: the import starts in the background (about 2,000,000 rows in under two hours). Starting an import that already started answers it as it is.
contacts:writeCan wait for approvalPath parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The import id. |
Example request
curl -X POST "https://www.nutalk.ai/v1/contact-imports/3c9a0f7e-5d21-4b8e-9a47-6f1e2d3c4b5a/start" \ -H "Authorization: Bearer YOUR_API_KEY"The import, queued.
{
"success": true,
"data": {
"id": "3c9a0f7e-5d21-4b8e-9a47-6f1e2d3c4b5a",
"status": "queued",
"campaign_id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"contact_list_id": null,
"file_name": "base-octubre.csv",
"file_size": 148210544,
"progress": {
"bytes_processed": 61340112,
"percent": 41,
"estimated_total_rows": 2000480
},
"rows": {
"processed": 827950,
"added": 801204,
"updated": 0,
"reenrolled": 0,
"duplicates": 9112,
"rejected": 17634
},
"rejected_by_reason": {
"invalid_phone": 16920,
"missing_phone": 410,
"do_not_contact": 304
},
"rejects_available": 17634,
"contacts_created": 786015,
"error": null,
"created_at": "2026-10-07T15:02:11.512Z",
"started_at": "2026-10-07T15:04:40.031Z",
"completed_at": null
}
}Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | No import with this id in your workspace (ids of other workspaces answer the same). |
| 409 | upload_incomplete | A part is missing or has another size (details.missing_parts). Upload it and call again. |
| 409 | campaign_not_accepting | The campaign was completed or archived since the import was created. |
| 409 | campaign_not_active | The campaign is no longer active and API import is off in its settings (it was paused, or the setting turned off, during the upload). |
| 409 | not_startable | The import was cancelled or failed. |
Plus the errors every request can return (401, 403, 429, 500): Errors.
Get an import's progress
/v1/contact-imports/:idStatus, bytes read, and what happened to the rows so far. Poll it every few seconds while it runs.
contacts:readThe fields of an import:
id: The import id.
status: awaiting_upload until you call /start; queued then processing while it runs; paused from the dashboard; completed, failed (see error) or cancelled.
campaign_id: The campaign the rows join.
contact_list_id: The list the rows join.
file_name: The name you gave the file.
file_size: The file's size in bytes.
progress: How far the import got.
rows: Every row lands in one of added, updated, duplicates or rejected.
rejected_by_reason: Refused rows by reason: missing_phone, invalid_phone (not a Mexican, US or Canadian mobile or landline, or no country code with no phone_country), agent_number (one of your own numbers), do_not_contact (the person asked not to be contacted), cooldown.
rejects_available: Refused rows you can download (the first 250,000).
contacts_created: Contacts the import created (the rest already existed).
error: Why a failed import stopped (code, message); while it retries, the last error. campaign_scope: the list came to feed an outbound campaign during the import, and the key that started it lacks campaigns:write.
created_at: When the import was created.
started_at: When processing started.
completed_at: When it finished.
Path parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The import id. |
Example request
curl -X GET "https://www.nutalk.ai/v1/contact-imports/3c9a0f7e-5d21-4b8e-9a47-6f1e2d3c4b5a" \ -H "Authorization: Bearer YOUR_API_KEY"The import.
{
"success": true,
"data": {
"id": "3c9a0f7e-5d21-4b8e-9a47-6f1e2d3c4b5a",
"status": "processing",
"campaign_id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"contact_list_id": null,
"file_name": "base-octubre.csv",
"file_size": 148210544,
"progress": {
"bytes_processed": 61340112,
"percent": 41,
"estimated_total_rows": 2000480
},
"rows": {
"processed": 827950,
"added": 801204,
"updated": 0,
"reenrolled": 0,
"duplicates": 9112,
"rejected": 17634
},
"rejected_by_reason": {
"invalid_phone": 16920,
"missing_phone": 410,
"do_not_contact": 304
},
"rejects_available": 17634,
"contacts_created": 786015,
"error": null,
"created_at": "2026-10-07T15:02:11.512Z",
"started_at": "2026-10-07T15:04:40.031Z",
"completed_at": null
}
}Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | No import with this id in your workspace (ids of other workspaces answer the same). |
Plus the errors every request can return (401, 403, 429, 500): Errors.
Cancel an import
/v1/contact-imports/:id/cancelStops the import for good and deletes its file. Rows already imported stay imported (enrolled people may already be called). Cancelling a cancelled import answers it as it is.
contacts:writePath parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The import id. |
Example request
curl -X POST "https://www.nutalk.ai/v1/contact-imports/3c9a0f7e-5d21-4b8e-9a47-6f1e2d3c4b5a/cancel" \ -H "Authorization: Bearer YOUR_API_KEY"The cancelled import.
{
"success": true,
"data": {
"id": "3c9a0f7e-5d21-4b8e-9a47-6f1e2d3c4b5a",
"status": "cancelled",
"campaign_id": "f8d1c003-b292-448d-b8d8-1dd521b94b3b",
"contact_list_id": null,
"file_name": "base-octubre.csv",
"file_size": 148210544,
"progress": {
"bytes_processed": 61340112,
"percent": 41,
"estimated_total_rows": 2000480
},
"rows": {
"processed": 827950,
"added": 801204,
"updated": 0,
"reenrolled": 0,
"duplicates": 9112,
"rejected": 17634
},
"rejected_by_reason": {
"invalid_phone": 16920,
"missing_phone": 410,
"do_not_contact": 304
},
"rejects_available": 17634,
"contacts_created": 786015,
"error": null,
"created_at": "2026-10-07T15:02:11.512Z",
"started_at": "2026-10-07T15:04:40.031Z",
"completed_at": null
}
}Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | No import with this id in your workspace (ids of other workspaces answer the same). |
| 409 | not_cancellable | The import already completed or failed. |
Plus the errors every request can return (401, 403, 429, 500): Errors.
Download the refused rows
/v1/contact-imports/:id/rejectsThe rows the import did not take, as CSV: your file's columns and separator, each row as written, then why it was not imported and its row number. Fix them and import the file again.
contacts:readPath parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The import id. |
Query parameters
| Name | Type | Description |
|---|---|---|
locale | string | The language of the reason column. One of: en, es. Default: |
Example request
curl -X GET "https://www.nutalk.ai/v1/contact-imports/3c9a0f7e-5d21-4b8e-9a47-6f1e2d3c4b5a/rejects?locale=es" \ -H "Authorization: Bearer YOUR_API_KEY"200 A CSV file (UTF-8 with a byte order mark).
Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | No import with this id in your workspace (ids of other workspaces answer the same). |
Plus the errors every request can return (401, 403, 429, 500): Errors.