GDPR Article 20 streaming export of a full tenant
const url = 'https://api.aerandil.com/v1/tenant-export-requests';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"tenant_id":"2489E9AD-2EE2-8E00-8EC9-32D5F69181C0"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}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.
Authorizations
Section titled “Authorizations”Request Bodyrequired
Section titled “Request Bodyrequired”object
Examplegenerated
{ "tenant_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0"}Responses
Section titled “Responses”Streaming NDJSON dump. Each line is one TenantExportLine object (object / link / event / error). An empty tenant produces a 200 with an empty body.
object
object
Org-scope envelope (tenant/brand/portfolio/property/venue).
object
When the link starts being in effect (migration 0011).
When the link stops being in effect; null is open-ended (still in effect).
Typed link-property bag (migration 0012); {} for adjacency-only links.
object
object
Underlying ObjectEvent discriminant — object_created, object_updated, object_deleted, object_linked, object_unlinked. (Distinct from the top-level kind which is always event for this variant.)
object
object
object
object
Service api_name (e.g. connector.opera_pms).
object
object
object
object
Service api_name (e.g. connector.opera_pms).
Model the agent ran with (e.g. claude-opus-4-7).
Issuer of the agent (e.g. anthropic, openai).
Version string under the vendor’s namespace.
The source DB’s global surrogate ordinal — a within-stream resume cursor only. A relocation import reassigns it; use tenant_seq for portable ordering.
Portable per-tenant ordinal (UNIQUE (tenant_id, tenant_seq), migration 0019) that travels with the tenant across a cross-cell relocation. Always present for exported (tenant-owned) rows. (The source-local commit_xid CDC horizon is deliberately not exported — a destination re-stamps its own.)
object
Stream-level failure surfaced in-band. Emitted as the last line when the dump errors after headers have been sent — the receiver tails the body and observes the error rather than getting a non-200 status.
Example
{ "kind": "object"}Headers
Section titled “Headers”Always no-store — streaming, not cacheable.
Missing the tenant_admin:write scope. Tenant- level tokens MUST NOT carry this scope.
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
object
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).
Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.
Human-readable explanation of the failure.
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.
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
object
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).
Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.
Human-readable explanation of the failure.
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" }}