Unsubscribe

Add a working unsubscribe link and one-click header to an email, and understand what happens when a recipient uses it

Marketing and other non-transactional mail needs a way for the recipient to stop receiving it, and mailbox providers now require one: Gmail and Yahoo reject or junk bulk mail without an RFC 8058 one-click unsubscribe header. send.dev handles the whole loop for you. You set one field on the request; send.dev adds the headers, hosts the link, records the opt-out on your suppression list, and refuses to send that recipient more marketing mail.

Transactional mail (receipts, password resets, alerts) does not need any of this and should not carry an unsubscribe link. Leave the fields off.

Enabling it on an email

Three request fields on Send Email and each item of Send Batch control unsubscribe behavior:

FieldTypeDescription
unsubscribebooleanAdd unsubscribe headers and a link to this email.
categorystringtransactional (the default) or marketing. Setting marketing implies unsubscribe: true, and additionally makes the send fail if the recipient has already unsubscribed.
listIdstringOptional label of at most 64 characters (A-Z a-z 0-9 . _ : -) naming the list or stream this email belongs to, for example weekly-digest. It is carried through to the recipient's unsubscribe record so you can tell which list they left.

In practice: send marketing mail with category: "marketing" and a listId, and you are done. Use a bare unsubscribe: true only for the unusual case of mail that should carry a link but should still reach recipients who opted out.

One recipient per email

The unsubscribe link is bound to a single recipient, so an unsubscribe-enabled email must have exactly one address in to and no cc or bcc. Anything else is rejected:

{
  "error": {
    "code": "validation_error",
    "message": "Validation failed",
    "details": [
      { "field": "to", "message": "Unsubscribe-enabled messages (unsubscribe: true or category: \"marketing\") must have exactly one recipient in 'to' and no cc/bcc; use /emails/batch for multiple recipients" }
    ]
  }
}

To reach many people, use Send Batch with one item per recipient. Each item gets its own link.

What is added to the email

send.dev adds two headers:

List-Unsubscribe: <https://api.do.dev/v1/send/u/{token}>, <mailto:unsubscribe@do.dev?subject={token}>
List-Unsubscribe-Post: List-Unsubscribe=One-Click

This is the pair Gmail, Yahoo, Apple Mail, and Outlook look for to show their own "Unsubscribe" button next to the sender name. When the recipient uses it, the client makes a POST to the link with no page load; the recipient never leaves their inbox.

The body also gets a link, in one of two ways:

  • If your html or text contains the placeholder {{unsubscribe_url}}, it is replaced with the recipient's URL. Put it wherever your design wants it. In HTML, use it as an href: <a href="{{unsubscribe_url}}">Unsubscribe</a>. The placeholder works inside templates too, and does not need to be declared as a template variable.
  • If the placeholder is not present, a short footer is appended: a small grey "Don't want these emails? Unsubscribe." line in HTML, or Don't want these emails? Unsubscribe: <url> in text.
Send a marketing email
curl -X POST https://api.do.dev/v1/send/emails/send \
-H "Authorization: Bearer $SEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "from": { "email": "news@mail.example.com", "name": "Example Store" },
  "to": ["customer@example.net"],
  "subject": "This week at Example Store",
  "html": "<p>New arrivals…</p><p><a href=\"{{unsubscribe_url}}\">Unsubscribe</a></p>",
  "category": "marketing",
  "listId": "weekly-digest"
}'
Response202
{
"id": "msg_3f9c2a1d7b8e4c0f9a1b2",
"status": "queued"
}

category, listId, and unsubscribe are echoed back by Get an email.

The hosted endpoint

The link points at an endpoint send.dev operates. You do not host anything, and it needs no API key: the token in the URL is a signed record of your account, the recipient, and the listId, and cannot be forged or altered.

GET/v1/send/u/{token}

A person clicked the link. Records the unsubscribe and shows a plain confirmation page.

POST/v1/send/u/{token}

One-click unsubscribe from a mail client (RFC 8058). Records the unsubscribe and returns JSON.

Both are also served at https://api.send.dev/v1/u/{token}. Tokens do not expire; a link in a two-year-old email still works.

The POST response is:

{ "unsubscribed": true, "email": "customer@example.net", "listId": "weekly-digest" }

A tampered or truncated token returns 400 validation_error (or a "This link is not valid" page for GET). If the opt-out could not be recorded, the response is 503 service_unavailable and the mail client or person retries.

What happens on unsubscribe

  1. The address is added to your suppression list with reason: "unsubscribe" and the listId from the token, if any. If the address was already suppressed for a bounce or complaint, that record keeps its reason and gains the unsubscribe on top.
  2. A send.email.unsubscribed webhook event is emitted with email, listId, source (one-click or link), and unsubscribedAt. Use it to update your own list.
  3. Subsequent sends to that address are affected as described next.

Unsubscribing is idempotent. A second click does nothing and emits no second event.

Enforcement at send time

You sendRecipient has unsubscribedResult
category: "marketing"YesRejected: 400 recipient_unsubscribed. Nothing is sent.
category: "transactional" or no categoryYesDelivered. Order confirmations and password resets keep working.
Anything, with "block all" enabled on your accountYesRejected: 400 recipient_unsubscribed.

The rejection lists who opted out so you can clean your list:

{
  "error": {
    "code": "recipient_unsubscribed",
    "message": "1 recipient(s) have unsubscribed from your emails. Remove them, or send with category \"transactional\" if this is not marketing mail.",
    "unsubscribedRecipients": [
      { "email": "customer@example.net", "listId": "weekly-digest" }
    ]
  }
}

In a batch, the item is rejected with the same envelope inside results and the rest of the batch proceeds.

An unsubscribe blocks marketing mail to the address regardless of listId: someone who left weekly-digest will not receive product-launches either. The listId is recorded for your reporting, not used as a scope for enforcement. If you run genuinely separate lists that need separate consent, keep that state on your side and only submit recipients who are subscribed to the list you are sending.

Block all

By default an unsubscribe stops only marketing mail. Some teams prefer that an unsubscribe stop everything, including transactional mail, either as a policy or because they never send transactional mail through send.dev. That is an account-level setting ("Unsubscribes block all mail"), which is being added to the dashboard settings page. With it on, any send to an unsubscribed address fails with recipient_unsubscribed, whatever its category. Until the control appears in the dashboard, contact support to have it enabled.

Seeing who has unsubscribed

Unsubscribes are ordinary suppression records, so the suppressions endpoints work on them:

curl "https://api.do.dev/v1/send/suppressions?reason=unsubscribe" \
  -H "Authorization: Bearer $SEND_API_KEY"
{
  "suppressions": [
    {
      "email": "customer@example.net",
      "reason": "unsubscribe",
      "listId": "weekly-digest",
      "status": "suppressed",
      "createdAt": 1757100000000,
      "lastEventAt": 1757100000000
    }
  ],
  "nextCursor": null
}

Releasing an unsubscribe with POST /v1/send/suppressions/{email}/unsuppress is possible but should only be done when the person has clearly asked to be re-subscribed. Sending marketing mail to someone who opted out is the quickest route to spam complaints, which do far more damage to your deliverability than a smaller list.

Checklist for marketing mail

  • Send from a domain (or subdomain) you use only for marketing, so its reputation is separate from your transactional mail. See Domains.
  • Set category: "marketing" and a listId on every message.
  • Send one email per recipient, via batch for volume.
  • Place {{unsubscribe_url}} where people can find it, or accept the default footer.
  • Subscribe a webhook to send.email.unsubscribed and send.email.complained and remove those addresses from your own list.
  • New accounts are on a probation tier with low hourly and daily caps; plan a marketing launch after it lifts.