Consent Docs

Node.js client

Install @piedap/consent on your server, create a ConsentClient and look up every client method with its parameters, return value and errors.

Reference11 min readFor developers using the browser runtime and the server and mobile SDKs

ConsentClient calls the API from your Node.js server with a secret key. Each method is listed here with its parameters, what it returns and the errors it throws.

Requirements#

  • Node.js 18 or later. The package has no dependencies.
  • A secret key, sk_…. See Keys and where to use them.
  • Code that runs on your server. The client refuses to start in a browser.

Install#

Your deployment serves the package, so there is no registry to set up.

Shell
npm install https://console.dpconsent.com/consent/sdk/v1/packages/piedap-consent-1.0.7.tgz

The package works with import and with require.

JavaScript
import { ConsentClient } from '@piedap/consent/server';
// or
const { ConsentClient } = require('@piedap/consent/server');

Create the client#

new ConsentClient()#

Creates a client that keeps no state between calls. Create one when your application starts and share it. Pass one object with these fields:

Parameter Type Required or default Description
apiUrl string Required The deployment address. A trailing slash is removed.
secretKey string Required The secret key, sk_….
timeoutMs number 10000 How long to wait for each attempt.
maxRetries number 2 Further attempts after the first, for calls that are safe to repeat. 0 turns retries off.
fetch typeof fetch The global fetch A replacement fetch, for tests or a proxy.

Returns a ConsentClient.

Errors Throws Error in a browser, when secretKey is missing, and when secretKey starts with pk_.

consent.mjsJavaScript
import { ConsentClient } from '@piedap/consent/server';

export const client = new ConsentClient({
  apiUrl: 'https://console.dpconsent.com/consent',
  secretKey: process.env.PIEDAP_SECRET_KEY,
});

Retries and timeouts#

  • Only calls that are safe to repeat are retried: every GET, and any write you gave an idempotencyKey.
  • A retry follows a network error, a timeout, or a 502, 503 or 504 response.
  • The client waits 400 ms before the second attempt and 800 ms before the third.
  • Every other failure, including 429, is thrown at once.
  • checkConsent() waits one second unless you pass timeoutMs, and never retries.
  • Redirects are never followed, so the key is never sent to another host.
  • Each request sends User-Agent: piedap-consent-node/1.0.7.

Methods#

Every method returns a promise. The scope is what the secret key must hold. Every secret key holds consent:read and consent:write.

Method Calls Scope Fields and responses
resolveForm() GET /sdk/v1/form None Forms
recordConsent() POST /sdk/v1/consent consent:write Consent
getConsent() GET /sdk/v1/consent/{consentId} consent:read Consent
updateConsent() PATCH /sdk/v1/consent/{consentId} consent:write Consent
withdrawConsent() POST /sdk/v1/consent/{consentId}/withdraw consent:write Consent
receipt() GET /sdk/v1/consent/{consentId}/verify consent:read Receipts
publicKeys() GET /sdk/v1/keys None Receipts
startEmailConsent() POST /sdk/v1/email-consent consent:write Email consent
recordActivity() POST /sdk/v1/activity consent:write Activity
consentPortalLink() POST /sdk/v1/pages/consent-portal-links consent:write Hosted page links
unsubscribeLink() POST /sdk/v1/pages/unsubscribe-links None Hosted page links
findSubject() POST /sdk/v1/subjects/find subjects:read People
exportSubject() POST /sdk/v1/subjects/export subjects:export People
withdrawSubject() POST /sdk/v1/subjects/withdraw subjects:withdraw People
eraseSubject() POST /sdk/v1/subjects/erase subjects:erase People
linkIdentifiers() POST /sdk/v1/subjects/link subjects:link People
linkRequest() POST /sdk/v1/requests/link requests:write Rights requests
checkConsent() POST /sdk/v1/consent/check consent:check Consent check

Every method throws ConsentApiError when a call fails. A key without the scope gets the code SCOPE_REQUIRED.

resolveForm()#

Returns the published form for a place or a form key, with its purposes. The params object and each of its fields are optional.

Parameter Type Required or default Description
params.country string Detected from the request Two-letter country code. Your server makes the request, so pass the visitor's country.
params.subdivision string None State or province code.
params.formKey string Chosen by region rules Serves one form instead of resolving by region.
params.locale string None The language to ask for.
params.consentId string None An existing consent, whose state is returned alongside.

Returns Promise<Record<string, any>>, the API's JSON. form is null when no form is served for that place.

Errors Throws ConsentApiError, with the code FORM_NOT_FOUND when no form has that formKey.

JavaScript
const { form } = await client.resolveForm({
  formKey: 'kalpa-account',
  country: 'IN',
});
const keys = form.purposes.filter((p) => !p.alwaysOn).map((p) => p.key);

recordConsent()#

Records one person's answers to a form. Pass one object with these fields:

Parameter Type Required or default Description
formKey string Required, unless formId is given The form's key.
formId string Required, unless formKey is given The form's id.
subjectId string Required The person's identifier.
subjectType SubjectType COOKIE_ID The kind of identifier. Always pass it for an email address, a phone number or your own id.
decisions DecisionInput[] Required One answer for every purpose that is not always on.
countryCode string Detected from the request Where the consent was collected. Without it the server locates your server, not the person.
subdivisionCode string None State or province code.
locale string None The language the form was shown in.
pageUrl string None The page the answer was given on. Up to 500 characters.
metadata Record<string, unknown> None Your own fields, stored with the record. Up to 8,192 bytes of JSON.
collectedAt string The time of the call When the consent was given, as an ISO 8601 time. Not in the future and not before 1990.
basis 'PRE_COMMENCEMENT' None With collectedAt: your statement that the person consented before the DPDP Act commenced.
noticeReference string None With collectedAt: the name or number of the notice the person saw. Never personal data.
noticeSha256 string None With collectedAt: the SHA-256 of that notice, 64 hex characters.
expiresAt string The form's term from the call With collectedAt: when this consent ends.
lastActivityAt string The time of the call With collectedAt: when the person last used your service.
capture CaptureInput None How consent was taken in a branch, by phone or on paper. Sealed with this write only.
interaction string API, or IMPORT with collectedAt The control that produced the answer, such as SAVE_PREFERENCES.
identifiers LinkedIdentifier[] None Up to five other identifiers to find the record by.
dataPrincipalType string None CHILD or PERSON_WITH_DISABILITY, when a parent or lawful guardian consents for the person.
guardian Guardian None How the parent or guardian was verified.
ageDeclaration { eighteenOrOver: boolean } None The person's own answer to "Are you 18 or older?". false records the person as a child, and a grant then needs guardian.
idempotencyKey string None A unique key for this write. It makes the call safe to retry.

Returns Promise<ConsentRecord>, the record as stored.

Errors Throws ConsentApiError. The code is INVALID_PURPOSE_DECISIONS when a purpose is not answered or not on the form. It is INVALID_PARTNER_DECISIONS for a partner answer the form version does not offer. A write with collectedAt that the records overrule throws with status 409 and one of five codes; isSkippedImport() tells them apart. See Bring in consent given earlier.

JavaScript
const record = await client.recordConsent({
  formKey: 'kalpa-account',
  subjectId: 'usr_8f2K1xQz',
  subjectType: 'USER_ID',
  countryCode: 'IN',
  interaction: 'SAVE_PREFERENCES',
  decisions: [
    { purposeKey: 'course-updates-by-email', granted: true },
    { purposeKey: 'class-reminders-by-sms', granted: false },
  ],
  identifiers: [
    { subjectType: 'EMAIL', subjectId: 'kavya.menon@example.in' },
  ],
  idempotencyKey: 'signup-usr_8f2K1xQz',
});

See Record consent.

getConsent()#

Returns one record by its id.

Parameter Type Required or default Description
consentId string Required The record's id, cns_….

Returns Promise<ConsentRecord | null>. The object also carries metadata, your own fields, and captureContext when the current state has one.

Errors Throws ConsentApiError, except for a 404. The method then returns null: the id is not known, or the record's expiresOn has passed.

JavaScript
const record = await client.getConsent('cns_3f1c9a7e2b8d4c6fa0e5d1b7c9f2a4e8');
const mayEmail = record?.decisions.some(
  (d) => d.purposeKey === 'course-updates-by-email' && d.granted,
);

Check status as well. A PENDING record has not been confirmed yet.

updateConsent()#

Changes answers on an existing record.

Parameter Type Required or default Description
consentId string Required The record to change.
decisions DecisionInput[] Required Only the purposes that changed. The others keep their answer.
options.pageUrl string None The page the change was made on.
options.interaction string None The control that produced the change, such as PREFERENCE_CENTRE.
options.identifiers LinkedIdentifier[] None More identifiers to link to the record.
options.capture CaptureInput None How this change was taken. A change without it clears the record's capture context; the earlier event keeps it.

Returns Promise<ConsentRecord>, the record as updated.

Errors Throws ConsentApiError. The code is CONSENT_RECORD_NOT_FOUND for an unknown id and CONSENT_RECORD_ERASED for an erased record. The call takes no idempotency key, so it is never retried.

JavaScript
await client.updateConsent(
  'cns_3f1c9a7e2b8d4c6fa0e5d1b7c9f2a4e8',
  [{ purposeKey: 'class-reminders-by-sms', granted: true }],
  { interaction: 'PREFERENCE_CENTRE' },
);

withdrawConsent()#

Withdraws one record.

Parameter Type Required or default Description
consentId string Required The record to withdraw.
reason string None Stored on the withdrawal.

Returns Promise<ConsentRecord>. Withdrawing twice is safe: the second call returns the record unchanged.

Errors Throws ConsentApiError, with the code CONSENT_RECORD_NOT_FOUND for an unknown id.

JavaScript
await client.withdrawConsent(
  'cns_3f1c9a7e2b8d4c6fa0e5d1b7c9f2a4e8',
  'Asked by phone',
);

receipt()#

Returns the signed receipt for one record.

Parameter Type Required or default Description
consentId string Required The record's id.

Returns Promise<SignedReceipt>. The server's verdict on the proof chain is at receipt.proof.verification.valid.

Errors Throws ConsentApiError, with the code CONSENT_RECORD_NOT_FOUND for an unknown id.

JavaScript
const signed = await client.receipt('cns_3f1c9a7e2b8d4c6fa0e5d1b7c9f2a4e8');
console.log(signed.receipt.proof.verification.valid);

publicKeys()#

Returns the deployment's public signing keys, for verifyReceipt().

Takes no parameters.

Returns Promise<{ keys: Jwk[] }>. The keys change rarely, so fetch them once and keep them.

Errors Throws ConsentApiError.

JavaScript
const keys = await client.publicKeys();

startEmailConsent()#

Starts a double opt-in for an email address. Pass one object with these fields:

Parameter Type Required or default Description
email string Required The address to confirm.
formKey string Required The form the person answered.
decisions DecisionInput[] Required The answers to hold until the person confirms.
locale string None The language the form was shown in.
returnUrl string None Where the person lands after confirming.
idempotencyKey string None A unique key for this write.

Returns Promise<{ consentId, status, verificationUrl, expiresOn }>. status is PENDING. Send verificationUrl to the address in your own email.

Errors Throws ConsentApiError.

JavaScript
const pending = await client.startEmailConsent({
  email: 'rohan.das@example.in',
  formKey: 'newsletter',
  decisions: [{ purposeKey: 'newsletter', granted: true }],
  returnUrl: 'https://www.example.in/subscribed',
});

See Email double opt-in.

recordActivity()#

Reports that a person signed in or used your service.

Parameter Type Required or default Description
subjectId string Required The person's identifier.
subjectType SubjectType None The kind of identifier. Pass it whenever the identifier is not an email address.

Returns Promise<{ records: number, activeOn: string }>.

Errors Throws ConsentApiError.

JavaScript
await client.recordActivity('usr_8f2K1xQz', 'USER_ID');

Creates a seven-day link to the hosted page where the person reviews, withdraws and downloads their consents.

Parameter Type Required or default Description
subjectId string Required The person's identifier.
subjectType SubjectType None The kind of identifier. Pass it whenever the identifier is not an email address.

Returns Promise<{ url: string, expiresOn: string }>.

Errors Throws ConsentApiError.

JavaScript
const { url } = await client.consentPortalLink('usr_8f2K1xQz', 'USER_ID');

See Hosted pages.

Creates a one-click unsubscribe link for a record, with the headers to add to your messages.

Parameter Type Required or default Description
consentId string Required The record's id.

Returns Promise<{ consentId, url, headers }>. headers holds List-Unsubscribe and List-Unsubscribe-Post.

Errors Throws ConsentApiError.

JavaScript
const { url, headers } = await client.unsubscribeLink(record.consentId);

findSubject()#

Returns every record held for a person.

Parameter Type Required or default Description
subjectId string Required The person's identifier.
subjectType SubjectType Required The kind of identifier.

Returns Promise<SubjectRecords>. Each record's matchedBy lists the identifier types that matched.

Errors Throws ConsentApiError.

JavaScript
const { records } = await client.findSubject('+919846011223', 'PHONE');

See Find a person.

exportSubject()#

Returns signed receipts and a processing summary for an access request.

Parameter Type Required or default Description
subjectId string Required The person's identifier.
subjectType SubjectType Required The kind of identifier.

Returns Promise<AccessPackage>.

Errors Throws ConsentApiError.

JavaScript
const pack = await client.exportSubject('usr_8f2K1xQz', 'USER_ID');

See Answer an access request.

withdrawSubject()#

Withdraws every consent the person has given, linked records included.

Parameter Type Required or default Description
subjectId string Required The person's identifier.
subjectType SubjectType Required The kind of identifier.
options.reason string None Stored on the withdrawal.
options.idempotencyKey string None A unique key for this write.

Returns Promise<{ withdrawn: number }>, the number of records withdrawn.

Errors Throws ConsentApiError.

JavaScript
await client.withdrawSubject('usr_8f2K1xQz', 'USER_ID', {
  reason: 'Account closed',
  idempotencyKey: 'close-usr_8f2K1xQz',
});

eraseSubject()#

Erases the person's identifiers on every record, linked records included, and removes the links.

Parameter Type Required or default Description
subjectId string Required The person's identifier.
subjectType SubjectType Required The kind of identifier.
options.reason string None Stored with the erasure.
options.idempotencyKey string None A unique key for this write.

Returns Promise<{ erased: number }>, the number of records erased.

Errors Throws ConsentApiError.

JavaScript
await client.eraseSubject('usr_8f2K1xQz', 'USER_ID', {
  reason: 'Erasure requested by the learner',
  idempotencyKey: 'erase-usr_8f2K1xQz',
});

See Withdraw and erase.

linkIdentifiers()#

Links more identifiers to every live record of a person.

Parameter Type Required or default Description
subjectId string Required The identifier the records are kept under.
subjectType SubjectType Required The kind of identifier.
identifiers LinkedIdentifier[] Required Up to five identifiers. Any proof is left out: a secret key needs none.
options.idempotencyKey string None A unique key for this write.

Returns Promise<{ records: number, added: number }>: records linked, and links added.

Errors Throws ConsentApiError.

JavaScript
await client.linkIdentifiers('usr_8f2K1xQz', 'USER_ID', [
  { subjectType: 'PHONE', subjectId: '+919846099887' },
]);

See Identifiers.

linkRequest()#

Points a rights request at the identifier the person's records are kept under.

Parameter Type Required or default Description
reference string Required The request's reference, such as DSR-2026-4F2A91.
subjectId string Required The person's identifier.
subjectType SubjectType Required The kind of identifier.

Returns Promise<{ reference: string, linkedRecordCount: number }>.

Errors Throws ConsentApiError, with the code REQUEST_NOT_FOUND when no request in the project has that reference.

JavaScript
await client.linkRequest('DSR-2026-4F2A91', 'usr_8f2K1xQz', 'USER_ID');

checkConsent()#

Asks whether one person allows a purpose now. Pass one object with these fields, and an optional second object:

Parameter Type Required or default Description
subjectId string Required The person's identifier.
subjectType SubjectType Required The kind of identifier.
purposeKey string Required The purpose to check.
partnerKey string None One partner the purpose lists.
alsoKnownAs { subjectType, subjectId }[] None Up to four other identifiers of the same person.
options.timeoutMs number 1000 How long to wait for the answer.

Returns Promise<ConsentCheckResult>. allowed is true only when the server answered granted. When no answer arrived, the result is unavailable and never allowed:

What happened cause status
No answer within timeoutMs timeout null
A network error network null
429, the check or key budget rate_limited 429, with retryAfter
500 or above server_error The status

Errors Throws ConsentApiError for 400, 401, 403 and 404, such as SCOPE_REQUIRED or PURPOSE_NOT_FOUND. Throws Error before any request when alsoKnownAs has more than four entries.

JavaScript
const check = await client.checkConsent({
  subjectId: 'CIF00412873',
  subjectType: 'USER_ID',
  alsoKnownAs: [{ subjectType: 'PHONE', subjectId: '+919846011223' }],
  purposeKey: 'offers-by-sms',
});
if (check.allowed) await sendOffer(check.basedOn[0].consentId);

See Check consent before you process.

Types#

TypeScript
type SubjectType =
  | 'COOKIE_ID' | 'SESSION_ID' | 'EMAIL'
  | 'USER_ID' | 'PHONE' | 'DEVICE_ID';

interface DecisionInput {
  purposeKey: string;
  granted: boolean;
  partners?: Record<string, boolean>; // a partner left out follows granted
}

interface LinkedIdentifier {
  subjectType: SubjectType;
  subjectId: string;
  proof?: string; // only for writes from a browser
}

interface Guardian {
  relationship: 'PARENT' | 'LAWFUL_GUARDIAN';
  verification:
    | 'EXISTING_ACCOUNT' | 'VIRTUAL_TOKEN'
    | 'IDENTITY_DOCUMENT' | 'GUARDIANSHIP_ORDER';
  reference: string;
}

interface ConsentRecord {
  consentId: string;
  status:
    | 'GRANTED' | 'PARTIAL' | 'DENIED'
    | 'WITHDRAWN' | 'EXPIRED' | 'PENDING';
  formId: string;
  formVersion: number;
  formContentHash: string;
  decisions: {
    purposeKey: string;
    purposeName: string;
    category: string;
    granted: boolean;
    partners?: Record<string, boolean>;
  }[];
  test: boolean; // written with a test key
  countryCode: string | null;
  subdivisionCode: string | null;
  lawCode: string | null;
  collectedOn: string;
  expiresOn: string | null;
  withdrawnOn: string | null;
  proofHash: string;
}

interface SubjectRecords {
  subjectType: SubjectType;
  fingerprint: string;
  records: (ConsentRecord & {
    erasedOn: string | null;
    matchedBy: SubjectType[];
  })[];
}

interface AccessPackage {
  subjectType: SubjectType;
  generatedOn: string;
  summary: Record<string, any>;
  contact: Record<string, any>;
  receipts: SignedReceipt[];
}

interface SignedReceipt {
  receipt: Record<string, any>;
  signature: string;
  algorithm: 'Ed25519' | 'HMAC-SHA256';
  keyId?: string;
  publicKeysUrl?: string;
}

interface Jwk { kid: string; kty: string; crv: string; x: string }

type CaptureChannel =
  | 'BRANCH' | 'CALL_CENTRE' | 'IVR'
  | 'KIOSK' | 'FIELD_AGENT' | 'PAPER';
type CaptureMethod =
  | 'OTP' | 'SIGNED_DECLARATION' | 'RECORDED_CALL'
  | 'PAPER_FORM' | 'LINK_CONFIRMED' | 'AGENT_ATTESTED';

interface CaptureInput {
  channel: CaptureChannel;
  method: CaptureMethod;
  agentRef?: string; // stored only as a keyed hash
  locationRef?: string; // stored only as a keyed hash
  methodReference?: string; // never personal data
  documentSha256?: string;
}

interface CaptureContext {
  channel: CaptureChannel;
  method: CaptureMethod;
  agentRefHash?: string;
  locationRefHash?: string;
  methodReference?: string;
  documentSha256?: string;
  attestedByStaffOnly: boolean;
}

// ConsentRecord also has, when set:
//   basis?: 'PRE_COMMENCEMENT';
//   noticeReference?: string;
//   noticeSha256?: string;
//   captureContext?: CaptureContext; // secret-key responses only

type ConsentCheckReason =
  | 'granted' | 'refused' | 'withdrawn' | 'expired'
  | 'pending_confirmation' | 'erased' | 'no_record'
  | 'not_consent_based';

interface ConsentCheckBasis {
  consentId: string | null;
  proofHash: string | null;
  state:
    | 'granted' | 'refused' | 'withdrawn'
    | 'expired' | 'erased' | 'pending_confirmation';
  decidedAt: string | null;
  matchedBy: SubjectType[];
  attestedByStaffOnly: boolean;
}

interface ConsentCheck {
  unavailable: false;
  allowed: boolean;
  reason: ConsentCheckReason;
  recorded: Exclude<ConsentCheckReason, 'not_consent_based'>;
  purpose: {
    key: string;
    category: string;
    declaredBasis: string;
    legitimateUse: string | null;
    consentRequired: boolean;
  };
  partnerKey: string | null;
  validUntil: string | null;
  basedOn: ConsentCheckBasis[];
  notes: (
    | 'PARTNER_NOT_OFFERED' | 'PHONE_NOT_E164'
    | 'ATTESTED_BY_STAFF_ONLY' | 'WITHDRAWAL_IN_PROGRESS'
  )[];
  checkedAt: string;
  test: boolean;
}

interface ConsentCheckUnavailable {
  unavailable: true;
  allowed: false;
  reason: 'unavailable';
  cause: 'timeout' | 'network' | 'rate_limited' | 'server_error';
  status: number | null;
  retryAfter: number | null;
}

type ConsentCheckResult = ConsentCheck | ConsentCheckUnavailable;

Guardian is not an exported name. It is shown here for the shape of guardian.

Errors#

A failed call throws ConsentApiError.

Field Type Description
name string Always ConsentApiError.
message string The server's message. Several validation messages are joined with ; .
status number The HTTP status. 0 when no response arrived: message is then Request timed out or Network error.
code string | null The server's code, such as SCOPE_REQUIRED. Not every error has one. With no response, TIMEOUT or NETWORK_ERROR.
retryAfter number | null Seconds from the Retry-After header, set on a 429.
JavaScript
import { ConsentApiError } from '@piedap/consent/server';

try {
  await client.eraseSubject(user.id, 'USER_ID');
} catch (error) {
  if (!(error instanceof ConsentApiError)) throw error;
  if (error.status === 429) {
    await retryLater(error.retryAfter ?? 60);
  } else if (error.code === 'SCOPE_REQUIRED') {
    // grant subjects:erase to the key in the console
  } else {
    throw error;
  }
}

Every code is listed in Errors.

↑↓ to move, Enter to open