Python client
Install the piedap-consent package, create a ConsentClient and look up every client method, model and exception.
ConsentClient calls the API from your Python server with a secret key. Each method is listed here with its parameters, what it returns and the errors it raises.
Requirements#
- Python 3.9 or later. The package uses only the standard library and is fully typed.
- A secret key,
sk_…. See Keys and where to use them.
Install#
Your deployment serves the wheel, version 1.0.4.
PACKAGES=https://console.dpconsent.com/consent/sdk/v1/packages
pip install $PACKAGES/piedap_consent-1.0.4-py3-none-any.whlThe distribution is named piedap-consent. The module you import is piedap_consent.
Create the client#
ConsentClient()#
Creates a client that holds no open connections and no state that changes. Create one at start-up and share it, across threads too.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
api_url |
str |
Required | The deployment address: an absolute http or https address with no query string. |
secret_key |
str |
Required | The secret key, sk_…. Surrounding spaces and a trailing newline are removed. |
timeout |
float |
10.0 |
Seconds to wait for the connection, and for each read from it. |
max_retries |
int |
2 |
Further attempts after the first, for calls that are safe to repeat. 0 turns retries off. Keyword only. |
backoff_factor |
float |
0.25 |
Seconds before the first retry. Each later retry waits twice as long, plus a little jitter. Keyword only. |
Returns a ConsentClient. Its api_url property holds the address requests are sent to.
Errors Raises ValueError when secret_key is empty or a publishable key, pk_…, and when another argument is not valid.
import os
from piedap_consent import ConsentClient
client = ConsentClient(
api_url="https://console.dpconsent.com/consent",
secret_key=os.environ["PIEDAP_SECRET_KEY"],
)Retries and timeouts#
- Only calls that are safe to repeat are retried: every
GET, and any write you gave anidempotency_key. - A retry follows a connection failure, a timeout, or a
502,503or504response. - Every other failure, including
429, is raised at once. check_consent()waits one second unless you passtimeout, and never retries.- Redirects are never followed, so the key is never sent to another host.
- Retries are logged at
INFOon thepiedap_consentlogger. - Each request sends
User-Agent: piedap-consent-python/1.0.4.
Methods#
The scope is what the secret key must hold. Every secret key holds consent:read and consent:write.
| Method | Calls | Scope | Fields and responses |
|---|---|---|---|
resolve_form() |
GET /sdk/v1/form |
None | Forms |
record_consent() |
POST /sdk/v1/consent |
consent:write |
Consent |
get_consent() |
GET /sdk/v1/consent/{consentId} |
consent:read |
Consent |
update_consent() |
PATCH /sdk/v1/consent/{consentId} |
consent:write |
Consent |
withdraw_consent() |
POST /sdk/v1/consent/{consentId}/withdraw |
consent:write |
Consent |
verify() |
GET /sdk/v1/consent/{consentId}/verify |
consent:read |
Receipts |
start_email_consent() |
POST /sdk/v1/email-consent |
consent:write |
Email consent |
record_activity() |
POST /sdk/v1/activity |
consent:write |
Activity |
consent_portal_link() |
POST /sdk/v1/pages/consent-portal-links |
consent:write |
Hosted page links |
find_subject() |
POST /sdk/v1/subjects/find |
subjects:read |
People |
export_subject() |
POST /sdk/v1/subjects/export |
subjects:export |
People |
withdraw_subject() |
POST /sdk/v1/subjects/withdraw |
subjects:withdraw |
People |
erase_subject() |
POST /sdk/v1/subjects/erase |
subjects:erase |
People |
link_identifiers() |
POST /sdk/v1/subjects/link |
subjects:link |
People |
link_request() |
POST /sdk/v1/requests/link |
requests:write |
Rights requests |
check_consent() |
POST /sdk/v1/consent/check |
consent:check |
Consent check |
Every method raises a ConsentError when a call fails. A key without the scope gets the code SCOPE_REQUIRED. An argument that cannot be sent raises ValueError or TypeError before any request is made.
What the Node.js client has and this package does not
There is no method for one-click unsubscribe links, for the deployment's public keys, or for checking a receipt offline. Call POST /sdk/v1/pages/unsubscribe-links, GET /sdk/v1/keys and POST /sdk/v1/receipts/verify directly. See Hosted page links and Receipts.
resolve_form()#
Returns the published form for a place or a form key, with its purposes.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
country |
str |
Detected from the request | Two-letter country code. Your server makes the request, so pass the visitor's country. |
subdivision |
str |
None |
State or province code. |
form_key |
str |
Chosen by region rules | Serves one form instead of resolving by region. |
locale |
str |
None |
The language to ask for. |
consent_id |
str |
None |
An existing consent, whose state is returned alongside. |
Returns dict, the API's JSON with the API's own field names. form is None when no form is served for that place.
Errors Raises NotFoundError when no form has that form_key.
resolved = client.resolve_form(form_key="kalpa-account", country="IN")
form = resolved["form"]
keys = [p["key"] for p in form["purposes"] if not p.get("alwaysOn")]record_consent()#
Records one person's answers to a form. Every argument is keyword only.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
form_key |
str |
Required, unless form_id is given |
The form's key. |
form_id |
str |
Required, unless form_key is given |
The form's id. |
subject_id |
str |
Required | The person's identifier. |
decisions |
Iterable[DecisionInput] |
Required | One answer for every purpose that is not always on. |
subject_type |
SubjectType |
"COOKIE_ID" |
The kind of identifier. Always pass it for an email address, a phone number or your own id. |
country_code |
str |
Detected from the request | Where the consent was collected. Without it the server locates your server, not the person. |
subdivision_code |
str |
None |
State or province code. |
locale |
str |
None |
The language the form was shown in. |
page_url |
str |
None |
The page the answer was given on. |
metadata |
Mapping[str, Any] |
None |
Your own fields, stored with the record. |
collected_at |
datetime or str |
The time of the call | When the consent was given. It must carry a timezone. Not before 1990. |
basis |
str |
None |
With collected_at: "PRE_COMMENCEMENT", your statement that the person consented before the DPDP Act commenced. |
notice_reference |
str |
None |
With collected_at: the name or number of the notice the person saw. Never personal data. |
notice_sha256 |
str |
None |
With collected_at: the SHA-256 of that notice, 64 hex characters. |
expires_at |
datetime or str |
The form's term from the call | With collected_at: when this consent ends. |
last_activity_at |
datetime or str |
The time of the call | With collected_at: when the person last used your service. |
capture |
Capture or mapping |
None |
How consent was taken in a branch, by phone or on paper. Sealed with this write only. |
interaction |
str |
API, or IMPORT with collected_at |
The control that produced the answer, such as SAVE_PREFERENCES. |
idempotency_key |
str |
None |
A unique key for this write. It makes the call safe to retry. |
data_principal_type |
str |
None |
CHILD or PERSON_WITH_DISABILITY, when a parent or lawful guardian consents for the person. |
guardian |
Mapping[str, str] |
None |
How the guardian was verified: relationship, verification and reference. |
age_declaration |
AgeDeclaration or mapping |
None |
Answer to "Are you 18 or older?"; False records a child. |
identifiers |
Iterable[IdentifierInput] |
None |
Up to five other identifiers to find the record by. |
Returns Consent, the record as stored.
Errors Raises ValueError for both or neither of form_key and form_id, an empty decision list, a purpose listed twice, more than five identifiers, or a collected_at without a timezone. Raises ValueError for a basis other than PRE_COMMENCEMENT or a malformed notice_sha256. Raises ConsentError with the code INVALID_PURPOSE_DECISIONS when a purpose is not answered or not on the form. A write with collected_at that the records overrule raises ConsentError with status 409; is_skipped_import(error) is then True. See Bring in consent given earlier.
from piedap_consent import Decision, Identifier
consent = client.record_consent(
form_key="kalpa-account",
subject_id="usr_8f2K1xQz",
subject_type="USER_ID",
country_code="IN",
interaction="SAVE_PREFERENCES",
decisions=[
Decision("course-updates-by-email", True),
Decision("class-reminders-by-sms", False),
],
identifiers=[Identifier("EMAIL", "kavya.menon@example.in")],
idempotency_key="signup-usr_8f2K1xQz",
)
print(consent.consent_id, consent.status)Decisions and identifiers may also be plain mappings, such as {"purpose_key": "analytics", "granted": True}. The API's camel-case names work too. See Record consent.
get_consent()#
Returns one record by its id.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
consent_id |
str |
Required | The record's id, cns_…. |
Returns Consent, or None.
Errors Raises ConsentError, except for a 404. The method then returns None: the id is not known, or the record's expiry has passed.
consent = client.get_consent("cns_3f1c9a7e2b8d4c6fa0e5d1b7c9f2a4e8")
if consent and consent.granted("course-updates-by-email"):
send_course_update()update_consent()#
Changes answers on an existing record.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
consent_id |
str |
Required | The record to change. |
decisions |
Iterable[DecisionInput] |
Required | Only the purposes that changed. The others keep their answer. |
page_url |
str |
None |
The page the change was made on. |
interaction |
str |
None |
The control that produced the change, such as PREFERENCE_CENTRE. |
identifiers |
Iterable[IdentifierInput] |
None |
More identifiers to link to the record. |
capture |
Capture or mapping |
None |
How this change was taken. A change without it clears the record's capture context; the earlier event keeps it. |
Returns Consent, the record as updated.
Errors Raises NotFoundError for an unknown id. The call takes no idempotency key, so it is never retried.
client.update_consent(
"cns_3f1c9a7e2b8d4c6fa0e5d1b7c9f2a4e8",
[Decision("class-reminders-by-sms", True)],
interaction="PREFERENCE_CENTRE",
)withdraw_consent()#
Withdraws one record.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
consent_id |
str |
Required | The record to withdraw. |
reason |
str |
None |
Stored on the withdrawal. |
idempotency_key |
str |
None |
A unique key for this write. Keyword only. |
Returns Consent, the record as withdrawn.
Errors Raises NotFoundError for an unknown id.
consent = client.withdraw_consent(
"cns_3f1c9a7e2b8d4c6fa0e5d1b7c9f2a4e8",
reason="Asked by phone",
)
print(consent.status, consent.withdrawn_on)verify()#
Returns the signed receipt for one record. The Node.js client calls this method receipt().
| Parameter | Type | Required or default | Description |
|---|---|---|---|
consent_id |
str |
Required | The record's id. |
Returns Receipt. valid is True when the server replayed the proof chain and it matched.
Errors Raises NotFoundError for an unknown id.
receipt = client.verify("cns_3f1c9a7e2b8d4c6fa0e5d1b7c9f2a4e8")
print(receipt.valid)
archive(receipt.to_dict())start_email_consent()#
Starts a double opt-in for an email address.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
email |
str |
Required | The address to confirm. |
form_key |
str |
Required | The form the person answered. |
decisions |
Iterable[DecisionInput] |
Required | The answers to hold until the person confirms. |
locale |
str |
None |
The language the form was shown in. |
return_url |
str |
None |
Where the person lands after confirming. |
idempotency_key |
str |
None |
A unique key for this write. Keyword only. |
Returns PendingEmailConsent. Send its verification_url to the address in your own email.
Errors Raises ConsentError.
pending = client.start_email_consent(
"rohan.das@example.in",
"newsletter",
[Decision("newsletter", True)],
return_url="https://www.example.in/subscribed",
)See Email double opt-in.
record_activity()#
Reports that a person signed in or used your service.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
subject_id |
str |
Required | The person's identifier. |
subject_type |
SubjectType |
None |
The kind of identifier. Pass it whenever the identifier is not an email address. |
Returns dict with records and activeOn.
Errors Raises ConsentError.
client.record_activity("usr_8f2K1xQz", "USER_ID")consent_portal_link()#
Creates a seven-day link to the hosted page where the person reviews, withdraws and downloads their consents.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
subject_id |
str |
Required | The person's identifier. |
subject_type |
SubjectType |
None |
The kind of identifier. Pass it whenever the identifier is not an email address. |
Returns dict with url and expiresOn.
Errors Raises ConsentError.
link = client.consent_portal_link("usr_8f2K1xQz", "USER_ID")
print(link["url"], link["expiresOn"])See Hosted pages.
find_subject()#
Returns every record held for a person.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
subject_id |
str |
Required | The person's identifier. |
subject_type |
SubjectType |
Required | The kind of identifier. |
Returns dict with subjectType, fingerprint and records. Each record's matchedBy lists the identifier types that matched.
Errors Raises ConsentError.
found = client.find_subject("+919846011223", "PHONE")
for record in found["records"]:
print(record["consentId"], record["status"], record["matchedBy"])See Find a person.
export_subject()#
Returns signed receipts and a processing summary for an access request.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
subject_id |
str |
Required | The person's identifier. |
subject_type |
SubjectType |
Required | The kind of identifier. |
Returns dict, the API's JSON.
Errors Raises ConsentError.
pack = client.export_subject("usr_8f2K1xQz", "USER_ID")withdraw_subject()#
Withdraws every consent the person has given, linked records included.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
subject_id |
str |
Required | The person's identifier. |
subject_type |
SubjectType |
Required | The kind of identifier. |
reason |
str |
None |
Stored on the withdrawal. Keyword only. |
idempotency_key |
str |
None |
A unique key for this write. Keyword only. |
Returns int, the number of records withdrawn.
Errors Raises ConsentError.
withdrawn = client.withdraw_subject(
"usr_8f2K1xQz",
"USER_ID",
reason="Account closed",
idempotency_key="close-usr_8f2K1xQz",
)erase_subject()#
Erases the person's identifiers on every record, linked records included, and removes the links.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
subject_id |
str |
Required | The person's identifier. |
subject_type |
SubjectType |
Required | The kind of identifier. |
reason |
str |
None |
Stored with the erasure. Keyword only. |
idempotency_key |
str |
None |
A unique key for this write. Keyword only. |
Returns int, the number of records erased.
Errors Raises ConsentError.
erased = client.erase_subject(
"usr_8f2K1xQz",
"USER_ID",
idempotency_key="erase-usr_8f2K1xQz",
)See Withdraw and erase.
link_identifiers()#
Links more identifiers to every live record of a person.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
subject_id |
str |
Required | The identifier the records are kept under. |
subject_type |
SubjectType |
Required | The kind of identifier. |
identifiers |
Iterable[IdentifierInput] |
Required | One to five identifiers. |
idempotency_key |
str |
None |
A unique key for this write. Keyword only. |
Returns dict with records and added: records linked, and links added.
Errors Raises ValueError for an empty list, more than five identifiers, or an identifier that repeats.
client.link_identifiers(
"usr_8f2K1xQz",
"USER_ID",
[Identifier("PHONE", "+919846099887")],
)See Identifiers.
link_request()#
Points a rights request at the identifier the person's records are kept under.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
reference |
str |
Required | The request's reference, such as DSR-2026-4F2A91. |
subject_id |
str |
Required | The person's identifier. |
subject_type |
SubjectType |
Required | The kind of identifier. |
Returns dict with reference and linkedRecordCount.
Errors Raises NotFoundError when no request in the project has that reference.
client.link_request("DSR-2026-4F2A91", "usr_8f2K1xQz", "USER_ID")check_consent()#
Asks whether one person allows a purpose now.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
subject_id |
str |
Required | The person's identifier. |
subject_type |
SubjectType |
Required | The kind of identifier. |
purpose_key |
str |
Required | The purpose to check. |
partner_key |
str |
None |
One partner the purpose lists. Keyword only. |
also_known_as |
Iterable[IdentifierInput] |
None |
Up to four other identifiers of the same person. Keyword only. |
timeout |
float |
1.0 |
Seconds the whole check may take, from connecting to the last byte of the answer. Keyword only. |
Returns ConsentCheck. allowed is True only when the server answered granted. When no answer arrived, unavailable is True, reason is "unavailable", allowed is False, and cause is timeout, network, rate_limited or server_error.
Errors Raises ConsentError for 400, 401, 403 and 404, such as SCOPE_REQUIRED or PURPOSE_NOT_FOUND. Raises ValueError before any request for a timeout that is not positive, more than four also_known_as entries, or a repeated identifier.
from piedap_consent import Identifier
check = client.check_consent(
"CIF00412873",
"USER_ID",
"offers-by-sms",
also_known_as=[Identifier("PHONE", "+919846011223")],
)
if check.allowed:
send_offer(check.based_on[0].consent_id)See Check consent before you process.
Models#
All models are frozen dataclasses. Each from_dict ignores fields it does not know. Every time is a timezone-aware datetime.
| Model | Fields and methods |
|---|---|
Decision |
purpose_key, granted, purpose_name, category, partners; partner_granted(partner_key), to_dict() |
Identifier |
subject_type, subject_id; to_dict(). For sending only. |
Consent |
consent_id, status, decisions, form_id, form_version, form_content_hash, country_code, subdivision_code, law_code, collected_on, expires_on, withdrawn_on, proof_hash, test, basis, notice_reference, notice_sha256, capture_context, age_declaration; granted(purpose_key, partner_key=None) |
Capture |
channel, method, agent_ref, location_ref, method_reference, document_sha256; to_dict(). For sending only. |
CaptureContext |
channel, method, attested_by_staff_only, agent_ref_hash, location_ref_hash, method_reference, document_sha256 |
AgeDeclaration |
eighteen_or_over, parental_consent_url; to_dict() |
ConsentCheck |
allowed, reason, unavailable, cause, status_code, retry_after, recorded, purpose_key, purpose_category, declared_basis, legitimate_use, consent_required, partner_key, valid_until, based_on, notes, checked_at, test |
CheckBasis |
consent_id, proof_hash, state, decided_at, matched_by, attested_by_staff_only. consent_id and proof_hash may be None. |
Receipt |
consent_id, status, valid, reason, proof_hash, issued_on, signature, algorithm, key_id, payload; to_dict() |
PendingEmailConsent |
consent_id, status, verification_url, expires_on |
SubjectTypeis one ofCOOKIE_ID,SESSION_ID,EMAIL,USER_ID,PHONEandDEVICE_ID.DecisionInputis aDecisionor a mapping.IdentifierInputis anIdentifieror a mapping.DecisionandAgeDeclarationaccept only a realbool; a string such as"false"raisesTypeError.Consent.granted()isTrueonly when the purpose was allowed and the record is in force. Its status is thenGRANTED,PARTIALorDENIED, andexpires_onhas not passed.Receipt.payloadis the signed document as the server sent it. Keep it unchanged.
Errors#
Everything the package raises for a failed call is a ConsentError.
| Exception | Raised for |
|---|---|
AuthenticationError |
401: the key is wrong, revoked or not a secret key. |
NotFoundError |
404: the consent id, form or route is not known. |
RateLimitError |
429. retry_after holds the seconds from Retry-After, or None. |
ConsentConnectionError |
No response arrived: DNS, a refused connection, TLS or a timeout, after any safe retries. timed_out is True when the wait ran out. |
ConsentError |
Any other response that is not 2xx, or a 2xx body that is not the expected JSON. |
Every exception carries these attributes:
| Attribute | Type | Description |
|---|---|---|
message |
str |
What went wrong, in the server's words. Several validation messages are joined with ; . |
status_code |
int or None |
The HTTP status. None when no response arrived. |
code |
str or None |
The server's code, such as SCOPE_REQUIRED. |
request_id |
str or None |
The server's id for the request, when the body carried one. |
body |
Any |
The response body: decoded JSON, or text. |
from piedap_consent import ConsentError, RateLimitError
try:
client.erase_subject(user.id, "USER_ID")
except RateLimitError as error:
retry_later(error.retry_after or 60)
except ConsentError as error:
if error.code == "SCOPE_REQUIRED":
log_missing_scope("subjects:erase")
raiseEvery code is listed in Errors.