Subscriber API
Add, update, look up and unsubscribe newsletter contacts from your own software, with consent and double opt-in handled the same way as a signup form.
The subscriber API keeps the business's newsletter list in step with your own software: add someone the moment they sign up in your shop or booking system, keep their details current, and take them off when they ask.
Reading needs a key with newsletter:read. Adding, updating and unsubscribing need newsletter:manage. The account needs the newsletter, but not a transactional email plan. The dashboard's Subscriber API page lists the account's own lists and fields.
Everything here goes through the same rules as a signup form on the business's website: consent is recorded, lists that ask people to confirm send a confirmation email, and nobody who left is ever put back.
Lists
GET /api/v1/newsletter/lists
curl https://reggiodigital.com/api/v1/newsletter/lists \
-H "Authorization: Bearer $REGGIO_API_KEY"
{
"lists": [
{ "id": 42, "name": "News", "double_opt_in": true }
]
}
Every write names a list by its id. double_opt_in tells you whether a new subscriber has to confirm by email first.
Custom fields
GET /api/v1/newsletter/fields
curl https://reggiodigital.com/api/v1/newsletter/fields \
-H "Authorization: Bearer $REGGIO_API_KEY"
{
"fields": [
{ "key": "tier", "label": "Membership tier", "type": "text", "fallback": "member" },
{ "key": "renews_on", "label": "Renews on", "type": "date", "fallback": null }
]
}
Fields are defined by the business under Newsletter settings. You send values by key. Types are text, number, date (as in 2026-04-30) and url.
Adding or updating someone
POST /api/v1/newsletter/contacts
curl -X POST https://reggiodigital.com/api/v1/newsletter/contacts \
-H "Authorization: Bearer $REGGIO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: signup-8841" \
-d '{
"email": "sam@example.com",
"list_id": 42,
"first_name": "Sam",
"tags": [
"Bought"
],
"fields": {
"tier": "Gold"
},
"consent": {
"detail": "Ticked the newsletter box at checkout",
"ip": "203.0.113.4"
}
}'
One call does both:
- A new address joins the way a form signup does. On a list with
double_opt_in, they are emailed a confirmation and sit atpendinguntil they click it. Otherwise they aresubscribedstraight away, and any welcome sequence starts. Answers201. - An address already on the list has its names, fields and tags updated, and its status left exactly as it was. Answers
200. - Someone who unsubscribed or marked the business's mail as spam is never put back. Their details can be updated, and the response shows their current status so you can tell.
| Field | Required | What it is |
|---|---|---|
email |
Yes | The address. Stored in lower case. |
list_id |
Yes | A list on this account. Another account's list is a 404. |
first_name, last_name |
No | Up to 120 characters each. |
fields |
No | Up to 20 custom field values by key. An unknown key is a 422, and so is a value that does not fit the field's type. null clears a value. |
tags |
No | Up to 20 tags, each up to 60 characters. Added to the tags already there, never removed, so a sync cannot strip a tag someone added by hand. |
consent.detail |
No, but send it | Where this address came from, in words a person would understand later. Kept on the record. Without it we record that the address was added through the API. |
consent.ip, consent.user_agent |
No | The signup's IP address and browser, when you have them. |
The response is the membership:
{
"list_id": 42,
"email": "sam@example.com",
"status": "pending",
"status_description": "Waiting to confirm",
"first_name": "Sam",
"last_name": null,
"fields": { "tier": "Gold" },
"tags": ["Bought"],
"subscribed_at": null,
"unsubscribed_at": null
}
status is one of pending, subscribed, unsubscribed, cleaned (rested after a long stretch without engaging) or complained. subscribed_at is when they confirmed, or joined a list that does not ask.
This call accepts an Idempotency-Key.
Looking someone up
GET /api/v1/newsletter/contacts/{email}, with the address URL encoded.
curl https://reggiodigital.com/api/v1/newsletter/contacts/sam%40example.com \
-H "Authorization: Bearer $REGGIO_API_KEY"
{
"email": "sam@example.com",
"marketing_blocked": false,
"lists": [
{
"list_id": 42,
"email": "sam@example.com",
"status": "subscribed",
"status_description": "Subscribed",
"first_name": "Sam",
"last_name": null,
"fields": { "tier": "Gold" },
"tags": ["Bought"],
"subscribed_at": "2026-09-14T15:04:05+00:00",
"unsubscribed_at": null
}
]
}
lists has one entry for every list they are on. marketing_blocked is true when no marketing email from the business can reach them at all, for example after they unsubscribed from everything. An address we have never seen is a 404.
Taking someone off
POST /api/v1/newsletter/contacts/{email}/unsubscribe
curl -X POST https://reggiodigital.com/api/v1/newsletter/contacts/sam%40example.com/unsubscribe \
-H "Authorization: Bearer $REGGIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "Closed their account"
}'
- Without
list_id, they stop getting every marketing email from the business. That is what a person means when they say unsubscribe. Their receipts and other transactional email are not affected. - With
list_id, they leave that one list and stay on the others.
reason is optional and kept on the record. The response is the same shape as a lookup. Neither can be undone through the API: rejoining is the person's own decision, from their preferences page.
Bringing over a whole list
To bring an existing list across in one go, use the CSV import under Subscribers in the dashboard instead of looping over this API. The import checks the whole file before it writes anything, and it does not count against the account's rate limit.
Errors
The usual error shape. The ones this API returns are unauthorized, forbidden, no_plan, invalid_request, not_found and idempotency_conflict.