Waypoint3PL

API reference

Push orders to Waypoint and fetch live fulfilment statuses with a simple JSON REST API. Every account includes a sandbox environment, so you can integrate end-to-end before a single real parcel moves.

Authentication Environments Create an order List orders Fetch an order Cancel an order Shipping services Stock Errors Magento & Shopify

Authentication

Authenticate every request with your API key in the Authorization header. Your keys are on the API & Integrations page of the customer portal.

Authorization: Bearer wp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Keep keys out of client-side code and repositories. If a key leaks, regenerate it from the portal — the old key stops working immediately.

Environments

Your account has two isolated environments. The key you use decides which one you're talking to — the URLs are identical.

EnvironmentKey prefixBehaviour
Livewp_live_Real orders. Picked, packed and shipped by our warehouse.
Sandboxwp_test_Test orders. Visible in the portal's sandbox mode, never fulfilled.

Base URL: https://waypoint3pl.com/api/v1

POST/api/v1/orders

Create an order. We'll pick, pack and dispatch it (live), or simulate it (sandbox).

Request body

FieldTypeNotes
customer_namestringRequired. Your customer's name.
external_refstringStrongly recommended. The order's id in your own system (#1042, 000000145, anything). Shown beneath our reference in your portal so you can reconcile, and used to prevent duplicates. Aliases: external_id, order_number.
itemsarrayRequired. What we're picking — at least one item.
items[].namestringRequired. Product name as it should appear on the pick list.
items[].skustringYour SKU. Strongly recommended — it's what our pickers scan.
items[].quantityintegerDefaults to 1.
items[].unit_pricenumberPrice per unit, in the order currency.
totalnumberOrder value. If omitted, we calculate it from item prices.
currencystring3-letter code. Defaults to GBP.
ship_policystringtogether (one parcel — default) or separate (items may ship as they're ready).
destination.citystringDelivery city.
destination.countrystringTwo-letter code or name. Defaults to UK.
channelstringWhere the order came from: Shopify, Magento or Custom. Defaults to Custom.

Example

# Create a sandbox order
curl -X POST https://waypoint3pl.com/api/v1/orders \
  -H "Authorization: Bearer wp_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_name": "Jane Diaz",
    "external_ref": "#1042",
    "destination": {
      "line1": "14 Priory Gardens",
      "city": "Manchester",
      "postcode": "M20 2XJ",
      "country": "UK",
      "phone": "+44 7700 900123"
    },
    "channel": "Shopify",
    "ship_policy": "together",
    "currency": "GBP",
    "items": [
      { "name": "Lavender Soy Candle 220g", "sku": "LAV-220", "quantity": 2, "unit_price": 18.00 },
      { "name": "Gift Box Sleeve", "sku": "PKG-GBS", "quantity": 1, "unit_price": 2.50 }
    ]
  }'

# 201 Created
{
  "order_ref": "TEST-1013",
  "external_ref": "#1042",
  "status": "Received",
  "environment": "sandbox",
  "customer_name": "Jane Diaz",
  "destination": { "name": "Jane Diaz", "line1": "14 Priory Gardens", "city": "Manchester", "postcode": "M20 2XJ", "country": "UK", "shippable": true },
  "channel": "Shopify",
  "ship_policy": "together",
  "total": 38.5,
  "currency": "GBP",
  "items": [
    { "name": "Lavender Soy Candle 220g", "sku": "LAV-220", "quantity": 2, "unit_price": 18.0 },
    { "name": "Gift Box Sleeve", "sku": "PKG-GBS", "quantity": 1, "unit_price": 2.5 }
  ],
  "shipping": { "method": "Next Day Delivery", "speed": "next_day", "weight_kg": 1.25 },
  "courier": null,
  "tracking_number": null,
  "tracking_url": null,
  "cancellable": true,
  "received_at": "2026-07-31 12:04:11",
  "updated_at": "2026-07-31 12:04:11"
}

The delivery address

Send the full address — we can't put a parcel on a van without it. line1, city and postcode are what we actually need; the rest helps.

FieldMeaning
destination.line1 *Street address. Also accepted as address1 or street.
destination.line2Flat, unit, building. Also accepted as address2.
destination.city *Town or city.
destination.postcode *Postcode or ZIP. Also accepted as zip or postal_code.
destination.regionCounty, state or province. Also accepted as state or county.
destination.countryCountry name or 2-letter code. Defaults to UK.
destination.companyCompany name, for business deliveries.
destination.phoneContact number. Couriers ask for this on most express services.
destination.emailUsed only if a courier needs to contact the recipient.

* required before we can ship. We still accept an order without them — better in the system than rejected at the door — but the response comes back with address_warnings and destination.shippable: false, and the order is flagged in the portal until it's fixed. Check for that field while you still have the shopper's details to hand.

{
  "destination": {
    "name": "Jane Diaz",
    "line1": "14 Priory Gardens",
    "line2": "Flat 2",
    "city": "Manchester",
    "region": "Greater Manchester",
    "postcode": "M20 2XJ",
    "country": "UK",
    "phone": "+44 7700 900123"
  }
}

The recipient's name comes from customer_name, so you don't need to repeat it in the address.

Shipping preferences

All optional, but the more you send the better we can pick the right service.

FieldMeaning
shipping.method_codeYour platform's raw code for that option — flatrate_flatrate. This is the one that matters: we match it against your mapping table to pick the actual courier service. Map your codes under Shipping in the portal.
shipping.methodWhatever the shopper picked at checkout, in your own words — "Next Day Delivery". Shown to our warehouse team as-is.
shipping.speedNormalised so we can sort the pick queue: economy, standard, express, next_day or same_day.
shipping.weight_kgParcel weight in kilograms, if you know it.
ship_policytogether (one parcel) or separate (items may ship as they're ready).
group_refSet this when you split one basket across several orders — see below.

Picking a courier service

You map your checkout codes to our services once, in the portal under Shipping, and from then on every order that carries a shipping.method_code is matched automatically. Anything unmapped uses your fallback service.

The order response tells you what we picked and what it will cost, so you can reconcile against what the shopper paid:

# In any order response
"shipping": {
  "method": "Next Day Delivery",
  "method_code": "dpd_nextday",
  "quoted_service": "DPD Next Day",   // what your mapping chose
  "quoted_cost": 7.48,                // what you'll be billed
  "service": "DPD Next Day",          // what it actually went with
  "cost": 7.48
}

If we couldn't map a code, or the parcel is too heavy for the service you chose, the create response comes back with shipping_warnings and we pick a service that can carry it.

GET/api/v1/shipping/services

Every service we can offer, what each costs you, and how your codes currently map. Handy for building a mapping screen inside your own admin.

curl https://waypoint3pl.com/api/v1/shipping/services \
  -H "Authorization: Bearer wp_live_YOUR_KEY"

# 200 OK
{
  "currency": "GBP",
  "data": [
    { "code": "dpd-next-day", "name": "DPD Next Day", "courier": "DPD",
      "speed": "next_day", "max_weight_kg": 30, "price": 7.48 }
  ],
  "mappings": [ { "method_code": "dpd_nextday", "service_code": "dpd-next-day" } ],
  "default_service": { "code": "evri-standard", "name": "Evri Standard" }
}
Weight decides what we can use. Some services stop at 15 kg, others take 70. Send weight_kg on your products (below) and we'll always pick something that can actually carry the parcel.

Splitting one basket across several orders

If some items ship separately, send each part as its own order with its own external_ref, and put the shopper's original order number in group_ref on every part. They stay one purchase in the portal, and you can fetch them all back with GET /api/v1/orders?group_ref=1000123.

# Part 1 of order 1000123
{ "external_ref": "1000123-1", "group_ref": "1000123", "ship_policy": "separate",  }

# Part 2 of order 1000123
{ "external_ref": "1000123-2", "group_ref": "1000123", "ship_policy": "separate",  }

GET/api/v1/orders

List your most recent orders (up to 100, newest first) in the key's environment.

curl https://waypoint3pl.com/api/v1/orders \
  -H "Authorization: Bearer wp_live_YOUR_KEY"

# 200 OK
{ "environment": "live", "data": [ { "order_ref": "WP-84183",  } ] }

GET/api/v1/orders/{ref}

Fetch a single order — poll this to show live status, courier and tracking inside your own systems. {ref} can be either our order_ref or your own external_ref, so you can look an order up with whichever id you have to hand. URL-encode it if it contains characters like #.

# By our reference
curl https://waypoint3pl.com/api/v1/orders/WP-84183 \
  -H "Authorization: Bearer wp_live_YOUR_KEY"

# By your own reference (#1042, url-encoded)
curl https://waypoint3pl.com/api/v1/orders/%231042 \
  -H "Authorization: Bearer wp_live_YOUR_KEY"

Order status moves through: ReceivedPickingPackedDispatchedIn transitDelivered. Once dispatched, courier, tracking_number and tracking_url are populated. An order can also end at Cancelled or Returned.

Webhooks (we call you on every status change) are on the roadmap — until then, polling this endpoint is the way to sync status.
Safe to retry. Send the same external_ref twice and we won't create a second order — you'll get 200 with the original and "duplicate": true, rather than 201. So a webhook that fires twice can't put a parcel out twice.

POST/api/v1/orders/{ref}/cancel

Stop an order before it leaves the warehouse. Like the fetch endpoint, {ref} can be our reference or your own. Anything reserved for the order goes straight back to available stock.

curl -X POST https://waypoint3pl.com/api/v1/orders/1000123/cancel \
  -H "Authorization: Bearer wp_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Cancelled by the customer" }'

# 200 OK
{ "order_ref": "WP-84183", "status": "Cancelled", "cancellable": false,  }

Orders can be cancelled while they are Received, Picking or Packed. Once we've handed the parcel to a courier it's too late, and you get 409 Conflict — with the courier and tracking details so you can tell the customer where their parcel actually is:

# 409 Conflict
{
  "error": "Order WP-84183 can no longer be cancelled — it is already \"Dispatched\". Raise a return instead.",
  "status": "Dispatched",
  "cancellable": false,
  "courier": "DPD",
  "tracking_number": "DPD123456789",
  "tracking_url": "https://track.dpd.co.uk/…"
}
Safe to retry. Cancelling an order that's already cancelled returns 200 with "already_cancelled": true, never an error.

Check cancellable on any order response to know whether the button should still be live in your own UI.

Stock

We track stock per SKU as goods arrive and orders are picked. Pull these figures back into your store so you never sell what we can't ship.

FieldMeaning
on_handPhysically on our shelves.
allocatedAlready promised to orders we haven't dispatched.
availableon_hand − allocated. This is the number to publish on your storefront.
reorder_levelYour low-stock threshold. Drives the status field.
statusIn stock, Low stock or Out of stock.

GET/api/v1/stock

Every SKU we hold for you. Poll this on a schedule (every few minutes is plenty) to keep your storefront in step.

curl https://waypoint3pl.com/api/v1/stock \
  -H "Authorization: Bearer wp_live_YOUR_KEY"

# 200 OK
{
  "as_of": "2026-07-31 14:22:08",
  "data": [
    {
      "sku": "LAV-220",
      "name": "Lavender Soy Candle 220g",
      "on_hand": 180,
      "allocated": 3,
      "available": 177,
      "reorder_level": 40,
      "status": "In stock",
      "updated_at": "2026-07-31 14:04:11"
    }
  ]
}

GET/api/v1/stock/{sku}

One SKU — handy for a just-in-time check at checkout.

curl https://waypoint3pl.com/api/v1/stock/LAV-220 \
  -H "Authorization: Bearer wp_live_YOUR_KEY"

POST/api/v1/products

Register a SKU up front, or update its name, reorder level, weight and size. Sending the same SKU again updates it rather than creating a duplicate.

curl -X POST https://waypoint3pl.com/api/v1/products \
  -H "Authorization: Bearer wp_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sku": "LAV-220",
    "name": "Lavender Soy Candle 220g",
    "reorder_level": 40,
    "weight_kg": 0.42,
    "dimensions_cm": { "length": 12, "width": 8, "height": 8 }
  }'
Send weight and size if your system has them. They decide which courier services can carry an order — some stop at 15 kg. Anything you don't send, we weigh and measure at goods-in before it reaches a shelf, and the figures then appear on GET /api/v1/stock. Omitting a field never clears what we already hold, so a nightly catalogue push can't wipe our measurements.
You don't have to register SKUs first — an unknown SKU on an incoming order creates the product automatically at zero stock, so nothing is silently dropped. Physical stock only goes up when our warehouse books goods in.
If an order needs more than we have, we still accept it and return a stock_warnings array on the response — so you can flag a backorder immediately.

Errors

Errors return an appropriate HTTP status and a JSON body with an error message.

StatusMeaning
401Missing or invalid API key.
400Request body isn't valid JSON.
422Valid JSON, but a required field is missing (the message says which).
404Unknown endpoint, or no order with that reference in this environment.
405HTTP method not supported on this endpoint.

Magento & Shopify

Magento 2

Our Magento 2 extension is in development — it will push orders to Waypoint automatically and write status, courier and tracking back to your store. Until it ships, connect using the REST API above (most agencies wire this up in an afternoon), or ask your account manager to set up the integration with you.

Shopify

The Waypoint Shopify app is in development. In the meantime the REST API covers the same flow: push new orders on Shopify's orders/create webhook, and poll order status to update fulfilments and tracking.

Questions, or want early access to either integration? Email hello@waypoint3pl.com.