Consent Docs

Python client

Install the piedap-consent package, create a ConsentClient and look up every client method, model and exception.

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

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.

Shell
PACKAGES=https://console.dpconsent.com/consent/sdk/v1/packages
pip install $PACKAGES/piedap_consent-1.0.4-py3-none-any.whl

The 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.

consent.pyPython
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 an idempotency_key.
  • A retry follows a connection failure, a timeout, or a 502, 503 or 504 response.
  • Every other failure, including 429, is raised at once.
  • check_consent() waits one second unless you pass timeout, and never retries.
  • Redirects are never followed, so the key is never sent to another host.
  • Retries are logged at INFO on the piedap_consent logger.
  • 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.

Python
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")]

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.

Python
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.

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.

Python
consent = client.get_consent("cns_3f1c9a7e2b8d4c6fa0e5d1b7c9f2a4e8")
if consent and consent.granted("course-updates-by-email"):
    send_course_update()

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.

Python
client.update_consent(
    "cns_3f1c9a7e2b8d4c6fa0e5d1b7c9f2a4e8",
    [Decision("class-reminders-by-sms", True)],
    interaction="PREFERENCE_CENTRE",
)

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.

Python
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.

Python
receipt = client.verify("cns_3f1c9a7e2b8d4c6fa0e5d1b7c9f2a4e8")
print(receipt.valid)
archive(receipt.to_dict())

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.

Python
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.

Python
client.record_activity("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
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.

Python
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.

Python
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.

Python
pack = client.export_subject("usr_8f2K1xQz", "USER_ID")

See Answer an access request.

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.

Python
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.

Python
erased = client.erase_subject(
    "usr_8f2K1xQz",
    "USER_ID",
    idempotency_key="erase-usr_8f2K1xQz",
)

See Withdraw and erase.

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.

Python
client.link_identifiers(
    "usr_8f2K1xQz",
    "USER_ID",
    [Identifier("PHONE", "+919846099887")],
)

See Identifiers.

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.

Python
client.link_request("DSR-2026-4F2A91", "usr_8f2K1xQz", "USER_ID")

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.

Python
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
  • SubjectType is one of COOKIE_ID, SESSION_ID, EMAIL, USER_ID, PHONE and DEVICE_ID.
  • DecisionInput is a Decision or a mapping. IdentifierInput is an Identifier or a mapping.
  • Decision and AgeDeclaration accept only a real bool; a string such as "false" raises TypeError.
  • Consent.granted() is True only when the purpose was allowed and the record is in force. Its status is then GRANTED, PARTIAL or DENIED, and expires_on has not passed.
  • Receipt.payload is 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.
Python
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")
    raise

Every code is listed in Errors.

↑↓ to move, Enter to open