Marketing and subscribed mail that Gmail or Yahoo treat as bulk needs two headers together: List-Unsubscribe with an HTTPS URL, and List-Unsubscribe-Post: List-Unsubscribe=One-Click. The URL must accept an unauthenticated POST and suppress the address immediately. ArawaMail does not inject those headers. You attach them on the Send email API headers object, or on a Laravel mailable sent through sharasolns/sdp-email.
The two headers that actually count as one-click
RFC 8058 is the contract. Gmail Help lists both headers for marketing and subscribed bulk. One without the other is not one-click.
List-Unsubscribe:
List-Unsubscribe-Post: List-Unsubscribe=One-Click
A mailto fallback may sit in the same List-Unsubscribe header:
List-Unsubscribe: ,
Gmail and Yahoo render the HTTPS URL. The visible body unsubscribe link is still required on marketing and subscribed mail. A preference-center GET that asks the user to log in or click Confirm is not one-click, even if the same URL appears in the header.
Policy thresholds — the 5,000/day Gmail door, complaint ceilings, and the September timing — live in Gmail and Yahoo bulk-sender requirements (2026). This page owns syntax and framework wiring.
The HTTPS POST contract
The mailbox provider POSTs to your URL with no session cookie and no extra form fields you can rely on. Honor the request immediately. Google and Yahoo document a two-day processing window for the unsubscribe to take effect across their systems; your app should still write the suppression row on the first POST.
- Accept
POSTwithout authentication. - Treat a valid token in the query string (or a signed body) as sufficient identity.
- Idempotent: a second POST for the same token returns 200 and stays suppressed.
- Return 200 quickly. Do not redirect through a login wall.
- Do not require a confirmation page for the machine POST. A human GET of the same URL may show a “you are unsubscribed” page.
ArawaMail outgoing webhooks deliver inbound mailbox mail to your app as JSON. They are not bounce webhooks and they are not unsubscribe-event webhooks. The one-click POST lands on your HTTPS route. After that POST, your app or marketing tool owns the suppression row. ArawaMail is not a list-management ESP.
What must not carry these headers
Transactional receipts, password resets, OTPs, and similar one-shot product mail are generally exempt. Putting List-Unsubscribe on a password reset teaches Gmail that the message is subscribed mail and gives the recipient a one-click way to drop out of a stream that is not a list. Keep the header pair on newsletters and other opted-in marketing. Definitions of the two streams are in What is transactional email?; the domain split that keeps their reputations apart is in separate marketing and transactional subdomains.
Laravel: set headers on the mailable
The documented package is sharasolns/sdp-email. Configure MAIL_MAILER=sdp, SDP_EMAIL_KEY, and optionally SDP_EMAIL_ENDPOINT=https://app.arawamail.com and SDP_EMAIL_TIMEOUT=10. The sender domain must be sending-enabled and allowed by a key with the sending scope. Basic send setup is in Send email in Laravel and the Laravel quickstart.
The package supports custom headers on ordinary Laravel mailables. Use Laravel’s header API. There is no separate ArawaMail header helper.
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Mail\Mailables\Headers;
use Illuminate\Queue\SerializesModels;
class WeeklyDigest extends Mailable
{
use Queueable, SerializesModels;
public function __construct(public string $unsubscribeUrl) {}
public function envelope(): Envelope
{
return new Envelope(
subject: 'This week at Example',
);
}
public function headers(): Headers
{
return new Headers(
text: [
'List-Unsubscribe' => '<' . $this->unsubscribeUrl . '>',
'List-Unsubscribe-Post' => 'List-Unsubscribe=One-Click',
],
);
}
public function content(): Content
{
return new Content(
markdown: 'mail.weekly-digest',
);
}
}
Older mailables can still use $this->withSymfonyMessage() to call $message->getHeaders()->addTextHeader() for the same two names. Either path is fine; the SDP mailer forwards non-reserved headers.
A receipt mailable should omit both headers. If one codebase sends both streams, branch on the message type rather than adding the pair to a shared parent class.
Next.js and the Send email API
The documented HTTP body for POST /emails includes an optional headers object. Reserved names are ignored: from, to, cc, bcc, subject, message-id, reply-to, content-type, mime-version, content-transfer-encoding. List-Unsubscribe and List-Unsubscribe-Post are not reserved, so they pass through. Full field list: Send email API.
const response = await fetch('https://app.arawamail.com/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SDP_EMAIL_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'News ',
to: ['[email protected]'],
subject: 'This week at Example',
html: 'Stories inside.
',
headers: {
'List-Unsubscribe': '',
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
},
}),
});
The published Next.js quickstart for sdp-email-nextjs documents sdp.emails.send({ from, to, subject, html, text, reply_to, attachments }) and does not list a headers field. Until that client documents forwarding, use the HTTP headers object above from a Route Handler, Server Action, or other server-only module. Never put SDP_EMAIL_KEY in a Client Component. Basic Next.js send setup is in Send transactional email from Next.js and the Next.js quickstart.
ArawaMail outgoing webhooks are inbound mailbox events, not an unsubscribe pipeline. If you need your app to react when a human replies, that path is transactional email webhooks. The RFC 8058 POST is a separate route you own.
Minimal POST handler
The provider will POST an empty or near-empty body to the HTTPS URL in the header. Tokenize in the query string so you do not depend on a body parser.
// app/unsubscribe/route.ts (App Router, server-only)
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const token = req.nextUrl.searchParams.get('token');
if (!token) {
return NextResponse.json({ ok: false }, { status: 400 });
}
await suppressMarketingRecipient(token); // your DB write; idempotent
return NextResponse.json({ ok: true }, { status: 200 });
}
export async function GET(req: NextRequest) {
const token = req.nextUrl.searchParams.get('token');
if (token) {
await suppressMarketingRecipient(token);
}
return new NextResponse('You are unsubscribed from marketing mail.', {
status: 200,
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
}
The GET is for the visible body link and for humans who open the header URL. The POST is the one-click contract. Both should suppress without a login.
Failure modes that still show up in 2026
- Only
List-Unsubscribe, missingList-Unsubscribe-Post. - Header URL that only accepts GET.
- Header URL behind a session cookie or SSO wall.
- Double-confirm preference center treated as the one-click target.
- Headers copied onto invoices, OTPs, and password resets.
- Assuming ArawaMail will inject the pair because sending is active.
How we analyzed this
Header names and the marketing-versus-transactional split follow Gmail Help and RFC 8058. The ArawaMail send surface is the documented headers object on POST /emails, plus the Laravel package’s statement that custom headers on mailables are supported. The Next.js npm client is documented without a headers field as of this draft, so the HTTP example is the supported path and the client should be re-checked before anyone treats sdp.emails.send({ headers }) as a product API.
No first-class ArawaMail unsubscribe product, bounce-unsubscribe webhook, or automatic header injection is claimed, because none of those appear in public docs.
FAQ
Which two headers must appear together?
List-Unsubscribe with an HTTPS URL, and List-Unsubscribe-Post: List-Unsubscribe=One-Click.
Must the URL accept POST without a session cookie?
Yes. A login wall or confirm button is not one-click.
How do I set the headers in Laravel through sharasolns/sdp-email?
Return them from the mailable headers() method, or add text headers on the Symfony message. The SDP mailer forwards non-reserved headers.
How do I set them from Next.js?
Pass a headers object on POST /emails from server-side code. Do not treat sdp.emails.send({ headers }) as documented until the npm client lists that field.
Should invoices and OTP messages include List-Unsubscribe?
No. Keep the pair on marketing and subscribed mail only.
Does ArawaMail inject these headers for me?
Not documented. You attach them.
Related reading: Gmail and Yahoo bulk-sender requirements (2026), Send email in Laravel, Send transactional email from Next.js, Transactional email webhooks, What is transactional email?, Separate marketing and transactional subdomains, plus the docs for Send email, Laravel, and Next.js.