Skip to main content

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:

  1. Templates — the master list. Every consent template owned by the org, with kind (Annotator agreement, Data retention, etc.), version, and archive state.
  2. Defaults — which templates new projects require by default.
  3. Request log + audit history — every consent acceptance / revocation across all projects in the org, searchable + exportable.

Create a template​

From the Templates section:

  1. Click + New template.
  2. 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 (key data_sharing: only those are handed on outside the organisation, publishing included). Add your own rows for anything else ("Allow my voice in academic datasets").
  3. 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.

Templates are managed at the org level, but required at the project level:

  1. Open Project → Settings → Consent.
  2. Toggle which templates from the org library are required for THIS project.
  3. (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:

  1. A modal opens listing each required template by title.
  2. Each template has its body rendered + the per-purpose checkboxes (if defined).
  3. Annotator clicks I agree + Continue.
  4. 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.sign or consent.withdraw)

Both surfaces are exportable to CSV.

Only the signer can withdraw their own consent — there is no admin-side "withdraw on behalf of". From Profile → My consents (/users/<id>/consents):

  1. 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.
  2. History expands the full ledger for that row ("Signed v1 …, Withdrew …, Signed v2 …") — useful in disputes.
  3. Download fetches the PDF receipt of the latest action (see Signature export).
  4. 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.sign audit 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.withdraw in 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.

RoutePurpose
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​