Interactions CSV export: the bulk-pull contract and its RBAC split
The Interactions export endpoint (GET /interactions/export.csv) is the bulk-extraction counterpart to the unified search list (GET /interactions). It downloads the same unified row set — every messaging conversation and every voice call, filtered the same way — serialized as CSV in a single synchronous response.
This page names the export contract: what it pulls, why its gate is harder than the list’s, the exact role + scope matrix, the 20,000-row cap, the cursor contract it shares with the list, the sync-vs-async decision, and the custodial rules for the downloaded file. It is the concept sibling to the Interaction Search guide, which walks the operator workflow end to end.
What the export does
The export is a bulk pull of contact-linked interaction history, filterable by the same parameters as the search list. An operator filtering to one contact, one channel, or one date window in the dashboard, then selecting Export CSV, receives the filtered set as a downloadable file rather than a paginated view. The response is anattachment CSV with one header row and twelve columns, in order:
id, type, channel, status, delivery_status, contact_id, contact_name, contact_phone, contact_email, direction, preview, last_activity_at
The server shapes the file as <stem>-YYYY-MM-DD.csv; an optional filename query parameter sets the stem (sanitised to [A-Za-z0-9._-], capped at 80 characters, default interactions). Response headers set Cache-Control: no-store and X-Content-Type-Options: nosniff, so a bulk pull is never cached by an intermediary.
Why the export is gated harder than the list
The search list returns the same rows a caller can already read page-by-page through the conversations list and call log, so it accepts every authenticated session. The export is different in kind, not in degree: it is a mass, filterable, one-response egress of contact-linked interaction history across the whole organization. That asymmetry is deliberate. Bulk extraction of contact-linked PII is a data-loss risk that per-page viewing is not, so the export gate requires an elevated role (and, for API-key callers, a contacts scope) where the list requires neither. If your organization has opted into PII redaction under Settings → Privacy, the masked fields (contact_name, contact_phone, contact_email, preview) are masked identically on both doors — the one redaction gate funnels both paths, because masking only the export while the list stayed open would protect nothing.
Role + scope matrix
Two caller paths reach the export, and both must satisfy the gate:
For API-key callers the scope check substitutes for the role check: the export insists on
contacts:read exactly because bulk egress of contact-linked data runs through the contacts permission family — a key holding only conversations:read still reads the search list but 403s the export.
Dashboard example. A viewer selects Export CSV in the page header; the API answers 403 INSUFFICIENT_PERMISSIONS and the dashboard shows a friendly toast instead of a failed download. An owner (or admin/developer) running the same click receives the file. Grant the role at the organization level — no workaround exists.
API-key example. Re-issue the key including contacts:read, then call:
403 INSUFFICIENT_PERMISSIONS body names the missing role or scope outright; the runbook at Interactions list: CSV export spells out the exact error envelope.
The export family enforces this gate uniformly — the same role-plus-scope pair fronts the other bulk contact exports, so contacts:read grants (or withholds) them together.
The 20,000-row cap
Each export is capped at 20,000 rows per response, default and maximum. The server fetches one row past the cap to detect truncation without a second count query; when your filtered set exceeds the cap, the CSV is cut at 20,000 rows and the response carries anX-Export-Truncated: true header so a client detects the cut rather than silently parsing a partial file.
Stay below the cap by narrowing the existing filters — all of them apply to the export exactly as they do to the list:
- Date window —
since/untilbound the last-activity timestamp; export per week or per month instead of all time. - Channel or type —
channels=whatsapportypes=callsplits one oversized pull into per-channel or per-type files. - Contact —
contact_idrestricts to one contact for archival of a single relationship. - Status —
statusestrims terminal states you do not need to archive. limitbelow the cap — request fewer rows explicitly when a smaller slice is the goal.
The composite cursor contract, and what the export does with it
The list paginates on a composite keyset cursor over(last_activity_at, id), ordered descending — the tie-breaking id makes the ordering total, so a page boundary never skips or repeats a row sharing a timestamp. The full reasoning lives in the unified model.
The export consumes that same contract differently: it requests the first page with no cursor and a limit up to the 20,000-row cap, reading one bounded snapshot ordered by the same composite key. It never chains cursors — when the set fits under the cap, one snapshot serializes the whole result. When you are exporting via the search list, walk the cursor chain page by page (has_more tells you when to stop) and recognize that the conversation-to-CSV export is the bounded alternative the gate exists for.
Async vs sync
The export is synchronous: the request holds until the CSV body returns. Unlike scheduled exports elsewhere on the platform (see the async processing model), no202 job_id path exists here — there is no queued webhook event to watch for. Bound your filter set to something that resolves within a normal request window; the 20,000-row cap plus the 5-per-minute rate limit are the throughput budget, and narrowing filters is the only correct response to a slow export.
Data-handling rules for exported CSVs
The downloaded file leaves the platform’s custody. The CSV contains contact-linked PII — names, phone numbers, email addresses, and message previews — and Orbit treats the export as tenant-owned data in the tenant’s hands from the moment the response completes. Practical obligations that follow:- Custody and retention are yours — store the file where your data-handling policy permits, and delete it on your schedule; the platform neither retains the export nor tracks the copy.
- Redaction exports the masked view — if your organization opted into PII redaction, the masked form is what the file carries; treat even masked exports as personal data.
- The audit ledger records the pull — the audit entry (row count, truncation, cap) is the org-level record of who extracted what; the file itself is your artifact to govern.
- Downstream sharing — hand exports to auditors or archives under your own data-processing terms; the endpoint does not watermark or revoke a distributed file.
Related reading
- Interaction Search guide — the operator workflow on top of this contract.
- Unified interaction model — the projection both endpoints read.
- Export families model — where this export sits among the platform’s bulk-export surfaces.
- Troubleshooting: Interactions search export 403s and pagination — the runbook for the flat-403 and pagination-oddity failures.
- PII redaction vault — the redaction gate both doors funnel through.