> ## Documentation Index
> Fetch the complete documentation index at: https://docs.grantex.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# DPDP & GDPR Evidence Module

> Consent records bound to grants and versioned notices, withdrawal, erasure, grievances, a breach register and exports that help a Data Fiduciary evidence its DPDP Act, GDPR and EU AI Act obligations.

## Overview

Grantex's DPDP routes keep the records a Data Fiduciary needs to evidence its
obligations under India's Digital Personal Data Protection Act 2023 (DPDP Act)
and the Digital Personal Data Protection Rules 2025 (DPDP Rules), and to answer
GDPR access requests, for AI agent deployments. Each consent record is tied to
the Grantex grant that authorised the agent, and every DPDP state change is
written to the developer's tamper-evident audit chain.

<Warning>
  **What Grantex is, and is not.** Grantex is a technical control that helps a
  Data Fiduciary keep and produce evidence. It is **not** a Consent Manager
  registered with the Data Protection Board of India, it does **not** notify
  the Board or Data Principals on your behalf, it does not decide whether your
  processing is lawful, and this page is not legal advice. The obligations
  stay with the Data Fiduciary. Consult qualified counsel.
</Warning>

As of 30 September 2026 most DPDP obligations are **not yet in force**. The
[DPDP Rules 2025](https://www.meity.gov.in/static/uploads/2025/11/53450e6e5dc0bfa85ebd78686cadad39.pdf)
(G.S.R. 846(E), notified 13 November 2025) bring the notice, consent,
security, breach, erasure, rights and penalty provisions into force on
**13 May 2027**, and Consent Manager registration on 13 November 2026. See
[DPDP Act 2023](/compliance/dpdp-act-2023) for the section-by-section mapping
and [EU AI Act](/compliance/eu-ai-act) for the EU timeline.

## Where the features live

| Surface | How to reach it | Notes |
| - | - | - |
| REST API | `https://api.grantex.dev/v1/dpdp/*`, or your self-hosted auth service | Every feature on this page. See the [API reference](/api-reference/dpdp/create-consent-record) |
| TypeScript SDK | `import { Grantex } from '@grantex/sdk'`, then `grantex.dpdp.*` | Consent records, notices, withdrawal, erasure, grievances, exports |
| Python SDK | `from grantex import Grantex`, then `client.dpdp.*` | The same set; create calls take a params dataclass |
| Go SDK | `client.DPDP.*` | The same set |
| CLI | `grantex dpdp ...` | See [CLI](/integrations/cli) |
| `@grantex/dpdp` | Standalone functions and local helpers | See the package README |

The SDKs and CLI cover the original routes. The newer routes and fields (notice
lists and reads, grievance lists and updates, erasure request reads, the breach
register, `responsePeriodDays`, `consentNoticeVersion`, the structured notice
fields and the `eu-ai-act-evidence` export) are shown below as REST calls; call
them that way unless your SDK version lists them. [Release
Status](/release-status) says which versions are published.

## Consent notices

A consent notice is registered once per `noticeId`, `version` and `language`.
Grantex stores its content and a SHA-256 `contentHash`. A consent record names
the version shown and carries that hash in its signed proof, so a later change
to the notice text is detectable.

A notice can carry structured fields for what DPDP Rules r.3 asks a notice
under DPDP Act s.5 to contain: `itemisedPersonalData`, `purposeDetails` (with
the goods or services each purpose enables), `withdrawalUrl`, `rightsUrl`,
`boardComplaintUrl`, and a `contact` for the person able to answer questions
(s.8(9), r.9). Every response includes a `validation` block listing which r.3
elements are present or missing, and whether the language is English or an
Eighth Schedule language. The check is for presence, not quality: it is
evidence for your own review. With `DPDP_NOTICE_REQUIRE_RULE3=true` an
incomplete notice is refused with `400 NOTICE_INCOMPLETE`. Details:
[Consent Notice Content](/api-reference/dpdp/consent-notice-content).

```bash theme={null}
curl -X POST https://api.grantex.dev/v1/dpdp/consent-notices \
  -H "Authorization: Bearer $GRANTEX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "noticeId": "shopping-assistant",
    "version": "3",
    "language": "hi-IN",
    "title": "Shopping assistant: how we use your data",
    "content": "...the full notice text shown to the user...",
    "purposes": [{ "code": "order_placement", "description": "Place orders you approve" }],
    "itemisedPersonalData": [
      { "category": "contact", "description": "Email address for order confirmations" },
      { "category": "order_history", "description": "Items you ordered through the assistant" }
    ],
    "purposeDetails": [
      { "code": "order_placement", "description": "Place orders you approve", "goodsOrServices": "Ordering from merchant.example" }
    ],
    "withdrawalUrl": "https://app.example.com/privacy/withdraw",
    "rightsUrl": "https://app.example.com/privacy/rights",
    "boardComplaintUrl": "https://app.example.com/privacy/complain-to-the-board",
    "contact": { "designation": "Data Protection Officer", "email": "dpo@example.com" }
  }'
```

Grantex does not render the notice to the user. Showing it, in the language
the user chose, before asking for consent is your application's job.

## Consent records

A consent record binds a Data Principal's consent to an **active grant** and to
the **notice version** shown. Create the grant first, through the normal
Grantex authorisation flow, then record the consent:

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { Grantex } from '@grantex/sdk';

  const grantex = new Grantex({ apiKey: process.env.GRANTEX_API_KEY });

  const record = await grantex.dpdp.createConsentRecord({
    grantId: 'grnt_01HXYZ...',
    dataPrincipalId: 'user_abc123',
    purposes: [{ code: 'order_placement', description: 'Place orders you approve' }],
    consentNoticeId: 'shopping-assistant',
    processingExpiresAt: '2027-12-31T23:59:59Z',
  });

  console.log(record.recordId, record.consentProof?.proofJwt);
  ```

  ```python Python theme={null}
  from grantex import CreateConsentRecordParams, Grantex

  client = Grantex(api_key="gx_...")

  record = client.dpdp.create_consent_record(
      CreateConsentRecordParams(
          grant_id="grnt_01HXYZ...",
          data_principal_id="user_abc123",
          purposes=[{"code": "order_placement", "description": "Place orders you approve"}],
          consent_notice_id="shopping-assistant",
          processing_expires_at="2027-12-31T23:59:59Z",
      )
  )
  ```

  ```bash CLI theme={null}
  grantex dpdp consent create \
    --grant-id grnt_01HXYZ... \
    --principal-id user_abc123 \
    --notice-id shopping-assistant \
    --processing-expires-at 2027-12-31T23:59:59Z \
    --purposes '[{"code":"order_placement","description":"Place orders you approve"}]'
  ```
</CodeGroup>

What the server does:

* Refuses a grant that is not the developer's, or is revoked, suspended or
  expired (`400 INVALID_GRANT`). With `DPDP_ENFORCE_GRANT_PRINCIPAL=true` it
  also refuses a `dataPrincipalId` that is not the grant's principal.
* Uses the latest version of the notice unless `consentNoticeVersion` pins
  one; `consentNoticeLanguage` picks the language when that version exists in
  several.
* Signs a **consent proof**: a compact JWS (EdDSA over Ed25519) covering the
  record id, grant, principal, notice id, version and hash, the purpose codes
  and the consent time. It has no `exp`, because it is evidence the fiduciary
  may need for as long as it keeps the record (DPDP Act s.6(10) puts the
  burden of proving consent on the fiduciary). Verify it with the key its
  `kid` names in the JWKS at `jwksUri`. If it cannot be signed, no record is
  created (`503 CONSENT_PROOF_UNAVAILABLE`).
* Sets `retentionUntil` to 30 days after `processingExpiresAt`. This is a
  product default, not a legal retention period, and Grantex does not delete
  anything when it passes.

Record statuses are `active`, `withdrawn`, `expired` and `erased`. A record
becomes `expired` only when the consent expiry worker is on
(`DPDP_CONSENT_EXPIRY_ENABLED=true`); with `DPDP_CONSENT_EXPIRY_REVOKES_GRANT=true`
the worker also revokes the grant.

**Purpose limitation.** The record stores the purposes the principal agreed to
and the grant's scopes. Grantex does not compare an agent's later actions with
those purposes. What an agent can do is bounded by the grant's scopes, and a
grant can carry a purpose that tools check (see
[Purpose-Bound Grants](/concepts/purpose-bound-grants)). Choosing scopes that
match the consented purposes is your design decision.

Reads (`GET /v1/dpdp/consent-records`, `/v1/dpdp/consent-records/{recordId}`
and `/v1/dpdp/data-principals/{principalId}/records`) have no side effects. Record lists return what they always did (up to 100
records unfiltered, every match for a data principal) unless you send `limit`
(up to 200) or `cursor`, in which case they page and return `nextCursor`;
`totalRecords` is always the full count.

## Withdrawal

Under DPDP Act s.6(4) a Data Principal may withdraw consent at any time, as
easily as it was given. Offering that means, for example a control next to the
one that gave consent, is your application's job; it then calls:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const result = await grantex.dpdp.withdrawConsent('crec_01HXYZ...', {
    reason: 'Principal withdrew consent in account settings',
    revokeGrant: true,
    deleteProcessedData: true,
  });
  // result.status === 'withdrawn'; result.dataDeleted === false
  ```

  ```python Python theme={null}
  result = client.dpdp.withdraw_consent(
      "crec_01HXYZ...",
      reason="Principal withdrew consent in account settings",
      revoke_grant=True,
  )
  ```

  ```bash CLI theme={null}
  grantex dpdp consent withdraw crec_01HXYZ... \
    --reason "Principal withdrew consent in account settings" --revoke-grant
  ```
</CodeGroup>

* `reason` is required. Only an `active` record can be withdrawn: a second
  withdrawal is `409 ALREADY_WITHDRAWN`, and erased or expired records answer
  `409 CONSENT_ERASED` or `409 CONSENT_EXPIRED`.
* `revokeGrant: true` revokes the record's own grant (`revokedAt`, the
  revocation cache and a `grant.revoked` event); grants delegated from it keep
  working. An operator who sets `DPDP_REVOCATION_CASCADE=true` gets the same
  cascade as `DELETE /v1/grants/{id}` instead: delegated grants, credentials
  and wallet reservations. Without `revokeGrant`
  the grant stays active, unless the operator sets
  `DPDP_WITHDRAWAL_REVOKES_GRANT=true`, which makes an omitted `revokeGrant`
  mean `true`. After a withdrawal the fiduciary and its processors must cease
  processing within a reasonable time (s.6(6)). Revoking the grant stops
  processing that goes through Grantex; a service that verifies tokens offline
  sees the revocation only once it checks revocation state.
* `deleteProcessedData: true` emits a `dpdp.data_deletion.requested` webhook
  (`recordId`, `grantId`, `dataPrincipalId`, `requestedAt`). Grantex holds none
  of the data your application processed, so it deletes nothing itself
  (`dataDeleted` is always `false`); deleting that data in your systems and
  your processors' is your step (s.8(7)).

## Erasure

`POST /v1/dpdp/data-principals/{principalId}/erasure` acts on a Data
Principal's erasure request (DPDP Act s.12). In one transaction it revokes the
principal's active grants (their delegated grants too only with
`DPDP_REVOCATION_CASCADE=true`), marks the consent records `erased`, and stores
an erasure request (`ER-YYYY-<ULID>`) that says what was **retained** and why.
With `DPDP_ERASURE_EXPANDED=true` it also replaces grievance descriptions and
evidence with a fixed marker and deletes stored exports about the principal;
without it, both are kept and listed as retained:

| Retained | Why |
| - | - |
| Consent records, marked `erased` | The fiduciary must be able to prove consent (s.6(10)), and DPDP Rules r.8(3) require personal data, associated traffic data and processing logs to be kept for at least one year from processing, for the purposes in the Seventh Schedule |
| Audit entries, never modified | The one-year log retention in r.6(1)(e) and r.8(3); the entries also form a hash chain that any rewrite would break |
| Grievances (redacted with `DPDP_ERASURE_EXPANDED=true`) | The record of grievance handling; without the flag the text is kept too |
| Stored exports (only without `DPDP_ERASURE_EXPANDED`) | Expanded erasure is not enabled on the deployment |
| Your processed data | Not held by Grantex; erasing it is your step |

A repeat call returns the completed request with `200`. The request can be
read again from `GET /v1/dpdp/erasure-requests/{requestId}`.

<Note>
  The DPDP Rules' three-year inactivity erasure and its 48-hour advance notice
  (r.8(1) and r.8(2), Third Schedule) apply only to large e-commerce, online
  gaming and social media platforms. Grantex does not track inactivity or send
  that notice.
</Note>

## Grievances

A Data Fiduciary must offer a grievance mechanism and publish its response
period, which may not exceed 90 days (DPDP Act s.13; DPDP Rules r.14(3)).
Grantex records each grievance with a non-sequential reference
(`GRV-YYYY-<ULID>`), the period you publish (`responsePeriodDays`, 1 to 90; the
default of 7 is a product default, not a statutory period) and
`expectedResolutionBy`:

```bash theme={null}
curl -X POST https://api.grantex.dev/v1/dpdp/grievances \
  -H "Authorization: Bearer $GRANTEX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "dataPrincipalId": "user_abc123",
    "recordId": "crec_01HXYZ...",
    "type": "unauthorized-processing",
    "description": "Order history used for recommendations I did not agree to",
    "responsePeriodDays": 30
  }'
```

Move it through review with `PATCH /v1/dpdp/grievances/{grievanceId}`:
`submitted` to `in_review`, then `resolved` or `rejected` with a `resolution`;
any other move is `409 INVALID_TRANSITION`. List grievances with
`GET /v1/dpdp/grievances?status=in_review`. Each change emits
`dpdp.grievance.updated`. Grantex stores the due date; it does not chase it,
route the grievance to a person, or reply to the principal.

## Breach register

Under DPDP Act s.8(6) and DPDP Rules r.7, a fiduciary that becomes aware of a
personal data breach informs each affected Data Principal without delay,
informs the Board without delay, and sends the Board a detailed report
**within 72 hours** of becoming aware (extendable on written request). There
is no risk threshold. Separately, the CERT-In directions of 28 April 2022
require cyber incidents to be reported to CERT-In within 6 hours.

The breach register keeps your record of this:

```bash theme={null}
curl -X POST https://api.grantex.dev/v1/dpdp/breaches \
  -H "Authorization: Bearer $GRANTEX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Unauthorised read of an order history export",
    "nature": "confidentiality",
    "extent": "Order history of three customers",
    "awareAt": "2027-06-01T10:00:00Z",
    "affectedDataPrincipalIds": ["user_abc123", "user_def456", "user_ghi789"]
  }'
```

* The response carries `boardDetailedReportDueAt` (`awareAt` plus 72 hours, or
  a granted extension's date), `boardDetailedReportOverdue`, and
  `principalIntimation` (`intimatedCount`, `pendingCount`).
* `PATCH /v1/dpdp/breaches/{breachId}` records the Board intimation times, the
  r.7(2)(b) detailed report fields, an extension, and the status (`open`,
  `initial_intimated`, `reported`, `closed`, in that order).
* `POST /v1/dpdp/breaches/{breachId}/principal-intimations` records who was
  told, by which channel, when, and which r.7(1)(a)-(e) content the message
  carried.
* Webhooks: `dpdp.breach.recorded`, `dpdp.breach.principal_intimation_due`,
  and, with `DPDP_BREACH_DEADLINE_ALERTS_ENABLED=true`,
  `dpdp.breach.board_report_due` before and after the 72-hour deadline.

**Grantex does not notify Data Principals and files nothing with the Board.**
It records what you tell it you sent. See
[Record Breach](/api-reference/dpdp/record-breach).

## Exports

`POST /v1/dpdp/exports` produces a JSON export, stored for 7 days
(`410 GONE` after that). Audit entries are capped at 1,000 per export, and
`truncated` says when the cap was hit.

| `type` | What it contains |
| - | - |
| `dpdp-audit` | Consent records, audit entries and grievances for the period, optionally for one principal |
| `gdpr-article-15` | The same records; with `dataPrincipalId`, an `article15` block with the purposes, recipients (agents and grant audiences), retention and source Grantex holds for that person, and `automatedDecisionMaking: { recorded: false }` |
| `eu-ai-act-evidence` | Sections mapped to Arts. 12, 14, 26, 50 and 73 of the EU AI Act, with an audit-chain integrity check, an `applicability` block and a disclaimer; see [EU AI Act Evidence Pack](/api-reference/dpdp/eu-ai-act-evidence) |
| `eu-ai-act-conformance` | Kept for compatibility; prefer `eu-ai-act-evidence`. Despite its name it is not a conformity assessment |

<CodeGroup>
  ```typescript TypeScript theme={null}
  const access = await grantex.dpdp.createExport({
    type: 'gdpr-article-15',
    dateFrom: '2027-01-01T00:00:00Z',
    dateTo: '2027-06-30T23:59:59Z',
    dataPrincipalId: 'user_abc123',
  });
  ```

  ```python Python theme={null}
  from grantex import CreateExportParams

  access = client.dpdp.create_export(
      CreateExportParams(
          type="gdpr-article-15",
          date_from="2027-01-01T00:00:00Z",
          date_to="2027-06-30T23:59:59Z",
          data_principal_id="user_abc123",
      )
  )
  ```
</CodeGroup>

An export contains only what Grantex holds. A GDPR Art. 15 response, or a
summary under DPDP Act s.11, also needs the data your own systems process.

## Webhook events

| Event | When |
| - | - |
| `dpdp.consent.created` | A consent record was created |
| `dpdp.consent.withdrawn` | A record was withdrawn |
| `dpdp.consent.expired` | The expiry worker expired a record |
| `dpdp.data_deletion.requested` | A withdrawal asked you to delete processed data |
| `dpdp.erasure.completed` | An erasure request completed |
| `dpdp.grievance.filed`, `dpdp.grievance.updated` | A grievance was filed or changed status |
| `dpdp.breach.recorded`, `dpdp.breach.principal_intimation_due` | A breach was recorded |
| `dpdp.breach.board_report_due` | The Board report deadline is near or has passed (worker flag) |

Subscribe with [webhooks](/guides/webhooks). Breach events carry no principal
ids or breach text.

## Audit trail

Every DPDP state change appends an entry to the developer's hash-chained audit
log: `grantex.dpdp.consent_created`, `consent_withdrawn`, `consent_expired`,
`erasure_completed`, `grievance_filed`, `grievance_updated`, `notice_created`,
`export_created`, `breach_recorded`, `breach_updated`,
`breach_principals_intimated` and `breach_deadline_alerted`. Grantex never
rewrites or deletes audit entries; how long they are kept is the retention of
your database (the DPDP Rules ask for at least one year).

## Configuration flags

All default off and take the exact value `true`.

| Flag | Effect |
| - | - |
| `DPDP_WITHDRAWAL_REVOKES_GRANT` | An omitted `revokeGrant` on withdrawal means `true` |
| `DPDP_ENFORCE_GRANT_PRINCIPAL` | Refuse a consent record whose `dataPrincipalId` is not the grant's principal |
| `DPDP_CONSENT_EXPIRY_ENABLED` | Run the worker that marks records past `processingExpiresAt` as `expired` |
| `DPDP_CONSENT_EXPIRY_REVOKES_GRANT` | With the expiry worker, also revoke the grant |
| `DPDP_NOTICE_REQUIRE_RULE3` | Refuse notices missing an r.3 element or in another language |
| `DPDP_EXPORT_GDPR_REQUIRES_PRINCIPAL` | Refuse a `gdpr-article-15` export without `dataPrincipalId` |
| `DPDP_REVOCATION_CASCADE` | Withdrawal, erasure and the expiry worker revoke through the full grant cascade instead of only the record's own grant |
| `DPDP_ERASURE_EXPANDED` | Erasure also redacts grievances and deletes stored exports |
| `DPDP_REQUIRE_PERSISTENT_PROOF_KEY` | Refuse consent records (`503`) while the proof key is ephemeral (`ED25519_PRIVATE_KEY` unset) |
| `DPDP_REQUIRE_NOTICE_LANGUAGE` | Require `consentNoticeLanguage` when a notice version exists in several languages |
| `DPDP_BREACH_DEADLINE_ALERTS_ENABLED` | Run the breach deadline worker (`DPDP_BREACH_ALERT_LEAD_MINUTES`, default 720) |

Set `ED25519_PRIVATE_KEY` in production so consent proofs stay verifiable
across restarts and instances, and `ED25519_STABLE_KID=true` so the key id
does not change with the month the service restarts in. See
[Self-Hosting](/guides/self-hosting).

## What remains your responsibility

* Choosing the lawful basis (consent, or a legitimate use under s.7) and the
  purposes, and showing a notice that meets s.5 and r.3.
* Giving users a way to withdraw that is as easy as consenting, and ceasing
  processing in your systems and your processors' after a withdrawal.
* Erasing the data you and your processors hold, and giving the r.8(2)
  48-hour notice where the Third Schedule applies.
* Publishing your contact details and grievance response period, and
  answering grievances within it.
* Notifying affected principals and the Board of breaches, and CERT-In of
  cyber incidents.
* Verifiable parental consent for children (s.9, r.10), the duties of a
  Significant Data Fiduciary (s.10, r.13), and any cross-border restriction
  (s.16).

## Related Resources

* [DPDP Act 2023](/compliance/dpdp-act-2023) — section and rule mapping
* [EU AI Act](/compliance/eu-ai-act) — article mapping and timeline
* [DPDP integration](/integrations/dpdp) — endpoint list
* [Compliance Matrix](/guides/compliance-matrix) — cross-framework mapping
* [Principal Sessions](/sdks/typescript/principal-sessions) — authenticated links for the core agent-permission dashboard; not a DPDP rights portal
* [Blog: DPDP Act and AI Agents](/blog/dpdp-act-ai-agents)

## Ownership

Grantex is owned by Orchestrum Technologies LLP. Inventor and owner: Sanjeev Kumar. Ownership contact: [sanjeev@orchestrum.in](mailto:sanjeev@orchestrum.in) or [mishra.sanjeev@gmail.com](mailto:mishra.sanjeev@gmail.com).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.