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.
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
Your account has two isolated environments. The key you use decides which one you're talking to — the URLs are identical.
| Environment | Key prefix | Behaviour |
|---|---|---|
| Live | wp_live_ | Real orders. Picked, packed and shipped by our warehouse. |
| Sandbox | wp_test_ | Test orders. Visible in the portal's sandbox mode, never fulfilled. |
Base URL: https://waypoint3pl.com/api/v1
Create an order. We'll pick, pack and dispatch it (live), or simulate it (sandbox).
| Field | Type | Notes |
|---|---|---|
customer_name | string | Required. Your customer's name. |
external_ref | string | Strongly 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. |
items | array | Required. What we're picking — at least one item. |
items[].name | string | Required. Product name as it should appear on the pick list. |
items[].sku | string | Your SKU. Strongly recommended — it's what our pickers scan. |
items[].quantity | integer | Defaults to 1. |
items[].unit_price | number | Price per unit, in the order currency. |
total | number | Order value. If omitted, we calculate it from item prices. |
currency | string | 3-letter code. Defaults to GBP. |
ship_policy | string | together (one parcel — default) or separate (items may ship as they're ready). |
destination.city | string | Delivery city. |
destination.country | string | Two-letter code or name. Defaults to UK. |
channel | string | Where the order came from: Shopify, Magento or Custom. Defaults to Custom. |
# 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" }
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.
| Field | Meaning |
|---|---|
destination.line1 * | Street address. Also accepted as address1 or street. |
destination.line2 | Flat, unit, building. Also accepted as address2. |
destination.city * | Town or city. |
destination.postcode * | Postcode or ZIP. Also accepted as zip or postal_code. |
destination.region | County, state or province. Also accepted as state or county. |
destination.country | Country name or 2-letter code. Defaults to UK. |
destination.company | Company name, for business deliveries. |
destination.phone | Contact number. Couriers ask for this on most express services. |
destination.email | Used 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.
All optional, but the more you send the better we can pick the right service.
| Field | Meaning |
|---|---|
shipping.method_code | Your 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.method | Whatever the shopper picked at checkout, in your own words — "Next Day Delivery". Shown to our warehouse team as-is. |
shipping.speed | Normalised so we can sort the pick queue: economy, standard, express, next_day or same_day. |
shipping.weight_kg | Parcel weight in kilograms, if you know it. |
ship_policy | together (one parcel) or separate (items may ship as they're ready). |
group_ref | Set this when you split one basket across several orders — see below. |
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.
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_kg on your products (below) and we'll always pick something that can actually carry the parcel.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", … }
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", … } ] }
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: Received → Picking → Packed → Dispatched → In transit → Delivered. Once dispatched, courier, tracking_number and tracking_url are populated. An order can also end at Cancelled or Returned.
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.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/…" }
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.
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.
| Field | Meaning |
|---|---|
on_hand | Physically on our shelves. |
allocated | Already promised to orders we haven't dispatched. |
available | on_hand − allocated. This is the number to publish on your storefront. |
reorder_level | Your low-stock threshold. Drives the status field. |
status | In stock, Low stock or Out of 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" } ] }
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"
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 } }'
GET /api/v1/stock. Omitting a field never clears what we already hold, so a nightly catalogue push can't wipe our measurements.stock_warnings array on the response — so you can flag a backorder immediately.Errors return an appropriate HTTP status and a JSON body with an error message.
| Status | Meaning |
|---|---|
401 | Missing or invalid API key. |
400 | Request body isn't valid JSON. |
422 | Valid JSON, but a required field is missing (the message says which). |
404 | Unknown endpoint, or no order with that reference in this environment. |
405 | HTTP method not supported on this endpoint. |
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.
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.