> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tread.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Importer APIs (Beta)

> Bulk-load Orders, Projects, Tickets, and Files into Tread with async ingest endpoints, then check on results after the fact.

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.

<Note>
  This API is in **Beta**. Behavior may change. Contact [developers@tread.io](mailto:developers@tread.io) with questions or feedback.
</Note>

## 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](/api-reference/introduction#authentication) for how to get one.

```bash theme={null}
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](#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.

```json theme={null}
{
  "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.**

<Warning>
  `202 Accepted` is not success. Always check the imported record's state after you send a payload.
</Warning>

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](/integrations/webhooks).

Each imported record ends up in one of these states:

| State            | Meaning                                                                  |
| ---------------- | ------------------------------------------------------------------------ |
| `created`        | Received. Not yet processed.                                             |
| `processed`      | Succeeded.                                                               |
| `failed`         | Failed. Retries pending.                                                 |
| `exhausted`      | Failed. All retries used up.                                             |
| `skipped`        | A business rule blocked it — for example, a duplicate payload.           |
| `skipped_update` | An 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](#files).

## Orders

`POST /ingest/orders` creates or updates [Orders](/concepts/orders-projects). The root key is `orders` — send an array, even for a single record.

### Required fields

| Field                  | Notes                                                                            |
| ---------------------- | -------------------------------------------------------------------------------- |
| `external_id`          | Your system's ID for this order (Ext. ID — used to match it on future requests). |
| `load_at`              | Load date and time, ISO 8601. Interpreted in the company's timezone.             |
| `customer.external_id` | Your system's ID for the customer placing the order.                             |

### Example request

```bash theme={null}
curl -X POST https://api.tread-horizon.com/ingest/orders \
  -H "Authorization: Bearer $TREAD_TOKEN" \
  -H "Content-Type: application/json" \
  -d @payload.json
```

```json theme={null}
{
  "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](#external_id-matching-and-upsert-behavior).
* `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](/concepts/orders-projects). The root key is `projects`.

### Required fields

| Field                  | Notes                              |
| ---------------------- | ---------------------------------- |
| `external_id`          | Your system's ID for this project. |
| `customer.external_id` | Your system's ID for the customer. |

### Example request

```bash theme={null}
curl -X POST https://api.tread-horizon.com/ingest/projects \
  -H "Authorization: Bearer $TREAD_TOKEN" \
  -H "Content-Type: application/json" \
  -d @payload.json
```

```json theme={null}
{
  "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" }
      ]
    }
  ]
}
```

<Warning>
  `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.
</Warning>

### 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](/concepts/tickets-timesheets) — the proof-of-load record for one Load. Root key is `tickets`.

### Required fields

| Field             | Notes                                                     |
| ----------------- | --------------------------------------------------------- |
| `ticket_number`   | Unique per company.                                       |
| `service_date`    | ISO 8601.                                                 |
| `quantity`        | Numeric quantity.                                         |
| `unit_of_measure` | Exact enum value — see [Enum reference](#enum-reference). |
| `truck_id`        | Identifies the truck.                                     |

### Example request

```bash theme={null}
curl -X POST https://api.tread-horizon.com/ingest/tickets \
  -H "Authorization: Bearer $TREAD_TOKEN" \
  -H "Content-Type: application/json" \
  -d @payload.json
```

```json theme={null}
{
  "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](/concepts/orders-projects). It uses `multipart/form-data`, not JSON — every other Importer endpoint uses JSON.

### Required fields

| Field                  | Notes                                         |
| ---------------------- | --------------------------------------------- |
| `file`                 | Binary file upload. A URL string is rejected. |
| `external_id`          | The target Order's `external_id`.             |
| `file_attachable_type` | Currently 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

```bash theme={null}
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 record  | Matched by                                         |
| -------------- | -------------------------------------------------- |
| Customer       | Name, then `external_id`                           |
| Material       | Name, then `external_id`                           |
| Site           | Name and `external_id` together, then either alone |
| Equipment Type | Name or `external_id`                              |
| Foreman        | Email, 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:

| Array                                                | Behavior                                                        |
| ---------------------------------------------------- | --------------------------------------------------------------- |
| `foremen`, `vendors`, `collaborators`, `salespeople` | Replace — the whole array is swapped                            |
| `phases`, `materials` (Projects only)                | Sync — items missing from the payload are deleted               |
| `sites`                                              | Upsert — 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 measure** — `Load`, `Tonne`, `Ton`, `Yard`, `Meter`, `Foot`, `Liter`, `Hour`, `Bushel`, `Gallon`, `CubicMeter`, `Mile`, `Kilometer`, `Barrel`, `Bag`, `Pallet`. Default: `Ton`.

**Rate type** — `RatePerHour` (default), `RatePerDay`, `RatePerLoad`, `RatePerTon`, `RatePerTonne`, `RatePerYard`, `RatePerBushel`, `RateCommission`, `RateFlatCommission`, `RateFlatRate`.

**Site waypoint** — `pickup`, `staging`, `drop_off`, `weigh_point`.

**Site type** — `Plant`, `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

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/api-reference/introduction#authentication">
    Get a bearer token for these endpoints.
  </Card>

  <Card title="Webhooks" icon="webhook" href="/integrations/webhooks">
    Get notified when an imported payload fails to process.
  </Card>

  <Card title="Orders and Projects" icon="diagram-project" href="/concepts/orders-projects">
    How the dispatch hierarchy fits together.
  </Card>

  <Card title="Tickets and Timesheets" icon="receipt" href="/concepts/tickets-timesheets">
    What a Ticket records, and how it becomes a Settlement.
  </Card>
</CardGroup>
