very-code_
← Back to homepage

Cargoboard Logistics API Integration

Open-source PHP client for the Cargoboard API: binding freight prices, bookings, labels and confirmations, real-time tracking with webhooks, invoices and ADR dangerous-goods data, from groupage to whole vehicles across 32 European countries.

PHP 8.2+ REST API Freight / LTL Webhooks PHPStan level 8 Packagist Open Source

What is cargoboard-php?

Cargoboard is a digital freight forwarder: instead of asking for a quote and waiting for someone to answer, you send the shipment and get a binding price back in the same request - then book that exact price. It handles groupage, part loads by loading metre, full truck loads, whole vehicles and parcels across 32 European countries.

cargoboard-php is an open-source PHP client covering every endpoint of their public API, plus the Track & Trace webhook payload and the customer-facing tracking links. It is typed to PHPStan level 8, validates a request against Cargoboard's documented rules before it goes out, and was verified end to end against the live sandbox: quotation, booking, order retrieval, tracking, label and confirmation PDFs, and cancellation.

Version 1.1 added the parts you only learn from running an integration in production - which is exactly where it came from: field feedback from an integrator on a live account, with every claim re-checked against Cargoboard's OpenAPI definition before it was implemented.

What the API covers

Every endpoint Cargoboard publishes, behind typed request and response objects.

quote()
A binding price for a shipment, with the runtime, the delivery window, the full surcharge breakdown and the CO2 figure.
bookQuotation()
Books that exact quoted price rather than re-pricing at booking time.
placeOrder()
Books directly, skipping the quotation step when the price is not the point.
listQuotations()
fetchQuotation()
The stored quotations, with filtering, paging and the booking state of each.
listOrders()
fetchOrder()
The stored orders: status, physical shipment status, lines with their barcodes, partners, invoices and the actual settled price.
cancelOrder()
Cancels a booking - which, since Cargoboard has no update endpoint, is also how you correct one.
fetchLabels()
fetchConfirmation()
Shipment labels in A4 or A6 and the order confirmation, as PDF bytes.
fetchTracking()
The full status feed with milestones, locations and the refined collection and delivery windows.
listInvoices()
fetchInvoicePdf()
Invoices with amounts, due dates and paid/overdue state, and the PDF behind each one.
fetchAdrData()
ADR dangerous-goods data by UN number, convertible straight into a declaration on a shipment line.

What it does

Price, then book that price

A quotation returns a binding price with its cost items broken out - freight, surcharges, tolls - so you can show a customer what they are paying for, then book the same quotation ID without a second pricing round.

Every transport type

Groupage and LTL by pallet or carton, part loads priced by loading metre, and five whole-vehicle classes from a curtain van to a 40-tonne truck - each with its payload ceiling and its permitted products enforced locally.

Parcel mode

The same request, sent with one header, is priced and booked as a parcel instead of freight. Switching mode also switches on the parcel rule set: 32 kg physical and volumetric, side and girth limits, twenty per pickup per day, no dangerous goods.

Tracking that reads properly

Around a third of live tracking events carry no message at all - they carry the refined pickup and delivery windows instead. describe() renders each event as the line a human should see, and the feed is deduplicated on the event ID.

Dangerous goods

ADR data looked up by UN number and converted directly into a declaration on a line, including packaging instructions, special regulations and the special-provision-188 check.

Webhooks and tracking links

The Track & Trace payload is parsed into the same shape as the API events, and customer-facing tracking URLs are built for you - including the variant that skips the captcha.

Two rule sets, one request

A quotation needs a post code and a country; a booking needs names, streets, cities and a pickup date. The same request object is validated under whichever set applies, so a missing booking field fails with a field-level message instead of an HTTP 422.

Lenient where it should be

Parsing is deliberately forgiving: an unrecognised enum value becomes null and keeps its raw string, so a new status code invented on Cargoboard's side cannot break a running integration.

Validation, and the warnings that are not errors

Every quotation and booking is checked locally first, with messages prefixed by the same field paths Cargoboard uses in its own 422 responses. The rules cover mandatory address fields per mode, Monday-to-Friday pickups and deliveries, pickup-window consistency, FIX-only delivery dates, per-line content and dimensions, loading-metre geometry, vehicle payload ceilings and their required products, insurance needing a declared goods value, EUR-only values, dangerous-goods declarations and the full parcel rule set.

Some rules Cargoboard enforces at the depot rather than with an HTTP status. A private consignee booked without either a delivery appointment or permission to leave the goods is accepted by the API - and then causes trouble on the day of delivery. Refusing that booking locally would mean the library overruling the API, so those cases are returned as warnings, logged on every quote and booking, never thrown.

$errors   = $client->validateLocally($request);  // blocks the call
$warnings = $client->warningsFor($request);      // logged, never thrown

Three things the schemas do not tell you

  • A tracking event with no message is not an empty event. Roughly a third of the feed has message: null and carries the refined collection and delivery windows instead - the most useful thing on the feed. The obvious fallback chain prints a bare status number for all of them.
  • The event ID is the only safe deduplication key. A shipment notified by both phone and e-mail produces several events sharing a code and a timestamp, so keying stored rows on that pair drops events silently. The ID appears in no published schema; the library reads it anyway.
  • The dangerous-goods lookup answers 202 with an empty body when it has not cached a UN number yet. Treated as a success it would parse into a declaration with no hazard class, so it raises a dedicated exception instead.

Cargoboard also has no update endpoint. A booked order cannot be amended - only cancelled and rebooked, or fixed by support. That is worth designing around before the first order goes out, not after.

Quick start

composer require very-code-com/cargoboard-php
$client = CargoboardClient::sandbox('your-api-key');

$request = new ShipmentRequest(
    product:   Product::Standard,
    shipper:   new Shipper(
        address:  new Address('40239', CountryCode::DE, 'Duesseldorf', 'Examplestreet 12a'),
        name:     'Producer ABC GmbH & Co. KG',
        pickupOn: '2026-09-01',
    ),
    consignee: new Consignee(
        address: new Address('41061', CountryCode::DE, 'Moenchengladbach', 'Examplestreet 5'),
        name:    'Consignee ABC AG',
    ),
    lines:     [new Line('Werkzeugmaschine', 1, PackageType::EuroPallet, 120, 80, 120, 200.0)],
);

$quotation = $client->quote($request);          // 90.85 EUR, 1-2 days
$order     = $client->bookQuotation($quotation->id, $request);

file_put_contents('labels.pdf', $client->fetchLabels($order->id));

foreach ($client->fetchTracking($order->id)->timeline() as $event) {
    printf("%-5s %s\n", $event->code, $event->describe());
    // 540   Estimates updated: collection 18.08. 07:00-15:00, delivery 19.08. 06:00 - 21.08. 14:00
}

On the sandbox nothing is executed and no truck is scheduled, though you still receive the confirmation e-mail so the data can be checked. On production every booking is a real, billable transport.

Built to stay working

The exception hierarchy is granular on purpose - authentication, ADR sync pending, not found, conflict, unprocessable entity, rate limit with a retry hint, and server errors that know whether they are retryable - so a caller can catch exactly the case it has an answer for.

Continuous integration runs the unit suite on PHP 8.2, 8.3 and 8.4 plus a lowest-dependency leg that proves the declared floor actually works, PHPStan level 8, a dependency audit against the advisory database, and a coverage gate that fails below 85%. A nightly integration workflow books, tracks and cancels a real sandbox order end to end.

One diagnostic worth repeating from the README: if a key that "should work" returns 403, re-copy it character by character first. Cargoboard answers with an identical 403 for a mistyped key, a missing header, a wrong-environment key, an unactivated account and an endpoint the key is not entitled to. Nothing in the status, body or headers tells them apart.

Who is this for?

PHP teams moving pallets rather than parcels - a manufacturer shipping machinery, a distributor running LTL freight across Europe, an ERP that needs a binding price inside a checkout or an order flow. It suits anyone who wants freight pricing and booking to be an ordinary typed service call, with the carrier's undocumented behaviour already handled and written down.