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.
No operations match this filter.
/contactsList 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).
curl https://sendhq.cc/api/v1/contacts \
-X GET \
-H "Authorization: Bearer $SENDHQ_API_KEY"{
"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
}/contactsAdd 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.
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"}'{
"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"
}/contacts/:idRetrieve a contact by ID or email
Retrieve a contact by ID or email
curl https://sendhq.cc/api/v1/contacts/id_value \
-X GET \
-H "Authorization: Bearer $SENDHQ_API_KEY"{
"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"
}/contacts/:idUpdate 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.
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"}}'{
"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"
}/contacts/:idDelete 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.
curl https://sendhq.cc/api/v1/contacts/id_value \
-X DELETE \
-H "Authorization: Bearer $SENDHQ_API_KEY"{
"ok": true
}/contact-importsImport 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.
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}'{
"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"
}/contact-importsList recent contact imports
List recent contact imports
curl https://sendhq.cc/api/v1/contact-imports \
-X GET \
-H "Authorization: Bearer $SENDHQ_API_KEY"{
"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
}/listsList contact lists with member counts
List contact lists with member counts
curl https://sendhq.cc/api/v1/lists \
-X GET \
-H "Authorization: Bearer $SENDHQ_API_KEY"{
"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
}/listsCreate 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.
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"}'{
"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"
}/lists/:idRetrieve a list
Retrieve a list
curl https://sendhq.cc/api/v1/lists/id_value \
-X GET \
-H "Authorization: Bearer $SENDHQ_API_KEY"{
"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"
}/lists/:idRename a list or change its signup form
Rename a list or change its signup form
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}'{
"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"
}/lists/:idDelete a list without deleting its contacts
Removes the list and its memberships. Returns 409 while a broadcast to the list is sending.
curl https://sendhq.cc/api/v1/lists/id_value \
-X DELETE \
-H "Authorization: Bearer $SENDHQ_API_KEY"{
"ok": true
}/lists/:id/contactsAdd 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`.
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"]}'{
"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"
}
}/lists/:id/contacts/:contact_idRemove a contact from a list
Remove a contact from a list
curl https://sendhq.cc/api/v1/lists/id_value/contacts/contact_id_value \
-X DELETE \
-H "Authorization: Bearer $SENDHQ_API_KEY"{
"ok": true
}/broadcastsList broadcasts
List broadcasts
curl https://sendhq.cc/api/v1/broadcasts \
-X GET \
-H "Authorization: Bearer $SENDHQ_API_KEY"{
"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
}/broadcastsCreate 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.
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>"}'{
"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"
}/broadcasts/:idRetrieve a broadcast and its delivery totals
Retrieve a broadcast and its delivery totals
curl https://sendhq.cc/api/v1/broadcasts/id_value \
-X GET \
-H "Authorization: Bearer $SENDHQ_API_KEY"{
"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
}
}/broadcasts/:idEdit a draft broadcast
Edit a draft broadcast
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"}'{
"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"
}/broadcasts/:idDelete a draft broadcast
Delete a draft broadcast
curl https://sendhq.cc/api/v1/broadcasts/id_value \
-X DELETE \
-H "Authorization: Bearer $SENDHQ_API_KEY"{
"ok": true
}/broadcasts/:id/sendSend 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.
curl https://sendhq.cc/api/v1/broadcasts/id_value/send \
-X POST \
-H "Authorization: Bearer $SENDHQ_API_KEY"{
"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
}
}/broadcasts/:id/cancelCancel 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.
curl https://sendhq.cc/api/v1/broadcasts/id_value/cancel \
-X POST \
-H "Authorization: Bearer $SENDHQ_API_KEY"{
"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
}
}/broadcasts/:id/recipientsList 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.
curl https://sendhq.cc/api/v1/broadcasts/id_value/recipients \
-X GET \
-H "Authorization: Bearer $SENDHQ_API_KEY"{
"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
}
}