Skip to content

S3 Bulk Data Export

Steer publishes a daily snapshot of your tenant’s data to a dedicated Amazon S3 bucket. The payload schema matches the data model — the same columns, the same semantics — delivered as gzipped JSON files you can pull on your own schedule.

1️⃣ Receive Credentials

Steer provisions a dedicated IAM user for your tenant and emails you the AWS access key and secret key.

2️⃣ List Your Prefix

Every object lives under s3://s3-steer-dwh-prod-export/{tenant_id}/. List it to discover what’s available.

3️⃣ Download & Decompress

Files are gzip-compressed JSON. Download, gunzip, and parse with any standard tooling.

Steer will send your AWS credentials to the email address on file during onboarding.

🔐 What You Receive

  • AWS_ACCESS_KEY_ID — unique per tenant
  • AWS_SECRET_ACCESS_KEY — store in a secret manager, never commit to source control
  • Your tenant_id — the UUID that prefixes all of your objects (e.g. fb296814-1cd0-4417-b3ea-d76092b82841)

The IAM user tied to your credentials is scoped to read-only access on your tenant’s prefix only. The attached policy grants:

s3:ListBucket
s3:GetObject
s3:GetObjectVersion
s3:GetBucketLocation

No write, delete, or cross-tenant access is possible with these keys.

All objects follow a predictable path structure:

s3://s3-steer-dwh-prod-export/
└── {tenant_id}/
├── appointment/
│ └── 2026-04-16/
│ ├── appointment_0_0_0.json.gz
│ └── appointment_0_1_0.json.gz
├── campaign/
│ ├── 2026-04-15/
│ │ └── campaign_0_0_0.json.gz
│ └── 2026-04-16/
│ └── campaign_0_0_0.json.gz
├── invoice/
│ └── 2026-04-16/
│ ├── invoice_0_0_0.json.gz
│ ├── invoice_0_2_0.json.gz
│ └── invoice_0_3_0.json.gz
├── customer/
├── message_directmail/
├── message_email/
├── message_text/
├── review/
├── shop/
├── survey/
└── vehicle/
  • Prefix pattern: {tenant_id}/{data_type}/{YYYY-MM-DD}/ — one prefix per day
  • Case sensitivity: tenant_id values are case-sensitive in S3 keys — preserve the exact casing of the UUID you receive.
  • Cadence: one snapshot per data type, per day, written as several objects under that day’s prefix
  • Contents: the objects under one day’s prefix together form a full snapshot of the tenant’s data for that type as of the export time — not a delta
  • Format: gzip-compressed newline-delimited JSON (NDJSON), schema-identical to the corresponding Analytics API endpoint
  • Retention: objects are retained indefinitely; historical dates remain downloadable
FolderSchema matchPrimary keyUpdated-at column
appointment/Appointment data modelSTEER_APPOINTMENT_IDSTEER_UPDATED_AT
campaign/Campaign data modelSTEER_CAMPAIGN_IDSTEER_UPDATED_AT
customer/Customer data modelSTEER_CUSTOMER_IDSTEER_UPDATED_AT
invoice/Invoice data modelSTEER_INVOICE_IDSTEER_UPDATED_AT
message_directmail/Message (direct mail) data modelSTEER_MESSAGE_IDSTEER_UPDATED_AT
message_email/Message (email) data modelSTEER_MESSAGE_IDSTEER_UPDATED_AT
message_text/Message (text) data modelSTEER_MESSAGE_IDSTEER_UPDATED_AT
review/Review data modelSTEER_REVIEW_IDSTEER_UPDATED_AT
shop/Shop data modelSTEER_SHOP_IDSHOP_UPDATED_AT
survey/Survey data modelSTEER_SURVEY_IDSTEER_UPDATED_AT
vehicle/Vehicle data modelSTEER_VEHICLE_IDSTEER_UPDATED_AT

Field-level definitions live on the individual Data Models pages in the sidebar (Customer, Vehicle, Invoice, Appointment, Campaign, Message, Review, Survey, and Shop).

Replace the placeholder values with your credentials and tenant_id.

Terminal window
AWS_ACCESS_KEY_ID=YOUR_KEY \
AWS_SECRET_ACCESS_KEY=YOUR_SECRET \
aws s3 ls s3://s3-steer-dwh-prod-export/YOUR_TENANT_ID/ --recursive

Example output:

2026-04-15 14:23:51 201516 YOUR_TENANT_ID/campaign/2026-04-15/campaign_0_0_0.json.gz
2026-04-16 11:53:43 201109 YOUR_TENANT_ID/campaign/2026-04-16/campaign_0_0_0.json.gz
2026-04-16 11:53:58 23604112 YOUR_TENANT_ID/customer/2026-04-16/customer_0_0_0.json.gz
2026-04-16 11:53:58 23570220 YOUR_TENANT_ID/customer/2026-04-16/customer_0_1_0.json.gz
2026-04-16 11:56:54 281978862 YOUR_TENANT_ID/invoice/2026-04-16/invoice_0_0_0.json.gz
2026-04-16 11:56:54 281978863 YOUR_TENANT_ID/invoice/2026-04-16/invoice_0_2_0.json.gz
...

Because each day is a full snapshot, clients compute deltas themselves. The recommended approach is to download two consecutive day prefixes and join on the data type’s primary key. Load every object of a day before comparing — a partially loaded day looks like mass deletions.

Strategy 1 — Filter by STEER_UPDATED_AT (fastest)

Section titled “Strategy 1 — Filter by STEER_UPDATED_AT (fastest)”

Every record carries an update timestamp. To find what changed on 2026-04-16, you only need the later snapshot:

import glob, gzip, json
from datetime import datetime, timezone
def load_day(day_directory):
for path in sorted(glob.glob(f"{day_directory}/*.json.gz")):
with gzip.open(path, "rt", encoding="utf-8") as f:
for line in f:
yield json.loads(line)
cutoff = datetime(2026, 4, 15, tzinfo=timezone.utc)
changed = [
invoice for invoice in load_day("invoice_2026-04-16")
if datetime.fromisoformat(invoice["STEER_UPDATED_AT"]) > cutoff
]
print(f"{len(changed):,} invoices changed since {cutoff.date()}")

This catches inserts and updates but not deletes.

Strategy 2 — Diff two snapshots on the primary key (complete)

Section titled “Strategy 2 — Diff two snapshots on the primary key (complete)”

When you need to detect deletes too, load both days and diff on the primary key:

import glob, gzip, json
def load_day(day_directory, pk):
records = {}
for path in sorted(glob.glob(f"{day_directory}/*.json.gz")):
with gzip.open(path, "rt", encoding="utf-8") as f:
for line in f:
record = json.loads(line)
records[record[pk]] = record
return records
PK = "STEER_INVOICE_ID"
prev = load_day("invoice_2026-04-15", PK)
curr = load_day("invoice_2026-04-16", PK)
inserted = curr.keys() - prev.keys()
deleted = prev.keys() - curr.keys()
updated = {
k for k in curr.keys() & prev.keys()
if curr[k]["STEER_UPDATED_AT"] != prev[k]["STEER_UPDATED_AT"]
}
print(f"+{len(inserted)} ~{len(updated)} -{len(deleted)}")

See the primary-key table above for the correct PK column per data type.

  • Timezone: the date in the prefix reflects the UTC date of export. Object names carry no date.
  • Publish time: a day’s objects typically land by ~12:00 UTC and are written together. If a day’s prefix is missing, re-check later or fall back to the previous day — no partial days are published.
  • No notifications: Steer does not currently emit S3 event notifications. Poll the prefix or schedule downloads against expected publish time.
  • Versioning: object versions are preserved (s3:GetObjectVersion is granted), but in normal operation a given day’s prefix is written once.
  • Missed-day recovery: if a day’s prefix is missing past the publish window, contact Steer support — exports can be regenerated within the retention window.
SymptomLikely causeFix
AccessDenied on aws s3 lsWrong tenant_id in the path, or listing the bucket rootList exactly s3://s3-steer-dwh-prod-export/{your_tenant_id}/ (case-sensitive)
Empty listing for today’s prefixExport hasn’t landed yet (before ~12:00 UTC)Retry after the publish window, or use yesterday’s prefix
NoSuchKey for an object name you built yourselfObject names cannot be derived — the numbers in them are writer ids with gapsList {tenant_id}/{data_type}/{YYYY-MM-DD}/ and use the keys it returns
Record count far below expectationsOnly some of the day’s objects were readRead every key under the day prefix — a day is several objects
InvalidAccessKeyId / SignatureDoesNotMatchCredentials mistyped or rotatedRe-check env vars; request rotation via Steer support if needed
Python MemoryError reading a dayHolding the whole day in memoryStream with gzip.GzipFile and parse one line at a time

Need help? Contact Steer support with your tenant_id and the S3 key you were trying to access.