Recording consent proves what someone agreed to. It says nothing about what your application actually did. The Consent Gate is a server-to-server call your backend makes before it processes — so “we only use data people consented to” becomes something enforced at a chokepoint and counted, rather than asserted.
One authoritative answer for every purpose, every principal — instead of a consent flag copied into four services that drift apart.
Cached both sides. Most calls never reach the database, so gating a request costs less than the branch you would otherwise write.
Every check is counted. You can show the Board how many times the application asked and how many times it was refused.
The SDK is a single zero-dependency file. The design rule is that calling the gate must be less code than not calling it.
import { DpdpConsent } from "./dpdp-consent";
const dpdp = new DpdpConsent({ apiKey: process.env.DPDP_API_KEY! });
// One principal, one purpose
if (await dpdp.allows({ externalId: user.id }, "marketing")) {
await sendCampaignEmail(user);
}
// The case that actually bites: filter before a bulk send
const sendable = await dpdp.filterAllowed(allUsers, (u) => u.id, "marketing");
await esp.send(sendable);A POST, not a GET — the principal identifier is personal data and must not end up in access logs, proxy caches or browser history as a query string.
POST /api/v1/consent/check
Authorization: Bearer sk_live_...
Content-Type: application/json
{
"externalId": "usr_8812",
"purposes": ["marketing", "personalisation"]
}{
"resolvedBy": "externalId",
"found": true,
"decisions": {
"marketing": { "allowed": false, "reason": "WITHDRAWN", "purposeId": "cl…" },
"personalisation": { "allowed": true, "reason": "GRANTED", "purposeId": "cl…" }
},
"checkedAt": "2026-07-26T09:14:22.104Z"
}The gate never answers with a bare boolean. You always get why, so a refusal is actionable and a support ticket is answerable.
| Reason | Outcome | Meaning |
|---|---|---|
| GRANTED | allow | Consent is on record, live, and covers this purpose. |
| LEGITIMATE_USE | allow | The purpose rests on a Section 7 legitimate use, so consent is not the basis. |
| DENIED | deny | The principal was asked and did not agree. |
| WITHDRAWN | deny | Consent was given and later withdrawn (Section 6(4)). |
| EXPIRED | deny | The validity period lapsed, or a minor aged out. |
| REQUIRES_RECONSENT | deny | The notice changed materially; consent must be collected again. |
| NO_RECORD | deny | No consent record exists for this principal in this project. |
| BLOCKED_MINOR | deny | A child account and a tracking, behavioural-monitoring or targeted-advertising purpose (Section 9(3)). |
| UNKNOWN_PURPOSE | deny | The purpose named does not exist in this project. |
Note LEGITIMATE_USE: a purpose carried on a Section 7 legitimate use is lawful without consent, so answering “denied for want of consent” would simply be wrong. And note that BLOCKED_MINOR is evaluated before legal basis — Section 9(3) is a prohibition, not a question of which basis applies, so no guardian approval unlocks it.
If the gate is unreachable, the SDK denies. That is the safe default: not being able to establish that processing is permitted is a reason to stop, not to continue. You can opt a specific purpose into onError: "allow" where an outage blocking traffic is the worse outcome — but you should decide that deliberately, per purpose, and write down why.
No, and any vendor claiming otherwise is overselling. Your engineers can always query your own database directly. What the gate provides is a single authoritative answer, a chokepoint that is cheaper to use than to bypass, and a decision log that makes not asking visible by its absence. Under Section 8(2) of the DPDP Act the Data Fiduciary remains responsible for compliance regardless of any contract with a processor — so the accountability stays with you either way.
The SDK denies by default. If you cannot establish that processing is permitted, you should not process. You can set onError to 'allow' for specific purposes where an outage blocking traffic is the worse outcome, but that is a trade you should make deliberately and document.
The SDK caches per principal for 15 seconds by default, and the server caches a resolved principal snapshot for 30 seconds, so most calls never touch the database. A cold call is a single indexed query.
Immediately. Withdrawing consent drops the cached snapshot for that principal, so the very next check reflects it. The cache TTL is only the backstop if that invalidation fails.
No. Decisions are counted in hourly buckets per project and purpose. A row per check would name a data principal every time, rebuilding exactly the personal-data pile this API exists to avoid — and it would be the largest table in the system within a week. The evidentiary value is in the counts.
Yes, and this is the case that matters most. A single forgotten check is rarely the problem; a nightly campaign job that selects every user and hands the lot to an email provider is. Use filterAllowed() to narrow the list first.
Add the gate to one purpose — the one that would hurt most to get wrong — and see the decision log fill up.
Get Started — It's FreeNo credit card required · Setup in under 10 minutes