---
title: Campaigns API | SendHQ Docs
description: Store contacts with their consent basis, organize them into lists with hosted double opt-in signup forms, import CSV files, and send broadcasts to a list with merge tags, per-list one-click unsubscribe, and per-recipient outcomes.
canonical: https://sendhq.cc/docs/api-reference/campaigns
last-updated: 2026-09-07
updated: 2026-09-07
---
# Campaigns

Store contacts with their consent basis, organize them into lists with hosted double opt-in signup forms, import CSV files, and send broadcasts to a list with merge tags, per-list one-click unsubscribe, and per-recipient outcomes.

GET `/contacts`

API key

## List contacts

Newest-first contacts with consent evidence and the lists they are subscribed to. Filter by \`list_id\`, \`status\` (\`subscribed\`, \`pending\`, \`unsubscribed\`, \`bounced\`, \`complained\`), or \`query\` (email or name substring).

**Query parameters** `list_id` `status` `query` `limit` `offset`

```shell
curl https://sendhq.cc/api/v1/contacts \
  -X GET \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
```

```json
{
  "data": [
    {
      "id": "ct_…",
      "email": "ada@example.net",
      "name": "Ada Lovelace",
      "fields": {
        "plan": "pro"
      },
      "status": "subscribed",
      "consent": {
        "source": "api",
        "basis": "Ticked the newsletter box at checkout on acme.com",
        "importId": null,
        "optedInAt": "2026-10-25T12:00:00.000Z",
        "confirmedAt": null,
        "ip": null,
        "userAgent": null
      },
      "unsubscribedAt": null,
      "lists": [
        "lst_…"
      ],
      "createdAt": "2026-10-25T12:00:00.000Z",
      "updatedAt": "2026-10-25T12:00:00.000Z"
    }
  ],
  "count": 1,
  "total": 1
}
```

POST `/contacts`

API key

## Add a contact with its consent basis

Add one contact. A subscribed contact (the default) requires \`consent_basis\`: how this person agreed to receive your email. Use \`status: "unsubscribed"\` to record an opt-out. Addresses already suppressed or unsubscribed from the workspace are stored with that status, never as subscribed. Returns 409 when the email already exists.

```shell
curl https://sendhq.cc/api/v1/contacts \
  -X POST \
  -H "Authorization: Bearer $SENDHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"ada@example.net","name":"Ada Lovelace","fields":{"plan":"pro"},"list_ids":["lst_…"],"consent_basis":"Ticked the newsletter box at checkout on acme.com"}'
```

```json
{
  "id": "ct_…",
  "email": "ada@example.net",
  "name": "Ada Lovelace",
  "fields": {
    "plan": "pro"
  },
  "status": "subscribed",
  "consent": {
    "source": "api",
    "basis": "Ticked the newsletter box at checkout on acme.com",
    "importId": null,
    "optedInAt": "2026-10-25T12:00:00.000Z",
    "confirmedAt": null,
    "ip": null,
    "userAgent": null
  },
  "unsubscribedAt": null,
  "lists": [
    "lst_…"
  ],
  "createdAt": "2026-10-25T12:00:00.000Z",
  "updatedAt": "2026-10-25T12:00:00.000Z"
}
```

GET `/contacts/:id`

API key

## Retrieve a contact by ID or email

Retrieve a contact by ID or email

```shell
curl https://sendhq.cc/api/v1/contacts/id_value \
  -X GET \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
```

```json
{
  "id": "ct_…",
  "email": "ada@example.net",
  "name": "Ada Lovelace",
  "fields": {
    "plan": "pro"
  },
  "status": "subscribed",
  "consent": {
    "source": "api",
    "basis": "Ticked the newsletter box at checkout on acme.com",
    "importId": null,
    "optedInAt": "2026-10-25T12:00:00.000Z",
    "confirmedAt": null,
    "ip": null,
    "userAgent": null
  },
  "unsubscribedAt": null,
  "lists": [
    "lst_…"
  ],
  "createdAt": "2026-10-25T12:00:00.000Z",
  "updatedAt": "2026-10-25T12:00:00.000Z"
}
```

PATCH `/contacts/:id`

API key

## Update a contact's name or fields, or unsubscribe it

Change \`name\`, merge \`fields\` (a null value removes a field), or set \`status\` to \`unsubscribed\`. A contact can never be resubscribed through the API: only the recipient can, by confirming a signup form, which clears their earlier unsubscribe from that list and from the workspace but never a bounce or complaint suppression.

```shell
curl https://sendhq.cc/api/v1/contacts/id_value \
  -X PATCH \
  -H "Authorization: Bearer $SENDHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Ada King","fields":{"plan":"team"}}'
```

```json
{
  "id": "ct_…",
  "email": "ada@example.net",
  "name": "Ada Lovelace",
  "fields": {
    "plan": "pro"
  },
  "status": "subscribed",
  "consent": {
    "source": "api",
    "basis": "Ticked the newsletter box at checkout on acme.com",
    "importId": null,
    "optedInAt": "2026-10-25T12:00:00.000Z",
    "confirmedAt": null,
    "ip": null,
    "userAgent": null
  },
  "unsubscribedAt": null,
  "lists": [
    "lst_…"
  ],
  "createdAt": "2026-10-25T12:00:00.000Z",
  "updatedAt": "2026-10-25T12:00:00.000Z"
}
```

DELETE `/contacts/:id`

API key

## Delete a contact and its list memberships

Removes the contact and its memberships. Suppressions and recorded unsubscribes are kept, so a deleted address still cannot be mailed if it is added again.

```shell
curl https://sendhq.cc/api/v1/contacts/id_value \
  -X DELETE \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
```

```json
{
  "ok": true
}
```

POST `/contact-imports`

API key

## Import contacts from CSV

Import up to 5,000 rows (5 MB) per request. The header row needs an \`email\` column; \`name\` (or \`first_name\` and \`last_name\`) and \`status\` are recognized and other columns become custom fields. \`consent_basis\` is required and is recorded on every new contact. Rows whose status says unsubscribed import as unsubscribed. Existing contacts keep their status; set \`update_existing\` to merge names and fields. Duplicate rows are skipped and invalid rows are reported by line.

```shell
curl https://sendhq.cc/api/v1/contact-imports \
  -X POST \
  -H "Authorization: Bearer $SENDHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"csv":"email,name,plan\nada@example.net,Ada Lovelace,pro\n","list_id":"lst_…","consent_basis":"Newsletter signups exported from Mailchimp (double opt-in)","update_existing":false}'
```

```json
{
  "id": "imp_…",
  "listId": "lst_…",
  "consentBasis": "Newsletter signups exported from Mailchimp (double opt-in)",
  "totalRows": 1,
  "accepted": 1,
  "created": 1,
  "existing": 0,
  "skipped": 0,
  "invalid": 0,
  "errors": [
    {
      "line": 3,
      "reason": "invalid email address"
    }
  ],
  "createdAt": "2026-10-25T12:00:00.000Z"
}
```

GET `/contact-imports`

API key

## List recent contact imports

List recent contact imports

```shell
curl https://sendhq.cc/api/v1/contact-imports \
  -X GET \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
```

```json
{
  "data": [
    {
      "id": "imp_…",
      "listId": "lst_…",
      "consentBasis": "Newsletter signups exported from Mailchimp (double opt-in)",
      "totalRows": 1,
      "accepted": 1,
      "skipped": 0,
      "invalid": 0,
      "createdAt": "2026-10-25T12:00:00.000Z"
    }
  ],
  "count": 1
}
```

GET `/lists`

API key

## List contact lists with member counts

List contact lists with member counts

```shell
curl https://sendhq.cc/api/v1/lists \
  -X GET \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
```

```json
{
  "data": [
    {
      "id": "lst_…",
      "name": "Product updates",
      "description": "Monthly news from Acme",
      "form": {
        "enabled": true,
        "from": "Acme <news@example.com>",
        "redirectUrl": "https://example.com/thanks",
        "url": "https://sendhq.cc/subscribe/lst_…"
      },
      "counts": {
        "subscribed": 0,
        "pending": 0,
        "unsubscribed": 0
      },
      "createdAt": "2026-10-25T12:00:00.000Z",
      "updatedAt": "2026-10-25T12:00:00.000Z"
    }
  ],
  "count": 1
}
```

POST `/lists`

API key

## Create a list and optional hosted signup form

Create a list. Set \`form_enabled\` to publish a hosted double opt-in signup form at \`form.url\`; \`form_from\` is the sender (on a verified domain) of the confirmation email, and \`redirect_url\` is an optional https page to show after confirming. A workspace may have 100 lists.

```shell
curl https://sendhq.cc/api/v1/lists \
  -X POST \
  -H "Authorization: Bearer $SENDHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Product updates","description":"Monthly news from Acme","form_enabled":true,"form_from":"Acme <news@example.com>","redirect_url":"https://example.com/thanks"}'
```

```json
{
  "id": "lst_…",
  "name": "Product updates",
  "description": "Monthly news from Acme",
  "form": {
    "enabled": true,
    "from": "Acme <news@example.com>",
    "redirectUrl": "https://example.com/thanks",
    "url": "https://sendhq.cc/subscribe/lst_…"
  },
  "counts": {
    "subscribed": 0,
    "pending": 0,
    "unsubscribed": 0
  },
  "createdAt": "2026-10-25T12:00:00.000Z",
  "updatedAt": "2026-10-25T12:00:00.000Z"
}
```

GET `/lists/:id`

API key

## Retrieve a list

Retrieve a list

```shell
curl https://sendhq.cc/api/v1/lists/id_value \
  -X GET \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
```

```json
{
  "id": "lst_…",
  "name": "Product updates",
  "description": "Monthly news from Acme",
  "form": {
    "enabled": true,
    "from": "Acme <news@example.com>",
    "redirectUrl": "https://example.com/thanks",
    "url": "https://sendhq.cc/subscribe/lst_…"
  },
  "counts": {
    "subscribed": 0,
    "pending": 0,
    "unsubscribed": 0
  },
  "createdAt": "2026-10-25T12:00:00.000Z",
  "updatedAt": "2026-10-25T12:00:00.000Z"
}
```

PATCH `/lists/:id`

API key

## Rename a list or change its signup form

Rename a list or change its signup form

```shell
curl https://sendhq.cc/api/v1/lists/id_value \
  -X PATCH \
  -H "Authorization: Bearer $SENDHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Product news","form_enabled":false}'
```

```json
{
  "id": "lst_…",
  "name": "Product updates",
  "description": "Monthly news from Acme",
  "form": {
    "enabled": true,
    "from": "Acme <news@example.com>",
    "redirectUrl": "https://example.com/thanks",
    "url": "https://sendhq.cc/subscribe/lst_…"
  },
  "counts": {
    "subscribed": 0,
    "pending": 0,
    "unsubscribed": 0
  },
  "createdAt": "2026-10-25T12:00:00.000Z",
  "updatedAt": "2026-10-25T12:00:00.000Z"
}
```

DELETE `/lists/:id`

API key

## Delete a list without deleting its contacts

Removes the list and its memberships. Returns 409 while a broadcast to the list is sending.

```shell
curl https://sendhq.cc/api/v1/lists/id_value \
  -X DELETE \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
```

```json
{
  "ok": true
}
```

POST `/lists/:id/contacts`

API key

## Add existing contacts to a list

Add up to 500 existing contacts by \`contact_ids\` or \`emails\`. Each membership mirrors the contact: subscribed contacts join subscribed, others join in their current state. Unknown references are returned in \`notFound\`.

```shell
curl https://sendhq.cc/api/v1/lists/id_value/contacts \
  -X POST \
  -H "Authorization: Bearer $SENDHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"emails":["ada@example.net"]}'
```

```json
{
  "listId": "lst_…",
  "added": 1,
  "notFound": [],
  "list": {
    "id": "lst_…",
    "name": "Product updates",
    "description": "Monthly news from Acme",
    "form": {
      "enabled": true,
      "from": "Acme <news@example.com>",
      "redirectUrl": "https://example.com/thanks",
      "url": "https://sendhq.cc/subscribe/lst_…"
    },
    "counts": {
      "subscribed": 0,
      "pending": 0,
      "unsubscribed": 0
    },
    "createdAt": "2026-10-25T12:00:00.000Z",
    "updatedAt": "2026-10-25T12:00:00.000Z"
  }
}
```

DELETE `/lists/:id/contacts/:contact_id`

API key

## Remove a contact from a list

Remove a contact from a list

```shell
curl https://sendhq.cc/api/v1/lists/id_value/contacts/contact_id_value \
  -X DELETE \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
```

```json
{
  "ok": true
}
```

GET `/broadcasts`

API key

## List broadcasts

List broadcasts

**Query parameters** `status` `limit` `offset`

```shell
curl https://sendhq.cc/api/v1/broadcasts \
  -X GET \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
```

```json
{
  "data": [
    {
      "id": "bc_…",
      "name": "October update",
      "status": "draft",
      "listId": "lst_…",
      "from": "Acme <news@example.com>",
      "replyTo": null,
      "subject": "What's new in October, {{first_name | friend}}",
      "previewText": "Three new features and a roadmap peek",
      "html": "<p>Hi {{first_name | there}},</p>",
      "text": null,
      "template": null,
      "scheduledAt": null,
      "startedAt": null,
      "completedAt": null,
      "nextAttemptAt": null,
      "lastError": null,
      "createdAt": "2026-10-25T12:00:00.000Z",
      "updatedAt": "2026-10-25T12:00:00.000Z"
    }
  ],
  "count": 1
}
```

POST `/broadcasts`

API key

## Create a broadcast draft

Create a draft for one list: \`from\` on a verified domain, \`subject\`, optional \`preview_text\`, and \`html\` and/or \`text\`, or a published hosted \`template\` ({id or key, optional version_id}). Raw content supports merge tags such as \`{{first_name}}\`, \`{{fields.plan}}\`, and fallbacks like \`{{first_name | there}}\`; HTML values are escaped.

```shell
curl https://sendhq.cc/api/v1/broadcasts \
  -X POST \
  -H "Authorization: Bearer $SENDHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"October update","list_id":"lst_…","from":"Acme <news@example.com>","subject":"What's new in October, {{first_name | friend}}","preview_text":"Three new features and a roadmap peek","html":"<p>Hi {{first_name | there}},</p><p>Here's what shipped.</p>"}'
```

```json
{
  "id": "bc_…",
  "name": "October update",
  "status": "draft",
  "listId": "lst_…",
  "from": "Acme <news@example.com>",
  "replyTo": null,
  "subject": "What's new in October, {{first_name | friend}}",
  "previewText": "Three new features and a roadmap peek",
  "html": "<p>Hi {{first_name | there}},</p>",
  "text": null,
  "template": null,
  "scheduledAt": null,
  "startedAt": null,
  "completedAt": null,
  "nextAttemptAt": null,
  "lastError": null,
  "createdAt": "2026-10-25T12:00:00.000Z",
  "updatedAt": "2026-10-25T12:00:00.000Z"
}
```

GET `/broadcasts/:id`

API key

## Retrieve a broadcast and its delivery totals

Retrieve a broadcast and its delivery totals

```shell
curl https://sendhq.cc/api/v1/broadcasts/id_value \
  -X GET \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
```

```json
{
  "id": "bc_…",
  "name": "October update",
  "status": "sending",
  "listId": "lst_…",
  "from": "Acme <news@example.com>",
  "replyTo": null,
  "subject": "What's new in October, {{first_name | friend}}",
  "previewText": "Three new features and a roadmap peek",
  "html": "<p>Hi {{first_name | there}},</p>",
  "text": null,
  "template": null,
  "scheduledAt": null,
  "startedAt": null,
  "completedAt": null,
  "nextAttemptAt": null,
  "lastError": null,
  "createdAt": "2026-10-25T12:00:00.000Z",
  "updatedAt": "2026-10-25T12:00:00.000Z",
  "totals": {
    "queued": 0,
    "sending": 0,
    "sent": 0,
    "held": 0,
    "skipped": 0,
    "failed": 0,
    "total": 0
  }
}
```

PATCH `/broadcasts/:id`

API key

## Edit a draft broadcast

Edit a draft broadcast

```shell
curl https://sendhq.cc/api/v1/broadcasts/id_value \
  -X PATCH \
  -H "Authorization: Bearer $SENDHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"subject":"October product update"}'
```

```json
{
  "id": "bc_…",
  "name": "October update",
  "status": "draft",
  "listId": "lst_…",
  "from": "Acme <news@example.com>",
  "replyTo": null,
  "subject": "What's new in October, {{first_name | friend}}",
  "previewText": "Three new features and a roadmap peek",
  "html": "<p>Hi {{first_name | there}},</p>",
  "text": null,
  "template": null,
  "scheduledAt": null,
  "startedAt": null,
  "completedAt": null,
  "nextAttemptAt": null,
  "lastError": null,
  "createdAt": "2026-10-25T12:00:00.000Z",
  "updatedAt": "2026-10-25T12:00:00.000Z"
}
```

DELETE `/broadcasts/:id`

API key

## Delete a draft broadcast

Delete a draft broadcast

```shell
curl https://sendhq.cc/api/v1/broadcasts/id_value \
  -X DELETE \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
```

```json
{
  "ok": true
}
```

POST `/broadcasts/:id/send`

API key

## Send a broadcast to its list

Snapshot the list's subscribed members and send one message to each in the background. Every message goes through the normal send checks: verified domain, the marketing plan (integration-trial workspaces may only send to the account email), suppressions, unsubscribes, sender reputation, and daily and monthly limits, and carries one-click List-Unsubscribe headers that remove the recipient from this list only (the unsubscribe page also offers leaving all marketing email; receipts and other transactional mail are never blocked). Members that cannot be mailed are recorded as skipped. Each contact is sent at most once per broadcast. When a limit or a reputation pause stops delivery, the broadcast keeps \`sending\` and resumes after \`nextAttemptAt\`. Returns 202.

**Optional headers** `Idempotency-Key`

```shell
curl https://sendhq.cc/api/v1/broadcasts/id_value/send \
  -X POST \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
```

```json
{
  "id": "bc_…",
  "name": "October update",
  "status": "sending",
  "listId": "lst_…",
  "from": "Acme <news@example.com>",
  "replyTo": null,
  "subject": "What's new in October, {{first_name | friend}}",
  "previewText": "Three new features and a roadmap peek",
  "html": "<p>Hi {{first_name | there}},</p>",
  "text": null,
  "template": null,
  "scheduledAt": null,
  "startedAt": null,
  "completedAt": null,
  "nextAttemptAt": null,
  "lastError": null,
  "createdAt": "2026-10-25T12:00:00.000Z",
  "updatedAt": "2026-10-25T12:00:00.000Z",
  "totals": {
    "queued": 0,
    "sending": 0,
    "sent": 0,
    "held": 0,
    "skipped": 0,
    "failed": 0,
    "total": 0
  }
}
```

POST `/broadcasts/:id/cancel`

API key

## Cancel a draft or sending broadcast

Stops a broadcast: recipients not yet sent are marked skipped (\`cancelled\`). Messages already handed to the provider are not recalled.

```shell
curl https://sendhq.cc/api/v1/broadcasts/id_value/cancel \
  -X POST \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
```

```json
{
  "id": "bc_…",
  "name": "October update",
  "status": "cancelled",
  "listId": "lst_…",
  "from": "Acme <news@example.com>",
  "replyTo": null,
  "subject": "What's new in October, {{first_name | friend}}",
  "previewText": "Three new features and a roadmap peek",
  "html": "<p>Hi {{first_name | there}},</p>",
  "text": null,
  "template": null,
  "scheduledAt": null,
  "startedAt": null,
  "completedAt": null,
  "nextAttemptAt": null,
  "lastError": null,
  "createdAt": "2026-10-25T12:00:00.000Z",
  "updatedAt": "2026-10-25T12:00:00.000Z",
  "totals": {
    "queued": 0,
    "sending": 0,
    "sent": 0,
    "held": 0,
    "skipped": 0,
    "failed": 0,
    "total": 0
  }
}
```

GET `/broadcasts/:id/recipients`

API key

## List a broadcast's recipients and outcomes

Per-recipient outcome (\`queued\`, \`sending\`, \`sent\`, \`held\`, \`skipped\` with a reason, or \`failed\` with the error) and the email ID for delivery events.

**Query parameters** `status` `limit` `offset`

```shell
curl https://sendhq.cc/api/v1/broadcasts/id_value/recipients \
  -X GET \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
```

```json
{
  "data": [
    {
      "contactId": "ct_…",
      "email": "ada@example.net",
      "status": "sent",
      "skipReason": null,
      "emailId": "em_…",
      "error": null,
      "attempts": 1,
      "sentAt": "2026-10-25T12:00:00.000Z"
    }
  ],
  "count": 1,
  "totals": {
    "queued": 0,
    "sending": 0,
    "sent": 0,
    "held": 0,
    "skipped": 0,
    "failed": 0,
    "total": 0
  }
}
```
