Privacy and consent
These endpoints cover data-subject (DSAR) privacy requests (data export and account deletion), consent record management, per-subject recording consent for playbooks, and Terms of Service / Privacy Policy acceptance tracking. Unless noted otherwise, endpoints are user-scoped: authenticate with X-API-Key: pk_live_xxxxxxxxxxxx or Authorization: Bearer <jwt>.
Privacy requests (DSAR)
A privacy request tracks one export or deletion job through its lifecycle. Request object fields:
| Field | Type | Description |
|---|---|---|
id |
string (UUID) | Request identifier |
user_id |
string | Owner of the request |
request_type |
string | export or deletion |
status |
string | pending, processing, completed, failed, or cancelled |
regulation |
string | null | gdpr, ccpa, cpra, or null |
requested_at |
string (ISO 8601) | Creation time |
acknowledged_at |
string | null | When processing started |
completed_at |
string | null | When the request completed |
expires_at |
string | null | Compliance deadline (see below) |
processed_by |
string | null | Admin who processed the request |
notes |
string | null | Processing notes |
expires_at is set at creation from the regulation deadline: 30 days for gdpr, 45 days for ccpa/cpra, 30 days otherwise.
POST /v1/privacy/export
Create a data-export request. The export is prepared asynchronously; download it once the status reaches completed.
Request body — regulation is optional (gdpr, ccpa, or omit):
{
"regulation": "gdpr"
}
curl -X POST https://api.perceive8.com/v1/privacy/export \
-H "X-API-Key: pk_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"regulation": "gdpr"}'
Response 201 Created
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"user_id": "u_abc123",
"request_type": "export",
"status": "pending",
"regulation": "gdpr",
"requested_at": "2026-01-15T10:30:00+00:00",
"acknowledged_at": null,
"completed_at": null,
"expires_at": "2026-02-14T10:30:00+00:00",
"processed_by": null,
"notes": null
}
POST /v1/privacy/delete
Create an account-deletion request. Only one pending or processing deletion request is allowed per account.
Request body
{
"regulation": "gdpr",
"confirmation": "DELETE MY ACCOUNT"
}
confirmation must be exactly "DELETE MY ACCOUNT" (400 otherwise). Returns 409 if a deletion request is already in flight.
Response 201 Created — a privacy request object with request_type: "deletion".
GET /v1/privacy/requests
List all privacy requests for the authenticated user.
Response 200 OK — array of privacy request objects.
GET /v1/privacy/requests/{request_id}
Get the status of one privacy request. Returns 404 if the request does not exist or belongs to another user.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
request_id |
path | string (UUID) | Privacy request identifier |
Response 200 OK — a privacy request object.
POST /v1/privacy/requests/{request_id}/cancel
Cancel a privacy request. Only pending requests can be cancelled (404 otherwise).
Parameters
| Name | In | Type | Description |
|---|---|---|---|
request_id |
path | string (UUID) | Privacy request identifier |
Response 200 OK — the privacy request object with status: "cancelled".
GET /v1/privacy/export/{request_id}/download
Get the download URL for a completed export. Returns 400 if the request is not an export or is not yet completed; 404 if the request or export file is not available.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
request_id |
path | string (UUID) | Export request identifier |
Response 200 OK — object with download_url (string), file_size (integer or null), and request_id (string UUID).
Consent records
Consent records are immutable entries capturing a grant or withdrawal of consent for a given type (for example tos, privacy_policy, audio_processing). Consent record fields:
| Field | Type | Description |
|---|---|---|
id |
string (UUID) | Consent record identifier |
consent_type |
string | Consent category |
consent_version |
string | Version string, e.g. "1.0" |
granted |
boolean | true for a grant, false for a withdrawal |
granted_at |
string (ISO 8601) | When the record was created |
withdrawn_at |
string | null | When consent was withdrawn |
source |
string | null | Origin of the record ("api" for records created via the API) |
GET /v1/privacy/consents
List all consent records for the authenticated user.
Response 200 OK — array of consent record objects.
POST /v1/privacy/consents
Record a consent grant or withdrawal. The server also stores the client IP address and User-Agent header with the record.
Request body
{
"consent_type": "audio_processing",
"consent_version": "1.0",
"granted": true
}
Response 201 Created — a consent record object.
GET /v1/privacy/consents/status
Summarize active consent for the authenticated user.
Response 200 OK — {"consents": {"<consent_type>": true | false}}: each consent type maps to whether the user currently has an active grant for it.
Combined legal-document status
GET /v1/consent/status
Return Terms of Service and Privacy Policy acceptance status in a single call, so a client can gate access with one request. If no current version of a document exists, that document counts as accepted.
Response 200 OK — top-level fields:
| Field | Type | Description |
|---|---|---|
tos |
object | Terms of Service status (item fields below) |
privacy_policy |
object | Privacy Policy status (item fields below) |
all_accepted |
boolean | true only when neither document needs acceptance |
Each status item has needs_acceptance (boolean), current_version, current_title, accepted_version (string or null), and accepted_at (ISO 8601 or null).
Recording consent
Per-subject recording consent for playbooks (for example HIPAA-adjacent use cases). These endpoints require the workspace admin role (owner also passes). The workspace is taken from the X-Workspace-Id header (or the workspace_id query parameter); when omitted, the caller's Personal workspace is used.
Consent subjects have a lifecycle status of active, revoked, pending, or expired. Every status change is written to an immutable per-subject audit log.
Template object fields: id (UUID), workspace_id (UUID), playbook_id (string), template_text (string), created_at, updated_at (ISO 8601).
GET /v1/consent/templates
List consent templates for the current workspace, most recently updated first.
Response 200 OK — array of template objects.
POST /v1/consent/templates
Create a consent template for a playbook. If a template already exists for the workspace and playbook_id, its text is updated instead (upsert).
Request body
{
"playbook_id": "clinical-intake",
"template_text": "I consent to the recording and processing of this call..."
}
Response 200 OK — the created or updated template object.
Subject object fields:
| Field | Type | Description |
|---|---|---|
id |
string (UUID) | Subject identifier |
workspace_id |
string (UUID) | Owning workspace |
subject_ref |
string | Your identifier for the subject (max 255 chars) |
playbook_id |
string | Playbook the consent applies to (max 100 chars) |
status |
string | active, revoked, pending, or expired |
consented_at |
string | null | When consent was granted |
revoked_at |
string | null | When consent was revoked |
expires_at |
string | null | When consent expires |
created_at, updated_at |
string (ISO 8601) | Creation and last-update times |
GET /v1/consent/subjects
List consent subjects for the current workspace, newest first.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
playbook_id |
query | string | Optional filter by playbook |
Response 200 OK — array of subject objects.
POST /v1/consent/subjects
Record consent for a new subject. The subject is created with status active and consented_at set to now; an audit entry with action consented is written.
Request body — expires_at is optional:
{
"subject_ref": "patient-12345",
"playbook_id": "clinical-intake",
"expires_at": "2027-01-15T10:00:00+00:00"
}
Response 200 OK — the created subject object.
PATCH /v1/consent/subjects/{subject_id}
Update a subject's consent status. Returns 400 for an invalid status; 404 if the subject does not exist in the current workspace.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
subject_id |
path | string (UUID) | Consent subject identifier |
Request body — status must be one of active, revoked, pending, expired:
{
"status": "revoked",
"expires_at": null
}
Side effects per status (an audit entry whose action is the new status is written on every update):
| Status | Effect |
|---|---|
active |
Sets consented_at to now, clears revoked_at, applies expires_at from the body |
revoked |
Sets revoked_at to now, clears expires_at |
expired |
Sets expires_at to now |
pending |
Clears consented_at and revoked_at |
Response 200 OK — the updated subject object.
GET /v1/consent/subjects/{subject_id}/audit
Return the audit log for a single subject, newest entry first. Returns 404 if the subject does not exist in the current workspace.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
subject_id |
path | string (UUID) | Consent subject identifier |
Response 200 OK — array of audit entries:
| Field | Type | Description |
|---|---|---|
id |
string (UUID) | Audit entry identifier |
consent_subject_id |
string (UUID) | Subject the entry belongs to |
action |
string | consented for creation, or the new status (active, revoked, pending, expired) for updates |
actor_id |
string (UUID) | null | User who performed the action |
timestamp |
string (ISO 8601) | When the action occurred |
details |
object | null | Status-specific metadata (e.g. expires_at) |
Terms of Service
GET /v1/tos/current
Return the current Terms of Service version. Public — no authentication required. Returns 404 if no current version exists.
Response 200 OK
{
"id": "8e7d6c5b-4a3f-4e2d-9c1b-0a9f8e7d6c5b",
"version": "2.1",
"title": "Terms of Service",
"effective_date": "2026-01-01T00:00:00+00:00",
"is_current": true,
"changelog": "Updated data-processing terms"
}
GET /v1/tos/acceptance-status
Check whether the authenticated user has accepted the current Terms of Service. If no current version exists, the response reports accepted: true and needs_acceptance: false.
Response 200 OK
{
"accepted": false,
"current_version": "2.1",
"accepted_at": null,
"needs_acceptance": true
}
POST /v1/tos/accept
Accept the current Terms of Service version. The server records the client IP address and User-Agent header. Returns 400 if version does not match the current version and 409 if the current version was already accepted.
Request body
{
"version": "2.1"
}
Response 201 Created — { "accepted": true, "version": "2.1" }.
GET /v1/tos/history
Return the authenticated user's Terms of Service acceptance history, newest first.
Response 200 OK — array of items with version, title, accepted_at (ISO 8601), and ip_address (string or null).
Privacy Policy
These endpoints mirror the Terms of Service endpoints for the Privacy Policy document; request and response shapes are identical.
GET /v1/privacy-policy/current
Return the current Privacy Policy version. Public — no authentication required. Returns 404 if no current version exists.
Response 200 OK — a version object with the same fields as GET /v1/tos/current.
GET /v1/privacy-policy/acceptance-status
Check whether the authenticated user has accepted the current Privacy Policy. If no current version exists, the response reports accepted: true and needs_acceptance: false.
Response 200 OK — same shape as TOS acceptance status: accepted, current_version, accepted_at, needs_acceptance.
POST /v1/privacy-policy/accept
Accept the current Privacy Policy version. Records the client IP address and User-Agent header. Returns 400 on version mismatch and 409 if the current version was already accepted.
Request body
{
"version": "1.4"
}
Response 201 Created — { "accepted": true, "version": "1.4" }.
GET /v1/privacy-policy/history
Return the authenticated user's Privacy Policy acceptance history, newest first.
Response 200 OK — array of items with version, title, accepted_at, and ip_address, same shape as TOS history.
Admin endpoints
Administrative endpoints for managing TOS and Privacy Policy versions, listing and processing DSAR requests, and configuring data-retention jobs exist under /v1/admin/tos, /v1/admin/privacy-policy, and /v1/admin/privacy. They are restricted to server-configured admin accounts and are not part of the public integration surface, so they are not documented here.