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 each presigned URL, gunzip the 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_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"
}
]
}
}
FieldDescription
tenantIdThe tenant this file belongs to.
typeThe requested data type.
fileNameFile name, following the pattern {type}_{YYYY-MM-DD}.gzip.
fileDateThe UTC date of the snapshot contained in the file.
sizeBytesCompressed file size in bytes.
presignedUrlTemporary, pre-authenticated S3 download URL — no additional credentials needed.
expiresAtTimestamp (UTC) after which the presignedUrl stops working.

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

Terminal window
curl --location --output invoice_2026-04-14.gzip '<presignedUrl>'
  • Compression: gzip (note the non-standard .gzip extension)
  • 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: each file is a full snapshot of the tenant’s data for that type as of the export date — not a delta

The .gzip suffix is non-standard, so pass -S .gzip to gunzip:

Terminal window
gunzip -k -S .gzip invoice_2026-04-14.gzip
head -n 1 invoice_2026-04-14

Each line is one record:

{"STEER_INVOICE_ID": "…", "STEER_TENANT_ID": "…", "STEER_SHOP_ID": "…", "STEER_UPDATED_AT": "2026-04-13 21:14:02.772", "…": "…"}
type valueSchema matchPrimary key
appointmentAppointmentSTEER_APPOINTMENT_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
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
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: download each presigned URL and parse the NDJSON payload
for 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:

  • 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).
  • Cadence: one file per data type, per tenant, per day. The date in the file name and fileDate reflect the UTC export date.
  • Snapshots, not deltas: every file contains 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
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.