Consent Docs

Bring in consent given earlier

Record consent collected before you used the product, from your server, with its date, basis and notice, without replacing newer answers.

How-to3 min readFor web, backend and mobile developers

Send each earlier consent from your server with collectedAt, the time it was given. A write that would replace a newer answer, a withdrawal or an erasure is refused and changes nothing. For a file, the console's Import consent from a file does the same.

Send each row#

  1. Use a secret key. Only a secret key may send collectedAt and the fields below.

  2. Send the row with collectedAt and an idempotency key taken from the source row.

    Node.jsJavaScript
    import { isSkippedImport } from '@piedap/consent/server';
    
    try {
      await client.recordConsent({
        formKey: 'kaveri-account-opening',
        subjectId: 'CIF00012345',
        subjectType: 'USER_ID',
        identifiers: [{ subjectType: 'PHONE', subjectId: '+919876543210' }],
        decisions: [
          { purposeKey: 'offers-by-sms', granted: true },
          { purposeKey: 'offers-by-email', granted: false },
        ],
        countryCode: 'IN',
        collectedAt: '2019-03-14T04:30:00.000Z',
        basis: 'PRE_COMMENCEMENT',
        noticeReference: 'AOF-2019 rev 3',
        lastActivityAt: '2026-07-02T11:20:00.000Z',
        idempotencyKey: 'aof-2019-row-4812',
      });
    } catch (error) {
      if (!isSkippedImport(error)) throw error;
      logSkip('row 4812', error.code);
    }
    Python
    from piedap_consent import (
        ConsentError,
        Decision,
        Identifier,
        is_skipped_import,
    )
    
    try:
        client.record_consent(
            form_key="kaveri-account-opening",
            subject_id="CIF00012345",
            subject_type="USER_ID",
            identifiers=[Identifier("PHONE", "+919876543210")],
            decisions=[
                Decision("offers-by-sms", True),
                Decision("offers-by-email", False),
            ],
            country_code="IN",
            collected_at="2019-03-14T04:30:00Z",
            basis="PRE_COMMENCEMENT",
            notice_reference="AOF-2019 rev 3",
            last_activity_at="2026-07-02T11:20:00Z",
            idempotency_key="aof-2019-row-4812",
        )
    except ConsentError as error:
        if not is_skipped_import(error):
            raise
        log_skip("row 4812", error.code)
    cURLShell
    curl -X POST https://console.dpconsent.com/consent/sdk/v1/consent \
      -H "Authorization: Bearer $PIEDAP_SECRET_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: aof-2019-row-4812" \
      -d '{
        "formKey": "kaveri-account-opening",
        "subjectId": "CIF00012345",
        "subjectType": "USER_ID",
        "identifiers": [
          { "subjectType": "PHONE", "subjectId": "+919876543210" }
        ],
        "decisions": [
          { "purposeKey": "offers-by-sms", "granted": true },
          { "purposeKey": "offers-by-email", "granted": false }
        ],
        "countryCode": "IN",
        "collectedAt": "2019-03-14T04:30:00.000Z",
        "basis": "PRE_COMMENCEMENT",
        "noticeReference": "AOF-2019 rev 3",
        "lastActivityAt": "2026-07-02T11:20:00.000Z"
      }'
  3. Log each skipped row with its code, and move on to the next row.

Stay under the key's budget: under ten writes a second for a test key. Use a key without consent:check. On a key that can check, each write sent with collectedAt also counts each identifier it names (subjectId and identifiers) against Checks per minute, and answers 429 CHECK_BUDGET_EXCEEDED over it.

A row that sends dataPrincipalType for a child or a person with a lawful guardian needs guardian, even when the record already holds the person.

Field What it holds
basis PRE_COMMENCEMENT: your statement that the person consented before the DPDP Act commenced.
noticeReference The name or number of the notice the person saw, up to 120 letters, digits, spaces and . _ : / # ( ) -. Never personal data.
noticeSha256 The SHA-256 of that notice, 64 hex characters.

All three are sealed into the record and shown on it, its receipt and every consent webhook. A later live answer or a change through PATCH clears them. A withdrawal or an erasure keeps them.

Your statement, for your counsel

The product records the basis you state and does not check it. The label and its use are for your counsel to review.

Handle rows that are skipped#

A skipped row answers 409, writes nothing and sends no webhook. None is worth retrying.

Code The records already hold
CONSENT_WITHDRAWN_ON_RECORD A withdrawal on this form, whatever its date
CONSENT_ALREADY_ON_RECORD An answer with this exact collectedAt, so the row was imported before
CONSENT_NEWER_ON_RECORD A newer answer on this form
CONSENT_ERASED_ON_RECORD An erasure of the person at or after collectedAt
CONSENT_HELD_UNDER_OTHER_IDENTIFIER A withdrawal, the same answer or a newer one under another identifier of the person

The last code looks at every identifier the row names in identifiers, and every identifier linked to one of them. Nothing else: a person known only under an identifier the row neither names nor links is out of reach. errorData names the record and its current state. The Node.js helper isSkippedImport and the Python is_skipped_import are true for these five codes only.

A paper or branch consent given again after a withdrawal is a new answer, not an earlier one. Send it without collectedAt. Keep the date on the paper in capture.methodReference or metadata. See Record consent taken in a branch, by phone or on paper.

Dates and phone numbers#

  • Each decision's date is collectedAt. The record expires after the form's term, counted from the day of the call, unless you send expiresAt.
  • expiresAt must be after the call and within 3,650 days of it.
  • lastActivityAt is when the person last used your service, for inactivity erasure. It must fall between collectedAt and now.
  • collectedAt must not be in the future or before 1990.
  • Phone numbers must be in E.164, such as +919876543210, or the row answers 400 PHONE_NOT_E164.

The console import sends no webhook for each record, only one consent.imported. The file's SHA-256 on an import run is what the browser reported. It does not prove which rows were written.

↑↓ to move, Enter to open