admin.data_exports.manage permission. There is no API endpoint for it.
What you need
- An S3-compatible bucket. AWS S3 works natively. Backblaze B2, Cloudflare R2, Wasabi, DigitalOcean Spaces and MinIO work with their endpoint URL.
- An access key scoped to that bucket. The minimal policy is shown in the destination dialog and needs only
s3:PutObject,s3:AbortMultipartUploadands3:ListMultipartUploadPartsonarn:aws:s3:::YOUR_BUCKET/*. - Optionally, a lifecycle rule on the bucket that aborts incomplete multipart uploads after a day. We check for it and warn if it is missing.
Setting up a destination
- Open Admin > Data Exports and choose New destination.
- Enter a name, the bucket name, region, access key ID and secret. Add a prefix if you want the files under a folder, and an endpoint URL for a non-AWS provider.
- Pick a schedule, the datasets to export, and, for parent organizations, which child organizations to include.
- Save. We write a small probe object under
_validation/in your bucket. If the write fails, the destination is not created and the error from your provider is shown.
Schedules and windows
Every window ends five minutes before the run starts so late carrier receipts are included. Each scheduled run continues from where the previous one ended, so a paused or failed run never leaves a gap; a destination that fell behind catches up seven days per run. The first run after a destination is created covers only the most recent hour or day.
A Full Export walks your whole history in seven-day chunks, up to two years back, and can be started once every 30 days per destination. Failed chunks can be retried without restarting the batch.
File layout
Files land under your prefix in a Hive-style path that Athena, Glue, Snowflake, Databricks, BigQuery and DuckDB discover without configuration:- Parquet with SNAPPY compression. Files rotate every 100,000 rows (
part-000,part-001, …). - The
year,month,dayandhourfolders are when the export ran, in UTC, not when the rows were created. Filter on the row timestamps for reporting periods. - A window with no rows writes nothing. The job still appears in the history with No rows.
- Rows inside a file are not ordered.
- Timestamps are Parquet
TIMESTAMP_MILLISin UTC. Phone numbers are E.164 (+15555550123); short-code senders appear as the short code itself (471016). - The
v1prefix is the schema version. New columns are added at the end; existing columns are never renamed or removed within a version.
Datasets
message_events
One row per outbound and inbound message. A message is exported again in a later window whenever its delivery status changes after it was first exported (for example a delivery receipt that arrives the next day). Keep the row with the greatestupdated_at per message_id.
project_summary
One row per project, written in the window that holds its completion time. Drafts, scheduled and in-progress projects are not exported until they complete.opt_out_events
One row per opt-out, in the window that holds its creation.click_events
One row per click on a tracked link, in the window that holds the click.Reading the files
- Athena or Glue: crawl the
v1/prefix or create a table with partition projection onschema_type,year,month,dayandhour. - Snowflake: create an external stage on the bucket and load with
COPY INTOor Snowpipe auto-ingest. - Databricks: point Auto Loader (
cloudFiles) at theschema_type=...prefix. - DuckDB:
SELECT * FROM read_parquet('s3://bucket/prefix/v1/schema_type=message_events/**/*.parquet').
message_events, deduplicate on message_id keeping the row with the greatest updated_at.
