> ## 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.

# Create Consent Record

> Record a data principal's consent against an active grant and a versioned consent notice, with a signed proof the Data Fiduciary can keep as evidence (DPDP Act s.6(10)).

## Endpoint

```
POST /v1/dpdp/consent-records
```

## Authentication

Requires a developer API key in the `Authorization` header.

## Request Headers

| Header | Value |
| - | - |
| `Authorization` | `Bearer <api_key>` |
| `Content-Type` | `application/json` |

## Request Body

| Field | Type | Required | Description |
| - | - | - | - |
| `grantId` | `string` | Yes | The grant to attach the consent record to |
| `dataPrincipalId` | `string` | Yes | The data principal (end-user) providing consent |
| `purposes` | `object[]` | Yes | Array of purpose objects (`{ code, description }`) |
| `consentNoticeId` | `string` | Yes | ID of the consent notice shown to the principal |
| `consentNoticeVersion` | `string` | No | The notice version shown to the principal. Default: the latest version of `consentNoticeId` (in `consentNoticeLanguage` when given) |
| `consentNoticeLanguage` | `string` | Only with `DPDP_REQUIRE_NOTICE_LANGUAGE=true` and a version in several languages | The language of the notice shown. With it and no pinned version, the newest notice row in this language is bound. Without it, the pinned version (or the version of the newest notice row) is bound through its newest row, whose language is recorded; with `DPDP_REQUIRE_NOTICE_LANGUAGE=true` a version registered in more than one language answers `400 NOTICE_LANGUAGE_REQUIRED` instead |
| `processingExpiresAt` | `string` | Yes | ISO-8601 date-time, in the future, when data processing permission expires |

The grant must be the developer's and active (not revoked, suspended or
expired). `dataPrincipalId` is the end user who gave consent, which in the
documented model is the grant's principal (`principal_id`). With
`DPDP_ENFORCE_GRANT_PRINCIPAL=true` a record whose `dataPrincipalId` differs
from the grant's principal is refused with `PRINCIPAL_MISMATCH`; with the flag
off (the default) the two are not compared, and an integration that keys its
data principals differently from its grant principals keeps working.

Limits: `grantId`, `dataPrincipalId` and `consentNoticeId` up to 256
characters; `consentNoticeVersion` and each purpose `code` up to 128; each
purpose `description` up to 1,000; at most 50 purposes.

### Purpose Object

| Field | Type | Description |
| - | - | - |
| `code` | `string` | Machine-readable purpose code (e.g., `"analytics"`, `"personalization"`) |
| `description` | `string` | Human-readable description of the purpose |

## Example Request

```bash theme={null}
curl -X POST https://api.grantex.dev/v1/dpdp/consent-records \
  -H "Authorization: Bearer gx_..." \
  -H "Content-Type: application/json" \
  -d '{
    "grantId": "grnt_01HXYZ...",
    "dataPrincipalId": "user_abc123",
    "purposes": [
      { "code": "analytics", "description": "Usage analytics for service improvement" },
      { "code": "personalization", "description": "Personalized recommendations" }
    ],
    "consentNoticeId": "notice_v2",
    "processingExpiresAt": "2027-01-01T00:00:00.000Z"
  }'
```

## Response -- 201 Created

```json theme={null}
{
  "recordId": "crec_01HXYZ...",
  "grantId": "grnt_01HXYZ...",
  "dataPrincipalId": "user_abc123",
  "consentNoticeId": "notice_v2",
  "consentNoticeVersion": "2.0",
  "consentNoticeLanguage": "en",
  "consentNoticeHash": "a1b2c3d4e5f6...",
  "noticeHash": "9f8e7d6c5b4a...",
  "consentProof": {
    "type": "JWS-EdDSA",
    "alg": "EdDSA",
    "kid": "grantex-ed25519-2026-04",
    "keyPersistence": "persistent",
    "proofJwt": "eyJ...",
    "jwksUri": "https://api.grantex.dev/.well-known/jwks.json",
    "signedAt": "2026-04-05T12:00:00.000Z"
  },
  "processingExpiresAt": "2027-01-01T00:00:00.000Z",
  "retentionUntil": "2027-01-31T00:00:00.000Z",
  "status": "active",
  "createdAt": "2026-04-05T12:00:00.000Z"
}
```

## Response Fields

| Field | Type | Description |
| - | - | - |
| `recordId` | `string` | Unique consent record ID |
| `grantId` | `string` | The grant this consent is attached to |
| `dataPrincipalId` | `string` | The data principal who gave consent |
| `consentNoticeId` | `string` | The consent notice ID |
| `consentNoticeVersion` | `string` | The notice version the record was given against |
| `consentNoticeLanguage` | `string` | The language of that notice version |
| `consentNoticeHash` | `string` | SHA-256 hash of that notice version's content |
| `noticeHash` | `string` | The notice hash of that notice: the whole notice, structured fields and language included (see [Consent Notice Content](/api-reference/dpdp/consent-notice-content#the-notice-hash)); stored on the record |
| `consentProof` | `object` | Signed proof of consent (see below) |
| `processingExpiresAt` | `string` | ISO-8601 timestamp when processing permission expires |
| `retentionUntil` | `string` | ISO-8601 timestamp for data retention limit (30 days after processing expiry) |
| `status` | `string` | Record status: `active` |
| `createdAt` | `string` | ISO-8601 creation timestamp |

### Consent Proof

`proofJwt` is a compact JWS (RFC 7515) signed with EdDSA over Ed25519
(RFC 8037). Its payload carries `recordId`, `grantId`, `dataPrincipalId`,
`consentNoticeId`, `consentNoticeVersion`, `consentNoticeHash` (the content
hash), `consentNoticeLanguage`, `noticeHash` (the whole notice), the purpose
codes, `consentGivenAt`, `iss` and `iat`, and no `exp`: the proof is evidence
the Data 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 the header's `kid` names in the JWKS at `jwksUri`. Set
`ED25519_PRIVATE_KEY` in production: without it each process generates its
own key, and a proof cannot be verified after a restart or by another instance.

`keyPersistence` says which kind of key signed the proof:

| Value | Meaning |
| - | - |
| `persistent` | The server's configured `ED25519_PRIVATE_KEY`. The proof verifies against the published JWKS on every instance and across restarts, for as long as the key is published |
| `ephemeral` | A key the serving process generated at boot because `ED25519_PRIVATE_KEY` is not set. The proof is **not verifiable across instances or restarts**: once that process restarts, or on any other instance, the JWKS no longer carries its key. Ask the operator to set `ED25519_PRIVATE_KEY` if proofs must stay verifiable |

A server started with `DPDP_REQUIRE_PERSISTENT_PROOF_KEY=true` refuses to
create a record while its key is ephemeral (`503
CONSENT_PROOF_KEY_NOT_PERSISTENT`, nothing stored).

If the proof cannot be signed, no record is created and the request fails
with `503 CONSENT_PROOF_UNAVAILABLE`.

The creation is recorded on the developer's audit chain as
`grantex.dpdp.consent_created`.

## Error Responses

| Status | Code | Description |
| - | - | - |
| 400 | `BAD_REQUEST` | Missing or malformed fields: `purposes` not a non-empty array of `{ code, description }`, `processingExpiresAt` not a future ISO-8601 date-time, a field over its length limit |
| 400 | `INVALID_GRANT` | Grant not found, not owned by the developer, or not active (revoked, suspended or expired) |
| 400 | `PRINCIPAL_MISMATCH` | `dataPrincipalId` is not the grant's principal (only with `DPDP_ENFORCE_GRANT_PRINCIPAL=true`) |
| 400 | `INVALID_NOTICE` | Consent notice, the pinned `consentNoticeVersion`, or that version in `consentNoticeLanguage`, not found |
| 400 | `NOTICE_LANGUAGE_REQUIRED` | The notice version exists in several languages and `consentNoticeLanguage` was not given (only with `DPDP_REQUIRE_NOTICE_LANGUAGE=true`) |
| 401 | `UNAUTHORIZED` | Invalid or missing API key |
| 503 | `CONSENT_PROOF_UNAVAILABLE` | The consent proof could not be signed; nothing was stored |
| 503 | `CONSENT_PROOF_KEY_NOT_PERSISTENT` | The signing key is ephemeral and the server sets `DPDP_REQUIRE_PERSISTENT_PROOF_KEY=true`; nothing was stored |

## SDK Examples

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

  const grantex = new Grantex({ apiKey: 'gx_...' });

  const record = await grantex.dpdp.createConsentRecord({
    grantId: 'grnt_01HXYZ...',
    dataPrincipalId: 'user_abc123',
    purposes: [
      { code: 'analytics', description: 'Usage analytics' },
    ],
    consentNoticeId: 'notice_v2',
    processingExpiresAt: '2027-01-01T00:00:00.000Z',
  });
  ```

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

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

  record = grantex.dpdp.create_consent_record(
      CreateConsentRecordParams(
          grant_id="grnt_01HXYZ...",
          data_principal_id="user_abc123",
          purposes=[{"code": "analytics", "description": "Usage analytics"}],
          consent_notice_id="notice_v2",
          processing_expires_at="2027-01-01T00:00:00.000Z",
      )
  )
  ```
</CodeGroup>

To pin `consentNoticeVersion` or `consentNoticeLanguage`, use the REST call
above unless your SDK version lists those fields.

## 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.