Skip to content

Create contact import

POST
/v1/contact-imports
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:

  1. POST /v1/contact-imports with the file’s header row in columns. The response carries uploadUrl, a link to PUT the file to.

  2. PUT the CSV file to uploadUrl, as the raw request body. The upload must start within one hour, before uploadExpiresAt. 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. If uploadUrl refuses 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 columns in 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 a PUT to the link after the import has started is ignored.

  3. Poll GET /v1/contact-imports/{importId} every 5 to 10 seconds, until status is failed or success.

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.

Media typeapplication/json

Declares the file about to be uploaded to start a bulk contact import. The response carries the link to PUT it to.

object
columns
required

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.

Array<string>
>= 1 items <= 200 items unique items
brandIds

Ids of brands to subscribe every imported contact to, on top of any Brand column. Every id must be a brand in your account.

Array<string>
>= 1 items <= 100 items unique items
publishBrandSubscribeEvents

When true, a brand-subscribe event is recorded for each brand new to a contact. Defaults to true; set to false to opt out.

boolean
default: true
duplicateContactStrategy
required

How a row merges into a contact that already exists.

string
Allowed values: overwrite keep skip
overwriteBrands

Under overwrite, whether a row’s brands replace the contact’s rather than adding to them. Defaults to false.

boolean
restoreDisabled

Whether a row may enable a disabled contact again. Defaults to false, leaving it disabled and counting it in skippedCount.

boolean

Example

{
"columns": [
"Email",
"Brand",
"first_name"
],
"duplicateContactStrategy": "overwrite"
}

The upload link for the new import.

Media typeapplication/json

Response of creating a contact import.

object
id
required

The import’s id, polled at GET /v1/contact-imports/{importId}.

string format: uuid
status
required

Always awaiting-upload.

string
Allowed values: awaiting-upload
uploadUrl
required

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.

string format: uri
uploadExpiresAt
required
string format: date-time

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.

Media typeapplication/json

Error envelope returned by every v1 endpoint for failures, following RFC 9457 Problem Details with code and errors extensions.

object
type
required

URI identifying the problem type; dereferences to its documentation.

string format: uri
status
required

HTTP status code applicable to this problem.

integer
title
required

Short, human-readable summary of the problem type.

string
detail
required

Detailed explanation specific to this occurrence.

string
code
required

Error code identifying the specific error condition.

string
Allowed values: invalid_request_error authentication_error permission_error not_found_error conflict_error api_error contact_exists operation_in_progress
instance

URI reference identifying this specific occurrence (the request URI).

string format: uri-reference
errors

Provides additional information when the error relates to specific request fields.

Array<object>
object
detail
required

Detailed explanation for this specific field error.

string
pointer

JSON Pointer (RFC 6901) to the request body value that caused the error (e.g. /email, /attributes/firstName).

string
parameter

The URI query parameter that caused the error (e.g. pageSize, name[invalid]).

string

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.

Media typeapplication/json

Error envelope returned by every v1 endpoint for failures, following RFC 9457 Problem Details with code and errors extensions.

object
type
required

URI identifying the problem type; dereferences to its documentation.

string format: uri
status
required

HTTP status code applicable to this problem.

integer
title
required

Short, human-readable summary of the problem type.

string
detail
required

Detailed explanation specific to this occurrence.

string
code
required

Error code identifying the specific error condition.

string
Allowed values: invalid_request_error authentication_error permission_error not_found_error conflict_error api_error contact_exists operation_in_progress
instance

URI reference identifying this specific occurrence (the request URI).

string format: uri-reference
errors

Provides additional information when the error relates to specific request fields.

Array<object>
object
detail
required

Detailed explanation for this specific field error.

string
pointer

JSON Pointer (RFC 6901) to the request body value that caused the error (e.g. /email, /attributes/firstName).

string
parameter

The URI query parameter that caused the error (e.g. pageSize, name[invalid]).

string

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.

Media typeapplication/json

Error envelope returned by every v1 endpoint for failures, following RFC 9457 Problem Details with code and errors extensions.

object
type
required

URI identifying the problem type; dereferences to its documentation.

string format: uri
status
required

HTTP status code applicable to this problem.

integer
title
required

Short, human-readable summary of the problem type.

string
detail
required

Detailed explanation specific to this occurrence.

string
code
required

Error code identifying the specific error condition.

string
Allowed values: invalid_request_error authentication_error permission_error not_found_error conflict_error api_error contact_exists operation_in_progress
instance

URI reference identifying this specific occurrence (the request URI).

string format: uri-reference
errors

Provides additional information when the error relates to specific request fields.

Array<object>
object
detail
required

Detailed explanation for this specific field error.

string
pointer

JSON Pointer (RFC 6901) to the request body value that caused the error (e.g. /email, /attributes/firstName).

string
parameter

The URI query parameter that caused the error (e.g. pageSize, name[invalid]).

string

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.

Media typeapplication/json

Error envelope returned by every v1 endpoint for failures, following RFC 9457 Problem Details with code and errors extensions.

object
type
required

URI identifying the problem type; dereferences to its documentation.

string format: uri
status
required

HTTP status code applicable to this problem.

integer
title
required

Short, human-readable summary of the problem type.

string
detail
required

Detailed explanation specific to this occurrence.

string
code
required

Error code identifying the specific error condition.

string
Allowed values: invalid_request_error authentication_error permission_error not_found_error conflict_error api_error contact_exists operation_in_progress
instance

URI reference identifying this specific occurrence (the request URI).

string format: uri-reference
errors

Provides additional information when the error relates to specific request fields.

Array<object>
object
detail
required

Detailed explanation for this specific field error.

string
pointer

JSON Pointer (RFC 6901) to the request body value that caused the error (e.g. /email, /attributes/firstName).

string
parameter

The URI query parameter that caused the error (e.g. pageSize, name[invalid]).

string

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"
}
Retry-After
integer

Seconds to wait before retrying.