# SmartLoads Partner API and webhooks

> The SmartLoads Partner API is a REST API at https://api.smartloads.io/v1. An agency admin creates an API key in SmartLoads, and any tool can then read that agency's loads, AI carrier leads and Cover Cards as JSON. Webhooks push events such as lead.matched_load and load.booked to your URL within about 30 seconds, signed with HMAC-SHA256, or straight into a Slack channel.

Source: https://smartloads.io/developers · Updated 2026-09-26

## Why SmartLoads has its own API

SmartLoads is an integration partner in its own right, not an add-on to one TMS. A brokerage, an agency or a TMS vendor connects to SmartLoads, so an agency's integrations don't depend on which TMS its parent brokerage runs. It's the same model TMS vendors use to connect to Truckstop.

What you can build with it:

- **Sync the live board into your TMS or a spreadsheet.** SmartLoads has already turned shipper emails, PDFs and sheets into clean loads, so you read the finished loads.
- **Route AI carrier leads to your own tools.** Every call the SmartLoads AI agents answer becomes a lead with the carrier, MC number, lane and the rate they asked for.
- **Pull Cover Cards.** Customer paperwork forwarded to SmartLoads comes back as structured fields matched to a load.
- **React to events as they happen.** Webhooks for new leads, lead-to-load matches, Cover Cards, bookings and loads leaving the board.

## Authentication

Every request carries an agency API key:

```
Authorization: Bearer sl_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```

`X-API-Key: sl_live_…` also works.

- Agency admins create keys in **Settings → Connectors → SmartLoads API**. A key is shown once; SmartLoads stores only its SHA-256 hash.
- Create one key per tool or partner so each can be revoked on its own. An agency can have 10 active keys.
- A key belongs to one agency, and every response is that agency's data only.
- **Scopes:** `loads:read`, `leads:read`, `cover_cards:read`.

## Conventions

- **Base URL:** `https://api.smartloads.io/v1`. JSON in and out.
- **Formats:** timestamps are ISO 8601 UTC; dates such as `board_date` and `ship_date` are `YYYY-MM-DD`.
- **Pagination:** lists return `{ "data": [...], "next_cursor": "…" | null }`, oldest first. Pass `next_cursor` back as `?cursor=`. `limit` defaults to 50, max 200.
- **Incremental sync:** save the newest `updated_at` you've seen and ask for `?updated_since=<timestamp>` next time.
- **Rate limit:** 120 requests per minute per key. Authenticated responses carry `X-RateLimit-Limit` and `X-RateLimit-Remaining`; over the limit you get `429` with `Retry-After: 60`.
- **Errors:** an HTTP status plus `{ "error": { "code": "…", "message": "…" } }`.

| Status | Code | Meaning |
|---|---|---|
| 400 | `invalid_status`, `invalid_cursor`, `invalid_updated_since` | Bad parameter |
| 401 | `missing_api_key`, `invalid_api_key` | No key, wrong key, or revoked |
| 403 | `insufficient_scope` | The key lacks the scope for this endpoint |
| 404 | `not_found` | Unknown endpoint, or no such record for this agency |
| 429 | `rate_limited` | More than 120 requests a minute |
| 503 | `rate_limit_unavailable` | The limit couldn't be checked; retry shortly |

## Endpoints

### GET /me

Checks a key and returns its agency and scopes.

```bash
curl https://api.smartloads.io/v1/me -H "Authorization: Bearer $SMARTLOADS_KEY"
```

```json
{ "agency": { "id": "…", "name": "Example Brokerage" },
  "key": { "name": "TMS sync", "prefix": "sl_live_ab12", "scopes": ["loads:read", "leads:read", "cover_cards:read"] } }
```

### GET /loads and /loads/{id}

Scope `loads:read`. Query parameters:

- `status`: `active` (the default: on the board, not archived), `open`, `booked`, `covered`, `archived` or `all`
- `updated_since`, `limit`, `cursor`

```json
{
  "id": "3a2489ad-…",
  "load_number": "SL-TX-2025-0418",
  "status": "open",
  "board_date": "2026-09-14", "ship_date": "2026-09-14", "delivery_date": null,
  "customer": { "name": "Example Steel" },
  "source": "email_xlsx",
  "pickup": { "city": "JEWETT", "state": "TX" },
  "delivery": { "city": "WEIMAR", "state": "TX" },
  "equipment": { "type": "Flatbed", "length_ft": 20, "tarp_required": false },
  "freight": { "commodity": "Rebar", "weight_lbs": 48075 },
  "miles": 120,
  "rate": { "basis": "flat", "customer_total": 1000, "per_ton": null, "target_pay": 750, "max_pay": 850 },
  "posted": { "truckstop_at": "…", "dat_at": null },
  "updated_at": "…"
}
```

For per-ton loads, `rate.basis` is `per_ton` and `rate.per_ton` is the customer's rate per ton.

### GET /leads and /leads/{id}

Scope `leads:read`. These are AI carrier leads: calls answered by the AI phone agents, plus carrier chat and board requests. Query parameters are `updated_since` (the default is the last 7 days), `limit` and `cursor`.

```json
{
  "id": "…", "created_at": "…", "updated_at": "…",
  "status": "pending",
  "load_id": "…",
  "channel": "synthflow",
  "carrier": { "name": "Example Trucking", "mc": "1234567", "contact_name": "Dana", "phone": "+15125550100" },
  "lane": { "pickup": { "city": "Jewett", "state": "TX" }, "delivery": { "city": "Weimar", "state": "TX" } },
  "equipment": "Flatbed",
  "rate_asked": "900",
  "summary": "Has a 48' flatbed empty in Jewett tomorrow morning.",
  "high_intent": true
}
```

The AI quotes only the starting rate and never books; a person books every load.

### GET /cover-cards and /cover-cards/{id}

Scope `cover_cards:read`. These are customer load documents forwarded to SmartLoads, read into fields and matched to a load. Query parameters:

- `status`: `matched` (the default), `possible`, `unmatched` or `all`
- `since`, `limit`, `cursor`

## Webhooks

Agency admins add endpoints in **Settings → Connectors → SmartLoads API → Webhooks**: a URL, the events to send and an optional name. SmartLoads POSTs each event, usually within 30 seconds.

| Event | When it fires |
|---|---|
| `lead.created` | A carrier lead comes in: AI phone agents, carrier chat, Telegram or the public board |
| `lead.matched_load` | A lead is tied to a load on the board, at intake or when someone attaches it later |
| `cover_card.ready` | Forwarded paperwork has been read, and again when someone confirms its load |
| `load.booked` | A load is booked |
| `load.covered` | A booked load has its carrier |
| `load.removed` | A load leaves the board: archived, cleared or deleted |

Each request looks like this:

```
POST https://your-tms.example.com/smartloads
Content-Type: application/json
X-SmartLoads-Event: lead.matched_load
X-SmartLoads-Delivery: 0b8f…
X-SmartLoads-Signature: t=1790000000,v1=<hex HMAC-SHA256>

{ "id": "5c1f…", "type": "lead.matched_load", "created_at": "2026-09-26T18:04:11Z",
  "agency_id": "…", "data": { "lead": { … }, "load": { … } } }
```

- `id` is the event id. The same event sent to two endpoints has the same id, so use it to drop duplicates.
- `data` uses the same shapes as the read API and shows the record as it was when the event happened.

### Verify the signature

Each endpoint has a signing secret (`whsec_…`), shown once when you add it. `v1` is the hex HMAC-SHA256 of `"<t>.<raw body>"`. Recompute it from the raw body, compare in constant time, and reject any `t` more than 5 minutes old.

```js
import crypto from "node:crypto";

function isFromSmartLoads(rawBody, header, secret) {
  if (!header) return false;
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (!(Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300)) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const given = Buffer.from(parts.v1 ?? "");
  const wanted = Buffer.from(expected);
  return given.length === wanted.length && crypto.timingSafeEqual(given, wanted);
}
```

### Retries

- Reply with any `2xx` within 10 seconds; do slow work after you reply. Redirects aren't followed.
- A failed delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours: 7 tries over about a day.
- If every delivery to an endpoint fails for 3 days, SmartLoads switches it off and shows why. Switching it back on resumes delivery.
- The URL must be public `https`. Private and internal addresses are refused.

### Slack

Paste a Slack incoming-webhook URL as the endpoint and events arrive as readable Slack messages. A matched lead shows the carrier, MC number, contact and phone, the load and lane, the rate they asked for, the call summary and a link to the lead. It's the quickest way to ping a channel when an AI lead matches a load.

## Frequently asked questions

### Does SmartLoads replace our TMS?

No. Your TMS stays the system of record for settlements, carriers and accounting. SmartLoads runs the operational front end: load intake, the live board, posting and carrier calls. The API is how your TMS or tools read what SmartLoads produces.

### Can the API book or cover loads?

Not in v1, which is read-only plus webhooks. Writing loads in from a TMS is next. Booked and covered status will only ever come from a person's action, never from AI.

### How do we get access?

If your agency uses SmartLoads, an admin creates a key in Settings → Connectors → SmartLoads API. If you're a TMS vendor or developer building for broker agencies, contact us through the form on smartloads.io.

### Is there a sandbox?

Not yet. A sandbox agency with sample loads and leads is on the roadmap alongside partner apps.
