Subscribers API
Add contacts one at a time or in bulk, look someone up by email address, apply tags, and unsubscribe an address through the API.
On this page›
This page covers the endpoints that read and write contacts. When you finish you will know how to add a single contact, load thousands at once, find someone by their email address, tag them, and unsubscribe them.
Before you start
You need an API token and the base URL from API authentication. Every request below also takes api_token as a query parameter, which the examples leave out for readability. You also need the list_uid of the list you are writing to, which comes back from GET /lists.
Method conventions#
The paths follow a consistent shape, so you can read the verb off the path:
- A bare collection path, such as
/subscribersor/lists, is aGET. - A path ending in
/createis aPOST. - A path naming an action, such as
/unsubscribeor/add-tag, is aPOST. - A path naming a single record, such as
/subscribers/{id}, is aGET.
Finding your lists and fields#
A subscriber belongs to a list, so start there.
| Purpose | Method | Path |
|---|---|---|
| List your lists | GET | /lists |
| Create a list | POST | /lists/create |
| Read one list | GET | /lists/{list_uid} |
| Add a custom field to a list | POST | /lists/{uid}/add-field |
The field names you send when creating a subscriber are the fields defined on that list, not a fixed schema. Email address is always present. Everything else, including first name, last name, company or a plan tier, exists only if it exists as a field on that list. Read the list first, or check Fields in the interface, and use those names exactly.
If you need a field that is not there yet, add it with /lists/{uid}/add-field before you start importing, rather than discovering halfway through that half your data had nowhere to land.
Adding one contact#
POST /subscribers/create
Send the email address plus values for whichever list fields you want to populate, and the identifier of the list the contact should join. A minimal body looks like this:
{
"EMAIL": "ana@example.com",
"FIRST_NAME": "Ana"
}
The exact key names come from your list's fields, so treat the shape above as illustrative rather than a fixed contract. Check your own list before you write the mapping.
Use this endpoint for events that happen one at a time: a signup on your own form, a new customer in your billing system, a person added by a member of your team in another tool.
Adding a contact can start an automation
If you have an automation that triggers when a contact joins a list, an API add fires it exactly as a form signup would. That is usually what you want, and occasionally a disaster: a one-off backfill of 8,000 historical customers into a list with a welcome sequence sends 8,000 welcome emails to people who signed up years ago.
Before any bulk write, check what is attached to the target list under Automations. If a sequence should not fire, import into a list with no automation attached and move contacts afterward, or pause the automation for the duration. See Automation overview.
Adding many contacts#
There are two bulk endpoints, both scoped to a list:
POST /lists/{list_uid}/subscribers/bulk
POST /lists/{list_uid}/subscribers/bulk/async
Both take a collection of subscriber records in one request, with the same per-record field names described above.
Use the plain bulk endpoint when the batch is small and you want the outcome before your code moves on, for example a few dozen records where you need to know immediately which ones were rejected.
Use the bulk/async endpoint when the batch is large. The request hands the work off to be processed in the background and returns without waiting for every record to be written. That avoids the two failure modes that bite synchronous bulk loads: your HTTP client times out partway through and you have no idea how much landed, or a proxy in between closes the connection and your retry loads everything twice.
The tradeoff is that a successful response to an async call means the work was accepted, not that every record is in place. Do not read the list back immediately and conclude that records are missing.
Loading a CSV instead
If your data is already a file, an import job is often less work than writing a bulk loader:
| Purpose | Method | Path |
|---|---|---|
| Import jobs for a list | GET | /lists/{list_uid}/import-jobs |
| Check one import job | GET | /import-jobs/{job_uid} |
Poll the job endpoint to see how a running import is progressing rather than guessing. The same import machinery is described from the interface side in Import contacts, including the column-mapping rules that decide where each CSV column ends up.
Reading contacts back#
| Purpose | Method | Path |
|---|---|---|
| List subscribers | GET | /subscribers |
| Read one subscriber by id | GET | /subscribers/{id} |
| Look up by email address | GET | /subscribers/email/{email} |
The lookup by address is the one you will use most from application code, because an email address is usually the only identifier you have. Your own system knows the customer's address. It does not know the internal subscriber id until you ask.
URL-encode the address before putting it in the path. A + in an address, common with plus-addressing like ana+news@example.com, is interpreted as a space if you leave it raw.
A sensible pattern for a sync job: look the address up, and if it is not found, create it. That keeps the job safe to re-run.
Tagging#
POST /subscribers/{id}/add-tag
POST /subscribers/{id}/remove-tag
Tags are labels you attach to a contact so you can find that group later. They are useful when the thing you want to record is not a value but a fact: attended the March webinar, bought the annual plan, asked for the Spanish version.
Both endpoints work on the subscriber id, so if you are starting from an email address you need the lookup call first.
Keep the tag vocabulary small and write it down somewhere. Tag sprawl, where webinar-march, March Webinar and webinar_2026_03 all exist, is the usual reason a segment quietly matches a third of the people it should.
Subscribing and unsubscribing#
POST /lists/{list_uid}/subscribers/{id}/subscribe
POST /lists/{list_uid}/subscribers/{id}/unsubscribe
POST /lists/{list_uid}/subscribers/email/{email}/unsubscribe
The subscribe and unsubscribe actions are per list, not per account. A contact can be subscribed to your product updates and unsubscribed from your newsletter at the same time. Pass the identifier of the list you mean.
The third form takes the email address directly, which saves a lookup. Use it when you are honoring an unsubscribe that reached you somewhere else, for example a customer who replied to a support ticket asking to be taken off. Those requests are legally the same as clicking the link in the email, and the fastest way to handle one correctly is a single call.
Unsubscribing is not deleting. The record stays so the address remains suppressed. If you delete a contact and later re-import the same address from an old file, you have resubscribed someone who asked to leave, which is both a complaint and a compliance problem.
If something goes wrong#
Fields come back empty after a write. The key names in your request do not match the list's fields. Field names are per list. A payload that works against one list can silently drop values against another.
Duplicates appear. You are creating rather than looking up first. Add a GET /subscribers/email/{email} check ahead of the create, and make sure your job is safe to run twice.
A bulk load looks incomplete. If you used the async endpoint, the work may still be running. Give it time and check the list count again before re-sending, because re-sending is how a partial import becomes a duplicated one.
People received email you did not expect to send. An automation on the target list fired on the add. Pause it, then check what else is attached to that list before the next write.
Last updated September 10, 2026