DPDPA Consent Management API: Technical Architecture for 22 Eighth Schedule Languages and English

DPDPA consent lifecycle: notice, grant, withdrawal and erasure, with Section 5, 6 and 8 citations and a Rule 6 audit trail

DPDPA Consent Management API: Technical Architecture for 22 Eighth Schedule Languages and English

Key Takeaways

  • A DPDPA consent management API must do three things GDPR tooling does not: serve consent notices in any of the 22 Eighth Schedule languages or English on demand, cryptographically bind the exact notice bytes to each consent record, and discharge the Section 6(10) burden of proof without retaining unnecessary personal data.
  • GDPR consent tooling which is typically a boolean consent_given flag with a policy version hash fails DPDPA compliance on multiple counts: it cannot prove which language the notice was rendered in, which exact bytes the principal saw, or produce the evidence Section 6(10) requires if consent is questioned in a proceeding.
  • Section 5(3) requires access to all 23 language renderings (22 Eighth Schedule + English) on request. Missing even one low-traffic language (Sanskrit, Santali, Bodo) at runtime is a direct statutory breach and not a best-practice gap.
  • Section 6(10) places the burden of proof squarely on the Data Fiduciary: where consent is the basis of processing and a question about it arises in a proceeding, you are obliged to prove that a notice was given and consent was given. Absence of evidence is not neutral, it shifts the case against you.
  • A three-tier cache (CDN edge โ†’ Redis regional โ†’ PostgreSQL) makes 23-language notice serving practical at p99 under 50 ms, because notice versions are immutable and cache keys rotate naturally on version change.
  • The consent audit log must be pseudonymised (HMAC-keyed principal ID) and hash-chained. Without pseudonymisation it becomes a shadow PII store; without a hash chain it is not tamper-evident under audit.
  • Rule 6(1)(e) and Rule 8(3) of the DPDP Rules 2025 (in force from 13 May 2027) set a one-year minimum for logs and processing data, and Rule 8(3) then expects erasure unless another law requires longer. Consent evidentiary retention under Section 6(10) is a separate question, so document the legal basis for any window beyond one year.
Key terms: Data Fiduciary โ€” entity that determines the purpose and means of processing personal data (GDPR equivalent: Data Controller). Data Principal โ€” the individual whose personal data is processed (GDPR equivalent: Data Subject). Consent Manager โ€” a Board-registered intermediary through which Data Principals give, manage, and withdraw consent (DPDPA Section 2(g)). Burden of Proof โ€” under Section 6(10), where consent is the basis of processing and a question about it arises in a proceeding, the Data Fiduciary is obliged to prove that a notice was given and consent was given. Eighth Schedule โ€” the Eighth Schedule to the Constitution of India lists 22 scheduled languages; together with English these form the 23 language combinations the API must support.

In this article:

  • What Does Section 6 Require for Consent API Design?
  • The Engineering Translation: From Clauses to API Contracts
  • Database Schema: PostgreSQL for Multilingual Consent
  • The 22-Language Notice Problem: Caching Strategy
  • The Consent Lifecycle: Sequence Diagram
  • The Cryptographic Audit Trail: Proving Consent Under Section 6(10)
  • DPDPA Consent Management API Surface: The Five Endpoints
  • Framework Control Mapping
  • FAQ
  • Key Takeaways
  • Downloadable Resources

Introduction

If you are designing a DPDPA consent management API for an Indian consumer product, the hardest part is not the REST surface. It is reconciling four requirements that are easy to state and painful to combine.

The notice must be available in every Eighth Schedule language the Data Principal may request. Every consent record must be individually re-constructible months later. The Data Fiduciary must prove consent was actually given, on demand. And none of the above can become an excuse to hoard personal data.

Most consent tooling built for GDPR fails one or more of those tests. A typical European implementation stores a boolean consent_given = true against a user ID, a timestamp, and maybe a policy version hash. That tells you nothing about which of the 22 languages the notice was rendered in, whether the translation was the approved one, what the exact bytes of the notice looked like at capture time, or how to produce that evidence on request. Under Section 6(10) of the DPDPA, the absence of that evidence is not neutral, it actively shifts the case against you.

This article is the engineering translation of Sections 5 and 6 of the DPDPA 2023, together with Rule 6 of the DPDP Rules 2025, into a working architecture. We design the database schema, map the consent lifecycle, solve the 22-language notice problem without blowing up latency, and build a cryptographic audit trail that holds up under audit. Every section reference below has been cross-checked against the gazette text of the Act and the Rules.

Section 6 is the consent clause of the DPDPA โ€” it sets the quality bar, the burden of proof, and the lifecycle obligations that an API has to discharge. Six clauses across Sections 5, 6, 8 and Rule 6 together shape the API contract; each maps to a concrete piece of architecture.

Before any schema design, the obligations need to be stated precisely.

Notice in the Data Principal's Chosen Language (Section 5(1) and 5(3))

Section 5 of the DPDPA 2023 governs the notice the Data Fiduciary must give before or at the time of requesting consent. Sub-section (1) defines the content:

Every request made to a Data Principal under section 6 for consent shall be accompanied or preceded by a notice given by the Data Fiduciary to the Data Principal, informing her,โ€” (i) the personal data and the purpose for which the same is proposed to be processed; (ii) the manner in which she may exercise her rights under sub-section (4) of section 6 and section 13; and (iii) the manner in which the Data Principal may make a complaint to the Board, in such manner and as may be prescribed. -- Section 5(1), DPDPA 2023

Sub-section (3) is the clause that makes Indian consent notices unlike any other jurisdiction's:

The Data Fiduciary shall give the Data Principal the option to access the contents of the notice referred to in sub-sections (1) and (2) in English or any language specified in the Eighth Schedule to the Constitution. -- Section 5(3), DPDPA 2023

The Eighth Schedule to the Constitution of India lists 22 scheduled languages: Assamese, Bengali, Bodo, Dogri, Gujarati, Hindi, Kannada, Kashmiri, Konkani, Maithili, Malayalam, Manipuri, Marathi, Nepali, Odia, Punjabi, Sanskrit, Santali, Sindhi, Tamil, Telugu and Urdu. With English as the baseline, that is 23 renderings of every notice version your API has to be able to serve on demand. "On demand" is the critical phrase -- the Data Principal picks the language, not the Data Fiduciary.

Section 6(1) sets the quality bar for consent itself:

The consent given by the Data Principal shall be free, specific, informed, unconditional and unambiguous with a clear affirmative action, and shall signify an agreement to the processing of her personal data for the specified purpose and be limited to such personal data as is necessary for such specified purpose. -- Section 6(1), DPDPA 2023

Five adjectives -- free, specific, informed, unconditional, unambiguous -- each imposes an engineering constraint. "Specific" and "limited to such personal data as is necessary" together mean one consent token per purpose, not a bundled "I agree to everything." "Informed" means the notice and the consent record have to be bound to each other: you must be able to prove exactly which notice the principal saw when she clicked.

The Burden of Proof Sits With the Fiduciary (Section 6(10))

This is the clause that shapes the audit trail more than any other:

Where a consent given by the Data Principal is the basis of processing of personal data and a question arises in this regard in a proceeding, the Data Fiduciary shall be obliged to prove that a notice was given by her to the Data Principal and consent was given by such Data Principal to the Data Fiduciary in accordance with the provisions of this Act and the rules made thereunder. -- Section 6(10), DPDPA 2023

Three things must be proved: that a notice was given, that consent was given, and that both were in accordance with the Act. The API has to emit artefacts that prove all three without the Fiduciary having to retain any more personal data than the consent transaction itself required.

Erasure When the Purpose Is Complete (Section 8(7))

A Data Fiduciary shall, unless retention is necessary for compliance with any law for the time being in force,โ€” (a) erase personal data, upon the Data Principal withdrawing her consent or as soon as it is reasonable to assume that the specified purpose is no longer being served, whichever is earlier; and (b) cause its Data Processor to erase any personal data that was made available by the Data Fiduciary for processing to such Data Processor. -- Section 8(7), DPDPA 2023

Consent withdrawal is a signal. Purpose completion is an inference. Both have to trigger the erasure pipeline -- which means the API must know, per consent record, which downstream processors received the data and must be fanned out to.

Logging and Monitoring (Rule 6(1)(c))

Rule 6(1) of the DPDP Rules 2025 enumerates the reasonable security safeguards required under Section 8(5) of the Act. Clause (c) addresses observability:

visibility on the accessing of such personal data, through appropriate logs, monitoring and review, for enabling detection of unauthorised access, its investigation and remediation to prevent recurrence; -- Rule 6(1)(c), DPDP Rules 2025

Every consent grant, view, update, withdrawal and erasure touches the personal data behind the consent, so each one is logged: that is how the Fiduciary gets the "visibility on the accessing of such personal data" the clause asks for.

Record Retention (Rule 6(1)(e))

for enabling the detection of unauthorised access, its investigation, remediation to prevent recurrence and continued processing in the event of such a compromise, retain such logs and personal data for a period of one year, unless compliance with any law for the time being in force requires otherwise; -- Rule 6(1)(e), DPDP Rules 2025

One year is the floor on the audit log retention window. Rule 8(3) sets a second one-year minimum: every Data Fiduciary keeps personal data, associated traffic data and other logs of the processing for at least one year from the date of processing, after which they are to be erased unless further retention is required by another law. Both rules apply from 13 May 2027. After that year, split the record: erase the full payloads, and keep only a minimal pseudonymised consent receipt (hashes, timestamps, notice version) as evidence under the burden-of-proof clause. Record the legal basis for keeping that receipt.

Those six clauses are the full legal surface. Everything that follows is the engineering translation.

The engineering translation is a one-to-one mapping from legal clause to API behaviour to framework control. The table below is the design contract: every row in it is something a code reviewer can check, and something an auditor can ask for evidence of.

Legal ClauseAPI BehaviourFramework Control
Section 5(1) -- notice contentGET /v1/notices/{purpose_id} returns a versioned, immutable notice bundleISO 27001 A.5.34, NIST Privacy CM.AW-P1, SOC 2 P1.1
Section 5(3) -- 22-language optionNotice bundle exposes content[lang] for any of 23 languages (English + Eighth Schedule)ISO 27001 A.5.34, NIST Privacy CM.AW-P1
Section 6(1) -- specific, purpose-boundOne consent_grant per (principal_id, purpose_id, notice_version) tuple; no bundled grantsISO 27001 A.5.12, NIST Privacy CT.PO-P1, SOC 2 P2.1
Section 6(10) -- burden of proofEach grant emits a signed consent receipt bound to the exact notice hashISO 27001 A.5.33, A.8.15, NIST Privacy CT.PO-P2, SOC 2 P3.2
Section 8(7) -- erasure on withdrawalWithdrawal triggers downstream erasure fan-out with processor acknowledgementISO 27001 A.8.10, NIST Privacy CT.DM-P5, SOC 2 P4.2
Rule 6(1)(c) -- loggingAppend-only audit table, hash-chained, one row per lifecycle eventISO 27001 A.8.15, NIST Privacy CT.DM-P8
Rule 6(1)(e), Rule 8(3) -- 1-year minimum log retentionPartitioned retention policy with configurable floorISO 27001 A.5.33, A.8.10

The schema is the durable evidence layer of the API. It has three concerns: hold notice content in every supported language as an immutable versioned asset, record each consent grant as a point-in-time event bound to the exact notice bytes the principal saw, and produce a hash-chained audit log that can be verified offline.

Entity Relationship Diagram

erDiagram
    DATA_PRINCIPAL ||--o{ CONSENT_RECORD : "grants / withdraws"
    PURPOSE ||--o{ NOTICE_VERSION : "has versions"
    NOTICE_VERSION ||--o{ NOTICE_TRANSLATION : "has 23 translations"
    NOTICE_VERSION ||--o{ CONSENT_RECORD : "bound to"
    CONSENT_RECORD ||--o{ CONSENT_AUDIT_EVENT : "produces"
    CONSENT_RECORD ||--o{ PROCESSOR_FANOUT : "fans out to"

    DATA_PRINCIPAL {
        uuid principal_id PK
        string pseudonym_hash
        timestamptz created_at
    }
    PURPOSE {
        uuid purpose_id PK
        string name
        string legal_basis
        string data_categories
        timestamptz created_at
    }
    NOTICE_VERSION {
        uuid notice_version_id PK
        uuid purpose_id FK
        int version_number
        bytea canonical_hash
        timestamptz effective_from
        timestamptz superseded_at
    }
    NOTICE_TRANSLATION {
        uuid translation_id PK
        uuid notice_version_id FK
        string language_code
        text rendered_content
        bytea content_hash
        string translator_signature
    }
    CONSENT_RECORD {
        uuid consent_id PK
        uuid principal_id FK
        uuid purpose_id FK
        uuid notice_version_id FK
        string language_shown
        bytea receipt_hash
        string status
        timestamptz granted_at
        timestamptz withdrawn_at
        timestamptz erasure_confirmed_at
    }
    CONSENT_AUDIT_EVENT {
        bigint event_id PK
        uuid consent_id FK
        string event_type
        bytea prev_event_hash
        bytea this_event_hash
        jsonb event_payload
        timestamptz occurred_at
    }
    PROCESSOR_FANOUT {
        uuid fanout_id PK
        uuid consent_id FK
        string processor_id
        string acknowledgement_status
        timestamptz acknowledged_at
    }

Alt Text: Entity relationship diagram for a DPDPA consent management API database schema showing data principals, purposes, versioned notices with 23-language translations, consent records, a hash-chained audit event table and a processor fanout table for erasure propagation.

DDL for the Core Tables

sql
-- One row per approved purpose. Purpose is the fundamental unit of consent
-- under Section 6(1) -- "limited to such personal data as is necessary
-- for such specified purpose."
CREATE TABLE purpose (
    purpose_id       UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name             TEXT NOT NULL,
    legal_basis      TEXT NOT NULL CHECK (legal_basis IN ('consent', 'legitimate_use')),
    data_categories  TEXT[] NOT NULL,
    created_at       TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Notices are versioned. Once a version is active, its content hash is
-- immutable. A policy change creates a new notice_version row.
CREATE TABLE notice_version (
    notice_version_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    purpose_id        UUID NOT NULL REFERENCES purpose(purpose_id),
    version_number    INT NOT NULL,
    canonical_hash    BYTEA NOT NULL,   -- SHA-256 of canonicalised JSON bundle
    effective_from    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    superseded_at     TIMESTAMPTZ,
    UNIQUE (purpose_id, version_number)
);

-- One translation row per supported language per notice version.
-- Expected count: 23 rows per notice_version (English + 22 scheduled).
CREATE TABLE notice_translation (
    translation_id       UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    notice_version_id    UUID NOT NULL REFERENCES notice_version(notice_version_id),
    language_code        TEXT NOT NULL,  -- ISO 639-1 / BCP-47 (en, hi, bn, ta, ...)
    rendered_content     TEXT NOT NULL,
    content_hash         BYTEA NOT NULL, -- SHA-256 of rendered_content bytes
    translator_signature TEXT NOT NULL,  -- Ed25519 sig by approved translator
    UNIQUE (notice_version_id, language_code)
);

-- A consent record is point-in-time evidence. Bound to the exact
-- notice_version_id the principal saw, and the language it was shown in.
CREATE TABLE consent_record (
    consent_id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    principal_id         UUID NOT NULL,
    purpose_id           UUID NOT NULL REFERENCES purpose(purpose_id),
    notice_version_id    UUID NOT NULL REFERENCES notice_version(notice_version_id),
    language_shown       TEXT NOT NULL,
    receipt_hash         BYTEA NOT NULL, -- Signed consent receipt (see ยงยง)
    status               TEXT NOT NULL CHECK (status IN ('granted','withdrawn','erased')),
    granted_at           TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    withdrawn_at         TIMESTAMPTZ,
    erasure_confirmed_at TIMESTAMPTZ
);

-- Append-only hash-chained audit log. Each row's this_event_hash binds
-- prev_event_hash + event_payload + occurred_at. Rule 6(1)(c),(e).
CREATE TABLE consent_audit_event (
    event_id        BIGSERIAL PRIMARY KEY,
    consent_id      UUID NOT NULL REFERENCES consent_record(consent_id),
    event_type      TEXT NOT NULL CHECK (event_type IN
                      ('NOTICE_SHOWN','GRANT','VIEW','WITHDRAW','ERASURE_FANOUT','ERASURE_CONFIRMED')),
    prev_event_hash BYTEA,
    this_event_hash BYTEA NOT NULL,
    event_payload   JSONB NOT NULL,
    occurred_at     TIMESTAMPTZ NOT NULL DEFAULT NOW()
) PARTITION BY RANGE (occurred_at);

-- Monthly partitions. Rule 6(1)(e) and 8(3) floor is 12 months; a longer window
-- (e.g. 24) needs a documented legal basis, since Rule 8(3) then expects erasure.
CREATE TABLE consent_audit_event_2026_04 PARTITION OF consent_audit_event
    FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');

-- Processor fanout for Section 8(7) erasure propagation.
CREATE TABLE processor_fanout (
    fanout_id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    consent_id             UUID NOT NULL REFERENCES consent_record(consent_id),
    processor_id           TEXT NOT NULL,
    acknowledgement_status TEXT NOT NULL DEFAULT 'pending',
    acknowledged_at        TIMESTAMPTZ
);

CREATE INDEX ON consent_record (principal_id, purpose_id);
CREATE INDEX ON consent_record (status) WHERE status = 'withdrawn';
CREATE INDEX ON consent_audit_event (consent_id, occurred_at);

Two design choices are worth flagging. First, notice_version and notice_translation are separated so that a version is defined by its canonical bundle hash -- if a single Tamil glyph changes, you get a new version row, never a silent mutation. Second, consent_record.receipt_hash holds a signed consent receipt, not the personal data -- the API must be able to prove consent was given without the audit log becoming a secondary personal-data store.

The 22-Language Notice Problem: Caching Strategy

The 22-language notice problem is the latency-versus-correctness tension at the heart of a multilingual consent API. Rendering 23 language variants on demand is easy; rendering them at p99 under 50 ms while keeping the bytes bit-exact to what the audit trail expects is harder. A three-layer cache solves it.

Three-tier notice cache from CDN edge to Redis to PostgreSQL with a client-side hash check, and the 23 notice languages: English and the 22 Eighth Schedule languages
Every notice version in 23 languages, served from cache and verified by hash.
  • Layer 1 -- CDN edge cache. Keyed on (purpose_id, notice_version_id, language_code). Because notice versions are immutable once active, cache-control can be public, max-age=31536000, immutable. The only invalidation event is a new notice version, which rotates the cache key naturally.
  • Layer 2 -- Redis regional cache. Same key, TTL 24 h. This absorbs cold-start traffic in regions that have not yet warmed their CDN nodes and handles the long tail of low-traffic scheduled languages (Sanskrit, Santali, Bodo) where the CDN might not keep the object resident.
  • Layer 3 -- PostgreSQL row lookup. Falls through only on cold cache. Returns the raw rendered_content, the content_hash, and the translator_signature.

A cache hit is still subject to a verification step: before the client renders the notice, the API returns both the content and its content_hash, and the frontend re-hashes locally to confirm. The content_hash is also the value that will be embedded in the consent receipt if the principal proceeds to grant, which closes the loop between "notice seen" and "consent given."

The consent lifecycle is the single orchestrated flow that joins notice request, grant, use, withdrawal, and erasure into one verifiable thread. Every arrow in the sequence below produces exactly one consent_audit_event row, so the diagram and the audit table are isomorphic, what you see is what an auditor walks.

sequenceDiagram
    participant DP as Data Principal
    participant API as Consent API
    participant DB as PostgreSQL
    participant KMS as Signing KMS
    participant PR as Downstream Processor

    DP->>API: GET /notices/{purpose_id}?lang=ta
    API->>DB: SELECT notice_translation WHERE lang='ta'
    DB-->>API: content, content_hash, translator_sig
    API-->>DP: Notice bundle (Tamil) + content_hash
    Note over API,DB: audit_event: NOTICE_SHOWN

    DP->>API: POST /consents {purpose_id, notice_version_id, content_hash}
    API->>API: Verify content_hash matches stored version
    API->>KMS: Sign consent receipt (principal, purpose, notice, ts)
    KMS-->>API: Ed25519 signature
    API->>DB: INSERT consent_record, consent_audit_event (GRANT)
    API-->>DP: 201 Created + signed receipt

    Note over API: Purpose-bound processing proceeds
    API->>DB: INSERT consent_audit_event (VIEW) on each data access

    DP->>API: DELETE /consents/{consent_id}
    API->>DB: UPDATE status='withdrawn', INSERT audit_event (WITHDRAW)
    API->>PR: POST /erasure {principal_pseudonym, purpose_id}
    PR-->>API: 202 Accepted
    API->>DB: INSERT processor_fanout, audit_event (ERASURE_FANOUT)
    PR->>API: POST /erasure/ack {fanout_id}
    API->>DB: UPDATE erasure_confirmed_at, audit_event (ERASURE_CONFIRMED)

Alt Text: Sequence diagram of the full DPDPA consent lifecycle from notice request in Tamil through consent grant, signed receipt issuance, ongoing purpose-bound data access, withdrawal, downstream processor erasure fanout and confirmation.

The cryptographic audit trail is the evidentiary spine of the API, the artefact that turns "we got consent" from a claim into a proof. The burden-of-proof clause obliges the Data Fiduciary, if consent is questioned in a proceeding, to prove that a notice was given and that consent was given "in accordance with the provisions of this Act." The audit trail has to produce that proof without turning into a secondary PII store. The pattern is a signed consent receipt plus a hash-chained event log.

When a principal submits a grant, the API computes a canonical string and signs it with an Ed25519 key held in KMS:

python
# Python 3.12+, cryptography library. Signs a consent receipt for a single
# DPDPA consent management API grant event. The receipt proves:
#   (1) a notice was given (notice_version_id + content_hash_shown)
#   (2) consent was given (principal pseudonym + timestamp)
#   (3) the binding between them is cryptographically verifiable
from datetime import datetime, timezone
from hashlib import sha256
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey


def build_consent_receipt(
    principal_pseudonym: str,    # HMAC of principal_id, not the id itself
    purpose_id: str,
    notice_version_id: str,
    content_hash_shown: bytes,    # hash the principal's client actually rendered
    language_shown: str,
    signing_key: Ed25519PrivateKey,
) -> dict:
    granted_at = datetime.now(timezone.utc).isoformat()
    canonical = "|".join([
        "DPDPA-CR-v1",
        principal_pseudonym,
        purpose_id,
        notice_version_id,
        content_hash_shown.hex(),
        language_shown,
        granted_at,
    ]).encode("utf-8")

    receipt_hash = sha256(canonical).digest()
    signature = signing_key.sign(receipt_hash)

    return {
        "version": "DPDPA-CR-v1",
        "principal_pseudonym": principal_pseudonym,
        "purpose_id": purpose_id,
        "notice_version_id": notice_version_id,
        "content_hash": content_hash_shown.hex(),
        "language_shown": language_shown,
        "granted_at": granted_at,
        "receipt_hash": receipt_hash.hex(),
        "signature": signature.hex(),
    }

Three design details matter here. The principal_pseudonym is an HMAC-SHA256 of the principal's internal ID under a KMS-held key, not the ID itself -- the audit log should not be a directory of your user base. The content_hash_shown is the hash the client actually rendered, which is verified server-side against the stored notice_translation.content_hash before the signature is issued -- if they disagree, the grant is rejected. And the Ed25519 signature is produced by a KMS key whose public half is published on a stable URL, so an auditor can verify receipts offline without hitting your API.

The Hash-Chained Event Log

Each row of consent_audit_event carries prev_event_hash and this_event_hash. On insert:

python
# Computes the hash chain entry for a single consent lifecycle event.
# Each event in the DPDPA consent management API audit log is bound to the
# previous one, so any tampering with historical rows breaks the chain and
# is detectable by the auditor walking it forwards.
import json
from hashlib import sha256


def compute_event_hash(
    prev_event_hash: bytes,
    consent_id: str,
    event_type: str,
    event_payload: dict,
    occurred_at_iso: str,
) -> bytes:
    # Canonicalise with sorted keys so that replay-hashing is deterministic.
    payload_bytes = json.dumps(event_payload, sort_keys=True,
                               separators=(",", ":")).encode("utf-8")
    header = b"|".join([
        prev_event_hash,
        consent_id.encode("utf-8"),
        event_type.encode("utf-8"),
        occurred_at_iso.encode("utf-8"),
    ])
    return sha256(header + b"|" + payload_bytes).digest()

Every hour, the most recent this_event_hash per partition is notarised to an external anchor -- an append-only ledger, a transparency log, or a timestamping authority. That anchoring is what turns "hash chain" into "tamper-evidence the auditor can verify offline." Without it, a privileged operator with database access could in principle rewrite history.

The API surface is intentionally small: five routes that together discharge every consent obligation in the Act. Everything else, analytics, dashboards, processor integrations, is composition over these primitives.

MethodPathPurposeKey DPDPA Clause
GET/v1/notices/{purpose_id}?lang={code}Serve versioned notice in one of 23 languagesS. 5(1), 5(3)
POST/v1/consentsCapture a grant, emit signed receiptS. 6(1), 6(10)
GET/v1/consents/{consent_id}/receiptReturn the signed receipt for auditS. 6(10)
DELETE/v1/consents/{consent_id}Record withdrawal, trigger erasure fanoutS. 6(4), 8(7)
GET/v1/audit/{consent_id}Return the hash-chained event logRule 6(1)(c),(e)

POST /v1/consents is the one endpoint worth sketching in code, because the entire burden-of-proof story lives inside it.

How to Implement the Grant Endpoint

  1. Fetch the active notice version for the requested notice_version_id and verify it has not been superseded.
  2. Fetch the translation row for the language the client claims to have shown (language_shown).
  3. Verify the client-reported content_hash_shown matches the stored notice_translation.content_hash, this binds Section 5(3) (language) to Section 6(1) (informed consent).
  4. Compute the principal pseudonym as HMAC-SHA256 of principal_id under the KMS pseudonym key.
  5. Call build_consent_receipt(...) and request an Ed25519 signature from KMS.
  6. In a single database transaction, INSERT consent_record and two consent_audit_event rows (NOTICE_SHOWN + GRANT).
  7. Return HTTP 201 with the signed receipt.
python
# FastAPI handler for a DPDPA consent management API grant endpoint.
# This is the single most important code path in the system: it binds
# notice content, principal action, and signed evidence into one atomic
# transaction. Any failure mode here is a burden-of-proof failure mode.
from datetime import datetime, timezone
from fastapi import APIRouter, HTTPException

router = APIRouter()

@router.post("/v1/consents", status_code=201)
async def grant_consent(body: ConsentGrantRequest) -> ConsentGrantResponse:
    # 1. Verify the notice version is currently active.
    notice = await db.fetch_notice_version(body.notice_version_id)
    if notice is None or notice.superseded_at is not None:
        raise HTTPException(409, "notice_version_superseded")

    # 2. Verify the client-reported content hash matches stored bytes for
    #    the language it claims to have shown. This is the binding between
    #    S. 5(3) (language) and S. 6(1) (informed consent).
    translation = await db.fetch_notice_translation(
        body.notice_version_id, body.language_shown
    )
    if translation is None:
        raise HTTPException(400, "unsupported_language")
    if translation.content_hash != body.content_hash_shown:
        raise HTTPException(400, "content_hash_mismatch")

    # 3. Build and sign the receipt.
    pseudonym = hmac_sha256(body.principal_id, key=PSEUDONYM_KEY).hex()
    receipt = build_consent_receipt(
        principal_pseudonym=pseudonym,
        purpose_id=str(notice.purpose_id),
        notice_version_id=str(notice.notice_version_id),
        content_hash_shown=translation.content_hash,
        language_shown=body.language_shown,
        signing_key=SIGNING_KEY,
    )

    # 4. Insert consent_record and NOTICE_SHOWN + GRANT audit events
    #    in a single transaction.
    async with db.transaction():
        consent_id = await db.insert_consent_record(
            principal_id=body.principal_id,
            purpose_id=notice.purpose_id,
            notice_version_id=notice.notice_version_id,
            language_shown=body.language_shown,
            receipt_hash=bytes.fromhex(receipt["receipt_hash"]),
            granted_at=datetime.now(timezone.utc),
        )
        await db.append_audit_event(consent_id, "NOTICE_SHOWN",
                                    {"language": body.language_shown,
                                     "content_hash": receipt["content_hash"]})
        await db.append_audit_event(consent_id, "GRANT",
                                    {"receipt": receipt})

    return ConsentGrantResponse(consent_id=consent_id, receipt=receipt)

Note the explicit datetime.now(timezone.utc) rather than datetime.utcnow() -- the latter is naive and deprecated, and a naive timestamp on a consent receipt is a defect that an auditor will flag.

Framework Control Mapping

The framework control mapping is the bridge between DPDPA obligations and the certifications your auditor already understands. Every consent obligation in this design aligns to a specific ISO 27001:2022 Annex A control, a NIST Privacy Framework subcategory, and a SOC 2 Trust Services Criterion, so an existing certification programme can absorb DPDPA without parallel evidence.

FrameworkControl IDControl NameDPDPA Obligation Discharged
ISO 27001:2022A.5.34Privacy and protection of PIIS. 5(1), 5(3) notice content
ISO 27001:2022A.5.33Protection of recordsS. 6(10) evidentiary retention
ISO 27001:2022A.5.12Classification of informationS. 6(1) purpose-bound data
ISO 27001:2022A.8.15LoggingRule 6(1)(c) event logging
ISO 27001:2022A.8.10Information deletionS. 8(7) erasure on withdrawal
ISO 27001:2022A.8.24Use of cryptographyConsent receipt signing
NIST PrivacyCT.PO-P1Consent mechanisms (context-aware)S. 6(1) free, specific, informed
NIST PrivacyCT.PO-P2Processing conditions establishedS. 6(1) purpose limitation
NIST PrivacyCT.PO-P3Consent administration and redressS. 6(4), S. 13 withdrawal & grievance
NIST PrivacyCT.DM-P5Data destroyed according to policyS. 8(7) erasure
NIST PrivacyCT.DM-P8Audit/log records with data minimisationRule 6(1)(c),(e)
NIST PrivacyCM.AW-P1Notice mechanismsS. 5(1) notice
SOC 2 TSCP1.1Privacy noticeS. 5(1), 5(3)
SOC 2 TSCP2.1Choice and consentS. 6(1)
SOC 2 TSCP3.2Explicit consent before collectionS. 6(1), 6(10)

FAQ

Myth: A GDPR-compliant consent tool can be adapted for DPDPA with minor modifications.

No. A typical GDPR implementation stores a boolean consent_given flag, a timestamp, and a policy version hash. That architecture fails DPDPA on three separate counts: it cannot prove which language the notice was rendered in, it does not cryptographically bind the exact notice bytes to the consent record, and it cannot produce the evidentiary package Section 6(10) requires if consent is questioned in a proceeding. GDPR does not have an equivalent to Section 5(3)'s 22-language obligation or Section 6(10)'s burden-of-proof mechanism. You cannot retrofit a GDPR consent tool for DPDPA without a ground-up redesign of the receipt and audit-trail layers.

What are the 22 Eighth Schedule languages?

The Eighth Schedule to the Constitution of India lists: Assamese (as), Bengali (bn), Bodo (brx), Dogri (doi), Gujarati (gu), Hindi (hi), Kannada (kn), Kashmiri (ks), Konkani (kok), Maithili (mai), Malayalam (ml), Manipuri (mni), Marathi (mr), Nepali (ne), Odia (or), Punjabi (pa), Sanskrit (sa), Santali (sat), Sindhi (sd), Tamil (ta), Telugu (te), and Urdu (ur). Together with English (en), these are the 23 language combinations the API must support. Note that Bhojpuri is NOT in the Eighth Schedule despite being widely spoken; do not include it in the statutory 22.

What happens if a Data Fiduciary cannot prove consent was given?

Under Section 6(10), where consent is the basis of processing and a question about it arises in a proceeding, the Data Fiduciary is obliged to prove that a notice was given and consent was given. If it cannot, the absence of evidence is not neutral. It effectively shifts the case against the Fiduciary. The Board can take this into account when determining whether a breach of the Act occurred. This is why the cryptographic audit trail โ€” signed consent receipt bound to the exact notice hash, plus a tamper-evident hash-chained event log โ€” is a statutory requirement, not an engineering nicety.

How long must consent records be retained?

Two separate clocks apply. Rule 6(1)(e) sets a one-year floor for security logs โ€” the logs produced by Rule 6(1)(c) monitoring โ€” and Rule 8(3) sets the same one-year minimum for personal data, associated traffic data and other logs of the processing, after which they are to be erased unless another law requires further retention (both from 13 May 2027). Section 6(10) adds an evidentiary need: if consent is questioned in a proceeding, the Data Fiduciary must prove notice and consent. Rule 8(3) and Section 8(7) both point to erasure, so don't keep full records indefinitely. Split them instead: keep full consent and audit payloads for the one-year minimum, then erase them, and keep only a minimal pseudonymised receipt (hashes, timestamps, notice version) for as long as the consent is live plus the period you need to defend it. Document the legal basis for that receipt; how long to keep it is a question for counsel.

How do I test that the hash chain is intact in production?

Write a verification job that walks the consent_audit_event table for a given consent_id in event_id ascending order, recomputing this_event_hash from prev_event_hash + consent_id + event_type + occurred_at + event_payload at each step and comparing against the stored value. A mismatch at row N means either row N or some predecessor was tampered with. Run this job on a sample of chains nightly and alert on any failure. The external notarisation anchor (transparency log or timestamping authority) provides the second verification layer: the anchor's record of the most recent hash per partition cannot be retroactively altered, so an auditor can verify the chain head independently of your database.

Can I use a single consent grant for multiple purposes?

No. Section 6(1) requires consent to be "specific" and "limited to such personal data as is necessary for such specified purpose." Bundled consent โ€” a single grant covering analytics, marketing, and account management โ€” is non-compliant. Each purpose must have its own consent_grant row, its own signed receipt, and its own withdrawal pathway. This is also why the schema models (principal_id, purpose_id, notice_version_id) as the natural key of a consent record.

Key Takeaways

  • A DPDPA consent management API is defined by three forces: the 22 Eighth Schedule languages and English (23 total) of Section 5(3), the burden of proof in Section 6(10), and the erasure obligation in Section 8(7). Any architecture that satisfies all three automatically satisfies the rest.
  • The schema's irreducible core is purpose -> notice_version -> notice_translation -> consent_record -> consent_audit_event. Remove any table and you lose an evidentiary anchor.
  • Notices must be versioned and immutable. A signed content_hash embedded in the consent receipt is what binds "what the principal saw" to "what the principal agreed to."
  • Caching the 23-language notices at CDN and Redis layers is safe precisely because the objects are immutable -- invalidation happens by version rotation, not mutation.
  • The audit log has to be pseudonymised and hash-chained. Without a hash chain, it is not tamper-evident; without pseudonymisation, it is a shadow PII store that itself becomes a Section 8(5) liability.
  • Always use datetime.now(timezone.utc) for receipt timestamps. Naive timestamps are not auditable.
  • Rule 6(1)(e) and Rule 8(3) set a one-year floor for logs. After one year, erase full payloads and keep only a minimal pseudonymised consent receipt as Section 6(10) evidence, with its legal basis documented.

PostgreSQL Schema

OpenAPI Specification

Python Cryptographic Library

Implementation Checklist

Quick Reference Card

README

Next Steps

Designing the schema is the easy half. Operating this consent API in production means wiring the translator signing workflow, running the KMS signing key rotation, monitoring the erasure fanout for stuck processors, and producing the Section 6(10) evidence pack on demand. If your team is building a consent platform for the Indian market or retrofitting consent capture for DPDPA compliance, StateCheck's privacy engineering team can accelerate the design review and audit-readiness phase.

Related reading from the StateCheck DPDPA documentation hub: