Consent Docs

Python helpers

The helper functions of the piedap_consent package for webhook signatures, identity proofs, fingerprints, capture hashes, skipped imports and cookies.

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

piedap_consent exports helper functions next to the client. They check webhook signatures, compute proofs, fingerprints and capture hashes, spot skipped imports, and read the consent cookie.

Requirements#

  • Python 3.9 or later.
  • For verify_webhook(): the endpoint's signing secret, whsec_….
  • For identity_proof(): the key's identity secret, idv_….
  • For subject_fingerprint() and capture_reference_hash(): the project's fingerprint key, sfk_….

Install#

The helpers ship in the same package as the client. See Install.

Import the helpers#

Python
from piedap_consent import (
    capture_reference_hash,
    identity_proof,
    is_skipped_import,
    parse_consent_cookie,
    parse_partner_cookie,
    subject_fingerprint,
    verify_webhook,
)

The helpers are plain functions. They make no network calls and need no client.

Functions#

verify_webhook()#

Checks that a webhook delivery is genuine and recent.

Parameter Type Required or default Description
payload bytes, bytearray, memoryview or str Required The raw request body, before any JSON parsing.
signature_header str or None Required The X-Piedap-Signature header.
timestamp_header str or None Required The X-Piedap-Timestamp header.
secret str or bytes Required The endpoint's signing secret.
tolerance_seconds int 300 How far the timestamp may be from now, in either direction.
now float The system clock The current Unix time.

Returns bool: True only when the signature matches the raw body and the timestamp is within the tolerance. Missing or malformed headers give False.

Errors Raises ValueError for an empty secret or a negative tolerance. Both are set-up mistakes, not bad deliveries.

hooks.pyPython
import os

from flask import Flask, abort, request
from piedap_consent import (
    SIGNATURE_HEADER,
    TIMESTAMP_HEADER,
    verify_webhook,
)

app = Flask(__name__)


@app.post("/webhooks/piedap")
def consent_hook():
    if not verify_webhook(
        request.get_data(),
        request.headers.get(SIGNATURE_HEADER),
        request.headers.get(TIMESTAMP_HEADER),
        secret=os.environ["PIEDAP_WEBHOOK_SECRET"],
    ):
        abort(401)
    return "", 204

In Django, pass request.body as the payload. See Verify signatures.

identity_proof()#

Computes the proof that lets a browser or an app record consent under a signed-in person.

Parameter Type Required or default Description
identity_secret str Required The key's identity secret.
subject_type str Required EMAIL, USER_ID or PHONE.
subject_id str Required The identifier, exactly as the page or app sends it.

Returns str: the lower-case hex HMAC-SHA256 of <subject_type>:<subject_id>.

Errors Raises ValueError for any other subject_type and for an empty identity_secret.

Python
import os

from piedap_consent import identity_proof

proof = identity_proof(
    os.environ["PIEDAP_IDENTITY_SECRET"],
    "USER_ID",
    user.id,
)

See Signed-in visitors.

subject_fingerprint()#

Computes the fingerprint that receipts and webhooks carry for a person.

Parameter Type Required or default Description
fingerprint_key str Required The project's fingerprint key.
subject_id str Required The identifier you recorded consent under.
subject_type str None The kind of identifier. Without it, a value that contains @ is treated as an email address.

Returns str: the lower-case hex HMAC-SHA256 of the identifier. An email address is put in lower case first.

Errors Raises ValueError for an empty fingerprint_key. Here the identifier comes before the type.

Python
import os

from piedap_consent import subject_fingerprint

fingerprint = subject_fingerprint(
    os.environ["PIEDAP_FINGERPRINT_KEY"],
    "usr_8f2K1xQz",
    "USER_ID",
)

See Match events to users.

capture_reference_hash()#

Computes the agent_ref_hash or location_ref_hash that a capture context carries, so you can match it to your own staff id or site code.

Parameter Type Required or default Description
fingerprint_key str Required The project's fingerprint key.
kind str Required "agent" for a member of staff, "location" for a branch or site.
reference str Required Your id, as sent in Capture.

Returns str: the lower-case hex HMAC-SHA256 of <kind>:<reference>, with the reference trimmed and in lower case.

Errors Raises ValueError for another kind, an empty fingerprint_key, or a reference that is empty or not printable ASCII. Raises TypeError when reference is not a string.

Python
import os

from piedap_consent import capture_reference_hash

agent_ref_hash = capture_reference_hash(
    os.environ["PIEDAP_FINGERPRINT_KEY"],
    "agent",
    "EMP-40213",
)

See Record consent taken in a branch, by phone or on paper.

is_skipped_import()#

Tells a write with collected_at that the records overrule from a real error.

Parameter Type Required or default Description
error BaseException Required What record_consent() raised.

Returns bool: True for a ConsentError with status 409 and a code in SKIPPED_IMPORT_CODES.

Errors None.

Python
from piedap_consent import ConsentError, is_skipped_import

try:
    client.record_consent(**row)
except ConsentError as error:
    if not is_skipped_import(error):
        raise
    skipped.append((row["idempotency_key"], error.code))

See Bring in consent given earlier.

Reads the two cookie values the browser runtime wrote.

Parameter Type Required or default Description
value str or None Required The piedap_consent cookie value, raw or percent-encoded.
partners str or None None The piedap_partners cookie value.

Returns a ConsentCookie, or None.

Errors None. A missing or malformed cookie gives None, so a damaged cookie reads as "no answer".

Python
from piedap_consent import (
    COOKIE_NAME,
    PARTNER_COOKIE_NAME,
    parse_consent_cookie,
)

cookie = parse_consent_cookie(
    request.cookies.get(COOKIE_NAME),
    request.cookies.get(PARTNER_COOKIE_NAME),
)

analytics = bool(cookie and cookie.granted("analytics"))
matomo = bool(cookie and cookie.granted("analytics", "matomo"))

The example reads Flask's request.cookies. In Django, use request.COOKIES.

Reads a piedap_partners value on its own.

Parameter Type Required or default Description
value str or None Required The cookie value, raw or percent-encoded.

Returns dict[str, tuple[str, ...]], purpose key to the partner keys the visitor refused, or None.

Errors None. A missing or malformed cookie gives None.

Python
declined = parse_partner_cookie(request.cookies.get(PARTNER_COOKIE_NAME))
print(declined)  # {'analytics': ('matomo', 'plausible')}

Types#

ConsentCookie#

A frozen dataclass.

Field Type Description
version int The format version of the cookie.
consent_id str The record's id. It is the text pending for the short time between the visitor's answer and the API confirming it.
form_version int The version of the form that was answered.
decisions dict[str, bool] Purpose key to allowed.
decided_at datetime When the visitor answered, in UTC.
partners dict[str, tuple[str, ...]] Purpose key to the partner keys the visitor refused.
granted(key, partner=None) bool Whether the purpose is allowed. With partner, whether that partner is allowed too.

The cookie is not evidence

A visitor controls their own cookies. Use the cookie to decide what to render. Where the answer must be right, read the record with client.get_consent(cookie.consent_id).

The cookie formats are in Cookies and browser storage. The models the client returns are in Models.

Constants#

Name Value
COOKIE_NAME piedap_consent
PARTNER_COOKIE_NAME piedap_partners
SIGNATURE_HEADER X-Piedap-Signature
TIMESTAMP_HEADER X-Piedap-Timestamp
CHECK_TIMEOUT 1.0, the seconds check_consent() waits unless told otherwise
MAX_ALSO_KNOWN_AS 4
SKIPPED_IMPORT_CODES CONSENT_WITHDRAWN_ON_RECORD, CONSENT_ALREADY_ON_RECORD, CONSENT_NEWER_ON_RECORD, CONSENT_ERASED_ON_RECORD, CONSENT_HELD_UNDER_OTHER_IDENTIFIER

↑↓ to move, Enter to open