Import contacts
/api/v1/contacts/importBulk-create or update contacts. Existing contacts are matched on email address and updated rather than duplicated.
Request body
filestringcontactsarray<array>list_idstring
Request
/api/v1/contacts/importcurl -X POST 'https://app.mailyte.com/api/v1/contacts/import' \
-H 'Authorization: Bearer mk_live_YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"file": "<string>",
"contacts": [],
"list_id": "<string>"
}'const response = await fetch('https://app.mailyte.com/api/v1/contacts/import', {
method: 'POST',
headers: {
Authorization: 'Bearer mk_live_YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"file": "<string>",
"contacts": [],
"list_id": "<string>"
}),
});
const { data } = await response.json();import requests
response = requests.post(
"https://app.mailyte.com/api/v1/contacts/import",
headers={"Authorization": "Bearer mk_live_YOUR_API_KEY"},
json={
"file": "<string>",
"contacts": [],
"list_id": "<string>"
},
)
data = response.json()["data"]<?php
$response = Http::withToken('mk_live_YOUR_API_KEY')
->post('https://app.mailyte.com/api/v1/contacts/import', [
'file' => '<string>',
'contacts' => [],
'list_id' => '<string>',
]);
$data = $response->json('data');require "net/http"
require "json"
uri = URI("https://app.mailyte.com/api/v1/contacts/import")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer mk_live_YOUR_API_KEY"
request["Content-Type"] = "application/json"
request.body = {
"file": "<string>",
"contacts": [],
"list_id": "<string>"
}.to_json
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }Response
Success.
dataobjectThe counters always sum: `imported + skipped_invalid + skipped_duplicate + skipped_over_plan == total`.
objectstringcontact_importtotalintegerRows we were given. For a CSV this is DATA rows — a recognised header row is not counted.
importedintegerRows that became new contacts.
skipped_invalidintegerRows with a missing or unparseable email address, or with no value for a field this organization marked required. An invalid row is skipped and counted, never fatal: a 10,000-row file with one bad address still imports 9,999.
skipped_duplicateintegerAddresses already known to this organization, plus repeats within the batch itself. Import is insert-or-SKIP, not an upsert — an address already on file keeps its name, its custom fields and, most importantly, its state, so re-importing a list can never resurrect somebody who unsubscribed.
skipped_over_planintegerRows past the subscribed contacts your Marketing plan holds. The rows that fit are imported; `plan_limit` names the plan that takes the rest.
error_countintegerHow many rows failed in total. Equal to `skipped_invalid + skipped_duplicate + skipped_over_plan`, and the number to trust — `errors` below is capped, so a file where everything failed reports 4,000 here and lists 100.
errorsarray<object>The failed rows, in FILE ORDER, so you can read this beside your spreadsheet. Capped at 100: the first hundred show the pattern — a mis-mapped column, a stray header — and `error_count` says how many there really were. An empty array means every row was accepted.
rowinteger1-based, counting DATA rows, so it lines up with your spreadsheet after the header.
emailstringThe address exactly as you sent it, before normalising — empty when the row had none.
reasonstringmissing_email | invalid_email | duplicate_in_file | already_exists | missing_required_field | over_planMachine-readable. `duplicate_in_file` and `already_exists` both count toward `skipped_duplicate` but are different problems with different fixes: one is a typo in your file, the other is a contact you already have. `missing_required_field` means your organization marked a contact field required and this row has no value for it — the fix is a column, not an address, and `message` names which one.
messagestringThe same thing in words, safe to show a person. For a duplicate it names the earlier row.
plan_limitobjectNull unless rows were stopped by your plan. Then the same context a 402 carries: `limit_type`, `limit`, `current`, `suggested_product`, `cta_label` and `upgrade_url`.
listobjectThe list named by `list_id`, with how many of the file's contacts joined it (new ones and ones you already had). Null when no list was given.
idstringaddedinteger
Returned inside the standard envelope.
Errors
| Status | When |
|---|---|
401 | The API key is missing, unknown, revoked or expired. All four answer identically, on purpose: distinguishing them would confirm which keys exist. |
403 | The key is valid but may not do this: it lacks the required scope, its IP allowlist does not include you, or this endpoint does not accept API keys. |
404 | No such resource in this organization. |
422 | The request was understood but the values were not acceptable. |
429 | Too many requests, or the organization has spent its sending allowance. `Retry-After` says how long to wait. |
Every status, with what causes it and what to do, is on the error reference.