1️⃣ Receive Credentials
Steer provisions a dedicated IAM user for your tenant and emails you the AWS access key and secret key.
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 tenantAWS_SECRET_ACCESS_KEY — store in a secret manager, never commit to source controltenant_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:ListBuckets3:GetObjects3:GetObjectVersions3:GetBucketLocationNo 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/{tenant_id}/{data_type}/{YYYY-MM-DD}/ — one prefix per daytenant_id values are case-sensitive in S3 keys — preserve the exact casing of the UUID you receive.| Folder | Schema match | Primary key | Updated-at column |
|---|---|---|---|
appointment/ | Appointment data model | STEER_APPOINTMENT_ID | STEER_UPDATED_AT |
campaign/ | Campaign data model | STEER_CAMPAIGN_ID | STEER_UPDATED_AT |
customer/ | Customer data model | STEER_CUSTOMER_ID | STEER_UPDATED_AT |
invoice/ | Invoice data model | STEER_INVOICE_ID | STEER_UPDATED_AT |
message_directmail/ | Message (direct mail) data model | STEER_MESSAGE_ID | STEER_UPDATED_AT |
message_email/ | Message (email) data model | STEER_MESSAGE_ID | STEER_UPDATED_AT |
message_text/ | Message (text) data model | STEER_MESSAGE_ID | STEER_UPDATED_AT |
review/ | Review data model | STEER_REVIEW_ID | STEER_UPDATED_AT |
shop/ | Shop data model | STEER_SHOP_ID | SHOP_UPDATED_AT |
survey/ | Survey data model | STEER_SURVEY_ID | STEER_UPDATED_AT |
vehicle/ | Vehicle data model | STEER_VEHICLE_ID | STEER_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.
AWS_ACCESS_KEY_ID=YOUR_KEY \AWS_SECRET_ACCESS_KEY=YOUR_SECRET \aws s3 ls s3://s3-steer-dwh-prod-export/YOUR_TENANT_ID/ --recursiveExample output:
2026-04-15 14:23:51 201516 YOUR_TENANT_ID/campaign/2026-04-15/campaign_0_0_0.json.gz2026-04-16 11:53:43 201109 YOUR_TENANT_ID/campaign/2026-04-16/campaign_0_0_0.json.gz2026-04-16 11:53:58 23604112 YOUR_TENANT_ID/customer/2026-04-16/customer_0_0_0.json.gz2026-04-16 11:53:58 23570220 YOUR_TENANT_ID/customer/2026-04-16/customer_0_1_0.json.gz2026-04-16 11:56:54 281978862 YOUR_TENANT_ID/invoice/2026-04-16/invoice_0_0_0.json.gz2026-04-16 11:56:54 281978863 YOUR_TENANT_ID/invoice/2026-04-16/invoice_0_2_0.json.gz...Copy the whole day prefix — a day is several objects:
AWS_ACCESS_KEY_ID=YOUR_KEY \AWS_SECRET_ACCESS_KEY=YOUR_SECRET \aws s3 cp --recursive \ s3://s3-steer-dwh-prod-export/YOUR_TENANT_ID/invoice/2026-04-16/ \ ./invoice_2026-04-16/Decompress the day into one NDJSON file and inspect it:
cat ./invoice_2026-04-16/*.json.gz | gunzip -c > invoice_2026-04-16.jsonhead -n 1 invoice_2026-04-16.jsonConfigure credentials via environment variables, IAM role, or ~/.aws/credentials. Do not hardcode access keys.
import boto3import gzipimport json
s3 = boto3.client("s3") # picks up credentials from env / IAM / ~/.aws/credentials
BUCKET = "s3-steer-dwh-prod-export"TENANT = "YOUR_TENANT_ID"PREFIX = f"{TENANT}/customer/2026-04-16/"
records = []for page in s3.get_paginator("list_objects_v2").paginate(Bucket=BUCKET, Prefix=PREFIX): for s3_object in page.get("Contents", []): obj = s3.get_object(Bucket=BUCKET, Key=s3_object["Key"]) with gzip.GzipFile(fileobj=obj["Body"]) as gz: for line in gz: records.append(json.loads(line))
print(f"{len(records):,} customers")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.
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, jsonfrom 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.
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.
s3:GetObjectVersion is granted), but in normal operation a given day’s prefix is written once.| Symptom | Likely cause | Fix |
|---|---|---|
AccessDenied on aws s3 ls | Wrong tenant_id in the path, or listing the bucket root | List exactly s3://s3-steer-dwh-prod-export/{your_tenant_id}/ (case-sensitive) |
| Empty listing for today’s prefix | Export 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 yourself | Object names cannot be derived — the numbers in them are writer ids with gaps | List {tenant_id}/{data_type}/{YYYY-MM-DD}/ and use the keys it returns |
| Record count far below expectations | Only some of the day’s objects were read | Read every key under the day prefix — a day is several objects |
InvalidAccessKeyId / SignatureDoesNotMatch | Credentials mistyped or rotated | Re-check env vars; request rotation via Steer support if needed |
Python MemoryError reading a day | Holding the whole day in memory | Stream 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.