API reference

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 parameterslist_idstatusquerylimitoffset
Request
curl https://sendhq.cc/api/v1/contacts \
  -X GET \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
200 Response
{
  "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.

Request
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"}'
201 Response
{
  "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

Request
curl https://sendhq.cc/api/v1/contacts/id_value \
  -X GET \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
200 Response
{
  "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.

Request
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"}}'
200 Response
{
  "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.

Request
curl https://sendhq.cc/api/v1/contacts/id_value \
  -X DELETE \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
200 Response
{
  "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.

Request
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}'
201 Response
{
  "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

Request
curl https://sendhq.cc/api/v1/contact-imports \
  -X GET \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
200 Response
{
  "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

Request
curl https://sendhq.cc/api/v1/lists \
  -X GET \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
200 Response
{
  "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.

Request
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"}'
201 Response
{
  "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

Request
curl https://sendhq.cc/api/v1/lists/id_value \
  -X GET \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
200 Response
{
  "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

Request
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}'
200 Response
{
  "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.

Request
curl https://sendhq.cc/api/v1/lists/id_value \
  -X DELETE \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
200 Response
{
  "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`.

Request
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"]}'
200 Response
{
  "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

Request
curl https://sendhq.cc/api/v1/lists/id_value/contacts/contact_id_value \
  -X DELETE \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
200 Response
{
  "ok": true
}
GET/broadcasts
API key

List broadcasts

List broadcasts

Query parametersstatuslimitoffset
Request
curl https://sendhq.cc/api/v1/broadcasts \
  -X GET \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
200 Response
{
  "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.

Request
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>"}'
201 Response
{
  "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

Request
curl https://sendhq.cc/api/v1/broadcasts/id_value \
  -X GET \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
200 Response
{
  "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

Request
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"}'
200 Response
{
  "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

Request
curl https://sendhq.cc/api/v1/broadcasts/id_value \
  -X DELETE \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
200 Response
{
  "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 headersIdempotency-Key
Request
curl https://sendhq.cc/api/v1/broadcasts/id_value/send \
  -X POST \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
202 Response
{
  "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.

Request
curl https://sendhq.cc/api/v1/broadcasts/id_value/cancel \
  -X POST \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
200 Response
{
  "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 parametersstatuslimitoffset
Request
curl https://sendhq.cc/api/v1/broadcasts/id_value/recipients \
  -X GET \
  -H "Authorization: Bearer $SENDHQ_API_KEY"
200 Response
{
  "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
  }
}