Docs

Guide: double opt-in

Collect a pending email preference, send a confirmation link, and only write GRANTED once the person clicks.

Some regulations (ePrivacy, certain GDPR interpretations) require confirmed opt-in for marketing emails: record the intent, send a verification email, and only activate the subscription when the link is clicked. This guide covers the full flow.

Before you start

  • Create a preference-center purpose with category marketing (e.g. key newsletter).
  • Create a sk_ key for your server.
  • Have a transactional email service ready — you send the email; Croombit provides the token.

1. User submits a signup form

On your server, write a pending preference record and request a short-lived confirmation token.

// server — sk_ key
const { token, confirmUrl, expiresAt } = await tc.createPreferenceConfirmationToken({
  subject: 'email:' + user.email,
  values: [{ field: 'newsletter', value: true }],
  channel: 'email',
  redirectUrl: 'https://acme.com/subscription/confirmed',
});
  • Secret key only — a publishable (pk_) key gets 403. Whoever holds the token can confirm, so never mint it in the browser.
  • redirectUrl must be http(s) on one of your site's domains (or a subdomain of one); it is checked again when the link is clicked. Anything else → 422 redirect_not_allowed.
  • Optional to: Croombit emails the link itself. It must be the subject's own email (422 recipient_mismatch otherwise), and at most 3 emails per recipient per hour are sent (429).

2. Send the confirmation email

Build the link yourself and send it via your email provider. The confirmation endpoint is public — no key needed.

const confirmUrl = `https://api.croombit.com/v1/preferences/confirm?token=${token}`;

await mailer.send({
  to: user.email,
  subject: 'Please confirm your subscription',
  html: `<a href="${confirmUrl}">Yes, subscribe me</a>`,
});

3. The user clicks

Croombit writes the requested value(s) to the vault as confirmed, fires a preference.confirmed webhook, then redirects the user to your redirectUrl with ?doi=<status> appended. Nothing else to do on your end.

getPreferences returns { state: "PENDING" } until confirmed. Gate any sends on state === "GRANTED" — never on PENDING.
Tokens expire after 7 days by default (ttlSeconds, 5 minutes to 30 days). If the user never clicks, the preference stays PENDING indefinitely. You can request a fresh token at any time.