Import contacts

POST/api/v1/contacts/import
Requires anorganization API keywith the scopecontacts:write

Add up to 10,000 contacts in one request, each with a name and any custom fields. New contacts are added as subscribed. An address that is already a contact is left exactly as it is (name, fields and status) and counted in skipped_duplicate: import never updates anyone, so it can never re-subscribe somebody who unsubscribed. To update people, use PUT /contacts.

A bad row never fails the request. A row with no address, an invalid address, or no value for a custom field your account marks required is skipped, and the reply's errors names it by row number. Everything else is imported.

Two ways to send contacts. Send JSON with a contacts array, as in the example below. Or upload a CSV file as a form (multipart/form-data) in a field called file:

curl -X POST 'https://app.mailyte.com/api/v1/contacts/import' \
  -H 'Authorization: Bearer mk_live_YOUR_API_KEY' \
  -F 'file=@contacts.csv' \
  -F 'list_id=01JBT8XQ2M9WYC3K4F6R7S8T9V'

The file's first row should be a header with an email column and, optionally, a name column. Every other column becomes a custom field named after its header ("Signup Date" becomes signup_date). Without a header, the first column is read as the email and the second as the name. Send file or contacts, not both.

Checks. Once Bounce protection is on for your account, each new address uses one check and is checked in the background: email_status reads pending until the answer lands, then valid, risky, invalid or unknown. Read it with GET /contacts/{id}, or subscribe to the contact.checked webhook. If your checks run short, as many new contacts as there are checks for are checked and the rest are added as not_checked. With Bounce protection switched off they are added as not_checked. Until then an import's addresses are checked free, as before.

The reply carries X-Mailyte-Checks-Remaining and, when it applies, X-Mailyte-Checks-Warning: running_low | ran_out, once Bounce protection is on.

Request body

  • contactsarray<object>required

    The contacts to add, 1 to 10,000 per request.

    • emailstringrequired

      Their email address. Case does not matter: Ada@Example.com and ada@example.com are the same contact.

    • namestring

      Their name, as it should appear in your emails.

    • attributesobject

      Custom fields as key–value pairs, for example {"city": "Lagos", "plan": "pro"}. Use the keys from GET /contact-fields; a key your account has not seen before is kept too.

  • list_idstring

    Optional. The ID of a list to put every address in this request on, new contacts and ones you already had. Copy it from Contacts → Lists in the dashboard, or read it from GET /contact-lists.

Request

POST/api/v1/contacts/import
curl -X POST 'https://app.mailyte.com/api/v1/contacts/import' \
  -H 'Authorization: Bearer mk_live_YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "list_id": "01JBT8XQ2M9WYC3K4F6R7S8T9V",
    "contacts": [
      {
        "email": "ada@example.com",
        "name": "Ada Obi"
      },
      {
        "email": "tunde@example.com",
        "name": "Tunde Bello",
        "attributes": {
          "city": "Lagos",
          "plan": "pro"
        }
      }
    ]
  }'
The key names its own organization, so no X-Organization-ID header is needed.

Response

Success.

  • dataobject

    The counters always sum: imported + skipped_invalid + skipped_duplicate + skipped_over_plan == total.

    • objectstringcontact_import
    • totalinteger

      Rows we were given. For a CSV this is DATA rows — a recognised header row is not counted.

    • importedinteger

      Rows that became new contacts.

    • skipped_invalidinteger

      Rows skipped because the address was missing or invalid, or a required field was empty. Other rows still import.

    • skipped_duplicateinteger

      Addresses you already have, plus repeats in the file. Existing contacts are left as they are, so re-importing never re-subscribes anyone who unsubscribed.

    • skipped_over_planinteger

      Rows past the subscribed contacts your Marketing plan holds. The rows that fit are imported; plan_limit names the plan that takes the rest.

    • error_countinteger

      How 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 rows that failed, in file order, up to 100. error_count gives the full number. [] means every row was accepted.

      • rowinteger

        1-based, counting DATA rows, so it lines up with your spreadsheet after the header.

      • emailstring

        The 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_plan

        Why the row failed. duplicate_in_file: the address appears twice in your file. already_exists: you already have this contact. missing_required_field: a required contact field is empty, and message names it.

      • messagestring

        The same thing in words, safe to show a person. For a duplicate it names the earlier row.

    • plan_limitobject

      Null 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.

    • listobject

      The 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.

      • idstring
      • addedinteger

Returned inside the standard envelope.

Errors

StatusWhen
401The API key is missing, unknown, revoked or expired. All four answer identically, on purpose: distinguishing them would confirm which keys exist.
403The 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.
404No such resource in this organization.
422The request was understood but the values were not acceptable.
429Too 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.