# DPDPA Consent Management API Resources

**Production-ready schema, API spec, and cryptographic library for DPDPA-compliant multilingual consent management.**

This resource package accompanies the article **[Building a Multilingual DPDPA Consent Management API: Technical Architecture](./article.md)** and provides everything an engineering team needs to build, deploy, and audit a consent management system compliant with India's Digital Personal Data Protection Act 2023.

---

## What's Included

| Resource | Format | Purpose |
|----------|--------|---------|
| **[PostgreSQL Schema](./schema/consent-schema.sql)** | SQL (DDL) | Full production schema: 6 tables, 10 indexes, immutability trigger, hash-chain function, monthly partitioned audit table, current-status view |
| **[OpenAPI Specification](./api-spec/consent-management-api.yaml)** | OpenAPI 3.1 YAML | All 5 consent management endpoints with full request/response schemas, error codes, and DPDPA section annotations |
| **[Python Cryptographic Library](./src/consent_receipt.py)** | Python 3.12+ | `ConsentReceiptBuilder`, `HashChainAuditLog`, and `PseudonymService` classes |
| **[Implementation Checklist](./implementation-checklist.md)** | Markdown | 6-week phased rollout from infrastructure prerequisites to production hardening |
| **[Quick Reference Card](./quick-reference-card.md)** | Markdown (printable) | All 23 languages with BCP-47 codes, API surface, audit event types, hash chain algorithm, severity guide |
| **[README](./README.md)** | Markdown | This file — role-based quick starts, legal anchors, and common pitfalls |

---

## Legal Anchors

| Section / Rule | Obligation | Which Resource Implements It |
|----------------|-----------|------------------------------|
| **Section 5(1)** | Notice must inform Data Principal of purpose, rights, and complaint mechanism | `notice_version` + `notice_translation` tables; `GET /v1/notices/{purpose_id}` endpoint |
| **Section 5(3)** | Notice must be available in English or any of the 22 Eighth Schedule languages | `notice_translation` table (23 rows per version); `?lang=` query param; `EIGHTH_SCHEDULE_LANGUAGES` constant in Python library |
| **Section 6(1)** | Consent must be free, specific, informed, unconditional, unambiguous; one consent per purpose | `purpose` table; `duplicate_active_consent` error code; `ConsentReceiptBuilder.build()` |
| **Section 6(10)** | Data Fiduciary bears burden of proof; if consent is questioned in a proceeding, it must prove notice was given and consent was given | Signed consent receipt (`ConsentReceipt` schema); `receipt_hash` in `consent_record`; `HashChainAuditLog`; `ConsentReceiptBuilder.verify()` |
| **Section 8(7)** | Erase personal data on withdrawal; cause processors to erase | `processor_fanout` table; `DELETE /v1/consents/{consent_id}`; `ERASURE_FANOUT` + `ERASURE_CONFIRMED` audit events |
| **Rule 6(1)(c)** | Visibility on access to personal data through logs, monitoring and review | `consent_audit_event` (partitioned, hash-chained); `append_audit_event()` SQL function; `HashChainAuditLog.compute_event_hash()` |
| **Rule 6(1)(e), Rule 8(3)** | Retain logs (and, under Rule 8(3), processing data and traffic data) for minimum 1 year; then erase unless another law requires longer. Both apply from 13 May 2027 | Monthly partitions with documented drop schedule; retention guidance in implementation checklist Phase 5 |

**Key principle:** A consent record is not a boolean. It is a chain of evidence: notice version → translation hash → signed receipt → hash-chained audit log. Remove any link and the Section 6(10) burden of proof breaks.

---

## Resource Details

### 1. PostgreSQL Schema (`schema/consent-schema.sql`)

**What it does:** Defines the complete database layer for a production DPDPA consent management system.

**6 tables:**
- `purpose` — processing purposes (one consent per purpose; Section 6(1))
- `notice_version` — immutable versioned notice bundles (Section 5(1))
- `notice_translation` — per-language renderings, 23 rows per version (Section 5(3))
- `consent_record` — point-in-time grant evidence, append-only (Section 6(10))
- `consent_audit_event` — hash-chained lifecycle log, partitioned monthly (Rule 6(1)(c))
- `processor_fanout` — erasure propagation tracker (Section 8(7))

**Key functions and triggers:**
- `prevent_consent_mutation()` trigger — raises EXCEPTION for any status transition other than `granted→withdrawn` or `(granted|withdrawn)→erased`. Enforces consent_record immutability at the database layer.
- `append_audit_event(consent_id, event_type, payload)` — inserts a hash-chained audit event. Computes `sha256(prev_hash || consent_id || event_type || occurred_at || payload_canonical)` automatically. Application code never touches hash values directly.

**How to deploy:**
```bash
psql -d your_database -f schema/consent-schema.sql
```

**Add monthly partitions** (run before each month boundary):
```sql
CREATE TABLE consent_audit_event_2026_07 PARTITION OF consent_audit_event
    FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
```

### 2. OpenAPI Specification (`api-spec/consent-management-api.yaml`)

**What it does:** Defines the full REST surface for the consent management API in OpenAPI 3.1 format.

**5 endpoints:**

| Method | Path | Core Function |
|--------|------|--------------|
| `GET` | `/v1/notices/{purpose_id}` | Serve versioned, immutable notice in any of 23 languages |
| `POST` | `/v1/consents` | Capture grant; emit signed receipt |
| `GET` | `/v1/consents/{consent_id}/receipt` | Return signed receipt for audit |
| `DELETE` | `/v1/consents/{consent_id}` | Record withdrawal; dispatch erasure fanout |
| `GET` | `/v1/audit/{consent_id}` | Return paginated hash-chained event log |

**How to use:**
- Import into Stoplight, Swagger UI, Postman, or Insomnia for interactive exploration
- Generate server stubs: `openapi-generator-cli generate -i api-spec/consent-management-api.yaml -g python-fastapi`
- Generate client SDKs: replace `-g python-fastapi` with `-g typescript-fetch` or `-g java`

All `description` fields include DPDPA section citations. Error responses use machine-readable `error_code` enums for programmatic handling.

### 3. Python Cryptographic Library (`src/consent_receipt.py`)

**What it does:** Provides the three cryptographic classes required by the consent management API.

**ConsentReceiptBuilder:**
- `build(principal_id, purpose_id, notice_version_id, content_hash_shown, language_shown, pseudonym_key)` — produces a signed consent receipt using Ed25519. Always uses `datetime.now(timezone.utc)`.
- `verify(receipt, public_key_pem)` — offline verifier. No database access required.

**HashChainAuditLog:**
- `compute_event_hash(prev_hash, consent_id, event_type, payload, occurred_at)` — matches the `append_audit_event()` SQL function algorithm exactly. Use for Python-side chain construction or replay verification.
- `verify_chain(events)` — walks a list of event dicts (as returned by `GET /v1/audit/{consent_id}`) and returns `(is_valid, list_of_broken_links)`.

**PseudonymService:**
- `pseudonymise(principal_id)` — HMAC-SHA256 under a KMS-held key. Annual key rotation breaks linkability to current users while old receipts remain verifiable.
- `pseudonymise_batch(principal_ids)` — batch version for DSR responses.

**Dependencies:** `cryptography>=42.0` (for Ed25519). All other dependencies are Python stdlib.

**How to install:**
```bash
pip install cryptography>=42.0
```

### 4. Implementation Checklist (`implementation-checklist.md`)

**What it does:** A 6-week phased rollout plan with checkboxes, commands, and gate criteria for each phase.

**Phases:**
- Phase 0 (Week 1) — Prerequisites: PostgreSQL 15+, Redis, KMS, translator workflow
- Phase 1 (Week 2) — Schema deployment: DDL, trigger verification, reference data
- Phase 2 (Week 3) — Notice API: GET /notices, three-tier cache, all 23 languages
- Phase 3 (Week 4) — Consent grant: POST /consents, receipt signing, atomic transaction
- Phase 4 (Week 5) — Withdrawal and erasure fanout: DELETE /consents, processor acks
- Phase 5 (Week 6) — Hardening: chain verifier, KMS rotation, retention schedule, monitoring

**SDF-specific steps** are marked `[SDF]` throughout.

### 5. Quick Reference Card (`quick-reference-card.md`)

**What it does:** A printable desk reference covering all 23 languages, the API surface, audit event types, hash chain algorithm, and a severity guide.

**Sections:**
- Complete language table with ISO 639-1/3 codes and BCP-47 codes for all 23 languages
- API endpoint quick-lookup with legal anchors and error codes
- Schema entity table (6 tables, keys, purposes, DPDPA anchors)
- Audit event type table with required payload fields
- Consent receipt field table explaining what each field proves under Section 6(10)
- Hash chain verification algorithm (4 steps)
- Common error codes with resolution actions
- Severity guide: what breaks Section 6(10) vs Section 5(3) vs Rule 6(1)(c)

---

## Quick Start by Role

### Backend Engineer

**Goal:** Implement the consent grant endpoint with a working receipt and audit trail.

1. Run `schema/consent-schema.sql` against your PostgreSQL 15+ database
2. Seed `purpose` and `notice_version` rows; add at least one `notice_translation` per version
3. Import `api-spec/consent-management-api.yaml` into your IDE for endpoint schemas
4. Install `cryptography>=42.0` and copy `src/consent_receipt.py` to your service
5. Wire `ConsentReceiptBuilder.build()` into your `POST /v1/consents` handler
6. Use `append_audit_event()` SQL function for all audit writes — never compute hashes in application code
7. Test: `ConsentReceiptBuilder.verify(receipt, pub_key_pem)` must return `True` for every receipt issued

**First thing to verify:** `content_hash_shown` in the grant request matches `notice_translation.content_hash` for `(notice_version_id, language_shown)`. This is the binding between Section 5 notice and Section 6 consent.

### DPO / Compliance

**Goal:** Understand what the system produces as Section 6(10) evidence and validate it against the Act.

1. Read the **Legal Anchors** table above — confirm all 7 obligations are addressed
2. Review `quick-reference-card.md` — the Consent Receipt Fields table maps each field to its DPDPA obligation
3. Test the evidence pack procedure:
   - Request a consent receipt via `GET /v1/consents/{consent_id}/receipt`
   - Verify it offline using `ConsentReceiptBuilder.verify()` — this is what an auditor would do
   - Fetch the audit log via `GET /v1/audit/{consent_id}` and run `HashChainAuditLog.verify_chain()`
4. Review Phase 5 of `implementation-checklist.md` for the KMS key rotation and retention schedule
5. **[SDF]** Confirm the evidence pack generation runbook (Phase 5.5 of the checklist) is tested and documented

**Key check:** Does the system serve all 23 languages? Request `?lang=sat` (Santali) and `?lang=sa` (Sanskrit). Missing these is a Section 5(3) breach regardless of empirical traffic.

### CISO

**Goal:** Establish consent management as a defensible Section 6(10) and Rule 6(1) control.

1. Mandate `consent-schema.sql` deployment with `prevent_consent_mutation()` trigger active for all new consent systems
2. Require `append_audit_event()` SQL function for all audit writes — centralises hash chain control, removes application-layer hash computation risk
3. Confirm KMS key management:
   - Signing key ARN documented and IAM policy reviewed (only consent API service account)
   - Public key published at `/.well-known/consent-signing-key.pub`
   - Annual rotation schedule in the key management register
4. Mandate the nightly `HashChainAuditLog.verify_chain()` job (Phase 5.1 of implementation checklist) as a P1-alert control
5. Review the severity guide in `quick-reference-card.md` — any Critical issue there is a Section 6(10) failure mode

**Board inquiry readiness:** A chain-verified, partitioned audit log + offline-verifiable signed receipts + a published Ed25519 public key is the combination that makes the Section 6(10) burden of proof defensible. If the Board asks for evidence that a specific principal consented to a specific purpose on a specific date, this system produces it in under 30 minutes.

---

## Running the tests

The integration test (`tests/test_audit_chain.py`) starts a throwaway PostgreSQL container, applies `schema/consent-schema.sql`, and checks that rows written by `append_audit_event()` verify in Python (`verify_chain()`). It needs Docker running.

```bash
pip install -r requirements-dev.txt
pytest tests -v
```

Verified on 2026-10-17: 5 passed (PostgreSQL via testcontainers, Docker 28.3).

## Common Pitfalls

| Pitfall | Why It Fails DPDPA | Fix |
|---------|-------------------|-----|
| Using `datetime.utcnow()` instead of `datetime.now(timezone.utc)` in receipt timestamps | Naive timestamps are not auditable under Section 6(10). An auditor cannot determine the timezone — timestamps are part of the signed canonical string. | Replace every `utcnow()` with `datetime.now(timezone.utc)`. The Python library raises `ValueError` if a naive datetime is passed to `compute_event_hash()`. |
| LLM-generated translations instead of signed human translations | Section 6(1) "informed" consent requires the translation to be accurate and attestable. A misleading Bodo or Santali translation that understates data collection invalidates consent. Non-deterministic LLM output also breaks `content_hash` reproducibility for Section 6(10). | Use approved human translators; store their Ed25519 `translator_signature` in `notice_translation`. |
| Storing raw `principal_id` in `consent_audit_event.event_payload` | The audit log becomes a shadow PII database, creating a Section 8(5) liability beyond the consent transaction. | Use `PseudonymService.pseudonymise(principal_id)` — only the `principal_pseudonym` belongs in audit payloads. |
| Mutating a `notice_translation` row instead of creating a new `notice_version` | Section 6(10) requires that you can reconstruct exactly which notice the principal saw. A mutation makes that reconstruction impossible. | `notice_translation` rows are insert-only. Any content change requires a new `notice_version` row and a new set of `notice_translation` rows. |
| Deleting the KMS signing key after it is rotated | Old consent receipts reference the old key for verification. Deleting it makes those receipts unverifiable offline — a Section 6(10) failure for long-running consents. | Retain old public keys at versioned URLs (`/.well-known/consent-signing-key-v1.pub`). Rotate the key but never delete previous public keys. |

---

## Legal review notes

Citations re-checked on 2026-10-17 against the notified DPDP Rules 2025 (G.S.R. 846(E)) and the India Code Act text.

- `article.md`, FAQ "What are the 22 Eighth Schedule languages?": `check.mjs` flags this line under its Eighth Schedule language rule. The line is correct: it lists exactly the 22 languages in CLAUDE.md and names the commonly mistaken language only to say it is NOT in the Schedule.
- **Retention beyond one year: resolved and accepted (owner sign-off by StateCheck, 2026-10-17; not an external counsel review).** The receipt-retention period itself is still to be chosen per deployment. The 24-month default is gone. The package now splits the record: full consent and audit payloads for the Rule 8(3) one-year minimum, then a minimal pseudonymised receipt (hashes, timestamps, notice version) kept as S.6(10) evidence. How long to keep the receipt is for counsel.

---

## Related Resources

- **[DPDPA Threat Modeling with LINDDUN and STRIDE](../dpdpa-threat-modeling-linddun-stride/)** — model adversaries who target the consent log itself; STRIDE Tampering and Repudiation map directly to the hash chain and receipt signing controls in this package
- **[DPDPA Encryption Requirements Under Rule 6](/blog/dpdpa-encryption-requirements)** — the cryptographic obligations that apply to the consent database at rest
- **[Privacy by Design in the CI/CD Pipeline](/blog/dpdpa-privacy-by-design-cicd)** — enforce hash binding, pseudonymisation, and timezone-aware timestamps as pre-merge checks
- **[Consent Manager Registration Under DPDPA](/blog/dpdpa-consent-manager-registration)** — if building a Board-recognised Consent Manager platform rather than in-house Data Fiduciary API
- **[ISO 27001 Mapping for DPDPA](../dpdpa-iso-27001-mapping/)** — where Annex A controls (A.5.33, A.5.34, A.8.10, A.8.15, A.8.24) pick up the framework obligations these technical controls discharge

---

**[Talk to StateCheck's privacy engineering team about consent management design review](https://statechecksecurity.com/contact)**
