Skip to content

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 keys ppp_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 429 carries a Retry-After header saying exactly how long to wait.
Your first request
curl https://parcelpointpro.com/api/v1/account/balance \
  -H "Authorization: Bearer ppp_test_..."
Buying a label
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

POST /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

GET /api/v1/shipments

List shipments

Paginated, newest first. Filter with status, reference or created_after.

Requires shipments:read

GET /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

POST /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

POST /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

GET /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

GET /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

GET /api/v1/addresses

List saved addresses

Your address book — senders you ship from and recipients you ship to repeatedly.

Requires addresses:read

GET /api/v1/addresses/{address}

Read one address

Only addresses belonging to your own account are reachable.

Requires addresses:read

POST /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

GET /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. 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. 2 Compute HMAC-SHA256("<t>." + body) with your endpoint secret.
  3. 3 Compare it to v1 with a constant-time comparison, and reject anything whose t is 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.

Verifying, in PHP
[$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.