Skip to main content

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
}'
FieldNotes
fileRequired. The file id from step 1.
headersRequired. The file's header row. Empty → EmptyFile_400.
totalCountOptional. If sent it must be real, else NoRowsInFile_400. Omitted, rows are counted at run time.
columnMappingHeader name → target field. Values may be system field paths or custom field ids.
tags, categoryApplied to every imported record.
optionsImport behaviour, e.g. skipInvalid.
pauseImportDefault 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

MethodEndpointPurpose
GET/lead/importAll lead imports.
GET/lead/import/:idOne import — status, progress, row errors.
PUT/lead/import/:idUpdate — this is how you resume a paused import.
DELETE/lead/importDelete imports — { "ids": [...] }.

Status moves pendingprocessingcompleted / 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

MethodEndpointPurpose
GET/fileList file objects.
GET/file/:idOne file.
DELETE/fileDelete — { "ids": [...] }. Goes to the recycle bin.
GET/file/recycle-binDeleted files.
DELETE/file/recycle-binEmpty 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

RowsUse
1POST /lead or POST /contact
2–500POST /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.