Skip to main content
The Importer APIs bulk-load data into Tread. Send an array of Orders, Projects, Tickets, or Files in one request instead of calling the REST API record by record. Each endpoint saves your payload and processes it in the background. Use the Importer APIs for bulk loads — seeding historical data, a nightly sync from your ERP or accounting system, or a high-volume data feed. Use the REST API endpoints elsewhere in this reference when you’re creating or updating a single record.
This API is in Beta. Behavior may change. Contact developers@tread.io with questions or feedback.

Base URL and authentication

Every Importer endpoint lives under the same host as the rest of the API, with an /ingest prefix.
https://api.tread-horizon.com/ingest
Authentication uses the same bearer token as the rest of the API. See Authentication for how to get one.
Authorization: Bearer <access_token>
Content-Type depends on the endpoint. Orders, Projects, and Tickets take JSON:
Content-Type: application/json
Files takes a multipart upload instead — see Files. Every JSON payload can include an optional metadata object for your own tracking, like a batch ID or source system name. Tread stores it but doesn’t act on it.
{
  "orders": [ /* ... */ ],
  "metadata": { "source_system": "ERP", "batch_id": "2025-06-02-001" }
}

The async model

Orders, Projects, and Tickets process asynchronously. A 202 Accepted response means Tread saved your payload and queued it for processing. It does not mean the record was created or updated.
202 Accepted is not success. Always check the imported record’s state after you send a payload.
Two ways to check on a payload after you send it:
  • Reports > API Integrations in the Tread app. Shows Imported For, Import Received At, State, Last Error, and the raw payload you sent. Defaults to the last 7 days, most recent first.
  • Error webhooks, if you’ve configured one. Tread pushes an event when a payload fails to process. See Webhooks.
Each imported record ends up in one of these states:
StateMeaning
createdReceived. Not yet processed.
processedSucceeded.
failedFailed. Retries pending.
exhaustedFailed. All retries used up.
skippedA business rule blocked it — for example, a duplicate payload.
skipped_updateAn update was skipped because a user edited that field in the Tread app.
If a record is failed or exhausted, read its last_error field for the reason, fix the source data, and resend. Tread deduplicates automatically, so resending the same payload is safe. The Files endpoint is the one exception — it processes synchronously. See Files.

Orders

POST /ingest/orders creates or updates Orders. The root key is orders — send an array, even for a single record.

Required fields

FieldNotes
external_idYour system’s ID for this order (Ext. ID — used to match it on future requests).
load_atLoad date and time, ISO 8601. Interpreted in the company’s timezone.
customer.external_idYour system’s ID for the customer placing the order.

Example request

curl -X POST https://api.tread-horizon.com/ingest/orders \
  -H "Authorization: Bearer $TREAD_TOKEN" \
  -H "Content-Type: application/json" \
  -d @payload.json
{
  "orders": [
    {
      "external_id": "ORD-2025-0142",
      "load_at": "2025-06-02T07:30:00",
      "customer": { "external_id": "CUST-441", "name": "Acme Aggregates" },
      "material": { "external_id": "MAT-GRAVEL-34" },
      "sites": [
        { "waypoint": "pickup", "external_id": "SITE-QUARRY-1", "full_address": "4500 Quarry Rd, Denver, CO" },
        { "waypoint": "drop_off", "external_id": "SITE-JOB-19", "full_address": "800 Main St, Denver, CO" }
      ],
      "truck_count": 4,
      "service": "Hired Truck"
    }
  ]
}

Behavior notes

  • Matching is by external_id, scoped to your company. A match updates the order’s root fields — name, quantities, rates, notes, and so on.
  • customer, material, equipment_type, and sites are nested records. Tread creates them if they don’t exist but doesn’t update them on a match — see external_id matching.
  • foremen and vendors are arrays. Every payload replaces the whole array — it doesn’t merge with what’s already there. This is true even if you omit the field: omitting foremen does not preserve the existing foremen. Send the complete set every time.
  • If an order is canceled and you later send a payload with the same external_id in a non-canceled state, Tread renames the canceled order’s external_id to X-{N} and creates a new order. This is automatic — no cleanup needed on your end.

Projects

POST /ingest/projects creates or updates Projects. The root key is projects.

Required fields

FieldNotes
external_idYour system’s ID for this project.
customer.external_idYour system’s ID for the customer.

Example request

curl -X POST https://api.tread-horizon.com/ingest/projects \
  -H "Authorization: Bearer $TREAD_TOKEN" \
  -H "Content-Type: application/json" \
  -d @payload.json
{
  "projects": [
    {
      "external_id": "PRJ-2025-014",
      "name": "Highway 401 Resurfacing",
      "effective_at": "2025-05-01T00:00:00",
      "customer": { "external_id": "CUST-441", "name": "Acme Aggregates" },
      "phases": [
        { "name": "Mobilization", "code": "PH-01" },
        { "name": "Paving", "code": "PH-02" }
      ],
      "materials": [
        { "external_id": "MAT-ASPHALT-12", "total_quantity": 2500, "unit_of_measure": "Ton" }
      ]
    }
  ]
}
effective_at must be strictly before expires_at. If your source data has them equal — common for one-day projects — omit expires_at entirely. Don’t set it to end of day; just leave it off.

Behavior notes

  • Matching is by external_id. A match updates root fields — name, dates, notes, rates.
  • Phase sync is destructive. Every payload replaces the full phase list. A phase left out of the payload is deleted. Always send the complete set, not just what changed.
  • Materials sync the same way. A material left out of the payload is removed from the project.
  • Sites need an address. Every site needs full_address or lat/lon. If your source data only has a site name, set full_address to that name — Tread geocodes it automatically. Without one of these, the site fails validation and is silently dropped: the project still reports success, but that pickup or drop-off is missing and no error surfaces.
  • foremen replace on update, the same as Orders.
  • If a project’s name collides with a different project’s name, Tread appends -{external_id} to keep both.

Tickets

POST /ingest/tickets creates Tickets — the proof-of-load record for one Load. Root key is tickets.

Required fields

FieldNotes
ticket_numberUnique per company.
service_dateISO 8601.
quantityNumeric quantity.
unit_of_measureExact enum value — see Enum reference.
truck_idIdentifies the truck.

Example request

curl -X POST https://api.tread-horizon.com/ingest/tickets \
  -H "Authorization: Bearer $TREAD_TOKEN" \
  -H "Content-Type: application/json" \
  -d @payload.json
{
  "tickets": [
    {
      "ticket_number": "T-88213",
      "service_date": "2025-06-02T14:10:00Z",
      "quantity": 22.4,
      "unit_of_measure": "Ton",
      "truck_id": "T-101",
      "dispatch_number": "DISP-2025-0142",
      "material": { "external_id": "MAT-GRAVEL-34" },
      "customer": { "external_id": "CUST-441" }
    }
  ]
}

Behavior notes

  • Tread links a ticket to an order two ways: tread_order_id (direct, preferred) or dispatch_number plus a matching job start date (fallback). Send tread_order_id when you have it.
  • unit_of_measure normalization is case-insensitive but exact values are safer. "tons" becomes "Ton", but a string the system doesn’t recognize can silently fall back to the default (Ton) instead of erroring. Use the exact enum value.
  • site is a single object on Tickets — unlike Orders and Projects, where sites is an array.

Files

POST /ingest/files attaches a file to an existing (or soon-to-exist) Order. It uses multipart/form-data, not JSON — every other Importer endpoint uses JSON.

Required fields

FieldNotes
fileBinary file upload. A URL string is rejected.
external_idThe target Order’s external_id.
file_attachable_typeCurrently only Order is supported.
Optional: category (Inspection Report, Site Map, Scale Ticket (External), Scale Ticket (Internal), Timesheet, or Other) and description. Maximum file size is 10 MB. Accepted types: JPEG, PNG, WebP, GIF, and PDF.

Example request

curl -X POST https://api.tread-horizon.com/ingest/files \
  -H "Authorization: Bearer $TREAD_TOKEN" \
  -F "file=@/path/to/scale-ticket.jpg" \
  -F "external_id=ORD-2025-0142" \
  -F "file_attachable_type=Order" \
  -F "category=Scale Ticket (External)"

Behavior notes

Files process synchronously — the one exception among the Importer APIs.
  • 201 Created — the Order already exists. Tread attaches the file immediately.
  • 202 Accepted — the Order doesn’t exist yet. Tread stores the file and attaches it automatically once a matching Order arrives — files and orders can arrive in either order. No extra call needed.
  • 422 — the file failed validation (wrong type or over 10 MB).

external_id matching and upsert behavior

Every Importer endpoint uses external_id — your system’s identifier — to decide whether to create a new record or update an existing one. It’s scoped per company and case-sensitive. Root records (Order, Project, Ticket) work as you’d expect: found by external_id → update; not found → create. Nested records (customer, material, equipment type, sites, foremen) work differently. Tread creates them if they don’t exist, but generally doesn’t update their fields once matched. Sending a different name for a customer that already exists doesn’t rename it.
Nested recordMatched by
CustomerName, then external_id
MaterialName, then external_id
SiteName and external_id together, then either alone
Equipment TypeName or external_id
ForemanEmail, then phone, then external_id
To rename a customer, material, or other master-data record, use the REST API elsewhere in this reference — not the Importer APIs. Arrays follow one of three patterns on update:
ArrayBehavior
foremen, vendors, collaborators, salespeopleReplace — the whole array is swapped
phases, materials (Projects only)Sync — items missing from the payload are deleted
sitesUpsert — matched by waypoint type; the record’s ID is preserved

Enum reference

Exact string values Tread expects. A value outside this list either gets normalized (unit of measure) or rejected. Unit of measureLoad, Tonne, Ton, Yard, Meter, Foot, Liter, Hour, Bushel, Gallon, CubicMeter, Mile, Kilometer, Barrel, Bag, Pallet. Default: Ton. Rate typeRatePerHour (default), RatePerDay, RatePerLoad, RatePerTon, RatePerTonne, RatePerYard, RatePerBushel, RateCommission, RateFlatCommission, RateFlatRate. Site waypointpickup, staging, drop_off, weigh_point. Site typePlant, Quarry, JobSite, Depot, EquipmentHomeBase, Daily, Other (default).

Common pitfalls

  • 202 doesn’t mean it worked. Check the imported record’s state before you treat a payload as done.
  • Nested records don’t update on match. Renaming a customer or material in your payload has no effect once that record already exists. Use the REST API to rename it.
  • Sites without an address are dropped, silently. No full_address or lat/lon on a site means Tread drops it — and the Project or Order still reports success.
  • Invalid phone numbers are dropped, silently. A phone that doesn’t normalize to E.164 is stripped from the contact. No error, no last_error entry.
  • Duplicate payloads are skipped, not reprocessed. Sending the exact same payload twice marks the second one skipped. This is expected, not a bug.
  • Phase sync deletes what you leave out. Every Project payload replaces the entire phase list, not just the phases you’re changing.
  • Foremen and vendors replace, they don’t merge. Every payload with a foremen or vendors array swaps the whole thing, even if you omit the field.
  • Unit of measure is picky. "tons" normalizes to "Ton", but a string the system doesn’t recognize silently falls back to the default (Ton) instead of erroring. Use exact enum values.

Managing individual records

Need to create, update, or delete one record at a time instead of bulk-loading? Use the REST API endpoints elsewhere in this reference. They’re synchronous — a 201 or 200 means it’s done — and they support full field updates on nested records, including renaming a customer or material. The Importer APIs are for bulk loads only.

Going further

Authentication

Get a bearer token for these endpoints.

Webhooks

Get notified when an imported payload fails to process.

Orders and Projects

How the dispatch hierarchy fits together.

Tickets and Timesheets

What a Ticket records, and how it becomes a Settlement.