Wanload Partner API
For TMS platforms, load boards, shippers with their own software, and developers. One endpoint to post a load, a sandbox that never reaches a carrier, and retries that cannot create duplicates. Free to integrate.
wl_test_. The key is shown once.Base URL: https://www.wanload.com/api/v1. All requests and responses are JSON.
Every request carries your key in the x-api-key header.
x-api-key: wl_<64 hex chars> # live x-api-key: wl_test_<64 hex chars> # sandbox
We store only a hash of the key, so it cannot be shown again — keep it somewhere safe. Each key belongs to exactly one Wanload account: every load it posts is owned by that account, and it can only read, update or cancel that account's loads. There is no way to post under another account's identity.
wl_test_ key cannot see, edit or cancel live loads, and a live key cannot touch sandbox ones — even on the same account.POST /api/v1/loads
curl -X POST https://www.wanload.com/api/v1/loads \
-H "x-api-key: wl_test_..." \
-H "Content-Type: application/json" \
-d '{
"external_id": "YOUR-LOAD-1",
"origin_city": "Dallas", "origin_state": "TX",
"destination_city": "Atlanta", "destination_state": "GA",
"pickup_date": "2026-10-01",
"equipment_type": "dry_van",
"rate": 1850
}'Returns 201 and the full load record. Store the id — every other endpoint is keyed by it. The load_code is the short human-facing reference your customer will see in Wanload.
{
"success": true,
"data": {
"id": "8f14e45f-ea4e-4c72-a0b1-9a2c3d4e5f60",
"load_code": "A1B2C3",
"status": "posted",
"external_id": "YOUR-LOAD-1",
"origin_city": "Dallas",
"origin_state": "TX",
"rate": "1850.00",
"is_sandbox": true,
"created_at": "2026-09-26T14:02:11.402Z"
}
}| Field | Type | Notes |
|---|---|---|
origin_city | string | — |
origin_state | string | 2-letter code; normalised to uppercase |
destination_city | string | — |
destination_state | string | 2-letter code; normalised to uppercase |
pickup_date | string | YYYY-MM-DD |
equipment_type | string | See the list below |
rate | number | USD, minimum 500 |
| Field | Type | Notes |
|---|---|---|
external_id | string | Max 255. Your id for this load — see Safe retries. |
origin_zip | string | — |
destination_zip | string | — |
origin_lat / origin_lng | number | Geocoded coordinates, if you have them |
destination_lat / destination_lng | number | — |
delivery_date | string | YYYY-MM-DD; must be on or after pickup_date |
pickup_time_start / pickup_time_end | string | Pickup window |
delivery_time_start / delivery_time_end | string | Delivery window |
load_type | enum | ftl | partial | ltl |
weight | number | Positive |
length / height / width | number | Positive |
pieces | integer | Positive |
description | string | Max 1000. Stored as the load's commodity; HTML is stripped. |
special_requirements | string | Max 1000; HTML is stripped |
pickup_contact_name | string | Max 100 |
pickup_contact_phone | string | Max 20, valid phone number |
delivery_contact_name | string | Max 100 |
delivery_contact_phone | string | Max 20, valid phone number |
reference_number | string | Max 100. The shipper's BOL/PO — free text, not unique. |
temperature_min / temperature_max | integer | Reefer loads |
distance | number | Miles, positive |
payment_terms | enum | immediate | net7 | net15 | net30 | net45 | net60 |
payment_method | enum | ach | check | wire | factoring | credit_card | zelle | venmo | paypal |
reference_number is your customer's BOL or PO — free text, and several loads may share one. To carry your load id, and to make retries safe, use external_id.Send the exact lower-case value. Carrier equipment filters match on these, so a display label like "Dry Van" would store a load no carrier filter finds.
auto_carrierbox_truckcargo_vanconestogadry_vandump_trailerflatbedhopperhotshotintermodallivestocklowboypower_onlyreeferrgnsprinter_vanstep_decktankerIf a request times out you cannot tell whether the load was created. Two mechanisms make retrying safe; you can use either or both.
Send your own id for the load and simply retry. The first call creates it and returns 201; any repeat returns 200 with the original load and idempotent_replay: true. No second load, no second carrier broadcast, no second load.created webhook.
PATCH.external_id is immutable; PATCH cannot repoint it, so the mapping always has exactly one answer.For callers with no stable id of their own. Max 255 characters, honoured for 24 hours.
Idempotency-Key: 8f14e45f-ea4e-4c72-a0b1-9a2c3d4e5f60
| Situation | Response |
|---|---|
| Same key, same body | 200 with the original load, idempotent_replay: true |
| Same key, different body | 409 idempotency_key_reuse — replaying would return a load that is not the one you described |
| An identical request is still running | 409 request_in_progress with Retry-After |
| The earlier attempt failed | The key is released — a transient 500 stays retryable |
external_id is the stronger of the two. If a request carries both, external_id wins and the two can never disagree.GET /api/v1/loads — your loads, newest first. This is how you look up what became of an external_id, or re-sync after an outage without replaying POSTs.
| Parameter | Notes |
|---|---|
external_id | Return only loads with this id |
status | One of posted, booked, confirmed, en_route, picked_up, in_transit, delayed, delivered, cancelled |
include_cancelled | true/1 or false/0. Default false. |
page | 1-based. Default 1. |
limit | Default 20, max 100. |
curl "https://www.wanload.com/api/v1/loads?external_id=YOUR-LOAD-1&include_cancelled=true" \ -H "x-api-key: wl_test_..."
{
"success": true,
"data": {
"loads": [ { "id": "...", "load_code": "A1B2C3", "status": "posted", "external_id": "YOUR-LOAD-1" } ],
"pagination": { "total": 1, "limit": 20, "offset": 0, "hasMore": false }
}
}An unknown status, a typo'd boolean or an empty external_id returns 400 rather than being silently ignored.
| Endpoint | What it does |
|---|---|
GET /api/v1/loads/{id} | Fetch one load and its current status. |
PATCH /api/v1/loads/{id} | Update a load. Accepts the same fields as create, all optional. |
DELETE /api/v1/loads/{id} | Cancel a load. |
Update and cancel only work while the load is still posted. Once a carrier has booked it, Wanload's own lifecycle takes over and the API returns 409 with the current status — an external system must not be able to pull freight out from under a booked carrier.
A load id that is not yours returns 404, never 403, so ids cannot be probed.
Set a webhook_url on your key and we POST lifecycle events to it, so you do not have to poll.
| Event | Fires when |
|---|---|
load.created | A load was created through the API |
load.updated | A PATCH succeeded |
load.cancelled | A DELETE succeeded |
load.booked | A carrier booked the load |
load.delivered | The load was marked delivered |
Requests carry these headers:
x-wanload-event: load.booked x-wanload-signature: t=1758196800,v1=<hex hmac> x-wanload-delivery: <uuid, stable across retries of this event> x-wanload-attempt: <1-5>
Each key has a signing secret (whsec_…), returned once when the key is created. The signature covers "{timestamp}.{raw_body}".
import hmac, hashlib, time
def verify(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
ts = int(parts["t"])
if abs(time.time() - ts) > tolerance: # replay window
return False
expected = hmac.new(
secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256
).hexdigest()
# Any v1= may match: during a rotation we send two.
return any(
hmac.compare_digest(v, expected)
for k, v in (p.split("=", 1) for p in header.split(",") if "=" in p)
if k == "v1"
)==.v1= matches — during a rotation we send two.A failed delivery is retried up to 5 times: immediately, then after 1 minute, 5 minutes, 30 minutes and 2 hours.
5xx, 408 and 429.4xx — if your endpoint rejects the payload, repeating it will not help.x-wanload-delivery, which is stable across every attempt of the same event.2xx first, do the work after.Rotate from Settings → Integrations → Rotate webhook secret. For 24 hours afterwards we sign with both the new secret and the one it replaced, so your endpoint keeps verifying whether or not you have redeployed yet. You cannot rotate twice inside that window.
GET /api/v1/loads/{id}.| Limit | Scope | On exceeding |
|---|---|---|
| Requests per minute | Per account | 429 |
| 500 loads created per 24 hours | Per account; live and sandbox metered separately | 429 with daily_post_limit and Retry-After |
| 128 KB request body | Per request | 413 |
A retry answered as a replay does not count against the daily limit, so retrying safely costs you nothing. The limit is per account, not per key — minting extra keys does not raise it. If you legitimately need more, contact us rather than working around it.
Every response is { success: true, data } or { success: false, error }, and carries an x-wanload-request-id header. Quote that id and we can find your exact call.
| Status | Meaning |
|---|---|
400 | Validation failed, or the JSON was malformed. |
401 | Missing, malformed or revoked API key. |
402 | The account has an unpaid platform-fee balance (billing_blocked). |
403 | The account cannot post: inactive, wrong type, or missing a payment method, ID verification or broker authority. Some carry needs_onboarding and an onboarding_url. |
404 | No load with that id belongs to your account. |
409 | A conflict. external_id_conflict, idempotency_key_reuse, request_in_progress, or a load no longer in posted status. |
413 | Request body over 128 KB. |
429 | Rate limited, or the daily posting allowance is spent. Honour Retry-After. |
503 | Temporarily unavailable. Retryable — the body sets retryable: true. |
Sandbox keys skip every trust check, because sandbox loads never reach a carrier. A live key needs the posting account to be in good standing:
Nothing else changes: same base URL, same payload, same behaviour. Swap the key.
If your customers post to Wanload through your platform, we can provision a Wanload account and key per customer, and bill each of them directly — you never touch payments. Email partners@wanload.com and we will issue you a partner provisioning key.