Reference

Full documentation.

Everything DPDP Comply does, with request / response shapes and code samples. Pair this with the integration guide for framework-specific embed snippets.

Quick start

Two lines to collect consent, one more to attribute it to a logged-in user.

html
<!-- 1. Drop the widget anywhere before </body> -->
<script
  src="https://your-domain.com/widget/banner.js"
  data-api-key="pk_live_xxxxxxxx"
  async
></script>
js
// 2. After your auth flow sets the user session, attribute consent:
const { token } = await fetch("/api/dpdp-identity-token").then(r => r.json());
await window.DPDPConsent.identify({ identityToken: token });

Find your API key under Project → Settings → API Keys. Anything embedded in the page is a publishable pk_live_… key — safe to ship.

Concepts & data model

The platform is multi-tenant: every customer is an Organization that owns one or more Projects. Each project carries its own banner, purposes, consent records, and API keys.

  • Organization — billing, team, grievance officer, SSO/MFA policies.
  • Project — a web property or app. Has its own widget config + API keys.
  • Purpose — a processing activity (Analytics, Marketing, Functional, …) with a legal basis and retention.
  • ConsentRecord — one per data principal session. Holds per-purpose ConsentItem rows.
  • RightsRequest — ACCESS / CORRECTION / ERASURE / NOMINATION / GRIEVANCE per DPDP §12–14.
  • NoticeVersion — versioned §5 notice that consents link back to.
  • Receipt — ISO/IEC 29184 signed JSON issued per ConsentRecord.

Legal mapping

Section 6 (consent), §9 (children), §11 (withdrawal), §12–14 (rights), §8(6) (retention), §13(3) (30-day SLA + grievance officer) all have direct surface in the product.

Widget

The embed script is a single IIFE bundle (~26 kB gzipped) that loads async and exposes window.DPDPConsent.

Public API

ts
interface DPDPConsent {
  show(): void;
  hide(): void;
  withdraw(purposeIds?: string[]): Promise<void>;
  getConsent(): { token, givenAt, expiresAt, purposes } | null;
  identify(i: { identityToken: string }): Promise<boolean>;
  forget(): void;
  getIdentity(): { identityToken, email?, externalId? } | null;
}

Features out of the box

  • Google Consent Mode v2 — emits consent events with purpose → signal mapping. Setup guide for GA4, Google Ads, Microsoft Clarity →
  • Script blocker — mark a script with type="text/plain" data-dpdp-purpose="analytics"; widget rewrites the type after consent.
  • GPC (Global Privacy Control) — auto-denies non-essential purposes when navigator.globalPrivacyControl is true.
  • Age gate — DPDP §9. Shows an "are you ≥ N?" screen when the project has children's data enabled.
  • Guardian OTP flow — email-verified parental consent for minors.
  • 23 languages — English + 22 Eighth Schedule. Picks from navigator.languages, honours ?dpdp-lang=xx override.
  • A/B testing — swap banner variants, track conversions per variant.
  • Geo-targeting — per-country override rules for banner copy / behaviour.
  • Proof of notice — POSTs a display event before consent so §6(3) is auditable.

Script-blocker example

html
<!-- Google Analytics only loads after consent to the "analytics" purpose -->
<script
  type="text/plain"
  data-dpdp-purpose="analytics"
  src="https://www.googletagmanager.com/gtag/js?id=G-XXXX"
></script>
<script type="text/plain" data-dpdp-purpose="analytics">
  window.dataLayer = window.dataLayer || [];
  function gtag(){ dataLayer.push(arguments); }
  gtag("js", new Date());
  gtag("config", "G-XXXX");
</script>

Identify & attribution

Without identify(), records fall back to hashed IP (shown as "anonymous"). Call identify() after login with a short-lived HMAC token minted on your server.

1. Mint the token server-side

ts
// Node / Next.js API route on your server
import crypto from "crypto";

function identitySecret(projectId: string) {
  return crypto.createHmac("sha256", process.env.NEXTAUTH_SECRET!)
    .update(`identify:${projectId}`).digest();
}

function b64url(buf: Buffer) {
  return buf.toString("base64")
    .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}

export function createIdentityToken(email: string, externalId: string, projectId: string) {
  const now = Math.floor(Date.now() / 1000);
  const payload = { email, externalId, projectId, iat: now, exp: now + 300 };
  const p = b64url(Buffer.from(JSON.stringify(payload)));
  const s = crypto.createHmac("sha256", identitySecret(projectId)).update(p).digest();
  return `${p}.${b64url(s)}`;
}

2. Forward to the widget

js
// After login
const { token } = await fetch("/api/dpdp-identity-token").then(r => r.json());
await window.DPDPConsent.identify({ identityToken: token });

// On logout
window.DPDPConsent.forget();

Don't trust raw email

If the widget accepted identify({ email }) from the page, any script could attribute a consent to someone else's email. The HMAC token proves the host app's server saw a real session. 5-minute TTL by default.

Public REST API

Every endpoint under /api/v1/* uses an API key in the Authorization: Bearer pk_live_… header. Same calls the widget makes — perfect for mobile apps, server-to-server sync, and Postman debugging.

MethodPathPurpose
GET/widget-configProject banner + purposes + translations.
POST/consentRecord a consent decision.
GET/consent?token=…Read back a record.
DELETE/consent/:tokenWithdraw consent (DPDP §11).
PATCH/consent/:token/identifyAttach an identity to an anonymous record.
GET/consent/:token/receiptSigned Kantara CR v1.1 receipt.
POST/receipt/verifyVerify a receipt (public, no key needed).
POST/notice/displayRecord that the notice was shown (proof of notice).
POST/consent/guardian-otpSend OTP to the guardian email.
POST/consent/guardian-verifyVerify OTP, return a guardian session token.
POST/age-verificationPersist an age-gate response.
GET/withdraw/:tokenPreview a signed one-click withdrawal.
POST/withdraw/:tokenExecute the signed withdrawal.
POST/portal/export/requestData principal initiates a §13 export.
POST/portal/export/verifyVerify OTP and enqueue the export.
GET/portal/export/download/:tokenSingle-use download of the ZIP.

POST /consent — record consent

bash
curl -X POST https://your-domain.com/api/v1/consent \
  -H "Authorization: Bearer pk_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "purposeIds": ["cm_abc", "cm_def"],
    "principalEmail": "user@example.com",
    "consentAction": "acceptAll",
    "metadata": { "source": "web", "pageUrl": "https://app.example.com/" },
    "identityToken": "eyJ…"
  }'

Response: { consentToken, status, givenAt, expiresAt }

DELETE /consent/:token — withdraw

bash
curl -X DELETE https://your-domain.com/api/v1/consent/CNS-abc123 \
  -H "Authorization: Bearer pk_live_xxxxxxxx"

Signed consent receipts

Every project gets an Ed25519 keypair at creation. Each consent record can be downloaded as a signed JSON receipt conforming to ISO/IEC 29184 / Kantara CR v1.1. The public key is embedded in the receipt so verification survives key rotation.

Fetch a receipt

bash
curl https://your-domain.com/api/v1/consent/CNS-abc123/receipt \
  -H "Authorization: Bearer pk_live_xxxxxxxx"

Receipt shape

json
{
  "specVersion": "1.1.0",
  "receiptId": "ckx…",
  "jurisdiction": "IN",
  "consentTimestamp": "2026-04-20T08:14:52.231Z",
  "publicKey": "<base64 Ed25519 SPKI>",
  "dataFiduciary": {
    "name": "Acme Corp",
    "website": "https://acme.com",
    "grievanceOfficerName": "Priya Sharma",
    "grievanceOfficerEmail": "dpo@acme.com"
  },
  "dataPrincipal": {
    "ref": "<sha256 hash>",
    "emailMasked": "u***r@example.com"
  },
  "noticeVersion": "clxy…",
  "noticeDisplayEventId": "clxa…",
  "withdrawalUrl": "https://…/withdraw/signed/…",
  "purposes": [
    { "purposeId": "cm_abc", "name": "Analytics", "legalBasis": "CONSENT",
      "status": "GRANTED", "retentionDays": 365, "expiresAt": "2027-04-20T08:14:52.231Z" }
  ],
  "signature": "<base64 Ed25519 signature>"
}

Verify a receipt (anywhere)

bash
curl -X POST https://your-domain.com/api/v1/receipt/verify \
  -H "Content-Type: application/json" \
  -d '{ "receipt": { ... the JSON above ... } }'

# => { "valid": true, "issuerKnown": true, "receiptId": "…", "consentTimestamp": "…", "issuer": "Acme Corp" }

Notices & re-consent

Notices are versioned. Each publish is immutable; the active version is what the widget shows and what ConsentRecord.noticeVersionId points at.

  • Proof of notice — each banner render writes a NoticeDisplayEvent keyed by widget session. The consent record back-links to it.
  • 23 translations — mark a translation reviewed to serve it to principals; machine translations stay server-side until human sign-off.
  • Material change → re-consent — publish with requiresReconsent: true and the rights worker flips matching active consents to REQUIRES_RECONSENT in batches of 500.
  • Change flagsNEW_PURPOSE, DATA_CATEGORY_EXPANDED, RETENTION_EXTENDED, etc. Shown to principals in the re-consent email.
  • Grievance officer auto-sync — when dpoSource=ORG_SETTINGS, updating the org-level officer auto-patches every notice that opted in.
ts
// /app/api/notice/publish example shape (dashboard does this for you)
await publishNotice(orgSlug, projectSlug, {
  summary: "We collect …",
  fullContent: "…",
  dataCategories: ["email", "device_id"],
  requiresReconsent: true,
  changeFlags: "NEW_PURPOSE, DATA_CATEGORY_EXPANDED",
});

Data principal rights

Data principals submit requests via the portal. Each falls under one of five DPDP types and inherits a 30-day SLA (§13(3)).

  • ACCESS (§11) — copy of data held.
  • CORRECTION (§12) — update inaccurate fields.
  • ERASURE (§13) — right to be forgotten.
  • NOMINATION (§14) — nominee in case of death / incapacity.
  • GRIEVANCE (§13(3)) — general complaint to the data fiduciary.

Escalation ladder

Hourly worker fires up to four graduated escalations per request; each tier has a unique-per-request row so the worker is replay-safe.

TierTriggerAction
REMINDER≤5 days leftEmail assigned handler (or DPO).
ESCALATED≤2 days leftEmail all OWNER/ADMIN members + Slack.
OVERDUE_FINALpast deadlineAuto-status → OVERDUE, DPO notified.
BREACH_LOGGEDafter OVERDUE_FINALAudit entry for compliance report.

Data principal portal

Every project ships a public portal at /{orgSlug}/{projectSlug} with these pages:

  • /withdraw — manual withdrawal by consent token.
  • /withdraw/signed/:token — one-click withdrawal from the confirmation email.
  • /rights — submit a rights request.
  • /rights/:lookupToken — check status, reply to handler messages.
  • /preferences — adjust per-purpose consent.
  • /export — §13 self-service data export (email → OTP → ZIP).
  • /privacy/notice — public notice (?lang=hi, etc.).
  • /privacy/policy — published privacy policy document.
  • /guardian/:token — guardian dashboard to review/revoke minor consents.

Self-service export ZIP contents

text
consent/
  CNS-abc123.json         — signed Kantara CR v1.1 receipt
  CNS-def456.json
rights/
  RR-xyz789.json          — request + handler messages
README.txt                — plain-text cover note

Children's data (DPDP §9)

When a project opts in, the widget shows an age gate before the banner. Users under the threshold enter a guardian flow that issues a single-use session token the consent API validates before writing the record.

  • Threshold — default 18, configurable 13–21.
  • Minor tracking block (§9(5)) — purposes flagged isTrackingPurpose / isBehavioralMonitoring / isTargetedAdvertising are hard-rejected for minors with HTTP 422.
  • Guardian email OTP — 6-digit code, 5-attempt limit per 24h, 10-minute TTL.
  • Guardian session token — single-use, 30-minute TTL, required in the consent body.
  • Guardian portal/guardian/:token lists all consents approved by the guardian with bulk-revoke.
  • Aged-out worker — daily at 04:00. When a minor turns 18 (if DOB was captured) the record auto-expires and the guardian is notified.
  • Integrity sweep — weekly check for minor records containing blocked purposes; alarms OWNERs.

Compliance tooling

Surfaces aimed at DPDP Board auditors and internal review.

RoPA register

One editable overlay per Purpose with principal categories, sensitivity tier (STANDARD / SENSITIVE / CRITICAL), cross-border flag, retention narrative. Immutable snapshots on every review. CSV and JSON export. 90-day review reminder.

bash
curl "https://your-domain.com/api/ropa/export?orgSlug=acme&projectSlug=web&format=csv" \
  --cookie "<session>"

Vendor / processor register

Per-org register of processors, sub-processors, joint controllers. DataProcessingAgreement lifecycle (DRAFT → ACTIVE → EXPIRED / TERMINATED) with expiry alerts at 30 and 7 days, plus auto-EXPIRE when the deadline passes. VendorAudit rows drive a per-vendor risk score. Each vendor links to any number of purposes via PurposeVendor.

Grievance officer

Org-level contact fields (name / email / phone / address) with an immutable GrievanceOfficerChange log. Notices that opted into ORG_SETTINGS dpoSource auto-patch when the org-level officer changes. Public page at /{orgSlug}/grievance-officer meets §13(3).

Audit hash chain

Every audit log row is hashed into a per-org chain (SHA-256 of previous hash + canonical payload). Weekly anchors snapshot the chain tip for tamper-evidence.

Enterprise auth

Two-factor authentication (TOTP)

Per-user TOTP with 10 bcrypt-hashed backup codes. Secret is AES-256-GCM encrypted with MFA_ENCRYPTION_KEY. Login: password → /api/auth/mfa/precheck /api/auth/mfa/challenge → NextAuth JWT. OWNERs can enforce org-wide with a configurable grace period; non-enrolled users are redirected to /enroll-mfa.

Custom RBAC

Built-in OWNER/ADMIN/VIEWER plus any number of custom roles with 28 permission codes across 10 domains (CONSENT_READ, RIGHTS_HANDLE, BILLING_MANAGE, …).

ts
import { requirePermission } from "@/lib/permissions";

// In a server action / API route, after requireOrgMember():
await requirePermission(userId, orgId, "BILLING_MANAGE");

SSO (SAML / OIDC)

Enterprise-plan only. One SsoConnection per org with SAML metadata URL or OIDC discovery URL + client credentials. Email-domain routing: users whose@company.com matches are bounced straight to the IdP on the login page. JIT provisioning creates the User + Membership on first sign-in with defaultRoleId applied.

SCIM 2.0

Same connection gets a SCIM token via Settings → SCIM. Endpoints:

text
GET    /api/scim/v2/Users          ?filter=userName eq "x@y.com"
POST   /api/scim/v2/Users          provision new
GET    /api/scim/v2/Users/{id}
PUT    /api/scim/v2/Users/{id}
PATCH  /api/scim/v2/Users/{id}     flip active / rename
DELETE /api/scim/v2/Users/{id}     deprovision membership
GET    /api/scim/v2/Groups         org roles as SCIM Groups

Webhooks

Configure outbound webhooks at Project → Webhooks. Each delivery is signed and retried with exponential backoff. Subscribe to any of:

  • consent.given
  • consent.withdrawn
  • consent.expired
  • rights.submitted
  • rights.resolved
json
{
  "event": "consent.given",
  "payload": {
    "consentToken": "CNS-abc123",
    "projectId": "clx…",
    "principalEmail": "user@example.com",
    "purposeIds": ["cm_abc", "cm_def"],
    "givenAt": "2026-04-20T08:14:52.231Z"
  }
}

Rate limits & quotas

Every API key belongs to a rate tier. Limits apply per-key, per-minute, with a sliding window. Enterprise custom limits are set via apiKeyRateCustomMax.

TierRequests / minute
FREE60
BASIC600
PRO6,000
ENTERPRISEcustom

IP allowlist

Optional per-key CIDR allowlist. Requests from IPs outside the list return 403 IP_NOT_ALLOWED. Blocked hits are logged as API_KEY_IP_DENIED.

80% proximity alert

When a key crosses 80% of its minute budget, a webhook fires api_key.rate_warning and an optional Slack message is sent.

Environment variables

For self-hosted deployments. Most have sensible defaults — only a handful are required.

VariableRequiredPurpose
DATABASE_URLyesPostgres 16 connection string.
REDIS_URLyesRedis 7 for queues + caches.
NEXTAUTH_URLyesCanonical app URL.
NEXTAUTH_SECRETyesSession signing + identity/withdrawal tokens.
MFA_ENCRYPTION_KEYif MFA/SSO/receipts32 hex bytes — encrypts TOTP secrets + OIDC client secret + Ed25519 receipt keys.
RESEND_API_KEYyesTransactional email provider.
EXPORT_STORAGE_ROOTnoDirectory for principal export ZIPs. Default .exports/.
AI_API_KEY / AI_BASE_URL / AI_MODELfor AIUsed by document generation + machine translation.
TWILIO_ACCOUNT_SID / ...optionalEnables guardian OTP SMS fallback.

Key rotation

Rotating MFA_ENCRYPTION_KEY invalidates every stored TOTP secret, OIDC client secret, and receipt private key. All users will need to re-enroll MFA, and SSO connections + receipt signing keypairs must be regenerated.

Need something not here?

See the integration guide for framework-specific embeds, or talk to sales about custom deployments.