Skip to content

GDPR Article 20 streaming export of a full tenant

POST
/v1/tenant-export-requests
curl --request POST \
--url https://api.aerandil.com/v1/tenant-export-requests \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "tenant_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0" }'

Stream the tenant’s full footprint as application/x-ndjson — one JSON object per line, ordered objects → links → events, ascending within each section. Backs the GDPR Article 20 portability commitment and the contractual right to data return on offboarding.

Operator-level scope: tenant_admin:write. Same contract surface as the per-tenant erasure endpoint; a tenant cannot legitimately exfiltrate itself via API.

Snapshot semantics. All three sub-queries run inside one REPEATABLE READ transaction, so the dump reflects a single consistent snapshot — concurrent writes during the stream don’t surface as half-committed state.

Mid-stream errors are signalled by a final NDJSON line of the form {"kind":"error","message":"..."} — headers were already sent so the wire status stays 200; the in-band error line is the only signal a consumer who tails the body can observe.

Not idempotent. Each call materialises a fresh dump; retries re-run the export. A streamed response is hard to replay safely from a cache anyway (the body is produced once and forgotten).

Reads through the admin pool (BYPASSRLS) — the export crosses the per-tenant RLS boundary by definition; tenant scoping comes from the explicit WHERE tenant_id = $1 predicate, not the GUC.

Media typeapplication/json
object
tenant_id
required
string format: uuid
Examplegenerated
{
"tenant_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0"
}

Streaming NDJSON dump. Each line is one TenantExportLine object (object / link / event / error). An empty tenant produces a 200 with an empty body.

Media typeapplication/x-ndjson
One of: discriminator: kind
object
created_at
required
string format: date-time
deleted_at
string format: date-time
nullable
deleted_by
nullable
id
required
string format: uuid
kind
required
string
Allowed value: object
Allowed values: object
object_type
required
string
properties
required
object
scope
required

Org-scope envelope (tenant/brand/portfolio/property/venue).

tenant_id
required
string format: uuid
updated_at
required
string format: date-time
version
required
integer
Example
{
"kind": "object"
}
Cache-Control
string
Allowed value: no-store

Always no-store — streaming, not cacheable.

Missing the tenant_admin:write scope. Tenant- level tokens MUST NOT carry this scope.

Media typeapplication/json

Structured error wrapper. Every non-2xx response from any Aerandil endpoint serialises to this shape; the HTTP status carries the failure class (4xx/5xx), the body adds a stable code for programmatic dispatch plus a human-readable message.

object
error
required
object
code
required

Stable, programmatic error code (e.g. unauthorized, forbidden, not_found, invalid_body, invalid_url, precondition_failed, no_secondary_key). Clients dispatch on code, not on the message (which may be localised or contextualised in future versions).

string
instance_id

Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.

string
message
required

Human-readable explanation of the failure.

string
parameters

Per-error structured detail. The shape varies by code (e.g. forbidden_action carries {"action": "..."}; precondition_failed carries {"current_version": N}).

Examplegenerated
{
"error": {
"code": "example",
"instance_id": "example",
"message": "example",
"parameters": "example"
}
}

Body invalid — invalid_body or unknown fields.

Media typeapplication/json

Structured error wrapper. Every non-2xx response from any Aerandil endpoint serialises to this shape; the HTTP status carries the failure class (4xx/5xx), the body adds a stable code for programmatic dispatch plus a human-readable message.

object
error
required
object
code
required

Stable, programmatic error code (e.g. unauthorized, forbidden, not_found, invalid_body, invalid_url, precondition_failed, no_secondary_key). Clients dispatch on code, not on the message (which may be localised or contextualised in future versions).

string
instance_id

Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.

string
message
required

Human-readable explanation of the failure.

string
parameters

Per-error structured detail. The shape varies by code (e.g. forbidden_action carries {"action": "..."}; precondition_failed carries {"current_version": N}).

Examplegenerated
{
"error": {
"code": "example",
"instance_id": "example",
"message": "example",
"parameters": "example"
}
}