Wanload Partner API

Post loads to Wanload from your own system

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.

Quick start

  1. Create a test key. In Wanload, open Settings → Integrations and create a key in Test mode. It works immediately — no verification needed — and starts with wl_test_. The key is shown once.
  2. Post a load. One request, below. Loads posted with a test key are flagged sandbox and stay invisible to real carriers, so you can exercise the whole flow safely.
  3. Go live. Swap the test key for a live one. Same URL, same payload. See Going live.

Base URL: https://www.wanload.com/api/v1. All requests and responses are JSON.

Authentication

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.

A sandbox key and a live key are separate worlds. A wl_test_ key cannot see, edit or cancel live loads, and a live key cannot touch sandbox ones — even on the same account.

Create a load

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"
  }
}

Load fields

Required

FieldTypeNotes
origin_citystring—
origin_statestring2-letter code; normalised to uppercase
destination_citystring—
destination_statestring2-letter code; normalised to uppercase
pickup_datestringYYYY-MM-DD
equipment_typestringSee the list below
ratenumberUSD, minimum 500

Optional

FieldTypeNotes
external_idstringMax 255. Your id for this load — see Safe retries.
origin_zipstring—
destination_zipstring—
origin_lat / origin_lngnumberGeocoded coordinates, if you have them
destination_lat / destination_lngnumber—
delivery_datestringYYYY-MM-DD; must be on or after pickup_date
pickup_time_start / pickup_time_endstringPickup window
delivery_time_start / delivery_time_endstringDelivery window
load_typeenumftl | partial | ltl
weightnumberPositive
length / height / widthnumberPositive
piecesintegerPositive
descriptionstringMax 1000. Stored as the load's commodity; HTML is stripped.
special_requirementsstringMax 1000; HTML is stripped
pickup_contact_namestringMax 100
pickup_contact_phonestringMax 20, valid phone number
delivery_contact_namestringMax 100
delivery_contact_phonestringMax 20, valid phone number
reference_numberstringMax 100. The shipper's BOL/PO — free text, not unique.
temperature_min / temperature_maxintegerReefer loads
distancenumberMiles, positive
payment_termsenumimmediate | net7 | net15 | net30 | net45 | net60
payment_methodenumach | check | wire | factoring | credit_card | zelle | venmo | paypal
reference_number is not external_id. 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.

Equipment types

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_decktanker

Safe retries

If a request times out you cannot tell whether the load was created. Two mechanisms make retrying safe; you can use either or both.

1. external_id (recommended)

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.

  • Unique per account, with live and sandbox counted separately — reusing a test id in production is safe.
  • Cancelling a load frees its id, so delete-and-repost works.
  • The body of a replayed request is ignored — you get the load as originally posted. To change a live load, use PATCH.
  • external_id is immutable; PATCH cannot repoint it, so the mapping always has exactly one answer.
  • Omitting it is allowed, but then retries will create duplicates.

2. Idempotency-Key header

For callers with no stable id of their own. Max 255 characters, honoured for 24 hours.

Idempotency-Key: 8f14e45f-ea4e-4c72-a0b1-9a2c3d4e5f60
SituationResponse
Same key, same body200 with the original load, idempotent_replay: true
Same key, different body409 idempotency_key_reuse — replaying would return a load that is not the one you described
An identical request is still running409 request_in_progress with Retry-After
The earlier attempt failedThe key is released — a transient 500 stays retryable
Generate the key once per logical load and reuse it across retries. A client that generates a fresh key per attempt gets no protection at all, which is exactly why external_id is the stronger of the two. If a request carries both, external_id wins and the two can never disagree.

List and reconcile

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.

ParameterNotes
external_idReturn only loads with this id
statusOne of posted, booked, confirmed, en_route, picked_up, in_transit, delayed, delivered, cancelled
include_cancelledtrue/1 or false/0. Default false.
page1-based. Default 1.
limitDefault 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 }
  }
}
One external_id can return several loads. Uniqueness covers only live loads, so each cancel-and-repost cycle leaves a cancelled row behind. The live one — at most one — is the load without a cancelled status. That is why this is a list and not a single-object lookup.

An unknown status, a typo'd boolean or an empty external_id returns 400 rather than being silently ignored.

Fetch, update, cancel

EndpointWhat 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.

Webhooks

Set a webhook_url on your key and we POST lifecycle events to it, so you do not have to poll.

EventFires when
load.createdA load was created through the API
load.updatedA PATCH succeeded
load.cancelledA DELETE succeeded
load.bookedA carrier booked the load
load.deliveredThe 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>

Verifying the signature

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"
    )
  • Verify against the raw request body, exactly as received. Parsing the JSON and re-serialising it will not match — key order and spacing are not preserved.
  • Use a constant-time comparison, not ==.
  • Reject timestamps outside your tolerance in either direction.
  • Accept the delivery if any v1= matches — during a rotation we send two.

Retries

A failed delivery is retried up to 5 times: immediately, then after 1 minute, 5 minutes, 30 minutes and 2 hours.

  • We retry on a network error, a timeout, 5xx, 408 and 429.
  • We do not retry other 4xx — if your endpoint rejects the payload, repeating it will not help.
  • Your endpoint must be idempotent. Deduplicate on x-wanload-delivery, which is stable across every attempt of the same event.
  • We time out after 10 seconds. Acknowledge with a 2xx first, do the work after.

Rotating the signing secret

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.

Webhooks are a hint, not the source of truth. The authoritative answer is always GET /api/v1/loads/{id}.

Limits

LimitScopeOn exceeding
Requests per minutePer account429
500 loads created per 24 hoursPer account; live and sandbox metered separately429 with daily_post_limit and Retry-After
128 KB request bodyPer request413

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.

Errors

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.

StatusMeaning
400Validation failed, or the JSON was malformed.
401Missing, malformed or revoked API key.
402The account has an unpaid platform-fee balance (billing_blocked).
403The account cannot post: inactive, wrong type, or missing a payment method, ID verification or broker authority. Some carry needs_onboarding and an onboarding_url.
404No load with that id belongs to your account.
409A conflict. external_id_conflict, idempotency_key_reuse, request_in_progress, or a load no longer in posted status.
413Request body over 128 KB.
429Rate limited, or the daily posting allowance is spent. Honour Retry-After.
503Temporarily unavailable. Retryable — the body sets retryable: true.

Going live

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:

  • A payment method on file. Wanload charges the platform fee to the account that owns the load, on delivery.
  • ID verification approved, for keys you created yourself.
  • For broker accounts, an FMCSA-verified USDOT or MC number. Operating authority is not waived — a card says the fee is collectable, not that the entity may broker freight.
  • No unpaid platform-fee balance.

Nothing else changes: same base URL, same payload, same behaviour. Swap the key.

Running a TMS?

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.

Wanload Developer API — post loads from your own system