# Collect newsletter sign-ups and manage unsubscribes

> Add a newsletter form to your storefront, record who agreed to marketing email, honour unsubscribe links and export the people you may mail.

Source: https://www.coritan.com/docs/organizations/newsletter/

In the dashboard:

- /dashboard/organizations/…/traffic/subscribers: https://www.coritan.com/dashboard/organizations

A *lead* is an address a visitor gives your storefront's newsletter form. A *suppression* is an address that gets no marketing email from your organization. This page covers the form, how customers agree to marketing email, the unsubscribe link in every marketing email, the suppression list and the export of everyone you may mail.

Marketing email is email a customer did not ask for about their order or service: the newsletter, offers, onboarding messages and requests for a review. It goes only to someone who agreed to it and is not on your suppression list. Email about a customer's own orders and services, such as an unpaid invoice, needs no agreement. The suppression list still stops it when the address is on it.

## Before you begin

- You need the Owner or Admin role to read or change anything on this page. Your team uses the routes under `/staff/marketing/subscribers` in the staff console, under Marketing, and an owner can call the same routes under `/subscribers`. The newsletter form and the unsubscribe link need no sign-in.
- The newsletter form is off until you turn it on. Send `PUT /api/v1/orgs/{org_slug}/subscribers/settings` with `{"enabled": true}`. While it is off, the form's route answers `404`.
- Your storefront has to serve the page that the unsubscribe link opens. See [Unsubscribe links](#unsubscribe-links).

### Newsletter settings

`GET /subscribers/settings` reads these keys and `PUT` changes the ones you send:

| Key | Default | Meaning |
| --- | --- | --- |
| `enabled` | `false` | Whether the form accepts sign-ups. |
| `turnstile` | `true` | Whether the form asks for a Turnstile check. It applies when your brand has Turnstile keys, as described in [Sign customers in to your storefront](/docs/organizations/storefront/customer-sign-in/#before-you-begin). |
| `consent_text` | `Send me news and offers by email. I can unsubscribe at any time.` | The words beside the form's checkbox. We store them as the visitor's agreement when the form sends no words of its own. At most 300 characters. An empty text goes back to the default. |

A longer `consent_text` answers `422` with `detail` and `field`. A save that changes nothing writes no audit entry, and a change is audited as `subscribers.settings_updated`.

## Set it up in your dashboard

Owners and admins run the newsletter form in the dashboard. Your team can do the same in the staff console, as [Marketing audience in the staff console](/docs/organizations/staff-console/marketing-audience/) describes. A member with another role does not see the page, and its address opens **Overview**.

1. In the [dashboard](https://www.coritan.com/dashboard/organizations), open the organization, then **Growth**, then **Subscribers**.
2. Under **Newsletter form**, turn on **Accept sign-ups**. Turn on **Check visitors with Turnstile** to ask for a check before a sign-up counts. It applies once your storefront has Turnstile keys.
3. Change "Words beside the checkbox" if you want words of your own. The page counts the characters, and 300 is the most. Emptying the field brings back the default words, which the card shows under it.
4. Select **Save changes**. The page says "Newsletter form turned on." when the switch was the only change, and "Newsletter settings saved." otherwise. A value the API refuses shows its sentence under the field.
5. Read the three figures under the card: **Leads**, **Consenting customers** and **Suppressed**.

When it works, the badge on the card reads **On** and a visitor who signs up appears under **Leads**.

**Leads** lists the addresses newest first. Each row shows its source, **Agreed** or **Did not agree**, a **Suppressed** badge when the address is on the suppression list, and a **Customer** badge, with the address linking to the customer, when it belongs to one. Search by address, choose "All leads", "Agreed to email" or "Did not agree", or type a source. The search, the filters and the page stay in the address. A row shows the words the person agreed to only when they differ from the card's current words. To delete a lead, open its actions menu and select **Delete lead…**. Only the lead goes: the customer, their agreement and the suppression list stay as they are.

Select **Export CSV** to save the people you may mail: leads who agreed and customers who agreed to marketing email, and never an address on the suppression list. A file holds at most 100,000 addresses. You can export 12 times an hour, and the page names the wait when you reach the limit.

**Suppression list** shows each address with why it is on the list ("Unsubscribed", "Bounced", "Marked as spam" or "Added by hand") and how it got there. Select **Add address…**, type the email address and select **Add address** to stop all marketing email to it. The reason is always "Added by hand", and an address already on the list is left as it is. To take an address off, open its actions menu and select **Take off the list…**. That never gives anyone's agreement back, so a person who withdrew still gets no marketing email until they agree again.

## Add the form to your storefront

Post the visitor's address to `POST /api/v1/orgs/{org_slug}/storefront/newsletter`:

```bash
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/storefront/newsletter" \
  -H "Content-Type: application/json" \
  -d '{"email": "alex@example.com", "source": "footer", "consent_text": "Send me news and offers by email. I can unsubscribe at any time.", "website": "", "turnstile_token": "..."}'
```

The answer is `202` with:

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

| Field | Required | Meaning |
| --- | --- | --- |
| `email` | yes | The address. It must be one mail can reach: an address on a `.invalid` domain, or one over 254 characters, answers `422` with `field` set to `email`. |
| `source` | no | Where the form is, such as `footer` or `checkout`. At most 64 characters, with control characters taken out. It defaults to `newsletter`. |
| `consent_text` | no | The words the visitor agreed to, as your form showed them. At most 500 characters. Left out, we store the `consent_text` setting. |
| `website` | no | A field no person sees. Leave it empty. A request that fills it answers `202` and stores nothing. |
| `turnstile_token` | when the check is on | The Cloudflare Turnstile answer. A missing or failed one answers `403` with `detail.error` set to `turnstile_failed`, before anything is stored. |

### What the answer never says

The answer is the same `202 {"ok": true}` for a new address, an address that signed up before, an address of one of your customers and an address on your suppression list. No answer holds an address. A visitor cannot use the form to find out who is a customer or who unsubscribed.

### What a sign-up stores

- A lead with the address in lower case, the `source`, `consent` set to `true`, the words the visitor agreed to, the visitor's IP address and the time.
- A second sign-up with the same address keeps the first time and changes only what it sends: `source` and `consent_text` when the form sends them, and the IP address.
- A customer of your organization with the same address is linked to the lead and agrees to marketing email, with the source `newsletter`. A customer of another organization with that address is not touched.
- An address on your suppression list stays on it. The form never removes a suppression. We keep the lead with `consent` set to `false` and we leave the consent of a customer with that address as it was.

### Limits

| Limit | Value |
| --- | --- |
| Sign-ups from one visitor address | 10 an hour |
| Sign-ups for your organization | 2,000 an hour |

Past either limit the answer is `429` with `Retry-After`, and nothing is stored.

## How customers agree to marketing email

Every customer account starts with `marketing_consent` set to `false`. A customer says yes in one of three places:

| Where | Source stored | How |
| --- | --- | --- |
| Sign-up | `signup` | `marketing_consent` on `POST /auth/register`. It is `false` unless your form sends `true` for a ticked checkbox. |
| Their account | `account` | `marketing_consent` on `PATCH /auth/me`. Sending `true` also takes their address off your suppression list, because they asked for the email from inside their own session. |
| The newsletter form | `newsletter` | A sign-up with their address, unless the address is suppressed. |

We store the answer with the time it was given. A repeat of the answer already stored changes nothing, so the time and the source keep saying when the customer first agreed. `GET /auth/me` returns `marketing_consent`, `marketing_consent_at` and `marketing_consent_source`. See [Sign customers in to your storefront](/docs/organizations/storefront/customer-sign-in/#register-a-customer).

A customer whose address is on your suppression list can still register with `marketing_consent` set to `true`. We store the answer as sent, and the suppression still stops the email.

When a customer closes their account, we delete the leads with their address and keep the suppression, so a later sign-up with the same address is still not mailed. The consent columns stay as they were. The customer's data export has a Marketing section with their consent, their lead rows and whether their address is suppressed.

## Unsubscribe links

Every marketing email carries a link in its `unsubscribe_url` variable:

```text
https://shop.example.com/unsubscribe?t=<token>
```

The token names your organization and the address, and it never expires, so an old email still unsubscribes. Your storefront serves the page at `/unsubscribe`, reads `t` from its address and calls the API with it:

- `GET /api/v1/orgs/{org_slug}/storefront/unsubscribe?t=<token>` answers what the page shows and changes nothing, because mail programs and link scanners open links with GET:

  ```json
  { "unsubscribed": false, "email_hint": "a***@example.com", "brand_name": "Acme Hosting" }
  ```

  `email_hint` keeps the first character of the address and the domain, and hides the rest.
- `POST /api/v1/orgs/{org_slug}/storefront/unsubscribe?t=<token>` puts the address on your suppression list and answers `{"ok": true}`. The reason is `unsubscribed` and the source is `link`. Doing it twice changes nothing more.

A token that is wrong, damaged or made for another organization answers `404` with `This link is not valid.`, the same answer as an unknown organization. At most 120 `POST` requests a minute from one visitor address.

Each marketing email also carries the `List-Unsubscribe` header with a one-click address on the API, and `List-Unsubscribe-Post: List-Unsubscribe=One-Click`. Mail programs send a `POST` to that address with the form `List-Unsubscribe=One-Click`. The route does not read the body, so the form, JSON such as `{}` and an empty body all work.

Adding an address to the suppression list also:

- cancels the automatic emails still waiting for that address;
- takes marketing consent back from a customer with that address (source `unsubscribe`, or `staff` for an entry made in the console);
- marks a lead with that address as not consenting.

## Suppressions {#suppressions}

The suppression list belongs to your organization. An address that another brand suppressed is not on yours, and one you suppressed is still mailable by them.

`GET /subscribers/suppressions` lists it, newest first, with `q` for part of an address, `limit` (1 to 200, default 50) and `offset`. Each row has `id`, `email`, `reason` and `source`:

| Reason | Set by |
| --- | --- |
| `unsubscribed` | A person selecting the unsubscribe link in an email, with the source `link`. |
| `manual` | Your team, with the source `console`. |

A row can also have the reason `bounced` or `complained`.

To stop marketing email to one address yourself, send `POST /subscribers/suppressions` with `{"email": "alex@example.com", "reason": "manual"}`. The reason must be `manual`, and a bad address or reason answers `422` with `detail` and `field`. An address already on the list stays as it was and the answer has `added` set to `false`. A new entry is audited as `suppression.added`.

`DELETE /subscribers/suppressions/{suppression_id}` takes an address off the list, audited as `suppression.lifted`. It does not give anyone's consent back: a customer or lead who withdrew has to agree again before marketing email reaches them.

## Read the leads

`GET /subscribers` lists leads, newest first. Each row has `id`, `email`, `source`, `consent`, `consent_text`, `customer_id`, `suppressed` and `created_at`. Filter with `q` for part of an address, `source` and `consent` (`true` or `false`), and page with `limit` (1 to 200, default 50) and `offset`. `total` is every lead the filters match.

`counts` covers the whole organization:

| Count | Meaning |
| --- | --- |
| `leads` | Every lead. |
| `consenting_customers` | Active customers who agreed to marketing email. |
| `suppressed` | Addresses on the suppression list. |

> [!WARNING]
> `DELETE /subscribers/{lead_id}` deletes one lead for good. It does not change the customer with that address, their consent or the suppression list, so an address that unsubscribed stays unsubscribed. It is audited as `subscriber.deleted` with the lead's `id` and source, never its address.

## Export the people you may mail

`GET /subscribers/export.csv` answers a CSV file with the columns `email`, `source`, `consent`, `kind` and `created_at`.

- The file lists leads who agreed to email, and active customers who agreed to marketing email. An address that is both is listed once, as a lead.
- It never lists an address on the suppression list, a lead who did not consent or a customer who did not.
- A cell that starts with `=`, `+`, `-` or `@` begins with an apostrophe, so a spreadsheet shows it as text instead of running it.
- A file holds at most 100,000 rows. When your list is longer, the answer has the header `X-Export-Truncated: true`.
- You can export 12 times an hour. After that the answer is `429`.
- Every export is audited as `subscribers.exported` with the row count before the file is answered.

## Result

A visitor who signs up appears in `GET /subscribers` with `consent` set to `true`. A visitor or customer who selects the unsubscribe link appears in `GET /subscribers/suppressions`, and neither the export nor any marketing email includes their address from then on.

## Troubleshooting

### The form answers 404

The newsletter form is off, or the organization in the address is wrong. Turn it on with `PUT /subscribers/settings` and `{"enabled": true}`.

### The form answers 403 with `turnstile_failed`

Your brand has Turnstile keys and the request carried no valid `turnstile_token`. Show the Turnstile challenge on the form and send its answer, or set `turnstile` to `false`.

### The form answers 429

One visitor address made more than 10 sign-ups in an hour, or your organization received more than 2,000. Wait for the time in `Retry-After`.

### A customer agreed to marketing email and gets none

Look for their address on the suppression list with `GET /subscribers/suppressions?q=`. A suppression wins over consent. If they want the email back, they can turn marketing email on in their account, which lifts it, or you can delete the row.

### An unsubscribe link answers 404

The token is wrong, damaged or from another organization. Check that your storefront passes `t` through unchanged and calls the API of the organization that sent the email.

## What your customers see

A storefront that Coritan serves draws the form, the sign-up box, the account switch and the unsubscribe page for you. This section says what a visitor reads, so your support team can answer questions about them. A storefront you build yourself calls the routes above and writes its own words.

### The newsletter form

The form sits in the footer of every page while the form is on. It reads "Get news and offers", with an "Email address" field, a "Subscribe" button and, under them, the words from your `consent_text` setting. It sends the address, the source `footer`, the words it showed and, when the check is on, the Turnstile answer.

- The check loads when the visitor first selects the field, not when the page loads. It shows only when `turnstile` is on and your brand has Turnstile keys. A visitor who presses "Subscribe" before the check has answered reads "Finish the check below, then press Subscribe."
- A person never sees the `website` field, and it cannot take focus. A script that fills it gets the same `202` and nothing is stored.
- On success the form gives way to one line, "You are on the list." We send no confirmation email, so the line never tells the visitor to check an inbox.
- A refusal shows under the field in one sentence, and the check runs again so the next try has a fresh answer:

| What happened | What the visitor reads |
| --- | --- |
| The address is not shaped like an address | "Enter a valid email address." |
| `422` from the API | "Enter a valid email address." or "That address cannot receive email. Check it and try again." |
| `403` | "We could not confirm that you are a person. Try again." |
| `429` | "Too many sign-ups right now. Try again later." |
| `404`, because the form is off | "Sign-ups are closed right now." |
| No answer from the server | "We could not reach the server. Check your connection and try again." |
| Any other answer | "Something went wrong. Try again." |

The form keeps its height through every answer, so nothing on the page moves.

### The sign-up box

The password sign-up form has an unticked box: "Email me news and offers. I can unsubscribe at any time." The form sends its value as `marketing_consent`. A storefront whose customers sign in with a code has no such form, and no checkout or social sign-in asks, because the API takes no consent there. A customer on those storefronts says yes in their account.

### The account switch

The account has a "Marketing email" section with a switch, "Email me news and offers". Under it the page says "On since" or "Off since" with the date of the customer's last answer, or "You have not chosen yet." It also says what the switch covers: "Emails about your orders, invoices and account always reach you."

The switch sends `PATCH /auth/me` with `marketing_consent`, and the page then reads "Saved. You will get news and offers by email." or "Saved. You will not get news and offers by email." A save that fails leaves the switch where it was and reads "Could not save your choice. Try again." Turning the switch on also takes the customer's address off your suppression list. A support session cannot change it.

### The unsubscribe page

`/unsubscribe?t=<token>` is the page a marketing email's link opens. It needs no sign-in, tells search engines to skip it and is not in your sitemap.

1. Opening the link reads the token with `GET` and changes nothing. The page shows "Unsubscribe from marketing email", the brand name, the address with its local part hidden and an "Unsubscribe" button: "You are about to stop marketing email from Acme Hosting to a***@example.com."
2. Only the button sends the `POST`. The page then reads "You are unsubscribed" and "You will not get marketing email from Acme Hosting again." An address that is already on the list reads that line at once, with no button.
3. A link that is not valid, or that has no token, reads "This link is not valid" and offers "Go to your account", where the same choice is a switch.
4. When the API cannot be reached the page says so and offers "Try again". A rate limit reads "Too many requests. Try again in a minute."

Every line says that email about orders, invoices and the account still comes.

## Related

- [Sign customers in to your storefront](/docs/organizations/storefront/customer-sign-in/)
- [How customers and orders are attributed](/docs/organizations/attribution/)
- [Audit log](/docs/organizations/audit-log/)

## API

- `POST /api/v1/orgs/{org_slug}/storefront/newsletter`: Sign a visitor up to the storefront's newsletter (https://www.coritan.com/docs/api/reference/organizations/storefront/storefront/#op-post-api-v1-orgs-org-slug-storefront-newsletter)
- `GET /api/v1/orgs/{org_slug}/storefront/unsubscribe`: Read the unsubscribe link of a marketing email (https://www.coritan.com/docs/api/reference/organizations/storefront/storefront/#op-get-api-v1-orgs-org-slug-storefront-unsubscribe)
- `POST /api/v1/orgs/{org_slug}/storefront/unsubscribe`: Unsubscribe the address of a marketing email (https://www.coritan.com/docs/api/reference/organizations/storefront/storefront/#op-post-api-v1-orgs-org-slug-storefront-unsubscribe)
- `GET /api/v1/orgs/{org_slug}/subscribers`: List the newsletter's leads and count who can be mailed (https://www.coritan.com/docs/api/reference/organizations/organizations-members/subscribers/#op-get-api-v1-orgs-org-slug-subscribers)
- `GET /api/v1/orgs/{org_slug}/subscribers/settings`: Read the newsletter form's settings (https://www.coritan.com/docs/api/reference/organizations/organizations-members/subscribers/#op-get-api-v1-orgs-org-slug-subscribers-settings)
- `PUT /api/v1/orgs/{org_slug}/subscribers/settings`: Turn the newsletter form on or off and change its words (https://www.coritan.com/docs/api/reference/organizations/organizations-members/subscribers/#op-put-api-v1-orgs-org-slug-subscribers-settings)
- `GET /api/v1/orgs/{org_slug}/subscribers/export.csv`: Download the addresses that can be mailed as a CSV file (https://www.coritan.com/docs/api/reference/organizations/organizations-members/subscribers/#op-get-api-v1-orgs-org-slug-subscribers-export-csv)
- `GET /api/v1/orgs/{org_slug}/subscribers/suppressions`: List the addresses that get no marketing email (https://www.coritan.com/docs/api/reference/organizations/organizations-members/subscribers/#op-get-api-v1-orgs-org-slug-subscribers-suppressions)
- `POST /api/v1/orgs/{org_slug}/subscribers/suppressions`: Add an address to the suppression list (https://www.coritan.com/docs/api/reference/organizations/organizations-members/subscribers/#op-post-api-v1-orgs-org-slug-subscribers-suppressions)
- `DELETE /api/v1/orgs/{org_slug}/subscribers/suppressions/{suppression_id}`: Take an address off the suppression list (https://www.coritan.com/docs/api/reference/organizations/organizations-members/subscribers/#op-delete-api-v1-orgs-org-slug-subscribers-suppressions-suppression-id)
- `DELETE /api/v1/orgs/{org_slug}/subscribers/{lead_id}`: Delete a lead (https://www.coritan.com/docs/api/reference/organizations/organizations-members/subscribers/#op-delete-api-v1-orgs-org-slug-subscribers-lead-id)
- `GET /api/v1/orgs/{org_slug}/staff/marketing/subscribers`: List the newsletter's leads and count who can be mailed (https://www.coritan.com/docs/api/reference/organizations/org-staff-marketing/staff/#op-get-api-v1-orgs-org-slug-staff-marketing-subscribers)
- `GET /api/v1/orgs/{org_slug}/staff/marketing/subscribers/settings`: Read the newsletter form's settings (https://www.coritan.com/docs/api/reference/organizations/organizations-members/staff/#op-get-api-v1-orgs-org-slug-staff-marketing-subscribers-settings)
- `PUT /api/v1/orgs/{org_slug}/staff/marketing/subscribers/settings`: Turn the newsletter form on or off and change its words (https://www.coritan.com/docs/api/reference/organizations/organizations-members/staff/#op-put-api-v1-orgs-org-slug-staff-marketing-subscribers-settings)
- `GET /api/v1/orgs/{org_slug}/staff/marketing/subscribers/export.csv`: Download the addresses that can be mailed as a CSV file (https://www.coritan.com/docs/api/reference/organizations/org-staff-marketing/staff/#op-get-api-v1-orgs-org-slug-staff-marketing-subscribers-export-csv)
- `GET /api/v1/orgs/{org_slug}/staff/marketing/subscribers/suppressions`: List the addresses that get no marketing email (https://www.coritan.com/docs/api/reference/organizations/org-staff-marketing/staff/#op-get-api-v1-orgs-org-slug-staff-marketing-subscribers-suppressions)
- `POST /api/v1/orgs/{org_slug}/staff/marketing/subscribers/suppressions`: Add an address to the suppression list (https://www.coritan.com/docs/api/reference/organizations/org-staff-marketing/staff/#op-post-api-v1-orgs-org-slug-staff-marketing-subscribers-suppressions)
- `DELETE /api/v1/orgs/{org_slug}/staff/marketing/subscribers/suppressions/{suppression_id}`: Take an address off the suppression list (https://www.coritan.com/docs/api/reference/organizations/org-staff-marketing/staff/#op-delete-api-v1-orgs-org-slug-staff-marketing-subscribers-suppressions-suppression)
- `DELETE /api/v1/orgs/{org_slug}/staff/marketing/subscribers/{lead_id}`: Delete a lead (https://www.coritan.com/docs/api/reference/organizations/org-staff-marketing/staff/#op-delete-api-v1-orgs-org-slug-staff-marketing-subscribers-lead-id)
