Imports & files
For anything larger than a few hundred records, use the import pipeline rather than bulk create. Imports stream through leadx-ms-import with progress, pause and resume, and can handle tens of thousands of rows.
The flow
1. POST /upload → presigned S3 POST + fileId
2. upload the file to S3 → directly, not through leadx-api
3. POST /lead/import → create the import with a column mapping
4. GET /lead/import/:id → poll progress
5. PUT /lead/import/:id → resume if you created it paused
1. Register the file
curl -X POST https://api.leadx.in/api/key/protected/upload \
-H "apikey: $KEY" -H "Content-Type: application/json" \
-d '{ "name": "leads-q3.csv", "type": "text/csv", "size": 2481003 }'
{
"ok": true,
"success": true,
"data": {
"data": { "url": "https://...s3...", "fields": { "key": "...", "policy": "..." }, "_id": "665e..." },
"url": "https://bucket.s3.region.amazonaws.com/665e....csv"
}
}
data.data._id is the file id you pass to the import. data.data.url + fields are a presigned S3 POST.
:::note Uploads are deduplicated
A file with the same name and MIME type already registered for your company is returned as-is with fileExists: true, and no new presigned POST is issued. Use a unique filename per import if you want a fresh upload.
:::
2. Upload to S3
curl -X POST "$S3_URL" \
-F key="$KEY_FIELD" -F policy="$POLICY" -F x-amz-signature="$SIG" \
-F file=@leads-q3.csv
Post the returned fields first, then file last — S3 requires that order. The bytes never pass through leadx-api.
3. Create the import
curl -X POST https://api.leadx.in/api/key/protected/lead/import \
-H "apikey: $KEY" -H "Content-Type: application/json" \
-d '{
"file": "665e...",
"headers": ["Full Name", "Email", "Phone", "Company"],
"totalCount": 18422,
"columnMapping": {
"Full Name": "name",
"Email": "email",
"Phone": "contactNumber",
"Company": "companyData.name"
},
"tags": ["665a..."],
"options": { "skipInvalid": true },
"pauseImport": true
}'
| Field | Notes |
|---|---|
file | Required. The file id from step 1. |
headers | Required. The file's header row. Empty → EmptyFile_400. |
totalCount | Optional. If sent it must be real, else NoRowsInFile_400. Omitted, rows are counted at run time. |
columnMapping | Header name → target field. Values may be system field paths or custom field ids. |
tags, category | Applied to every imported record. |
options | Import behaviour, e.g. skipInvalid. |
pauseImport | Default true — created paused so you can review the mapping first. |
/contact/import is identical; the path decides whether rows become leads or contacts.
:::warning Imports count against your plan The import checks the plan's record allowance before it starts. A file that would exceed it is refused up front rather than half-loaded. :::
4. Track and control
| Method | Endpoint | Purpose |
|---|---|---|
GET | /lead/import | All lead imports. |
GET | /lead/import/:id | One import — status, progress, row errors. |
PUT | /lead/import/:id | Update — this is how you resume a paused import. |
DELETE | /lead/import | Delete imports — { "ids": [...] }. |
Status moves pending → processing → completed / failed. Poll GET /lead/import/:id; there is no webhook.
Imported records keep an importId and an importSnapshot, so you can always find or roll back everything one file produced:
?filters={"importId":"665e..."}
Managing files
| Method | Endpoint | Purpose |
|---|---|---|
GET | /file | List file objects. |
GET | /file/:id | One file. |
DELETE | /file | Delete — { "ids": [...] }. Goes to the recycle bin. |
GET | /file/recycle-bin | Deleted files. |
DELETE | /file/recycle-bin | Empty the bin — permanent. |
GET /file, GET /file/:id and POST /upload sit above the subscription check, so they keep working for a lapsed tenant. Deletes do not.
Choosing an approach
| Rows | Use |
|---|---|
| 1 | POST /lead or POST /contact |
| 2–500 | POST /lead/bulk — immediate, itemised errors |
| 500+ | Import pipeline |
The bulk endpoint hard-caps at 500 because every item costs a duplicate lookup, a write and a workflow event inside one request.