Consent Docs

Set up email double opt-in

Start a confirmed email opt-in from your server, email the confirmation link yourself, and add an unsubscribe link to your messages.

How-to3 min readFor web, backend and mobile developers

Double opt-in proves that an email address belongs to the person who typed it. The consent is recorded as pending, and takes effect only when the person follows a link sent to that address.

You send the email

PIEDAP Consent does not send email. It gives you a confirmation link, and your own mail system sends the message.

Before you start#

  • A published form and its key.
  • A secret key. A publishable key is refused with 403 EMAIL_CONSENT_SERVER_ONLY.
  • The client from Record consent from your server.
  1. On your page, post the address and the ticked boxes to your own server.

  2. On your server, call startEmailConsent with an answer for every purpose that is not always on.

    Node.jsJavaScript
    const pending = await client.startEmailConsent({
      email: 'rohan.das@example.in',
      formKey: 'newsletter',
      decisions: [{ purposeKey: 'newsletter', granted: true }],
      locale: 'en',
      returnUrl: 'https://www.example.in/subscribed',
      idempotencyKey: `optin-${signupId}`,
    });
    Python
    from piedap_consent import Decision
    
    pending = client.start_email_consent(
        "rohan.das@example.in",
        "newsletter",
        [Decision("newsletter", True)],
        locale="en",
        return_url="https://www.example.in/subscribed",
        idempotency_key=f"optin-{signup_id}",
    )
    cURLShell
    curl -X POST https://console.dpconsent.com/consent/sdk/v1/email-consent \
      -H "Authorization: Bearer $PIEDAP_SECRET_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: optin-7c41" \
      -d '{
        "email": "rohan.das@example.in",
        "formKey": "newsletter",
        "decisions": [{ "purposeKey": "newsletter", "granted": true }],
        "locale": "en",
        "returnUrl": "https://www.example.in/subscribed"
      }'
  3. Email verificationUrl from the response to the address, exactly as returned. In Python the field is pending.verification_url.

  4. Send nothing else while the record's status is PENDING. Treat it as no consent.

  5. Wait for the consent.updated webhook, or read the record with getConsent, before you treat the consent as given.

The call answers 202 with the consentId, the status PENDING, the verificationUrl and expiresOn. The link works for 48 hours. To issue a new one, call startEmailConsent again.

What the person sees#

Opening the link changes nothing. It shows a page with a Confirm button, because mail scanners follow links. Pressing Confirm gives the record the status its answers lead to, usually GRANTED.

The person then lands on your returnUrl with ?consent=confirmed added, or ?consent=failed when the link had expired or was not valid.

Do not trust the query

Anyone can type ?consent=confirmed into the address bar. Use it to choose what the page says, not to decide whether consent was given.

A consent.granted webhook is sent when the pending record is created, with status: "PENDING". Check status before you act on a webhook.

Ask for an unsubscribe link for the consent record a message relies on. The call needs a secret key and no scope.

Node.jsJavaScript
const { url, headers } = await client.unsubscribeLink(consentId);
cURLShell
curl -X POST https://console.dpconsent.com/consent/sdk/v1/pages/unsubscribe-links \
  -H "Authorization: Bearer $PIEDAP_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "consentId": "cns_91b7d0c4a2e64f0f8d3c5a7b9e1f2c60" }'
  • Add both headers, List-Unsubscribe and List-Unsubscribe-Post, to the message as they are. Mail clients then show their own unsubscribe button.
  • Put url in the message body. Opened in a browser, it asks the person to confirm.
  • Either way the record becomes WITHDRAWN and a consent.withdrawn webhook is sent. Stop sending when it arrives.

The link does not expire, and it works for any consent record. The Python package has no helper for this call, so post to the endpoint.

Cases to know#

The address has answered before

If the address already confirmed a consent on this form, the new request changes nothing yet. The earlier consent applies until the new link is followed. If a request is still pending, its answers are replaced and a new link is issued.

A browser gets EMAIL_NEEDS_DOUBLE_OPT_IN

A form of the type Email and marketing opt-in has Require double opt-in turned on by default. While it is on, a publishable key cannot record consent for an EMAIL identifier on that form. Start the opt-in from your server instead. A recordConsent call with a secret key is recorded at once, so use it only for addresses you have already confirmed.

The address was erased

Starting the opt-in again for an erased address creates a new record. The erased one is never reused.

The link is never followed

The record stays PENDING until the form's consent term ends. It then becomes EXPIRED and a consent.expired webhook is sent.

↑↓ to move, Enter to open