API reference
Version 1
Everything below is read from the live route table, so it describes the API that is actually running rather than the one someone last wrote down.
- Base URL
- https://parcelpointpro.com/api/v1
- Authentication
- Bearer token
- Format
- JSON
Authentication
Send your key as a bearer token
Never in a query string — URLs end up in server logs, browser history and referrer headers, and a key that leaks that way is a key someone else can spend with.
-
Sandbox keys start
ppp_test_and live keysppp_live_. A sandbox key returns real label formats and tracking numbers and spends nothing. - Every refusal returns the same 401. We will not tell an unauthenticated caller whether a key exists, is revoked or is simply wrong.
-
Rate limits are per key. A
429carries aRetry-Afterheader saying exactly how long to wait.
curl https://parcelpointpro.com/api/v1/account/balance \ -H "Authorization: Bearer ppp_test_..."
curl -X POST https://parcelpointpro.com/api/v1/shipments \
-H "Authorization: Bearer ppp_test_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"from": { "name": "You", "line1": "1 Wellington Place",
"city": "Leeds", "postcode": "LS1 4AP" },
"to": { "name": "A Buchanan", "line1": "18 Kirkgate",
"city": "Otley", "postcode": "LS21 3HL" },
"parcel": { "weight_g": 2000, "length_mm": 350,
"width_mm": 250, "height_mm": 80 }
}'
Idempotency
A retry never buys twice
Send an Idempotency-Key header on anything that spends money. If the same key arrives again we return the original response and buy nothing more.
Same key, same request
You get the original response back, with an Idempotency-Replayed: true header. Nothing is charged again.
Same key, different request
A 422. Silently returning the first shipment would be worse — you asked for something else and would never find out.
The first attempt failed
Nothing was bought, so the key is free again. Fix the problem and retry with the same key.
Endpoints
Every endpoint in v1
Read from the router. If it is here, it exists.
Quotes
/api/v1/quotes
Price a parcel
Returns every service that can carry the parcel, cheapest first. Quoting costs nothing and does not reserve anything — the price is what you would pay if you bought immediately.
Requires
quotes:read
Shipments
/api/v1/shipments
List shipments
Paginated, newest first. Filter with status, reference or created_after.
Requires
shipments:read
/api/v1/shipments/{shipment}
Read one shipment
Takes the shipment reference we returned when you bought it (PPS-…), not a numeric id.
Requires
shipments:read
/api/v1/shipments
Idempotency-Key
Buy a label
Charges your balance and returns the shipment with its tracking number. This is the call that spends money, so it takes an idempotency key.
Requires
shipments:write
/api/v1/shipments/{shipment}/void
Idempotency-Key
Void a label
Asks the carrier to cancel, then reverses the charge. A label that has already been scanned cannot be voided.
Requires
shipments:void
/api/v1/shipments/{shipment}/label/{format}
Download a label
Returns a short-lived signed URL rather than the file itself, so you can hand it straight to a printer or a browser. Formats: pdf_6x4, pdf_a4, zpl.
Requires
labels:read
Tracking
/api/v1/tracking/{trackingNumber}
Track a parcel
The full scan history for a tracking number. The same events are pushed to your webhook endpoint as they happen, so polling this is optional.
Requires
tracking:read
Addresses
/api/v1/addresses
List saved addresses
Your address book — senders you ship from and recipients you ship to repeatedly.
Requires
addresses:read
/api/v1/addresses/{address}
Read one address
Only addresses belonging to your own account are reachable.
Requires
addresses:read
/api/v1/addresses
Idempotency-Key
Save an address
Creating the same address twice is harmless, but an idempotency key keeps your own records clean.
Requires
addresses:write
Account
/api/v1/account/balance
Read your balance
What is available to spend right now. On credit terms this is your remaining credit rather than a wallet balance.
Requires
account:read
Scopes
Grant only what the integration needs
Scopes are set per key. A warehouse system that only prints labels should not hold a key that can void them, and a reporting script should not hold one that can spend money.
| Scope | Allows |
|---|---|
| quotes:read | Price a parcel across every carrier. |
| shipments:read | List and read shipments. |
| shipments:write Spends | Buy labels. This scope spends money. |
| shipments:void | Cancel a label and receive the refund. |
| labels:read | Download label files. |
| tracking:read | Read tracking events. |
| addresses:read | Read the address book. |
| addresses:write | Create and edit saved addresses. |
| account:read | Read the account balance. |
Webhooks
Verify the signature before you trust the body
Every delivery carries an X-PPP-Signature header of the form t=<unix time>,v1=<hex>.
- 1 Take the raw request body — the exact bytes, not a re-encoded copy of the JSON. Two encoders disagree about key order and escaping, and that is the usual reason verification appears to fail at random.
-
2
Compute
HMAC-SHA256("<t>." + body)with your endpoint secret. -
3
Compare it to
v1with a constant-time comparison, and reject anything whosetis more than five minutes old. That is what stops an old, validly-signed delivery being replayed at you.
Answer with any 2xx. Anything else is retried with a backoff — ten seconds, then a
minute, five minutes, half an hour, two hours. Deduplicate on the
id in the payload, which is stable across retries.
[$t, $v1] = sscanf($header, 't=%d,v1=%s');
if (abs(time() - $t) > 300) {
abort(400); // too old to trust
}
$expected = hash_hmac('sha256', $t.'.'.$rawBody, $secret);
if (! hash_equals($expected, $v1)) {
abort(400);
}
Events
- shipment.purchased
- A label was bought and is ready to print.
- shipment.in_transit
- The carrier has collected the parcel.
- shipment.delivered
- The parcel was delivered. This is a terminal state.
- shipment.exception
- The carrier reported a problem — a failed delivery, a wrong address, a hold.
- shipment.voided
- A label was cancelled and the charge reversed.
- tracking.updated
- Any new carrier scan, including the ones that do not change the status.
- wallet.low_balance
- The balance fell below the threshold set on the account.
Build against sandbox first
Sandbox keys return real label formats and tracking numbers without spending anything. API access is switched on per account by our team — ask and we will enable it.