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. keynewsletter). - 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 gets403. Whoever holds the token can confirm, so never mint it in the browser. redirectUrlmust behttp(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_mismatchotherwise), 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.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.