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/{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
Bounces are the fastest way to lose your sending. Run any list you did not collect yourself through this first — it checks syntax, whether the domain has mail servers at all, throwaway providers, role addresses, and anything this workspace has already hard-bounced or been marked as spam by.
curl -X POST https://emailbump.com/api/v1/contacts/verify \
-H "Authorization: Bearer ebk_your_key" \
-H "Content-Type: application/json" \
-d '{ "emails": ["[email protected]", "[email protected]", "john@@gmail.com"] }'Verdict
undeliverableverdictoptionalProven: malformed, the domain publishes no MX or A record, or you have already hard-bounced or been marked as spam by this address. Exclude these.
riskyverdictoptionalAnything we have something to say about, whatever it scored: a throwaway-mailbox provider, an address we soft-bounced, a role address like info@, one nobody reads like noreply@. Deliverable, but worth a decision — and exactly what verify_mode=strict excludes.
unknownverdictoptionalA lookup did not complete. This says nothing about the address: never exclude or delete on it, and check again instead.
okverdictoptionalNothing known against it. Not the same as verified — see below.
Each address also carries a score (0–100, higher is worse) and the reasons behind it, so you can sort a list by what to deal with first. Only a certainty reaches 100: a pile of suspicions caps below it, because “three yellow flags” is not the same claim as “this address cannot work”.
We never open an SMTP session to ask a receiving server whether a mailbox exists, so we can prove an address cannot work and never that it does. Catch-all domains, full mailboxes and abandoned accounts all come back ok. For a cold, bought, or years-old list, run it through a dedicated verification service first — MillionVerifier, ZeroBounce, Kickbox, NeverBounce, Mailgun Validate — and import the cleaned file.
The response carries a guidance object saying all of this in full, including how bounces are measured here: per workspace over a rolling 24 hours, once at least 100 messages have gone out. Above 8% sending is paused automatically and stays paused until a person reviews it — Amazon SES suspends whole accounts above 5%, which is why it is enforced rather than advised.
Up to 10,000 addresses per call. The CSV importer runs the same checks on every row: pass verify_mode=basic to keep out what cannot receive mail, or strict to keep out anything flagged at all. An address we could not check is never held back.
The cost of a check is distinct domains, not addresses: ten thousand Gmail contacts are one lookup, three hundred unfamiliar B2B ones are three hundred. A list we can answer inside the request comes back { "mode": "inline", summary, guidance, results }. One that would spend a minute in DNS comes back { "mode": "job", "job": { "id", "status", "total", "checked" } } and runs in the background.
Poll GET /v1/contacts/verify/{job_id} every few seconds until status is completed — summary, guidance and flagged (the addresses worth acting on, worst first and capped) arrive with it. failed is the other terminal state. Branch on mode, never on whether results is present. Three checks may be queued per project at a time.
curl https://emailbump.com/api/v1/contacts/verify/JOB_ID \
-H "Authorization: Bearer ebk_your_key"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.
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": "[email protected]",
"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": "[email protected]",
"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.
subscribedbooleanoptionalMarketing consent. false stamps unsubscribed_at, same as the unsubscribe endpoint. Only set it to true on fresh, explicit consent.
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 suppression you know is wrong 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.
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) and search (email or name). 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/[email protected]" \
-H "Authorization: Bearer ebk_your_key"Subscribe / unsubscribe
Toggle marketing consent. Unsubscribing also sets unsubscribed_at.
curl -X POST https://emailbump.com/api/v1/contacts/5fef0867-b292-46ca-8371-e24d8575576c/unsubscribe \
-H "Authorization: Bearer ebk_your_key"