Skip to content

Data Export API

The Data Export API is the primary integration path for pulling Steer data into your data warehouse. Steer produces a daily snapshot of each tenant’s data as gzip-compressed JSON files; the API authenticates you with OAuth 2.0 and returns short-lived download links for those files.

1️⃣ Get an Access Token

Exchange your client_id and client_secret for a short-lived OAuth 2.0 access token (valid for 5 minutes).

2️⃣ Request File Links

Call the export endpoint for a data type. The response lists matching files with presigned S3 URLs (valid for 1 hour).

3️⃣ Download & Decompress

Download every URL in the response, gunzip each file, and parse the newline-delimited JSON records.

Base URLs

  • Authentication: https://auth.steercrm.com
  • API: https://app.steercrm.com

🔐 Credentials

Steer provisions API credentials during onboarding and sends them to the email address on file:

  • client_id — identifies your integration
  • client_secret — store in a secret manager, never commit to source control or logs

Authenticate using the OAuth 2.0 client credentials flow against the Steer token endpoint:

Terminal window
curl --location 'https://auth.steercrm.com/realms/steer/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'client_secret=YOUR_CLIENT_SECRET'

Use the token in the Authorization header of every API request:

Authorization: Bearer <access_token>

List the available export files for a data type and receive presigned download URLs:

GET /api/v1/public/data-export/{type}/files
Authorization: Bearer <access_token>
ParameterDescription
typeThe data type to export — see Available Data Types below.
ParameterTypeRequiredDescription
tenantIdUUIDNoRestrict results to one tenant. If omitted, files for all tenants available to your credentials are returned.
dateYYYY-MM-DDNoReturn the snapshot for a specific date. Cannot be combined with from/to.
from, toYYYY-MM-DDNoReturn snapshots within a date range (inclusive). Maximum span is 31 days. Cannot be combined with date.

Latest invoice files for all available tenants:

Terminal window
curl --location 'https://app.steercrm.com/api/v1/public/data-export/invoice/files' \
--header 'Authorization: Bearer <access_token>'
{
"status": "Success",
"data": {
"files": [
{
"tenantId": "3201e5c3-334c-4c26-a685-5a94b8a1b2e0",
"type": "invoice",
"fileName": "invoice_0_0_0.json.gz",
"fileDate": "2026-04-14",
"sizeBytes": 289134,
"presignedUrl": "https://s3-steer-dwh-export.s3.us-east-2.amazonaws.com/3201e5c3-334c-4c26-a685-5a94b8a1b2e0/invoice/2026-04-14/invoice_0_0_0.json.gz?X-Amz-Expires=3600&...",
"expiresAt": "2026-04-15T11:05:06.277965Z"
},
{
"tenantId": "3201e5c3-334c-4c26-a685-5a94b8a1b2e0",
"type": "invoice",
"fileName": "invoice_0_2_0.json.gz",
"fileDate": "2026-04-14",
"sizeBytes": 301887,
"presignedUrl": "https://s3-steer-dwh-export.s3.us-east-2.amazonaws.com/3201e5c3-334c-4c26-a685-5a94b8a1b2e0/invoice/2026-04-14/invoice_0_2_0.json.gz?X-Amz-Expires=3600&...",
"expiresAt": "2026-04-15T11:05:06.278113Z"
},
{
"tenantId": "3201e5c3-334c-4c26-a685-5a94b8a1b2e0",
"type": "invoice",
"fileName": "invoice_0_3_0.json.gz",
"fileDate": "2026-04-14",
"sizeBytes": 160661,
"presignedUrl": "https://s3-steer-dwh-export.s3.us-east-2.amazonaws.com/3201e5c3-334c-4c26-a685-5a94b8a1b2e0/invoice/2026-04-14/invoice_0_3_0.json.gz?X-Amz-Expires=3600&...",
"expiresAt": "2026-04-15T11:05:06.278204Z"
},
{
"tenantId": "00ebab6e-4564-4255-ab72-33e6314da271",
"type": "invoice",
"fileName": "invoice_0_0_0.json.gz",
"fileDate": "2026-04-14",
"sizeBytes": 21317593,
"presignedUrl": "https://s3-steer-dwh-export.s3.us-east-2.amazonaws.com/00ebab6e-4564-4255-ab72-33e6314da271/invoice/2026-04-14/invoice_0_0_0.json.gz?X-Amz-Expires=3600&...",
"expiresAt": "2026-04-15T11:05:06.2510227Z"
}
]
}
}
FieldDescription
tenantIdThe tenant this file belongs to.
typeThe requested data type.
fileNameFile name. Not unique — it repeats across dates and tenants; identify a file by tenantId + fileDate + fileName.
fileDateThe UTC date of the snapshot this file belongs to.
sizeBytesCompressed file size in bytes.
presignedUrlTemporary, pre-authenticated S3 download URL — no additional credentials needed.
expiresAtTimestamp (UTC) after which the presignedUrl stops working.

A snapshot is split across several files. Each one is a slice of the same dataset, so the complete snapshot is the union of every file sharing the same tenantId and fileDate — download them all and concatenate the records, in any order.

In the response above, tenant 3201e5c3… has three files for 2026-04-14; together they are that tenant’s complete invoice snapshot for the day.

Each presignedUrl is a direct download link — no Authorization header required:

Terminal window
curl --location --output invoice_0_0_0.json.gz '<presignedUrl>'
  • Compression: gzip (.json.gz)
  • Content: newline-delimited JSON (NDJSON) — one JSON object per line, not a single JSON array
  • Schema: identical to the corresponding Data Models page — same columns, same semantics
  • Scope: the files of one tenantId + fileDate together form a full snapshot of that tenant’s data for that type as of the export date — not a delta
  • Ordering: records are not ordered, within a file or across files
Terminal window
gunzip -k invoice_0_0_0.json.gz
head -n 1 invoice_0_0_0.json

Each line is one record:

{"STEER_INVOICE_ID": "…", "STEER_TENANT_ID": "…", "STEER_SHOP_ID": "…", "STEER_UPDATED_AT": "2026-04-13 21:14:02.772", "…": "…"}

To rebuild the whole snapshot, decompress every file of that date together:

Terminal window
cat invoice_0_*.json.gz | gunzip -c > invoice_2026-04-14.json
wc -l invoice_2026-04-14.json
type valueSchema matchPrimary key
appointmentAppointmentSTEER_APPOINTMENT_ID
callCallSTEER_CALL_ID
campaignCampaignSTEER_CAMPAIGN_ID
customerCustomerSTEER_CUSTOMER_ID
invoiceInvoiceSTEER_INVOICE_ID
message_direct_mailMessage (direct mail)STEER_MESSAGE_ID
message_emailMessage (email)STEER_MESSAGE_ID
message_textMessage (text/SMS)STEER_MESSAGE_ID
opportunity_hubOpportunity HubSTEER_CUSTOMER_ID + STEER_VEHICLE_ID (vehicle can be null)
reviewReviewSTEER_REVIEW_ID
shopShopSTEER_SHOP_ID
surveySurveySTEER_SURVEY_ID
vehicleVehicleSTEER_VEHICLE_ID

A complete sync cycle — authenticate, list the latest files, download, and parse:

import gzip
import json
import io
import requests
from collections import defaultdict
AUTH_URL = "https://auth.steercrm.com/realms/steer/protocol/openid-connect/token"
API_URL = "https://app.steercrm.com/api/v1/public/data-export"
CLIENT_ID = "YOUR_CLIENT_ID" # from environment / secret manager
CLIENT_SECRET = "YOUR_CLIENT_SECRET" # never hardcode in real code
# Step 1: get an access token (valid 5 minutes)
token_resp = requests.post(AUTH_URL, data={
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
})
token_resp.raise_for_status()
access_token = token_resp.json()["access_token"]
# Step 2: list the latest invoice files for all tenants
files_resp = requests.get(
f"{API_URL}/invoice/files",
headers={"Authorization": f"Bearer {access_token}"},
)
files_resp.raise_for_status()
files = files_resp.json()["data"]["files"]
# Step 3: group the files into snapshots — one per tenant and date
snapshots = defaultdict(list)
for export_file in files:
snapshots[(export_file["tenantId"], export_file["fileDate"])].append(export_file)
# Step 4: download every file of a snapshot and parse the NDJSON payloads
for (tenant_id, file_date), snapshot_files in snapshots.items():
records = []
for snapshot_file in snapshot_files:
download = requests.get(snapshot_file["presignedUrl"], stream=True)
download.raise_for_status()
with gzip.open(io.BytesIO(download.content), "rt", encoding="utf-8") as gz:
for line in gz:
records.append(json.loads(line))
print(f"tenant {tenant_id}: {len(records):,} invoices ({file_date}, {len(snapshot_files)} files)")

Because each snapshot is full, not a delta, your pipeline computes changes itself. Load every file of a tenantId + fileDate before comparing — a partially loaded snapshot looks like mass deletions:

  • Inserts & updates: filter records by the STEER_UPDATED_AT column against your last sync time.
  • Deletes: diff two consecutive snapshots on the data type’s primary key — a key present yesterday but missing today was deleted.
  • Warehousing pattern: truncate-and-replace for small types (shop, survey), MERGE on primary key + STEER_UPDATED_AT for large ones (invoice, customer, vehicle, appointment).
  • No STEER_UPDATED_AT: opportunity_hub scores are recalculated every day and carry no update timestamp. Replace the tenant’s rows with each snapshot instead of filtering by update time.
  • Cadence: one snapshot per data type, per tenant, per day, delivered as several files. fileDate carries the UTC export date — the file name does not.
  • Snapshots, not deltas: the files of one tenantId + fileDate together contain the complete dataset for that type as of the export time.
  • Token hygiene: tokens live 5 minutes and there is no refresh token — build automatic re-authentication into your client.
  • URL hygiene: presigned URLs live 1 hour — list files and download in the same job run; re-list to refresh expired URLs.
  • Historical data: past snapshot dates remain available — use date or from/to (max 31 days per request) to backfill.
SymptomLikely causeFix
401 Unauthorized on the files endpointAccess token expired (5-minute lifetime) or malformed Authorization headerRequest a new token; send Authorization: Bearer <token>
400 Bad Request with date parametersdate combined with from/to, or range wider than 31 daysUse either date or from/to; keep ranges ≤ 31 days
403 Forbidden when downloading from the presigned URLPresigned URL expired (1-hour lifetime) or URL was truncated/re-encodedCall the files endpoint again and use the fresh presignedUrl verbatim
Empty files arrayNo snapshot exists for the requested date/tenant yetRetry later or query without date parameters to get the latest available snapshot
Record count far below expectationsOnly some of the snapshot’s files were loadedA snapshot spans several files — load every entry sharing the same tenantId and fileDate
Downloaded files overwrite each otherSaving by fileName, which repeats across dates and tenantsSave under a path that includes tenantId and fileDate
404 Not Found on a file name you built yourselfFile names cannot be derived — the numbers in them are writer ids with gapsUse the presignedUrl values from the response verbatim
JSON parse error on the decompressed fileParsing the file as a single JSON documentThe payload is NDJSON — parse one JSON object per line

Need help? Contact Steer support with your client_id (never the secret), the endpoint you called, and the response you received.