Project API

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

GET/v1/contacts
POST/v1/contacts
GET/v1/contacts/{id}
PATCH/v1/contacts/{id}
DELETE/v1/contacts/{id}
POST/v1/contacts/{id}/subscribe
POST/v1/contacts/{id}/unsubscribe
POST/v1/contacts/verify
GET/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
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

undeliverableverdictoptional

Proven: 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.

riskyverdictoptional

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

unknownverdictoptional

A lookup did not complete. This says nothing about the address: never exclude or delete on it, and check again instead.

okverdictoptional

Nothing 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”.

ok is not verified

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.

Two shapes, told apart by mode

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

emailstringrequired

The contact's email address. Identifies the contact for the upsert.

first_namestringoptional

Given name.

last_namestringoptional

Family name.

companystringoptional

Company or organization.

citystringoptional

City.

countrystringoptional

Country.

phonestringoptional

Phone number.

timezonestringoptional

IANA timezone, e.g. America/New_York. Used by local-timezone and smart send times; we also learn it from where they open and click.

attributesobjectoptional

Any custom key/value attributes to store on the contact. Merged into anything already stored, so a second call never erases what the first recorded.

subscribedbooleanoptional

Marketing 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[]optional

List 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
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"]
  }'
201 created (200 when an existing contact is updated)
{
  "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 createoptional

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

subscribedbooleanoptional

Marketing consent. false stamps unsubscribed_at, same as the unsubscribe endpoint. Only set it to true on fresh, explicit consent.

email_statusstringoptional

Deliverability 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
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
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"