# Contacts API

Create, update, list, and manage consent for your audience.

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

## Create a contact

**Body parameters**

- `email` — *string*, required — The contact's email address. Must be unique in your project.
- `first_name` — *string*, optional — Given name.
- `last_name` — *string*, optional — Family name.
- `company` — *string*, optional — Company or organization.
- `city` — *string*, optional — City.
- `country` — *string*, optional — Country.
- `phone` — *string*, optional — Phone number.
- `timezone` — *string*, optional — IANA timezone, e.g. `America/New_York`; used by local-timezone and smart send times.
- `attributes` — *object*, optional — Any custom key/value attributes to store on the contact. Merged into whatever is already stored, so a second call never erases what the first recorded.
- `subscribed` — *boolean*, optional — Marketing consent. Defaults to `true`. Send `false` to record someone who signed up but did not opt in: they still receive transactional mail (receipts, password resets) and no marketing. Omit it when upserting an address you already have and their existing consent is left exactly as it was — that is how you avoid revoking consent someone gave you earlier. Only an explicit value changes it, in either direction.
- `list_ids` — *string[]*, optional — List ids to add the contact to in the same call. On a double-opt-in list they're added as `pending` and emailed a confirmation link; on a normal list, immediately.

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.

```bash
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" }
  }'
```

```json
{
  "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"
  }
}
```

**Sending the same email twice is safe.** This endpoint is an upsert, not a create — a new address is created and returns `201`, an address you already have is updated and returns `200`. The response says which:

```json
{ "contact": { "id": "5fef0867-…", "email": "jamie@example.com" }, "created": false }
```

So a form that submits twice, or an import you run again, updates the existing contact rather than erroring or duplicating it. Read `created` (or the status code) if you need to tell the two apart. On an update, `attributes` merge the same way `PATCH` does — keys you send win, keys you don't send stay, an explicit `null` removes one. Note that a key you *do* send overwrites the stored value: a re-submission carrying `marketing_consent: false` will overwrite a stored `true`, so only send consent-bearing keys when they carry a real answer.

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

- `subscribed` — *boolean*, optional — Marketing consent. `false` stamps `unsubscribed_at`, same as the unsubscribe endpoint. Only set it to `true` on fresh, explicit consent.
- `email_status` — *string*, optional — 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 sending any other value is accepted and then ignored — there is no way to suppress an address by hand.

Anything you leave out is untouched, and that now includes the contents of `attributes`: the keys you send are added or overwritten, and keys already on the contact that you don't mention are kept.

That matters because attributes are often a record of something — when someone entered a giveaway, that they accepted the rules, which form they came from. A later update that sets `plan` has no reason to know those exist, and until this changed it erased them.

To remove a key, send it as `null`:

```json
{ "attributes": { "plan": "pro", "trial_ends_at": null } }
```

`plan` is set, `trial_ends_at` is removed, everything else on the contact is left alone.

```bash
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. URL-encode the address (a literal `+` in a query string decodes as a space).

```bash
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%40example.com" \
  -H "Authorization: Bearer ebk_your_key"
```

## Subscribe / unsubscribe

Toggle marketing consent. Unsubscribing also sets `unsubscribed_at`.

```bash
curl -X POST https://emailbump.com/api/v1/contacts/5fef0867-b292-46ca-8371-e24d8575576c/unsubscribe \
  -H "Authorization: Bearer ebk_your_key"
```
