BIO.RE
Notifications

One-Click Unsubscribe (Gmail / Yahoo 2024)

RFC 8058 List-Unsubscribe-Post handler. Receives the `List-Unsubscribe=One-Click` form-urlencoded POST that Gmail and Yahoo 2024 mail clients send when the recipient clicks the inline unsubscribe button. Mirrors the public GET handler's side effects.

POST /api/v1/notifications/unsubscribe — 🌐 Public · Rate limit: 10 req / hour

Gmail and Yahoo's 2024 bulk-sender requirements mandate RFC 8058 one-click unsubscribe — a List-Unsubscribe-Post: List-Unsubscribe=One-Click body sent to the URL in the List-Unsubscribe email header. This endpoint accepts that POST and runs the same side effects as the public GET unsubscribe link: suppression list insert + per-event email-channel downgrade + GDPR consent record.

Token is the auth. Gmail's POST carries the recipient identity in the URL's query string (?email=&token=), not the body. The body is form-urlencoded List-Unsubscribe=One-Click. The signed HMAC token is verified server-side; an invalid token returns 200 OK with { success: false, message } (NOT 4xx) to avoid email-enumeration via timing.

Mirrors the GET handler exactly. processUnsubscribe() is shared between GET and POST — the only difference is the source parameter (email_one_click_get vs email_one_click_post) recorded on the ConsentRecord row. Side effects are identical: EmailSuppressionList insert (idempotent) + NotificationPreference.email = false per event + ConsentRecord opt-out row.

v2 scoped tokens differ from v1. v2_user_scope tokens carry userId + scope ('transactional' | 'marketing') directly. When scope === 'marketing', only marketing_campaign email preference is flipped off — transactional events keep flowing per RFC 8058 + GDPR Art. 21 (direct marketing only). v1_email_hex tokens always blanket-disable email across every NotificationDefault row.

Request

Query parameters

ParamTypeRequiredNotes
emailstringconditionalRecipient address. For v2_user_scope tokens, server can resolve from userId if omitted.
tokenstringSigned token from the email's List-Unsubscribe header URL. Missing → 200 OK with { success: false, message: 'Missing token.' }.

Headers

HeaderRequiredNotes
Content-Type: application/x-www-form-urlencoded✓ (per RFC 8058)Body is List-Unsubscribe=One-Click

Body

Form-urlencoded List-Unsubscribe=One-Click. The body is not load-bearing — the controller reads email + token from the query string. Body presence is the RFC 8058 signal that the mail client supports one-click.

Response

200 OKApiResponseOf<UnsubscribeResponseDto>

Valid token

{
  "success": true,
  "data": {
    "success": true,
    "message": "You have been unsubscribed from email notifications."
  }
}

Invalid / expired token

{
  "success": true,
  "data": {
    "success": false,
    "message": "Invalid or expired unsubscribe link."
  }
}

Missing token

{
  "success": false,
  "message": "Missing token."
}

Errors

HTTPReason
429Rate limit exceeded (10 req / hour — same budget as the GET handler)

The endpoint deliberately never returns 4xx for missing or invalid params — 2xx with { success: false } is the timing-safe shape used to deter address enumeration.

Side effects

Identical to the GET handler — both call processUnsubscribe() with a different source flag.

  1. Missing token{ success: false, message: 'Missing token.' }. No mutations.
  2. Verify token via UnsubscribeTokenService.verifyToken(token, email). Returns { valid, format, userId?, email?, scope?, expired? }.
  3. Resolve target user
    • v2_user_scope: userId from token. If email is missing in the query, look up User.email by ID.
    • v1_email_hex: email from token. Look up User.id by email.
  4. Suppress emailemailSuppressionList.findUnique({ where: { email } }); if missing, create with reason: 'user_unsubscribe', provider: null (idempotent).
  5. Disable email channel per event — for every row in NotificationDefault:
    • If scope === 'marketing', skip events other than marketing_campaign.
    • Otherwise, notificationPreference.upsert({ update: { email: false }, create: { email: false, push: true, inApp: true, webPush: true, sms: true } }).
  6. GDPR consent log — write ConsentRecord row with documentType: 'notification_preference', accepted: false, preferences.action: 'opt_out', preferences.source: 'email_one_click_post', IP + user-agent captured for Art. 7(1) demonstrability.
  7. Return { success: true, message: 'You have been unsubscribed from email notifications.' }.

Code samples

# Simulate Gmail's RFC 8058 POST. Token + email come from the
# List-Unsubscribe header URL embedded in the original email.
curl -X POST 'https://api.bio.re/api/v1/notifications/[email protected]&token=v2.user-uuid.marketing.1719072000.abc123def456' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'List-Unsubscribe=One-Click'
type UnsubscribeResponse = {
  success?: boolean;
  message: string;
};

async function oneClickUnsubscribe(
  email: string,
  token: string,
): Promise<UnsubscribeResponse> {
  const url = new URL('https://api.bio.re/api/v1/notifications/unsubscribe');
  url.searchParams.set('email', email);
  url.searchParams.set('token', token);
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: 'List-Unsubscribe=One-Click',
  });
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'Unsubscribe failed'), {
      code: json?.error?.code,
    });
  }
  return json.data;
}
List-Unsubscribe: <https://api.bio.re/api/v1/notifications/unsubscribe?email=fan%40example.com&token=v2.user-uuid.marketing.1719072000.abc123def456>, <mailto:[email protected]?subject=unsubscribe&body=token%3D...>
List-Unsubscribe-Post: List-Unsubscribe=One-Click

The mail client picks the https URL and POSTs the form-urlencoded body. The mailto: fallback is for clients that don't support one-click.

Source

SourcePathLines
Controller (POST handler)apps/api-core/src/modules/notification/user-notification.controller.ts318–336 (unsubscribeOneClick)
Controller (shared processUnsubscribe)apps/api-core/src/modules/notification/user-notification.controller.ts338–445
DTO (response)apps/api-core/src/modules/notification/dto/notification-client-response.dto.tsUnsubscribeResponseDto
Token serviceapps/api-core/src/modules/notification/unsubscribe-token.service.tsverifyToken() — accepts both v1_email_hex and v2_user_scope formats
GET handler (paired)apps/api-core/src/modules/notification/user-notification.controller.ts302–316 (unsubscribe) — see Email Unsubscribe (GET)
Prisma modelspackages/prisma/prisma/schema.prismaEmailSuppressionList, NotificationDefault, NotificationPreference, User, ConsentRecord
StandardRFC 8058 § 3.1One-click unsubscribe (Gmail / Yahoo 2024 bulk-sender requirement)
Live responseNOT verified — sourced from controller branches cited above

On this page