InPost API Setup: From Sandbox Token to First Label

Step-by-step InPost API integration: get an OAuth 2.1 sandbox token, query locker locations, create a shipment, and generate your first label.

InPost API Setup: From Sandbox Token to First Label

What you need before you start

You need three things before writing a line of code: a registered app in the InPost Developer Portal, sandbox client credentials, and a decision about which API generation you're building against. That last one matters more than it sounds.

InPost is mid-migration. The old country-specific ShipX API (the one every PHP SDK on GitHub was written against, including imper86/php-inpost-api and patryk-sawicki/inpost-laravel) is still live for Poland, still using OAuth 2.0, and still the only path for merchants who haven't been invited into the new flow. InPost has confirmed a "Friends and Family" phase is currently ongoing for PL-based merchants, and you need to contact InPost directly if you want into the first migration wave. If you're a Polish-only shipper already on ShipX, you may not have a choice yet — you're on legacy until InPost invites you off it.

For everyone else — anyone shipping into more than one InPost market, or starting fresh — the answer is the InPost Global API. It's InPost's reference documentation for integrating shipping, returns, tracking, and location services using REST APIs with OAuth 2.1. The APIs use REST, authenticate with OAuth 2.1 access tokens, and return HTTP response codes and responses encoded in JSON format. This tutorial builds against the Global API, because it's the one you'll be running in production a year from now regardless of where you start.

Checklist before you open a terminal:

  • An account on the InPost Developer Portal (developers.inpost-group.com) with a registered application
  • Sandbox client_id and client_secret — for now these come from your Integration Team on request, though InPost is rolling out self-service at merchant.inpost-group.com
  • Confirmation of which scopes your app was granted — you'll want at minimum a points-read and a shipments scope
  • A test country market with PUDO coverage (Poland or the UK are the easiest to validate against, given locker density)
  • curl, or an HTTP client that lets you set custom headers — you'll need X-Request-Id on every call

Sandbox to first label, step by step

This is the full path: token, locker lookup, shipment creation, label retrieval, webhook registration. Each step below is what actually returns in InPost's sandbox, not a paraphrase.

  1. Register the app and note your credentials. In the Developer Portal, create an application and record the client_id / client_secret pair. This client_credentials flow is best suited for machine-to-machine applications such as backend services, since it authenticates the application itself rather than a user, and InPost recommends it for simple integrations. Don't mix sandbox and production credentials in the same config file — that's a common source of "why is my token being rejected in prod" tickets.
  2. POST a shipment against that locker ID. Call the Shipping API's shipment creation endpoint with the point ID from step 3 as the delivery target, a service type (parcel locker vs. courier), and dimensions matching InPost's declared size classes. Because the Global API processes synchronously, tracking numbers are returned immediately in the shipment creation response, enabling instant label generation and fulfillment — you don't need to poll for a tracking number the way older integrations sometimes had to. A 201 with a shipment ID and status back means the call succeeded structurally; it doesn't yet mean the label exists.
  3. Fetch the label. The Shipping API lets you register shipments with InPost, retrieve shipment details, and download ready-to-print shipping labels in a range of formats. Depending on the requested Accept header, the API returns labels either as a binary file, or as a JSON object where the label content is provided as a Base64-encoded string. Pick JSON+Base64 if your stack already handles JSON everywhere; pick binary if you're piping straight to a printer driver. One caveat: the direct-to-thermal-printer format optimal for automated label printing is currently in pilot phase, available only for selected domestic services in Poland — don't design your whole label pipeline around it if you're shipping cross-border yet.
  4. Register a webhook instead of polling. The Global API includes webhook support so you receive real-time updates instead of having to query via API. When you subscribe, you're subscribing to a specific version — typically the latest at subscription time — and every webhook call identifies its payload version via the X-InPost-Api-Version header, formatted as a date. Be aware: during setup you select the event types you want, but no authentication mechanism is enforced by default on the webhook self-service side — instead, requests are signed, and InPost strongly recommends implementing request validation to prevent external attacks on your exposed endpoint. Don't skip that validation just because it's "recommended" rather than mandatory.
  5. Confirm the status transitions. Each tracking event includes a version identifier, and version V1 is supported indefinitely while later versions get 24 months of support from introduction — worth knowing before you hardcode a parser against V1 forever. If you're migrating off ShipX, note that Global Tracking event codes follow a different structure than ShipX PL statuses, and some ShipX statuses map to multiple new events, so a 1:1 status mapping table from your old integration won't hold.

Pull locker locations for your test postcode. Hit the Points endpoint on the Location API with your bearer token. Every request should carry a unique X-Request-Id — InPost echoes it back in the response header, which is what you'll grep for when a support ticket needs a trace:

curl https://api.inpost-group.com/location/v1/points \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -H "X-Request-Id: ee59dccf-d686-4ea4-b21c-781118ad163e"

The response looks like: { "count": 62388, "page": 1, "perPage": 25, "totalPages": 2496, "items": [ { "id": "GB_12345", "type": "APM", "country": "GB", "locationType": "INDOOR", "imageUrl": "https://test-api-images.easypack24.net/gb/images/GB_12345.jpg", "coordinates": { "latitude": 0.0, "longitude": 0.0 } } ] } — the exact shape you'll be mapping into your own locker picker UI. Note the sandbox point IDs are prefixed by country code (GB_, presumably PL_ elsewhere) — grab one id value and hold onto it for step 4. This is the payoff of the Global API's unified location model: a single Points API now returns both lockers and PUDO points regardless of country or region, so you're not querying separate endpoints per market.

Request an access token from /oauth/token. After registering your application and obtaining client credentials, request an access token using the /oauth/token endpoint, then pass it as Authorization: Bearer {access_token} on subsequent calls. Example request:

curl -X POST https://api.inpost-group.com/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"

InPost's authorization server supports two OAuth 2.1-compliant ways to pass client_id and client_secret during the token request — choose exactly one method per request, either as URL-encoded form fields or another supported mechanism, and don't send both at the same time. The tokens you get back are scoped to specific permissions, for example api:shipments:read or api:points:read. Tokens have a limited lifespan, so refresh them as needed rather than caching indefinitely.

How you know it worked

Four checks, in order. First: the shipment POST returns 201 with a non-null shipment ID and tracking number in the body — not just an ACCEPTED with an empty payload. Second: the label fetch returns either a binary stream with non-zero byte length, or a JSON body with a Base64 string that actually decodes to a valid PDF/ZPL header when you inspect it locally — an empty or truncated Base64 string is a silent failure mode worth building an assertion around. Third: a test event fires to your registered webhook endpoint within a reasonable window of the shipment status change, carrying the X-InPost-Api-Version header you expect. Fourth: polling or querying the shipment shows a status transition away from its initial created state — even in sandbox, a shipment that never leaves "created" after label generation is a sign something upstream didn't fully process it.

Failure mode: stale or delisted locker IDs

Locker networks change constantly — closures, capacity swaps, decommissions. If you cache a point ID from a Points API call made three weeks ago and use it directly in a shipment POST today, the failure doesn't show up at creation. It shows up at label generation, or worse, after the customer has already been told to collect from that locker.

Treat any cached point ID as perishable. Re-validate against the Points endpoint immediately before shipment creation if the cache is older than a session, and treat a 404 or 410 on a point ID as a signal to refresh and retry — not a hard failure to surface to the customer. This also intersects with idempotency: if your retry logic re-POSTs a shipment creation call with a fresh point ID after a locker-not-found error, make sure you're generating a new idempotency key or reference for that retry rather than reusing the original one, or you risk the API treating it as a duplicate of a request that never actually completed. The Global API gives you up to three merchant-defined searchable references per shipment for exactly this kind of internal tracking, so use one of them to tag retry attempts distinctly from the original request.

Where this fits in a multi-carrier stack

PUDO maturity varies sharply by country, and no single carrier covers all markets equally well. Poland leads Europe for locker density with 45,325 automated parcel machines and 17.3 OOH points per 10,000 inhabitants, while France has 17,510 lockers and the UK has 15,565. That's why most teams building serious European fulfillment don't stop at InPost. DHL Packstation, Mondial Relay, and the Poste/Correos locker networks each dominate in markets where InPost's own footprint is thinner.

If your integration effort is going into one carrier at a time, you'll eventually be maintaining five or six near-identical-but-not-quite integrations, each with its own OAuth quirks, its own point-ID lifecycle, its own webhook signing scheme. That's the case for a multi-carrier API layer. Platforms like nShift, Sendcloud, ShippyPro, EasyPost, Shippo, and Cargoson wrap InPost and the other major locker networks behind one contract, so you write the mapping once instead of once per carrier. Direct integration still wins when InPost is your dominant volume carrier and you need control over every field in the shipment payload; an abstraction layer wins when InPost is one of eight carriers and your engineering time is the scarcer resource.

Open questions worth a follow-up benchmark

A few things this tutorial can't settle from a single sandbox pass. Whether sandbox label formats have full parity with production — the pilot-phase note on thermal-printer output for Polish domestic services suggests not yet, everywhere. Whether webhook retry and backoff behavior under sustained load matches what a production integration needs, since the current webhook documentation doesn't specify retry counts or backoff intervals on InPost's side. And whether merchants still on ShipX PL who haven't been invited into the Friends and Family migration wave face a hard cutoff date, or an open-ended parallel-run period. Worth a dedicated benchmark post once InPost's self-service credential portal at merchant.inpost-group.com is fully live and testable end to end.