Platform API — Reference & External-App Integration Guide
Freight aggregation platform (white-label: ParcelMax / eParcel). REST API under
/api/v1/*, authenticated with OAuth 2.0 Client Credentials. This document describes the full API surface, scopes, authentication, and how an external application integrates.
Last updated: 2026-06-02.
1. The two API roles
The API is split into two functional roles, separated by who manages the credentials and by account scoping.
| Role 1 — Platform Integration | Role 2 — Customer / eCommerce | |
|---|---|---|
| Purpose | An external system maintains customer accounts + addresses, and signs users in via SSO | A 3rd-party ecommerce platform pushes orders and manages inventory |
| Managed by | system_admin (Platform Integration settings) | customer_admin (Customer API Management, per account) |
| Client binding | Usually system-wide (customer_account_id = null) | Bound to a specific customer account |
| Scopes | customers:*, addresses:*, auth:sso | orders:*, inventory:*, addresses:validate |
| Auth methods | OAuth 2.0 only | OAuth 2.0 or API key |
Both roles use the same token endpoint and the same Bearer-token model; they differ only in which scopes the client is granted and which endpoints those scopes unlock.
2. Base URLs
The base URL is per tenant + environment. Example:
| Tenant | Environment | Base URL |
|---|---|---|
| EasyParcel | Production | https://online.easyparcel.co.nz |
| EasyParcel | Preview | https://eparcel-preview.vercel.app |
All paths in this document are relative to the base URL (e.g. POST /api/v1/oauth/token).
3. Authentication — OAuth 2.0 Client Credentials
3.1 Obtaining credentials (one-time)
An administrator provisions an OAuth client for your integration. You receive:
client_id— public identifier, e.g.oauth_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxclient_secret— shown once at creation, stored only as a bcrypt hash. If lost, the client must be revoked and recreated.
The client also carries: its allowed scopes, an is_active flag, an optional IP allowlist, and a rate limit (default 60 req/min).
3.2 Step 1 — Get an access token
POST /api/v1/oauth/token
Accepts application/x-www-form-urlencoded or application/json.
| Field | Required | Notes |
|---|---|---|
grant_type | yes | Must be client_credentials |
client_id | yes | |
client_secret | yes | |
scope | no | Space-separated subset of the client's allowed scopes. Omit to receive all of the client's scopes. |
curl -X POST https://online.easyparcel.co.nz/api/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=oauth_xxxxxxxx" \
-d "client_secret=yyyyyyyy" \
-d "scope=customers:read customers:write"
Response 200 OK:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "customers:read customers:write"
}
The access_token is a JWT (HS256). Do not decode or modify it client-side — treat it as opaque.
Scope rule: if you request
scope, the server grants only the intersection of your request and the client's allowed scopes. If that intersection is empty you get400 invalid_scope. If you omitscope, you get all of the client's scopes.
3.3 Step 2 — Call the API with the token
curl https://online.easyparcel.co.nz/api/v1/customer-accounts \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
3.4 Token lifecycle
- Tokens expire after 1 hour (
expires_in: 3600). - Cache and reuse the token until shortly before expiry; request a new one as needed.
- There is no refresh token — just request a new token with the client credentials.
3.5 What is validated on every API call
All of the following must pass or the request is rejected:
- JWT signature valid and not expired
- The client is still active (re-checked against the database, not just trusted from the token)
- The token carries a scope the endpoint requires
- If the client has an IP allowlist, the caller's IP is in it
- The client's rate limit is not exceeded
4. Scopes
The canonical set is 10 scopes:
| Scope | Role | Description |
|---|---|---|
addresses:read | 1 | Read customer addresses |
addresses:write | 1 | Create and update customer addresses |
addresses:validate | 1 / 2 | Validate addresses using NZ Post |
customers:read | 1 | Read customer account information |
customers:write | 1 | Create and update customer accounts |
orders:read | 2 | Read eCommerce orders and tracking |
orders:write | 2 | Create and manage eCommerce orders |
inventory:read | 2 | Read products, stock levels, and movements |
inventory:write | 2 | Manage products, receive stock, and adjust inventory |
auth:sso | 1 | Generate single-use login tokens for SSO integration |
GET /api/v1/oauth/tokenreturns this list in itssupported_scopesfield (machine-readable discovery).
5. Limits & error responses
Rate limiting
- Per-client, default 60 requests/minute.
- Every response includes:
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset. - Exceeding it returns
429with{ "error": "rate_limit_exceeded" }.
Common errors
| HTTP | error | Meaning |
|---|---|---|
| 400 | invalid_request | Missing/invalid parameters |
| 400 | unsupported_grant_type | grant_type ≠ client_credentials |
| 400 | invalid_scope | Requested scope not allowed for this client |
| 401 | invalid_client | Bad client_id/client_secret, or client inactive |
| 401 | (auth) | Missing/expired/invalid Bearer token, or client deactivated |
| 403 | (scope/IP) | Token lacks the required scope, or caller IP not allowlisted |
| 429 | rate_limit_exceeded | Too many requests |
Error bodies follow { "error": "...", "error_description": "..." }.
6. Role 1 — Platform Integration endpoints
Requires a system-wide OAuth client with
customers:*/addresses:*/auth:ssoas needed. Where two scopes are listed, any one of them is sufficient.
Customer Accounts
| Method | Path | Scope |
|---|---|---|
| GET | /api/v1/customer-accounts | customers:read |
| POST | /api/v1/customer-accounts | customers:write |
| GET | /api/v1/customer-accounts/{id} | customers:read |
| PUT | /api/v1/customer-accounts/{id} | customers:write |
| DELETE | /api/v1/customer-accounts/{id} | customers:write |
Customer Addresses
| Method | Path | Scope (any of) |
|---|---|---|
| GET | /api/v1/customer-accounts/{id}/addresses | addresses:read or customers:read |
| POST | /api/v1/customer-accounts/{id}/addresses | addresses:write or customers:write |
| GET | /api/v1/customer-accounts/{id}/addresses/{addressId} | addresses:read or customers:read |
| PUT | /api/v1/customer-accounts/{id}/addresses/{addressId} | addresses:write or customers:write |
| DELETE | /api/v1/customer-accounts/{id}/addresses/{addressId} | addresses:write or customers:write |
Address Validation
| Method | Path | Scope (any of) |
|---|---|---|
| POST | /api/v1/addresses/validate | addresses:validate |
| GET | /api/v1/addresses/details/{id} | addresses:read or addresses:validate |
Example — list customer accounts:
curl https://online.easyparcel.co.nz/api/v1/customer-accounts \
-H "Authorization: Bearer <token>"
{
"data": [
{ "id": "…", "account_number": "OMAX-001", "account_name": "OfficeMax",
"contact_name": "…", "contact_email": "…", "is_active": true, "...": "..." }
],
"pagination": { "page": 1, "limit": 50, "total": 1, "totalPages": 1 }
}
7. SSO Integration (auth:sso)
Lets a 3rd-party auth system sign a user into the platform without the user entering credentials.
Flow
-
Get an OAuth token with the
auth:ssoscope (Section 3). -
Generate a single-use login token:
POST /api/v1/auth/login-token(Bearer token required)Field Required Notes emailyes If the user doesn't exist, they are auto-created defaultAccountyes Account number set as the user's active account on login; must exist and be in accountsaccountsyes Array of account numbers to link the user to; unknown numbers are silently skipped nameno Display name; defaults to the email. Updated on every SSO call roleno customerorcustomer_admin(defaultcustomer_admin). Updated on every SSO callcurl -X POST https://online.easyparcel.co.nz/api/v1/auth/login-token \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com", "name": "Jane Smith", "role": "customer_admin", "defaultAccount": "ACC002", "accounts": ["ACC002", "ACC003"] }'Response
201 Created:{ "token": "771d86b0-bd86-4478-874a-4b016ea13658" } -
Redirect the user's browser to complete sign-in:
https://online.easyparcel.co.nz/auth/sso?token=771d86b0-bd86-4478-874a-4b016ea13658
SSO security notes
- Login tokens are single-use (consumed when the user lands on the SSO page).
- They expire after 60 seconds — redirect promptly after generating.
- New users are auto-created with a random password (login only via SSO or password reset thereafter).
roleandnameare updated on every SSO call, including for existing users.defaultAccountmust be present in theaccountsarray.
8. Role 2 — Customer / eCommerce endpoints
Requires an OAuth client bound to a customer account, with
orders:*and/orinventory:*. These endpoints also accept an API key as an alternative to the Bearer token.
eCommerce Orders
| Method | Path | Scope | Description |
|---|---|---|---|
| POST | /api/v1/ecommerce/orders | orders:write | Create order for fulfillment |
| POST | /api/v1/ecommerce/orders/cancel | orders:write | Cancel order |
| POST | /api/v1/ecommerce/orders/fulfilled | orders:write | Mark order fulfilled externally |
| POST | /api/v1/ecommerce/orders/refunded | orders:write | Cancel order due to refund |
| GET | /api/v1/ecommerce/tracking/{job_keys} | orders:read | Get tracking with event history |
Order payloads carry an ecommerce_type (e.g. shopify) plus order/line/address details — see the OpenAPI spec and Postman collection (Section 10) for the full schema.
Inventory
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/inventory/products | inventory:read | List products (search, active_only, limit, offset) |
| POST | /api/v1/inventory/products | inventory:write | Create a product (sku, name required) |
| GET | /api/v1/inventory/stock | inventory:read | Stock levels (view=levels) or movements (view=movements) |
| POST | /api/v1/inventory/stock | inventory:write | action: receive | adjust | write_off (product_id or sku, quantity; notes required for adjust/write_off) |
| GET | /api/v1/inventory/inbound-orders | inventory:read | List inbound goods orders (id, status filters) |
| POST | /api/v1/inventory/inbound-orders | inventory:write | action: create | receive_line | confirm |
All Role 2 endpoints are account-scoped: the account is taken from the OAuth client, so a client can only see/affect its own account's data. A client with no associated account receives 403.
Example — create a product:
curl -X POST https://online.easyparcel.co.nz/api/v1/inventory/products \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{ "sku": "WIDGET-01", "name": "Blue Widget", "unit_weight": 0.25 }'
9. Quick-start checklist for an external app
- Obtain a
client_id+client_secretfrom an administrator, scoped to what you need (least privilege). - Note your tenant/environment base URL.
- On startup (and when the cached token nears expiry):
POST /api/v1/oauth/token→ cache theaccess_tokenfor ~1 hour. - Send
Authorization: Bearer <token>on every API call. - Handle
401(token expired/revoked → re-request a token) and429(back off usingX-RateLimit-Reset). - Keep the
client_secretin a secret manager. Rotate by revoking and recreating the client.
10. Resources
These are served by the platform (relative to the base URL):
- API Quick Start Guide —
/api-quickstart.md - Postman Collection (eCommerce) —
/api/docs/postman-ecommerce-collection - OpenAPI 3.0 Specification (eCommerce) —
/api/docs/openapi-ecommerce - Postman Collection (Customer Accounts) —
/api/docs/postman-collection - In-app docs: Platform Integration (
/settings/api-platform-integration, system_admin) and Customer API Management (/administration/api-management, customer_admin).
11. Appendix — token endpoint discovery
GET /api/v1/oauth/token (no auth) returns machine-readable metadata, including the live supported_scopes list:
{
"endpoint": "/api/v1/oauth/token",
"description": "OAuth 2.0 Token Endpoint - Client Credentials Grant",
"supported_grant_types": ["client_credentials"],
"supported_scopes": [
"addresses:read", "addresses:write", "addresses:validate",
"customers:read", "customers:write",
"orders:read", "orders:write",
"inventory:read", "inventory:write",
"auth:sso"
]
}