# Bulk data export

The bulk export streams whole tables of your workspace's data as newline-delimited JSON (JSON Lines). Use it to load a data warehouse, to back up your data, or to take your data with you.

- **On demand.** Nothing runs on a schedule and no file is stored. Each request reads your data at that moment and streams it to you. You can pull as often as you like.
- **Full or incremental.** Leave out `updated_after` for every row. Pass it to get only rows that changed.
- **One format.** JSON Lines, one JSON object per line, UTF-8. Add `.gz` to the URL for gzip. CSV and Parquet are not offered. Most warehouse loaders read JSON Lines directly.
- **Your workspace only.** Each request returns rows for the workspace that owns the key. Partner keys add `?firm_id=` (see [Partner API](partner-api.md)).
- **A link instead of a key.** To hand a dataset to a person, or get one through an AI assistant, ask for a 24-hour link with `POST /v1/exports` and `kind: workspace_data` (see [File exports](exports.md)).

## Datasets

| Dataset | One row per | Changed-since fields |
|---|---|---|
| `customers` | Customer (billing entity) | `updated_at` |
| `properties` | Property / jobsite, with coordinates | `updated_at` |
| `jobs` | Job | `updated_at` |
| `estimates` | Estimate header with totals | `updated_at` |
| `estimate_lines` | Estimate line item. All lines of an estimate are re-sent when the estimate changes | `updated_at` (of the estimate) |
| `change_orders` | Change order with value deltas | `updated_at` |
| `invoices` | Invoice with payment state | `updated_at` |
| `job_costs` | Job cost entry | `created_at`, `reviewed_at`, `reconciled_at` |
| `work_orders` | Crew work order (schedule and completion) | `updated_at` |
| `time_entries` | Crew clock-in/out: hours, category, wage rate | `clock_in`, `clock_out`, `approved_at`, `voided_at` |
| `allocations` | Work order, with its crew, people and equipment | `updated_at` (of the work order) |
| `events` | Event from the [event log](webhooks-and-events.md#event-feed) | `occurred_at` |

With `updated_after`, a row is included when any of its changed-since fields is later than the value you pass.

Row shapes:

- `customers`: the customer object from [Customers & sites](customers-and-sites.md), plus `archived_at`.
- `properties`: `id`, `name`, `address`, `external_ref`, `client_account_id`, `latitude`, `longitude`, `property_type`, `pci`, `archived_at`, `created_at`, `updated_at`.
- `jobs`: the job object from [Jobs](jobs.md), including `custom_fields`.
- `estimates`: the estimate summary from [Estimates & proposals](estimates-and-proposals.md) (`totals`, `current_version`, `awarded_at`, `archived_at`, ...), without line items.
- `estimate_lines`: the line item object from the estimate detail, plus `estimate_id`, `job_id`, `area_id`, `area_name`, `estimate_updated_at`.
- `change_orders`: `id`, `job_id`, `number`, `title`, `kind`, `status`, `source`, `stage`, `amount_delta`, `tax_delta`, `cost_delta`, `days_delta`, `ep_estimate_id`, `decision_comment`, `sent_at`, `signed_at`, `created_at`, `updated_at`.
- `invoices`, `job_costs`, `time_entries`, `allocations`: the same objects as `GET /v1/jobs/{id}/invoices`, `/costs`, `/labor` and `/allocation` in [Job costing](job-costing.md).
- `work_orders`: `id`, `job_id`, `title`, `status`, `phase`, `crew_id`, `display_qty`, `display_unit`, `completed_qty`, `completed_unit`, `estimated_days`, `scheduled_date`, `dispatched_at`, `completed_at`, `created_at`, `updated_at`.
- `events`: `id`, `event_id`, `event_type`, `occurred_at`, `entity_type`, `entity_id`, `job_id`, `data`. Note that `entity_type` and `entity_id` are flat here, unlike the nested `entity` object in `GET /v1/events`.

## List datasets

`GET /v1/exports`

**Permission:** Read jobs

```bash
curl https://trutec.ai/api/v1/exports \
  -H "X-API-Key: sk_your_api_key_here"
```

```json
{
  "data": [
    {
      "dataset": "customers",
      "description": "Customers (billing entities)",
      "changed_since_fields": ["updated_at"],
      "url": "/api/v1/exports/customers.jsonl.gz"
    },
    {
      "dataset": "job_costs",
      "description": "Job cost entries by category",
      "changed_since_fields": ["created_at", "reviewed_at", "reconciled_at"],
      "url": "/api/v1/exports/job_costs.jsonl.gz"
    }
  ]
}
```

## Download a dataset

`GET /v1/exports/{dataset}.jsonl` or `GET /v1/exports/{dataset}.jsonl.gz`

**Permission:** Read jobs

| Name | Type | Required | Description |
|---|---|---|---|
| `dataset` | path | Yes | A dataset name from the table above |
| `updated_after` | ISO 8601 | No | Only rows changed after this time. Use the watermark from your last pull |

```bash
# Full export, gzipped
curl -o jobs.jsonl.gz https://trutec.ai/api/v1/exports/jobs.jsonl.gz \
  -H "X-API-Key: sk_your_api_key_here" -D headers.txt

# Incremental: only rows changed since the last watermark
curl "https://trutec.ai/api/v1/exports/jobs.jsonl?updated_after=2026-10-11T06:00:00Z" \
  -H "X-API-Key: sk_your_api_key_here"
```

Response body (one object per line, shortened here):

```json
{"id":4812,"name":"Northgate Plaza","company":"Northgate Property Group","address":"1200 Commerce Dr, Austin, TX","status":"won","status_label":"Won","estimated_value":48250.0,"client_account_id":915,"location_id":2290,"external_ref":"ERP-JOB-20931","custom_fields":{},"created_at":"2026-10-01T14:02:09.331201","updated_at":"2026-10-12T13:48:41.102938"}
{"id":4813,"name":"Riverside Medical Sealcoat","company":"Riverside Health","address":"88 Lake Rd, Austin, TX","status":"lead","status_label":"Lead","estimated_value":null,"client_account_id":917,"location_id":2295,"external_ref":null,"custom_fields":{},"created_at":"2026-10-12T09:15:00.000000","updated_at":"2026-10-12T09:15:00.000000"}
```

Response headers:

| Header | Meaning |
|---|---|
| `Content-Type` | `application/x-ndjson`, or `application/gzip` for `.gz` |
| `X-TruTec-Export-Dataset` | The dataset name |
| `X-TruTec-Export-Watermark` | Save this. Send it as `updated_after` on your next pull |
| `Content-Disposition` | `attachment; filename="<dataset>.jsonl[.gz]"` |

Rows are streamed in `id` order. The response has no total count and no pagination. Read until the stream ends. An empty dataset returns `200` with an empty body.

Errors:

- `404` `Unknown dataset`, with a `datasets` list of valid names
- `404` `Use /api/v1/exports/<dataset>.jsonl or .jsonl.gz` (any other extension, such as `.csv`)
- `400` `updated_after must be an ISO 8601 timestamp`
- `403` `API key lacks job read permission`

## Keeping a copy in sync

1. Run a full export of every dataset once.
2. Store each dataset's `X-TruTec-Export-Watermark`.
3. On a schedule (for example nightly), pull each dataset with `updated_after=<stored watermark>` and upsert rows by `id` (`work_order_id` for `allocations`; `estimate_id` plus line `id` for `estimate_lines`). Then store the new watermark.
4. Run a full export now and then (for example weekly) to catch deletes.

The watermark is set one minute before the export started. A row saved while an export is running is sent again on the next pull, so upserts are safe and nothing is missed.

!!! warning
    Incremental pulls cannot show hard deletes. A deleted row simply stops appearing. Archived customers, properties and estimates carry `archived_at`. Compare a periodic full export with your copy to find rows that were removed.

Other limits of incremental pulls:

- `job_costs` has no `updated_at`. An edit to a cost entry after it was created shows up incrementally only when it is reviewed or reconciled. Full exports always have the current values.
- `estimate_lines` re-sends every line of an estimate when the estimate's `updated_at` moves. Replace all lines for that `estimate_id` rather than upserting single lines.

## What the export does not include

The export covers the business records above. These are **not** in it:

- **Measurements and takeoffs** (geometry, areas, counts, quantities). Use `GET /v1/measurements` and `GET /v1/measurements/{id}` ([Measurements](measurements.md)).
- **Photos, videos, documents and other files.** Use `GET /v1/jobs/{id}/photos` for job photos. Other files are not available by API.
- **Proposal documents, e-signatures and proposal messages.** The `proposal_*` webhooks carry status only.
- **Issued estimate versions and the estimate edit log.** The export has the live estimate. Use `GET /v1/estimates/{id}/versions`, `GET /v1/estimates/{id}?version=N` and `GET /v1/estimates/{id}/revisions`.
- **Estimate lines that are hidden from bid totals.** Suppressed and inactive services and inactive areas are left out, the same as on the customer proposal.
- **Invoice line items** and invoice bill-to and remit-to text. The `invoices` dataset has amounts, tax, discount, dates, status and QuickBooks sync state.
- **Notes, activity timeline, tasks, emails and text messages.**
- **Contacts (people)** as their own records. Customer billing contact fields and job contact fields are included.
- **Your settings and catalog:** services and pricing, rate books, pipeline stages, custom field definitions, crews and equipment lists (crews, people and equipment appear only as assigned on work orders in `allocations`), users and permissions.
- **Assessments and condition ratings**, and **webhook delivery history**.

## Access and your data

- Your data belongs to you. You can export it at any time while your workspace has API access.
- API access needs an active plan that includes it (Gold or higher). If the subscription is canceled, past due or the trial has ended, API calls, including exports, are refused. **Run a full export before you cancel.**
- Exports do not count toward your key's usage. See [Limits & billing](limits-and-billing.md).
- Exports are generated live and never written to storage, so there is no export file to expire or leak.
