Contacts API
Create, update, list, and manage consent for your audience.
View as Markdown ↗Project key. Acts in the project the key belongs to. An all-access key works too — name the project with an X-Project-Id header. How keys and scopes work.
Endpoints
/v1/contacts/v1/contacts/v1/contacts/export/v1/contacts/{id}/v1/contacts/{id}/v1/contacts/{id}/v1/contacts/{id}/subscribe/v1/contacts/{id}/unsubscribe/v1/contacts/verify/v1/contacts/verify/{job_id}Check a list before importing
POST /v1/contacts/verify checks 1–10,000 addresses without creating contacts or sending mail. Send { "emails": ["person@yourdomain.com"] }. Checks include syntax, domain mail routing, disposable/role-address signals, this project's known bounce or complaint history, and addresses known across Email Bump not to exist (known_to_bounce) or that our email provider won't deliver to (cant_be_delivered). This is not mailbox verification: ok means nothing known against the address, not proof that it exists or that you have consent.
Verdicts
undeliverableverdictoptionalA definitive blocking signal, such as invalid syntax, a reserved domain, no mail servers, null MX, or a known hard bounce or complaint.
riskyverdictoptionalAn established warning, such as a role address, disposable domain or repeated soft bounces. This is not proof that the mailbox exists.
unknownverdictoptionalThe lookup failed. Retry later; a failed lookup alone is not a reason to remove a contact.
okverdictoptionalNo known blocking or risk signal. Not a mailbox-level verification or permission to send.
Each result includes score, reasons and provider. Read guidance for the check's limitations. Never treat verification as permission to send purchased or unsolicited lists.
mode: "inline" includes results, summary and guidance. mode: "job" returns a queued job instead. Poll GET /v1/contacts/verify/{job_id}; its response is { "job": { ... } }. Stop when job.status is completed or failed, and check job.error on failure. A completed job supplies summary, guidance and a capped flagged list, not a full per-address results array. Up to three checks can be queued/running per project.
The dashboard CSV importer can apply verify_mode=basic (exclude undeliverable) or strict (also exclude risky). off skips those checks, but malformed and reserved-domain addresses are still rejected. A lookup failure alone never excludes a row; strict mode can still reject it for a separate established risk. Import is a dashboard session operation, not a public /v1/contacts/import endpoint.
Create or update a contact
This endpoint is an upsert. A new email creates a contact (201); an existing email updates the fields you send and leaves the rest untouched (200). The created flag in the response tells you which happened, so an import or sync loop is safe to re-run without handling conflicts. On an update, attributes are merged — keys you send win, keys you don't send stay, and an explicit null removes one. A field this endpoint doesn't take (say list_id instead of list_ids, or tags) is a 400 naming the valid fields, never silently dropped.
Body parameters
emailstringrequiredThe contact's email address. Identifies the contact for the upsert.
first_namestringoptionalGiven name.
last_namestringoptionalFamily name.
companystringoptionalCompany or organization.
citystringoptionalCity.
countrystringoptionalCountry.
phonestringoptionalPhone number.
timezonestringoptionalIANA timezone, e.g. America/New_York. Used by local-timezone and smart send times; we also learn it from where they open and click.
attributesobjectoptionalAny custom key/value attributes to store on the contact. Merged into anything already stored, so a second call never erases what the first recorded.
subscribedbooleanoptionalMarketing consent. Defaults to true; send false to store someone who did not opt in — they still receive transactional email, just not marketing. Omit it on an upsert and existing consent is left exactly as it was, so re-submitting a form can never revoke it; restating false keeps the original unsubscribed_at. true never resubscribes an address the project's Do not email list holds as unsubscribed or reported as spam — even one whose contact you have since deleted — and neither does leaving it out when the upsert creates the contact: the contact stays (or is created) unsubscribed, and the response carries a warnings entry saying why. Someone who unsubscribed can come back through POST /v1/contacts/{id}/subscribe once you have fresh consent; someone who reported spam only through a teammate, from the dashboard. A contact added with false, who never opted out, can still opt in with true. A change made here sends contact.subscribed or contact.unsubscribed.
list_idsstring[]optionalList ids to add the contact to in the same call. On a double-opt-in list the contact is added as pending and emailed a confirmation link; on a normal list they're added immediately.
curl -X POST https://emailbump.com/api/v1/contacts \
-H "Authorization: Bearer ebk_your_key" \
-H "Content-Type: application/json" \
-d '{
"email": "jamie@example.com",
"first_name": "Jamie",
"company": "Acme",
"attributes": { "plan": "pro" },
"list_ids": ["5b3a0ea3-5c58-423e-92ff-0aaac7be0af3"]
}'{
"created": true,
"contact": {
"id": "5fef0867-b292-46ca-8371-e24d8575576c",
"email": "jamie@example.com",
"first_name": "Jamie",
"company": "Acme",
"attributes": { "plan": "pro" },
"subscribed": true,
"email_status": "active",
"created_at": "2026-07-21T19:56:38Z"
}
}Update a contact
PATCH /v1/contacts/{id} changes only the fields you send. It takes the same profile fields as create, minus email, plus two the upsert doesn't:
Body parameters
first_name, last_name, company, city, country, phone, timezone, attributessame as createoptionalAnything you leave out is untouched — including keys already inside attributes. The keys you send are added or overwritten; the rest are kept. Send a key as null to remove it. timezone takes null or an empty string to clear it.
subscribedbooleanoptionalMarketing consent, set on purpose, like the subscribe and unsubscribe endpoints. false unsubscribes a subscribed contact, stamping unsubscribed_at and putting the address on the Do not email list; for someone already unsubscribed it changes nothing. true lifts this project's unsubscribe, so only send it on fresh, explicit consent; for someone who marked your email as spam it is a 400, since only a teammate can clear that, from the dashboard. A change sends contact.subscribed or contact.unsubscribed.
email_statusstringoptionalDeliverability state. You will read active, hard_bounced, soft_bounced or complained, but the only value you can write is active, which clears a soft bounce and resets the soft-bounce count. Email Bump maintains the rest from delivery events, so any other value is accepted and then ignored — there is no way to suppress an address by hand. Writing active on a hard_bounced or complained contact is refused with a 400: those hold for every project in the workspace, so only a teammate can clear them, from the dashboard.
curl -X PATCH https://emailbump.com/api/v1/contacts/5fef0867-b292-46ca-8371-e24d8575576c \
-H "Authorization: Bearer ebk_your_key" \
-H "Content-Type: application/json" \
-d '{ "company": "Acme Corp", "attributes": { "plan": "enterprise" } }'DELETE /v1/contacts/{id} removes the contact and its event history, and returns 204.
List & search
Filter by status (subscribed / unsubscribed for consent, or active / hard_bounced / soft_bounced / complained for deliverability; any other value is a 400) and search (email, name or company; not case-sensitive). To look one contact up by address, use email — an exact match that returns one contact or an empty set.
curl "https://emailbump.com/api/v1/contacts?search=jamie&status=subscribed&limit=25" \
-H "Authorization: Bearer ebk_your_key"
curl "https://emailbump.com/api/v1/contacts?email=jamie@example.com" \
-H "Authorization: Bearer ebk_your_key"Export to CSV
GET /v1/contacts/export answers with the whole audience as a file — text/csv, not JSON. It supports status and search filters, has no pagination, and streams, so a large audience starts arriving before all of it has been read. Unlike the contacts list, status=subscribed exports only subscribed contacts whose email_status is active.
Query parameters
statusstringoptionalsubscribed requires both marketing consent and email_status=active; unsubscribed requires consent to be false. Other non-empty values match email_status, for example hard_bounced. On the list endpoint, subscribed is consent alone and does not check deliverability.
searchstringoptionalSubstring match on email, name or company.
list_iduuidoptionalOnly contacts on this list, and the file gains a list_status column — confirmed, pending (double opt-in, never clicked) or unsubscribed (left this list, still in your audience).
segment_iduuidoptionalOnly contacts matching this saved segment, evaluated when you ask rather than from anything cached.
They combine, and they combine with each other: a list scoped to a segment scoped to a search is one file.
curl "https://emailbump.com/api/v1/contacts/export?status=subscribed" \
-H "Authorization: Bearer ebk_your_key" \
-o contacts.csv
# One list, everyone on it, including who never confirmed
curl "https://emailbump.com/api/v1/contacts/export?list_id=5b3a0ea3-5c58-423e-92ff-0aaac7be0af3" \
-H "Authorization: Bearer ebk_your_key" \
-o newsletter.csvThe columns are email, first_name, last_name, company, phone, country, city, timezone and subscribed, then the state we maintain — email_status, unsubscribed_at, created_at, id — followed by list_status for a list-scoped export and custom-attribute columns. Export selects the first 200 distinct attribute keys alphabetically across the project, then excludes keys that collide with the profile or four state columns. It is not a complete attribute backup for projects above that limit. Objects and arrays are written as JSON text; strings, numbers and booleans are written plainly. Missing or null values are empty cells.
Email Bump recognizes the profile headers automatically; other providers may require column mapping. Automatic import skips email_status, unsubscribed_at, created_at, id and list_status: it does not restore delivery history or list-confirmation state. The subscribed column can change marketing consent, so retain only consent you can substantiate. Custom attributes undergo normal date, number and boolean inference; JSON objects and arrays exported as text are not reconstructed as structured values.
In the dashboard the same file is Export CSV: on Contacts it exports what the page is showing, search box and status filter included; on a list, that list; and on the Segments row menu, that segment.
Subscribe / unsubscribe
Toggle marketing consent. Unsubscribing also sets unsubscribed_at and puts the address on the project's Do not email list. Subscribing lifts this project's unsubscribe from that list, so only call it on fresh, explicit consent. It is refused with a 400 for someone who marked your email as spam — only a teammate can clear that, from the dashboard — and it cannot clear a bounce, which also stays unmailable until a teammate clears it. unsubscribed_at and the contact.subscribed / contact.unsubscribed webhook only change when consent does, so unsubscribing someone twice keeps the time they left.
curl -X POST https://emailbump.com/api/v1/contacts/5fef0867-b292-46ca-8371-e24d8575576c/unsubscribe \
-H "Authorization: Bearer ebk_your_key"