JavaScript SDK
Embed DPDPA-compliant consent management on any website in minutes. The SDK handles widget rendering, consent recording, multilingual support, and proof storage automatically.
Installation
<script src="https://api.dpdpashield.in/sdk/dpdpashield-sdk.js"></script>Add this tag before your closing </body>. The SDK exposes a global DPDPAShield object.
Quick Start
Minimal working integration for the two most common setups.
Next.js App Router
import Script from 'next/script'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<Script
src="https://api.dpdpashield.in/sdk/dpdpashield-sdk.js"
strategy="afterInteractive"
/>
<Script
id="dpdpa-init"
strategy="afterInteractive"
dangerouslySetInnerHTML={{ __html: `
(function() {
function init() {
if (typeof DPDPAShield === 'undefined') {
setTimeout(init, 100);
return;
}
DPDPAShield.init({
apiKey: '${process.env.NEXT_PUBLIC_DPDPA_API_KEY}',
position: 'bottom-right'
});
}
init();
})();
`}}
/>
</body>
</html>
)
}Plain HTML
<!DOCTYPE html>
<html>
<body>
<!-- your content -->
<script src="https://api.dpdpashield.in/sdk/dpdpashield-sdk.js"></script>
<script>
DPDPAShield.init({
apiKey: 'dpdpa_live_YOUR_KEY_HERE',
position: 'bottom-right'
});
</script>
</body>
</html>Your API key is in Settings → API Keys. Keys starting with dpdpa_live_ are safe to embed publicly - they only allow consent recording, not account access.
Configuration Reference
DPDPAShield.init(config)| Parameter | Type | Required | Description |
|---|---|---|---|
apiKey | string | Required | Your tenant SDK key from Settings → API Keys. Starts with dpdpa_live_. |
position | string | Optional | Widget anchor position. One of: bottom-right | bottom-left | bottom-center | top-centerDefault: bottom-right |
showBadge | boolean | Optional | Show a persistent 36px shield button after the widget is dismissed. Users can click it to re-open the widget at any time.Default: true |
demo | boolean | Optional | Demo mode - loads a sample notice without making API calls. Useful for testing widget layout before your notice is configured.Default: false |
onConsent | function | Optional | Callback fired on every consent action. Called twice: once optimistically and once after API confirmation. See the onConsent section below. |
noticeId | string | Optional | UUID of the specific consent notice to display. Required when you have multiple published notices (e.g. one per product/website). Find this on Consent → Notices - click the copy icon next to any notice. Defaults to your most recently published notice when omitted. |
identifierHash | string | Optional | Pre-computed SHA-256 hex hash of the logged-in user's identifier (email/phone/userId), produced by your backend server. When provided, consent is attributed to this hash instead of the visitor's IP address. Must be exactly 64 lowercase hex characters. See Consent Attribution below. |
Consent Attribution
DPDPA consent records must be attributable to a specific data principal. DPDPA Shield supports three attribution paths depending on whether the visitor is anonymous or identified.
Client-side hash (recommended for logged-in users)
Your backend hashes the user's email server-side and injects it into the page. The SDK sends the hash to our API. No raw email ever travels over the network.
Server-to-server (most secure)
Your backend calls POST /consent/sdk-record directly with the raw identifier. We hash it server-side. Best for server-rendered pages or when you control the backend flow entirely.
Anonymous fallback (automatic)
No identifier provided - we use the visitor's real IP address as the identifier and hash it. Records are attributed to the IP. Suitable for anonymous pre-login visitors.
Path 1 - Client-side hash
Your backend generates the SHA-256 hash and injects it as a template variable. The raw email never appears in the browser.
import { createHash } from 'crypto'
// Hash must be SHA-256 of trimmed, lowercased identifier
function hashIdentifier(email: string): string {
return createHash('sha256')
.update(email.trim().toLowerCase())
.digest('hex')
}
// Inject into your page template
const identifierHash = hashIdentifier(currentUser.email)// currentUser.identifierHash is injected by your server-side template
DPDPAShield.init({
apiKey: 'dpdpa_live_YOUR_KEY',
identifierHash: '{{ currentUser.identifierHash }}',
// ^ 64-char hex string, e.g. "a3f9c2e1d8b7..."
});
// For anonymous visitors, omit identifierHash entirely:
DPDPAShield.init({
apiKey: 'dpdpa_live_YOUR_KEY',
// no identifierHash → falls back to IP attribution
});Path 2 - Server-to-server
Call POST /api/v1/consent/sdk-record from your backend with the raw identifier. We hash it server-side. Use this when your consent event is triggered server-side (e.g. after form submission) rather than in the browser.
await fetch('https://api.dpdpashield.in/api/v1/consent/sdk-record', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
apiKey: process.env.DPDPA_API_KEY,
identifier: user.email, // we hash this server-side
purposes: {
'purpose-uuid-1': true,
'purpose-uuid-2': false,
},
language: 'EN',
positionShown: 'bottom-right',
noticeId: 'your-notice-uuid', // optional - find this on Consent Notices page (copy ID button)
}),
})Important - hash consistency
Always normalise the identifier before hashing: identifier.trim().toLowerCase(). This is what DPDPA Shield does internally. If your hash and ours don't match, consent lookup by email will fail. Test with a known email: sha256("test@example.com") should produce 973dfe0d....
Looking up consent for a known user
Once consent is attributed via any path, your DPO can look it up by typing the full email in the Consent Records search box - we hash it and match. Partial email search is not supported (hashes can't be partially matched).
To look up programmatically, hash the email and call GET /api/v1/consent/status/:hash.
The onConsent callback
Called twice on every consent action - immediately (optimistic, before the API responds) and again after the API confirms the record. Use the second call for any server-side logic since only it has a consentRecordId.
DPDPAShield.init({
apiKey: 'dpdpa_live_xxx',
onConsent: function(result) {
// First call: result.consentRecordId is undefined
// Second call: consentRecordId is the confirmed UUID
console.log(result.status);
// 'ACCEPTED' | 'REJECTED' | 'PARTIAL'
console.log(result.consents);
// { [purposeId]: boolean }
// e.g. { 'purp_abc': true, 'purp_def': false }
console.log(result.language);
// 'EN' | 'HI' | 'TA' | ...
console.log(result.consentRecordId);
// UUID string - only set on second (confirmed) call
}
});Status values
ACCEPTEDAll non-required purposes accepted.REJECTEDAll non-required purposes rejected.PARTIALMixed - some accepted, some rejected.Reading consent state
The SDK writes consent state to localStorage after every user action. Read it to gate analytics, tracking, or other third-party features on your page.
const API_KEY = 'dpdpa_live_YOUR_KEY';
// Top-level consent record
// Shape: { id: string, status: string, ts: number, lang: string }
const consent = JSON.parse(
localStorage.getItem('dpdpa_consent_' + API_KEY) || 'null'
);
// Per-purpose breakdown - written by the SDK automatically
// after the API confirms the consent record.
// Shape: { [purposeId]: boolean }
const purposes = JSON.parse(
localStorage.getItem('dpdpa_purposes_' + API_KEY) || '{}'
);
// Check a specific purpose before loading third-party scripts
function isPurposeConsented(purposeId) {
try {
const p = JSON.parse(
localStorage.getItem('dpdpa_purposes_' + API_KEY) || '{}'
);
return p[purposeId] === true;
} catch {
return false;
}
}
// Gate analytics behind consent
if (isPurposeConsented('YOUR_ANALYTICS_PURPOSE_ID')) {
loadGoogleAnalytics();
}localStorage keys
dpdpa_consent_{apiKey}Top-level record. Shape: { id, status, ts, lang }.dpdpa_purposes_{apiKey}Per-purpose breakdown. Shape: { [purposeId]: boolean }. Written by the SDK after API confirmation.dpdpa_lang_{apiKey}User's selected widget language. Falls back to browser language.Suppressing the widget on specific pages
Authentication and checkout pages often have their own consent flows. Suppress the widget on those paths with a path-check before calling init().
(function() {
var skipPaths = [
'/auth/sign-up',
'/auth/login',
'/auth/verify-email',
'/checkout',
'/order-confirmation',
];
var currentPath = window.location.pathname;
var shouldSkip = skipPaths.some(function(p) {
return currentPath.startsWith(p);
});
if (shouldSkip) return;
DPDPAShield.init({
apiKey: 'dpdpa_live_YOUR_KEY_HERE',
position: 'bottom-right'
});
})();22 Indian languages
The widget automatically renders in the user's language when a translation exists. A built-in language switcher lets users override at any time. Add translations in Consent → Notices → Manage Translations.
ENEnglishHIHindi (हिन्दी)TATamil (தமிழ்)BNBengali (বাংলা)TETelugu (తెలుగు)MRMarathi (मराठी)GUGujarati (ગુજરાતી)KAKannada (ಕನ್ನಡ)MLMalayalam (മലയാളം)PAPunjabi (ਪੰਜਾਬੀ)OROdia (ଓଡ଼ିଆ)ASAssamese (অসমীয়া)URUrdu (اردو)KOKonkani (कोंकणी)SASanskrit (संस्कृतम्)SYSindhi (سنڌي)NSONepali (नेपाली)BODBodo (བོད་)SITSanthali (ᱥᱟᱱᱛᱟᱲᱤ)MAIMaithili (मैथिली)DOGDogri (डोगरी)The SDK falls back to English if a translation is unavailable for the user's language. Languages that have no translation are hidden from the widget's language picker.
Endpoints used by the SDK
These endpoints are called automatically - you do not call them directly in normal usage. They use X-API-Key header authentication, not Bearer tokens.
/api/v1/consent/public-noticeFetches the active published consent notice for this tenant, including all purposes, translations, available languages, and DPDPA Rule 3 disclosure fields.
Query
apiKey=dpdpa_live_xxxResponse
{ data: { id, title, purposes[], translations, availableLanguages, contactEmail, complaintText, withdrawalUrl } }/api/v1/consent/sdk-recordRecords a consent decision. Called automatically by the SDK on every Accept / Reject All / Save Preferences action. Also callable server-to-server from your backend. Creates an immutable ConsentRecord and stores a SHA-256 proof in S3.
Body
{
apiKey: string, // required
status: 'ACCEPTED' | 'REJECTED' | 'PARTIAL',
purposes: { [purposeId]: boolean },
language: string,
positionShown: string,
userAgent: string,
url: string,
// Identity attribution (optional - choose one):
identifierHash?: string, // path 1: 64-char SHA-256 hex
// pre-hashed by org backend
identifier?: string, // path 2: raw email/phone/userId
// hashed server-side by DPDPA Shield
// If neither provided → falls back to client IP
}Response
{
data: {
consentRecordId: string,
proofHash: string,
status: "recorded"
}
}/api/v1/consent/withdraw-publicRecords a consent withdrawal. Called by the Rights Portal withdrawal tab when a data principal submits their email. Rate-limited to 5 requests per IP per hour.
Body
{
apiKey: string,
email: string,
purposeIds?: string[],
withdrawalReason?: string
}Response
{
data: {
withdrawalId: string | null, // null if email has no active record
withdrawnAt: string,
withdrawnCount: number, // 0 if email has no active record
isFullWithdrawal: boolean,
purposesWithdrawn: string[]
}
}Common issues
On this page