Create contact import
const url = 'https://api.postlever.com/v1/contact-imports';const options = { method: 'POST', headers: {Authorization: '<Authorization>', 'Content-Type': 'application/json'}, body: '{"columns":["Email","Brand","first_name"],"duplicateContactStrategy":"overwrite"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://api.postlever.com/v1/contact-imports \ --header 'Authorization: <Authorization>' \ --header 'Content-Type: application/json' \ --data '{ "columns": [ "Email", "Brand", "first_name" ], "duplicateContactStrategy": "overwrite" }'Starts a bulk import of contacts from a CSV file, in three steps:
-
POST /v1/contact-importswith the file’s header row incolumns. The response carriesuploadUrl, a link toPUTthe file to. -
PUTthe CSV file touploadUrl, as the raw request body. The upload must start within one hour, beforeuploadExpiresAt. A started upload is imported once it finishes, however slow, as long as that is within 24 hours of creation:curl -X PUT --upload-file contacts.csv "<uploadUrl>"The file can be up to 5 GB, roughly 25 million contacts. A larger file is refused with
EntityTooLarge; split it and import the parts one after another. IfuploadUrlrefuses the upload for any other reason, create a new import and upload to its link.The file must be UTF-8 and comma-separated, with the header as its first row. Fields may be quoted; a byte order mark and CRLF or LF line endings are accepted, and blank lines are ignored.
The file’s header row must list
columnsin the same order, ignoring case and surrounding whitespace. The import starts once a file arrives; the last file uploaded before that is the one imported, and aPUTto the link after the import has started is ignored. -
Poll
GET /v1/contact-imports/{importId}every 5 to 10 seconds, untilstatusisfailedorsuccess.
Column names are contact schema attribute names, matched case-insensitively, as
GET /v1/contact-schema gives them, for example Email. A Brand column carries
brand names or ids to subscribe that row’s contact to, on top of any brandIds.
Several brands in one cell are separated by ;, for example Acme;Globex; a
literal ; in a brand name is written \;. Names are matched case-insensitively
and repeated brands count once. A value naming or matching no brand in your
account is counted in malformedCount and skipped. So is a row with any other cell
over 255 characters.
duplicateContactStrategy is required. Defaults: overwriteBrands is false,
restoreDisabled is false, and publishBrandSubscribeEvents is true. With
restoreDisabled: false, a row for a disabled contact leaves it disabled and is
counted in skippedCount.
Only one contact import can be open per account at a time, whether it was started
through this API, from the app, or by Optizmo. While any of them is awaiting-upload,
uploaded or importing, this call returns 409 operation_in_progress. Wait out the
Retry-After period, then retry. An import stays awaiting-upload until its file
arrives, for up to 24 hours after it was created, so an upload link that is never used
blocks new imports for 24 hours.
Authorizations
Section titled “Authorizations”Request Bodyrequired
Section titled “Request Bodyrequired”Declares the file about to be uploaded to start a bulk contact import. The response
carries the link to PUT it to.
object
The file’s header row, in order: the uploaded file’s header must list these
columns in the same order, ignoring case and surrounding whitespace. Each name
must match a contact schema attribute name case-insensitively, as
GET /v1/contact-schema gives it (for example Email), and one must be the
email attribute. A Brand column carries brand names or ids to subscribe that
row’s contact to. Several brands in one cell are separated by ;, for example
Acme;Globex; a literal ; in a brand name is written \;. Names are matched
case-insensitively and repeated brands count once.
Ids of brands to subscribe every imported contact to, on top of any Brand
column. Every id must be a brand in your account.
When true, a brand-subscribe event is recorded for each brand new to a contact.
Defaults to true; set to false to opt out.
How a row merges into a contact that already exists.
Under overwrite, whether a row’s brands replace the contact’s rather than
adding to them. Defaults to false.
Whether a row may enable a disabled contact again. Defaults to false, leaving
it disabled and counting it in skippedCount.
Example
{ "columns": [ "Email", "Brand", "first_name" ], "duplicateContactStrategy": "overwrite"}Responses
Section titled “Responses”The upload link for the new import.
Response of creating a contact import.
object
The import’s id, polled at GET /v1/contact-imports/{importId}.
Always awaiting-upload.
A presigned URL to PUT the CSV file to, as the raw request body. The upload must
start before uploadExpiresAt. The last file uploaded before the import starts is
the one imported, and the file starts importing as soon as it arrives, as long as
that is within 24 hours of the import being created.
Example
{ "id": "8f14e45f-ea8f-4b1a-9c2d-2b0a5a1f3c7e", "status": "awaiting-upload", "uploadUrl": "https://<presigned-upload-url>?X-Amz-Signature=...", "uploadExpiresAt": "2026-06-12T01:00:00Z"}A column does not name a contact schema attribute, a column is declared more than
once ignoring case, the columns do not include the schema’s email attribute, a
brandIds entry does not name a brand in the account (pointer /brandIds), or the
request body fails validation.
Error envelope returned by every v1 endpoint for failures, following RFC 9457
Problem Details with code and errors extensions.
object
URI identifying the problem type; dereferences to its documentation.
HTTP status code applicable to this problem.
Short, human-readable summary of the problem type.
Detailed explanation specific to this occurrence.
Error code identifying the specific error condition.
URI reference identifying this specific occurrence (the request URI).
Provides additional information when the error relates to specific request fields.
object
Detailed explanation for this specific field error.
JSON Pointer (RFC 6901) to the request body value that caused the error (e.g. /email, /attributes/firstName).
The URI query parameter that caused the error (e.g. pageSize, name[invalid]).
Example
{ "type": "https://docs.postlever.com/error-codes#invalid_request_error", "status": 400, "title": "Invalid request", "detail": "\"nickname\" is not an attribute in the contact schema", "code": "invalid_request_error", "instance": "/v1/contact-imports", "errors": [ { "detail": "\"nickname\" is not an attribute in the contact schema", "pointer": "/columns/1" } ]}Missing or invalid API key.
Error envelope returned by every v1 endpoint for failures, following RFC 9457
Problem Details with code and errors extensions.
object
URI identifying the problem type; dereferences to its documentation.
HTTP status code applicable to this problem.
Short, human-readable summary of the problem type.
Detailed explanation specific to this occurrence.
Error code identifying the specific error condition.
URI reference identifying this specific occurrence (the request URI).
Provides additional information when the error relates to specific request fields.
object
Detailed explanation for this specific field error.
JSON Pointer (RFC 6901) to the request body value that caused the error (e.g. /email, /attributes/firstName).
The URI query parameter that caused the error (e.g. pageSize, name[invalid]).
Example
{ "type": "https://docs.postlever.com/error-codes#authentication_error", "status": 401, "title": "Authentication failed", "detail": "Missing or invalid API key", "code": "authentication_error", "instance": "/v1/contacts"}Permission denied.
Error envelope returned by every v1 endpoint for failures, following RFC 9457
Problem Details with code and errors extensions.
object
URI identifying the problem type; dereferences to its documentation.
HTTP status code applicable to this problem.
Short, human-readable summary of the problem type.
Detailed explanation specific to this occurrence.
Error code identifying the specific error condition.
URI reference identifying this specific occurrence (the request URI).
Provides additional information when the error relates to specific request fields.
object
Detailed explanation for this specific field error.
JSON Pointer (RFC 6901) to the request body value that caused the error (e.g. /email, /attributes/firstName).
The URI query parameter that caused the error (e.g. pageSize, name[invalid]).
Example
{ "type": "https://docs.postlever.com/error-codes#permission_error", "status": 403, "title": "Permission denied", "detail": "api key required", "code": "permission_error", "instance": "/v1/contacts"}Another of the account’s contact imports, whether started through this API, from
the app, or by Optizmo, is awaiting its upload or still importing
(code: operation_in_progress). Retry after the number of seconds given in the
Retry-After header.
Error envelope returned by every v1 endpoint for failures, following RFC 9457
Problem Details with code and errors extensions.
object
URI identifying the problem type; dereferences to its documentation.
HTTP status code applicable to this problem.
Short, human-readable summary of the problem type.
Detailed explanation specific to this occurrence.
Error code identifying the specific error condition.
URI reference identifying this specific occurrence (the request URI).
Provides additional information when the error relates to specific request fields.
object
Detailed explanation for this specific field error.
JSON Pointer (RFC 6901) to the request body value that caused the error (e.g. /email, /attributes/firstName).
The URI query parameter that caused the error (e.g. pageSize, name[invalid]).
Example
{ "type": "https://docs.postlever.com/error-codes#operation_in_progress", "status": 409, "title": "Conflict", "detail": "A contact import is in progress for this account, retry later", "code": "operation_in_progress", "instance": "/v1/contact-imports"}Headers
Section titled “Headers”Seconds to wait before retrying.