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).
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 each presigned URL, gunzip the file, and parse the newline-delimited JSON records.
Base URLs
https://auth.steercrm.comhttps://app.steercrm.com🔐 Credentials
Steer provisions API credentials during onboarding and sends them to the email address on file:
client_id — identifies your integrationclient_secret — store in a secret manager, never commit to source control or logsAuthenticate using the OAuth 2.0 client credentials flow against the Steer token endpoint:
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'{ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6...", "expires_in": 300, "refresh_expires_in": 0, "token_type": "Bearer", "not-before-policy": 0, "scope": "email profile"}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}/filesAuthorization: Bearer <access_token>| Parameter | Description |
|---|---|
type | The data type to export — see Available Data Types below. |
| Parameter | Type | Required | Description |
|---|---|---|---|
tenantId | UUID | No | Restrict results to one tenant. If omitted, files for all tenants available to your credentials are returned. |
date | YYYY-MM-DD | No | Return the snapshot for a specific date. Cannot be combined with from/to. |
from, to | YYYY-MM-DD | No | Return snapshots within a date range (inclusive). Maximum span is 31 days. Cannot be combined with date. |
Latest invoice files for all available tenants:
curl --location 'https://app.steercrm.com/api/v1/public/data-export/invoice/files' \ --header 'Authorization: Bearer <access_token>'Snapshot for one tenant on a specific date:
curl --location 'https://app.steercrm.com/api/v1/public/data-export/invoice/files?tenantId=3201e5c3-334c-4c26-a685-5a94b8a1b2e0&date=2026-04-14' \ --header 'Authorization: Bearer <access_token>'All snapshots within a range (max 31 days):
curl --location 'https://app.steercrm.com/api/v1/public/data-export/invoice/files?from=2026-04-01&to=2026-04-30' \ --header 'Authorization: Bearer <access_token>'{ "status": "Success", "data": { "files": [ { "tenantId": "3201e5c3-334c-4c26-a685-5a94b8a1b2e0", "type": "invoice", "fileName": "invoice_2026-04-14.gzip", "fileDate": "2026-04-14", "sizeBytes": 751682, "presignedUrl": "https://s3-steer-dwh-export.s3.us-east-2.amazonaws.com/3201e5c3-334c-4c26-a685-5a94b8a1b2e0/invoice/invoice_2026-04-14.gzip?X-Amz-Expires=3600&...", "expiresAt": "2026-07-13T11:05:06.277965Z" }, { "tenantId": "00ebab6e-4564-4255-ab72-33e6314da271", "type": "invoice", "fileName": "invoice_2026-04-14.gzip", "fileDate": "2026-04-14", "sizeBytes": 21317593, "presignedUrl": "https://s3-steer-dwh-export.s3.us-east-2.amazonaws.com/00ebab6e-4564-4255-ab72-33e6314da271/invoice/invoice_2026-04-14.gzip?X-Amz-Expires=3600&...", "expiresAt": "2026-07-13T11:05:06.2510227Z" } ] }}| Field | Description |
|---|---|
tenantId | The tenant this file belongs to. |
type | The requested data type. |
fileName | File name, following the pattern {type}_{YYYY-MM-DD}.gzip. |
fileDate | The UTC date of the snapshot contained in the file. |
sizeBytes | Compressed file size in bytes. |
presignedUrl | Temporary, pre-authenticated S3 download URL — no additional credentials needed. |
expiresAt | Timestamp (UTC) after which the presignedUrl stops working. |
Each presignedUrl is a direct download link — no Authorization header required:
curl --location --output invoice_2026-04-14.gzip '<presignedUrl>'.gzip extension)The .gzip suffix is non-standard, so pass -S .gzip to gunzip:
gunzip -k -S .gzip invoice_2026-04-14.gziphead -n 1 invoice_2026-04-14Each line is one record:
{"STEER_INVOICE_ID": "…", "STEER_TENANT_ID": "…", "STEER_SHOP_ID": "…", "STEER_UPDATED_AT": "2026-04-13 21:14:02.772", "…": "…"}Stream-decompress and parse line by line — do not json.load() the whole file, both because it is NDJSON and because large types (e.g. invoice) can be multi-GB uncompressed:
import gzipimport json
records = []with gzip.open("invoice_2026-04-14.gzip", "rt", encoding="utf-8") as f: for line in f: records.append(json.loads(line))
print(f"{len(records):,} invoices")type value | Schema match | Primary key |
|---|---|---|
appointment | Appointment | STEER_APPOINTMENT_ID |
campaign | Campaign | STEER_CAMPAIGN_ID |
customer | Customer | STEER_CUSTOMER_ID |
invoice | Invoice | STEER_INVOICE_ID |
message_direct_mail | Message (direct mail) | STEER_MESSAGE_ID |
message_email | Message (email) | STEER_MESSAGE_ID |
message_text | Message (text/SMS) | STEER_MESSAGE_ID |
review | Review | STEER_REVIEW_ID |
shop | Shop | STEER_SHOP_ID |
survey | Survey | STEER_SURVEY_ID |
vehicle | Vehicle | STEER_VEHICLE_ID |
A complete sync cycle — authenticate, list the latest files, download, and parse:
import gzipimport jsonimport ioimport requests
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 managerCLIENT_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 tenantsfiles_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: download each presigned URL and parse the NDJSON payloadfor f in files: download = requests.get(f["presignedUrl"], stream=True) download.raise_for_status()
records = [] 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 {f['tenantId']}: {len(records):,} invoices ({f['fileDate']})")Because each file is a full snapshot, your pipeline computes changes itself:
STEER_UPDATED_AT column against your last sync time.shop, survey), MERGE on primary key + STEER_UPDATED_AT for large ones (invoice, customer, vehicle, appointment).fileDate reflect the UTC export date.date or from/to (max 31 days per request) to backfill.| Symptom | Likely cause | Fix |
|---|---|---|
401 Unauthorized on the files endpoint | Access token expired (5-minute lifetime) or malformed Authorization header | Request a new token; send Authorization: Bearer <token> |
400 Bad Request with date parameters | date combined with from/to, or range wider than 31 days | Use either date or from/to; keep ranges ≤ 31 days |
403 Forbidden when downloading from the presigned URL | Presigned URL expired (1-hour lifetime) or URL was truncated/re-encoded | Call the files endpoint again and use the fresh presignedUrl verbatim |
Empty files array | No snapshot exists for the requested date/tenant yet | Retry later or query without date parameters to get the latest available snapshot |
| JSON parse error on the decompressed file | Parsing the file as a single JSON document | The 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.