Consent library
The Consent Library is an org-level surface where Owners and Admins manage consent templates: documents annotators must accept before they can label tasks. Project owners then pick which templates to require on each project from Project Settings → Consent.
This is an AfriAnnotate feature on top of the upstream Label Studio codebase. It's designed for African data-labeling projects where contributor consent (PII handling, voice-recording reuse, data retention policy) is a regulatory expectation under POPIA, GDPR, and similar regimes.
Where it lives​
Organization → Resources → Consent (/organization/resources/consent).
The Resources tab groups the org's reusable assets —
Templates, Consent, Labels and Knowledge bases — behind one top-level
tab with a rail of sections on the left. Visible to org Owners, Admins, and platform
staff.
What's on the page​
Three sections:
- Templates — the master list. Every consent template owned by the org, with kind (Annotator agreement, Data retention, etc.), version, and archive state.
- Defaults — which templates new projects require by default.
- Request log + audit history — every consent acceptance / revocation across all projects in the org, searchable + exportable.
Create a template​
From the Templates section:
- Click + New template.
- Fill in:
- Title — what annotators see at the top of the consent dialog
- Kind — pre-defined categories (Annotator agreement, Data retention, Voice recording reuse, Image use, Custom)
- Body — the actual consent text, markdown supported
- Purposes — optional per-purpose toggles, one checkbox each
when signing. Two standard purposes are offered as one-click
rows because the platform acts on them: Training models
(key
model_training: only annotations whose authors accepted it go into a training set) and Sharing outside the organisation (keydata_sharing: only those are handed on outside the organisation, publishing included). Add your own rows for anything else ("Allow my voice in academic datasets").
- Click Save.
Templates are versioned automatically — each save bumps the version number. Older versions stay in the audit log for any annotator who already accepted them.
Edit / archive a template​
- Edit — opens the same form. Saving bumps the version. The next time an annotator opens a project that requires this template, they'll see the new version and re-accept.
- Archive — soft-deletes the template. New projects can't require it; existing acceptances stay in the audit log.
Per-project consent​
Templates are managed at the org level, but required at the project level:
- Open Project → Settings → Consent.
- Toggle which templates from the org library are required for THIS project.
- (Optional) Mark templates as gating — annotators can't open the labeling stream until they've accepted gating templates.
Non-gating templates show up as a banner at the top of the labeling UI; annotators can dismiss them.
What annotators see​
On their first task open in a project with required consent:
- A modal opens listing each required template by title.
- Each template has its body rendered + the per-purpose checkboxes (if defined).
- Annotator clicks I agree + Continue.
- The acceptance is logged with:
- User ID
- Template + version
- Per-purpose answers (if any)
- Timestamp
- Project ID
Re-accepting after a template version change works the same way — annotator sees a diff (current version vs accepted version) highlighted.
Audit log​
Every acceptance and revocation lands in:
- Per-template log on the Consent library page (filter by template)
- Org-wide audit log at Platform → Audit log
(filter
action = consent.signorconsent.withdraw)
Both surfaces are exportable to CSV.
Withdrawing consent​
Only the signer can withdraw their own consent — there is no
admin-side "withdraw on behalf of". From Profile → My consents
(/users/<id>/consents):
- Each row is one (template, project) pair with its current state — Active, Outdated, please re-sign (signed an older version), or Withdrawn. Filter by status or project; the list shows the 5 most recent rows until you click Show all.
- History expands the full ledger for that row ("Signed v1 …, Withdrew …, Signed v2 …") — useful in disputes.
- Download fetches the PDF receipt of the latest action (see Signature export).
- Withdraw (hidden once withdrawn) records the withdrawal immediately; no confirmation step.
What a withdrawal does:
- Appends an immutable withdrawal row to the signature ledger
(
signed_name = [WITHDRAWN], same template version, same project) — the original signature is never deleted, and the new row is chained into the org's hash chain like any signature. - Project access: the template goes back to pending for that project. Annotators are re-gated by the consent modal on their next labeling-page visit, and the server refuses annotation creation and draft saves until they re-sign (or stop working on the project). Project membership itself is untouched.
- Existing annotations are neither deleted nor altered — the data stays in the project; what stops is further processing under that consent. Handling of already-collected data is your org's policy (see your Data retention template).
- Re-signing is allowed at any time — the next signature supersedes the withdrawal. (Signing the same template + project again while a valid signature exists is a no-op; withdrawal is the only way to produce a fresh signature row.)
- Audit + receipts: logs
consent.withdraw(target = template, org = template's org, project id), emails the user a withdrawal record, and notifies the org owner that processing relying on that consent must stop (see below).
API: POST /api/consent/withdraw/ with {"template": <id>, "project": <id>} → {"ok": true, "withdrawn_at": "<iso>"}. Acts on
the calling user only.
Receipts & notifications​
Consent actions leave a paper trail on both sides:
- Signing emails the signer a receipt — template name, version,
and project — and records a
consent.signaudit event. Their full history lives on Profile → My consents. - Withdrawing emails the user a record of the withdrawal and
notifies the org owner, who must stop processing that relied
on the withdrawn consent (
consent.withdrawin the audit log). - Requiring a new consent on a project notifies every project member up-front, before the labeling-page gate surprises them.
See the full notifications reference.
Signature export​
Two exports, both read straight from the signature ledger (signatures and withdrawal rows).
Per-signature receipt (PDF)​
GET /api/consent/signatures/<id>/pdf/ — one PDF per ledger row,
titled Consent Receipt or Consent Withdrawal Receipt. Linked
from every row in Profile → My consents (Download), Project →
Settings → Consent → Signature & withdrawal history, and
Organization → Resources → Consent (PDF column).
Contents: signature id, document title + version signed, typed name,
account name + email on file, signed-at, country of residence, age
category, minor name (if any), project, origin IP, truncated
user-agent, and a 12-char receipt fingerprint (prefix of the
chain hash — cross-reference it with the verify-chain result). Signed
receipts append the template's agreement text (the body as currently
stored — the header pins the version that was signed); withdrawal
receipts state the withdrawal and reference the preserved original
instead.
Filename: afriannotate-consent-<id>-<hash8>.pdf.
Who can fetch: the signer (always), org Owner / Admin / Manager of the org the signature belongs to (active org must match), and platform staff. Anyone else gets 403 — annotators never see other people's receipts.
Ledger export (CSV)​
GET /api/consent/signatures/export/ — streamed CSV of the ledger.
- Organization → Resources → Consent → Export CSV — every row in
the active org (
afriannotate-consent-org-<id>.csv). Export with template body adds the template's current markdown body as a last column (?include_template_body=1). - Project → Settings → Consent → Signature & withdrawal history →
download — same file scoped to one project (
?project=<id>,afriannotate-consent-project-<id>.csv).
Scoping: always the caller's active org. Org Owner / Admin / Manager and platform staff get every row; any other user gets only their own rows (so annotators can pull a CSV of their own history). No active org → 400.
Columns (stable order): signature_id, signed_at, action (signed|withdrawn), user_id, user_email, user_full_name, template_id, template_title, template_kind, template_version_signed, template_version_current, project_id, signed_name, country_of_residence, age_attestation, minor_name, ip_address, user_agent, receipt_fingerprint_short[, template_body_markdown].
Withdrawal rows carry an empty signed_name.
API routes​
All under /api/consent/, authenticated. Signatures are immutable —
no PUT/PATCH/DELETE.
| Route | Purpose |
|---|---|
templates/ (CRUD) | Org template library; ?project= lists templates for a project you manage |
requirements/ (CRUD) | Per-project required templates (required_for, optional, sort_order); adding one notifies project members |
signatures/ (GET, POST) | Sign ({template, signed_name, project, purposes_accepted, …}); ?user=me lists your own rows across orgs, otherwise org-scoped, ?project= filter |
projects/<id>/pending/ | What the current user still has to sign for a project (drives the labeling gate; also reports profile_complete) |
starters/ | Vetted starter templates (GDPR / POPIA / NDPA / Kenya & Ghana DPA / Malabo / LGPD / CCPA / PIPL / PIPEDA) offered by the Pick a starter picker |
withdraw/ (POST) | Withdraw your own consent — see above |
signatures/<id>/pdf/ | Per-row receipt PDF |
signatures/export/ | Ledger CSV |
organizations/<id>/verify-chain/ | Hash-chain verification — see below |
Technical depth​
A few features that matter for compliance audits but aren't visible on the day-to-day UI:
Typed signatures​
Beyond the simple "I agree" checkbox, templates can require a
typed signature — the annotator types their full name into a
text field that must match their account's first_name + last_name
within a configurable Levenshtein distance. Useful for legal /
ethics-board-mandated consent where a click-through isn't strong
enough.
Configure per template: Templates → Edit → Require typed signature → yes / no.
Age attestation​
Templates can require age attestation — the annotator confirms they're over 18 (or another age threshold configured on the template) before the consent is recorded. Stored as a separate field on the signature so under-age signatures are filterable in the audit log.
GDPR Article 8 and POPIA section 35 both require explicit attestation for under-18 consent; this is how the platform supports it.
Country-of-residence capture​
Optionally capture the annotator's country of residence at signing time — a dropdown sourced from ISO 3166-1. Used for:
- Compliance reporting ("we have consent from N annotators in these 12 countries")
- Cross-border data-transfer notices (some templates show jurisdiction-specific addenda)
- Per-purpose filtering on export ("only include consents from annotators in EEA countries")
Tamper-evident audit log​
Every signature is HMAC-SHA256-hashed against the previous signature's hash, forming a chain. Any retroactive edit to a past signature breaks every subsequent hash, so post-hoc tampering is detectable.
Verification:
# Verify the whole chain for your active organisation
# (walks every signature in order; catches retroactive edits)
curl "{{platformURL}}/api/consent/organizations/<org_id>/verify-chain/" \
-H "Authorization: Bearer YOUR_PAT"
Returns
{"organization": <org_id>, "breaks": [{"id": <signature_id>, "reason": "prev_hash mismatch" | "signature_hash mismatch"}], "intact": true | false}
— an empty breaks list means the chain is intact. The call is
scoped to the caller's active organisation (<org_id> must match
it); there is no per-signature verify endpoint.
Implementation: label_studio/consent/models.py — see the
_hash_chain() function for the exact HMAC construction. The key
is the server SECRET_KEY, so rotating SECRET_KEY invalidates
the chain (expected; document the rotation in your ops runbook).
What's next​
- Members + invites — who's on the org
- Security model — how consent audits fit into the broader compliance + audit framework
- Profile + consents — what annotators see