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_givenflag 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.
What Does Section 6 Require for Consent API Design?
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.
Consent Must Be Free, Specific, Informed and for a Specified Purpose (Section 6(1))
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: From Clauses to DPDPA Consent Management API Contracts
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 Clause | API Behaviour | Framework Control |
|---|---|---|
| Section 5(1) -- notice content | GET /v1/notices/{purpose_id} returns a versioned, immutable notice bundle | ISO 27001 A.5.34, NIST Privacy CM.AW-P1, SOC 2 P1.1 |
| Section 5(3) -- 22-language option | Notice 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-bound | One consent_grant per (principal_id, purpose_id, notice_version) tuple; no bundled grants | ISO 27001 A.5.12, NIST Privacy CT.PO-P1, SOC 2 P2.1 |
| Section 6(10) -- burden of proof | Each grant emits a signed consent receipt bound to the exact notice hash | ISO 27001 A.5.33, A.8.15, NIST Privacy CT.PO-P2, SOC 2 P3.2 |
| Section 8(7) -- erasure on withdrawal | Withdrawal triggers downstream erasure fan-out with processor acknowledgement | ISO 27001 A.8.10, NIST Privacy CT.DM-P5, SOC 2 P4.2 |
| Rule 6(1)(c) -- logging | Append-only audit table, hash-chained, one row per lifecycle event | ISO 27001 A.8.15, NIST Privacy CT.DM-P8 |
| Rule 6(1)(e), Rule 8(3) -- 1-year minimum log retention | Partitioned retention policy with configurable floor | ISO 27001 A.5.33, A.8.10 |
Database Schema: PostgreSQL for Multilingual Consent
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
-- 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.
- Layer 1 -- CDN edge cache. Keyed on
(purpose_id, notice_version_id, language_code). Because notice versions are immutable once active, cache-control can bepublic, 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, thecontent_hash, and thetranslator_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: Sequence Diagram
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: Proving Consent Under Section 6(10)
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.
The Consent Receipt
When a principal submits a grant, the API computes a canonical string and signs it with an Ed25519 key held in KMS:
# 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:
# 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.
DPDPA Consent Management API Surface: The Five Endpoints
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.
| Method | Path | Purpose | Key DPDPA Clause |
|---|---|---|---|
| GET | /v1/notices/{purpose_id}?lang={code} | Serve versioned notice in one of 23 languages | S. 5(1), 5(3) |
| POST | /v1/consents | Capture a grant, emit signed receipt | S. 6(1), 6(10) |
| GET | /v1/consents/{consent_id}/receipt | Return the signed receipt for audit | S. 6(10) |
| DELETE | /v1/consents/{consent_id} | Record withdrawal, trigger erasure fanout | S. 6(4), 8(7) |
| GET | /v1/audit/{consent_id} | Return the hash-chained event log | Rule 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
- Fetch the active notice version for the requested
notice_version_idand verify it has not been superseded. - Fetch the translation row for the language the client claims to have shown (
language_shown). - Verify the client-reported
content_hash_shownmatches the storednotice_translation.content_hash, this binds Section 5(3) (language) to Section 6(1) (informed consent). - Compute the principal pseudonym as HMAC-SHA256 of
principal_idunder the KMS pseudonym key. - Call
build_consent_receipt(...)and request an Ed25519 signature from KMS. - In a single database transaction, INSERT
consent_recordand twoconsent_audit_eventrows (NOTICE_SHOWN + GRANT). - Return HTTP 201 with the signed receipt.
# 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.
| Framework | Control ID | Control Name | DPDPA Obligation Discharged |
|---|---|---|---|
| ISO 27001:2022 | A.5.34 | Privacy and protection of PII | S. 5(1), 5(3) notice content |
| ISO 27001:2022 | A.5.33 | Protection of records | S. 6(10) evidentiary retention |
| ISO 27001:2022 | A.5.12 | Classification of information | S. 6(1) purpose-bound data |
| ISO 27001:2022 | A.8.15 | Logging | Rule 6(1)(c) event logging |
| ISO 27001:2022 | A.8.10 | Information deletion | S. 8(7) erasure on withdrawal |
| ISO 27001:2022 | A.8.24 | Use of cryptography | Consent receipt signing |
| NIST Privacy | CT.PO-P1 | Consent mechanisms (context-aware) | S. 6(1) free, specific, informed |
| NIST Privacy | CT.PO-P2 | Processing conditions established | S. 6(1) purpose limitation |
| NIST Privacy | CT.PO-P3 | Consent administration and redress | S. 6(4), S. 13 withdrawal & grievance |
| NIST Privacy | CT.DM-P5 | Data destroyed according to policy | S. 8(7) erasure |
| NIST Privacy | CT.DM-P8 | Audit/log records with data minimisation | Rule 6(1)(c),(e) |
| NIST Privacy | CM.AW-P1 | Notice mechanisms | S. 5(1) notice |
| SOC 2 TSC | P1.1 | Privacy notice | S. 5(1), 5(3) |
| SOC 2 TSC | P2.1 | Choice and consent | S. 6(1) |
| SOC 2 TSC | P3.2 | Explicit consent before collection | S. 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_hashembedded 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.
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:
- Consent Manager Registration Under DPDPA: Technical Requirements and Infrastructure Mandates -- if you are building a platform to register as a Board-recognised Consent Manager rather than an in-house Data Fiduciary API.
- DPDPA Encryption Requirements Under Rule 6 -- the cryptographic obligations that apply to the consent database itself.
- Privacy by Design in the CI/CD Pipeline -- how to make the consent API's invariants (hash binding, pseudonymisation, timezone-aware timestamps) enforceable as pre-merge checks.
- DPDPA Threat Modeling with LINDDUN and STRIDE -- modelling adversaries who target the consent log itself.