malpka32 / inpost-buy-sdk
PHP client for InPost Buy (inpsa) API
Requires
- php: >=8.1
- ramsey/collection: ^2.0
- symfony/http-client-contracts: ^2.5 || ^3.0
- symfony/mime: ^6.4 || ^7.0
Requires (Dev)
- fakerphp/faker: ^1.23
- friendsofphp/php-cs-fixer: ^3.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^10.0
- symfony/http-client: ^6.4 || ^7.0
README
PHP client for the InPost Buy API (inpsa) — sell your products through InPost's marketplace. Categories, offers, orders — all wrapped in a clean, type-safe interface.
What is InPost Buy?
InPost Buy (inpsa) lets merchants integrate their product catalog and orders with InPost's platform. You publish offers, receive orders, and update their status — all via REST API. This SDK handles authentication, serialization, and mapping so you focus on business logic.
Features
- Categories — fetch product categories as a tree (read-only), with details and attributes
- Accept-Language — set response language (Polish
plor Englishen) viaLanguageenum - Offers — create single or batch offers with products, stock, and pricing; close/reopen; events; deposit types
- Offer attachments — list, upload, download, delete attachments (images etc.)
- Orders — list orders, fetch details, accept or refuse with status updates
- OAuth2 — client credentials grant (default) with in-memory caching; OAuth2 PKCE (Authorization Code flow) for merchant integrations (e.g. PrestaShop modules)
- Custom token provider — use
InPostBuyClient::createWithTokenProvider()with anyAccessTokenProviderInterface - Typed DTOs —
OfferDto,ProductDto,OrderDtoetc., no raw arrays in your code - Exceptions —
NotFoundException,BadRequestException,ServerExceptionetc. with HTTP status and error details
Requirements
- PHP 8.1+
- Symfony HttpClient (or any PSR-18-compatible client via Symfony contracts)
- ramsey/collection
Installation
composer require malpka32/inpost-buy-sdk
Quick Start
<?php use malpka32\InPostBuySdk\Client\InPostBuyClient; use malpka32\InPostBuySdk\Dto\Common\ListSort; use malpka32\InPostBuySdk\Dto\Offer\OfferStatus; use malpka32\InPostBuySdk\Dto\Order\OrderStatus; use Symfony\Component\HttpClient\HttpClient; $client = new InPostBuyClient( httpClient: HttpClient::create(), clientId: 'your-client-id', clientSecret: 'your-client-secret', organizationId: 'your-org-uuid', sandbox: true, // use false for production ); // Fetch categories $categories = $client->getCategories(); foreach ($categories as $category) { echo $category->name . " (" . $category->id . ")\n"; } // Fetch offers $offers = $client->getOffers(offerStatus: [OfferStatus::PUBLISHED], limit: 20); // Fetch orders $orders = $client->getOrders(status: OrderStatus::CREATED, sort: [ListSort::CREATED_AT_DESC]);
Usage
Language (Accept-Language)
API returns localized content (category names, error messages etc.). Set language via Language enum:
use malpka32\InPostBuySdk\Config\Language; // Polish (default) $client = new InPostBuyClient(..., language: Language::Polish); // English $client = InPostBuyClient::createWithTokenProvider(..., language: Language::English);
Supported values: Language::Polish (pl), Language::English (en).
Categories
Categories are returned as a tree (CategoryTreeCollection — each node has id, name, parentId, children).
$categories = $client->getCategories(); foreach ($categories as $node) { printf( "ID: %s | Name: %s | Parent: %s | Children: %d\n", $node->id, $node->name, $node->parentId ?? '-', count($node->children) ); }
You can iterate the tree recursively:
$tree = $client->getCategories(); // one API call, returns CategoryTreeCollection foreach ($tree as $root) { echo $root->name . "\n"; foreach ($root->children as $child) { echo " " . $child->name . "\n"; } }
Creating an Offer
An offer is built from nested DTOs: ProductDto, StockDto, PriceDto. Optionally add attributes (e.g. color, size) and dimensions.
use malpka32\InPostBuySdk\Client\InPostBuyClient; use malpka32\InPostBuySdk\Dto\Offer\OfferDto; use malpka32\InPostBuySdk\Dto\Offer\PriceDto; use malpka32\InPostBuySdk\Dto\Offer\Product\DimensionDto; use malpka32\InPostBuySdk\Dto\Offer\Product\ProductDto; use malpka32\InPostBuySdk\Dto\Offer\StockDto; use malpka32\InPostBuySdk\Collection\AttributeValueCollection; use malpka32\InPostBuySdk\Dto\Attribute\AttributeValueDto; $product = new ProductDto( name: 'Cool T-Shirt', description: 'Comfortable cotton t-shirt in various sizes.', brand: 'MyBrand', categoryId: '67909821-cc25-45ec-80ce-5ac4f2f01032', // from getCategories() sku: 'TSHIRT-001', ean: '5901234567890', attributes: AttributeValueCollection::fromAttributes( new AttributeValueDto('attr-color-uuid', ['Red'], 'en'), new AttributeValueDto('attr-size-uuid', ['M', 'L']) ), dimension: new DimensionDto(width: 200, height: 50, length: 300, weight: 200) // mm, g ); $offer = new OfferDto( externalId: 'SKU-TSHIRT-001', product: $product, stock: new StockDto(quantity: 10, unit: 'UNIT'), price: new PriceDto(amount: 99.99, currency: 'PLN', taxRateInfo: '23%') ); $result = $client->putOffer($offer); echo "Created offer ID: {$result->offerId}\n";
Batch Offers
Create multiple offers in one request:
use malpka32\InPostBuySdk\Collection\OfferCollection; $offers = OfferCollection::fromOffers($offer1, $offer2, $offer3); $ids = $client->putOffers($offers); foreach ($ids as $id) { echo "Created: $id\n"; }
Listing and Filtering Offers
use malpka32\InPostBuySdk\Dto\Common\ListSort; use malpka32\InPostBuySdk\Dto\Offer\OfferStatus; $offers = $client->getOffers( offerStatus: [OfferStatus::PENDING, OfferStatus::PUBLISHED], limit: 50, offset: 0, sort: [ListSort::UPDATED_AT_DESC] ); foreach ($offers as $offer) { echo $offer->externalId . " – " . $offer->product->name . "\n"; }
OAuth2 PKCE (merchant flow)
For integrations where merchants authorize via OAuth2 (e.g. PrestaShop modules), use PKCE flow:
use malpka32\InPostBuySdk\Auth\PkceOAuth2Client; use malpka32\InPostBuySdk\Auth\PkceTokenProvider; use malpka32\InPostBuySdk\Client\InPostBuyClient; use malpka32\InPostBuySdk\Config\InPostBuyEndpoints; use malpka32\InPostBuySdk\Config\Language; // 1. Initiate authorization – redirect merchant to $result['authorize_url'] $pkceClient = new PkceOAuth2Client($httpClient); $result = $pkceClient->initiateAuthorization( redirectUri: 'https://your-shop.com/module/callback', clientId: $clientId, sandbox: true, stateStorage: $yourPkceStateStorage, // implement PkceStateStorageInterface ); // 2. On callback – exchange code for tokens $tokens = $pkceClient->exchangeCodeForTokens( code: $_GET['code'], redirectUri: $redirectUri, clientId: $clientId, clientSecret: $clientSecret, state: $_GET['state'], tokenUrl: InPostBuyEndpoints::tokenUrl($sandbox), stateStorage: $yourPkceStateStorage, tokenStorage: $yourTokenStorage, // implement TokenStorageInterface ); // 3. Create client with token provider $tokenProvider = new PkceTokenProvider( $yourTokenStorage, $pkceClient, $clientId, $clientSecret, InPostBuyEndpoints::tokenUrl($sandbox), ); $client = InPostBuyClient::createWithTokenProvider( $httpClient, $tokenProvider, $organizationId, sandbox: true, language: Language::English, // optional: pl (default) or en );
Orders
use malpka32\InPostBuySdk\Dto\Common\ListSort; use malpka32\InPostBuySdk\Dto\Order\OrderPaymentStatus; use malpka32\InPostBuySdk\Dto\Order\OrderStatus; use malpka32\InPostBuySdk\Dto\Order\OrderStatusDto; use malpka32\InPostBuySdk\Dto\Order\OrderUpdateStatus; // List orders (optionally filter by status/payment status/sort) $orders = $client->getOrders( status: OrderStatus::CREATED, paymentStatus: OrderPaymentStatus::PAID, sort: [ListSort::CREATED_AT_DESC], ); foreach ($orders as $order) { echo $order->inpostOrderId . " – " . ($order->reference ?? 'no ref') . "\n"; } // Fetch single order $order = $client->getOrder('order-uuid-from-inpost'); if ($order !== null) { var_dump($order->status, $order->orderLines); } // Accept order $client->updateOrderStatus('order-uuid', new OrderStatusDto(status: OrderUpdateStatus::ACCEPTED)); // Refuse with reason $client->updateOrderStatus('order-uuid', new OrderStatusDto( status: OrderUpdateStatus::REFUSED, comment: 'Out of stock' ));
Error Handling
The SDK throws specific exceptions for HTTP errors:
| Exception | HTTP |
|---|---|
BadRequestException |
400 |
UnauthorizedException |
401 |
ForbiddenException |
403 |
NotFoundException |
404 |
UnprocessableEntityException |
422 |
TooManyRequestsException |
429 |
ServerException |
5xx |
All extend malpka32\InPostBuySdk\Exception\ApiException and provide:
getStatusCode()— HTTP status codegetResponseBody()— raw response bodygetErrorResponse()— parsed error (errorCode, errorMessage, details)isRetryable()— true for 5xx and 429getRetryAfterSeconds()— fromRetry-Afterheader when available
use malpka32\InPostBuySdk\Exception\NotFoundException; use malpka32\InPostBuySdk\Exception\ApiException; try { $order = $client->getOrder('non-existent'); } catch (NotFoundException $e) { echo "Order not found: " . $e->getMessage(); } catch (ApiException $e) { echo "API error: " . $e->getStatusCode(); if ($e->isRetryable()) { echo " – retry after " . ($e->getRetryAfterSeconds() ?? '?') . " seconds"; } }
Testing & quality
composer ci # full check: PHPStan, cs-check, tests composer test # run tests composer test:coverage # tests + coverage (clover + HTML in build/coverage/) composer phpstan # static analysis (level 10) composer cs-check # code style check (PSR-12, dry run) composer cs-fix # fix code style (PHP-CS-Fixer)
With Docker:
docker compose run --rm test ci # full check (recommended before commit) docker compose run --rm test # tests docker compose run --rm test phpstan # PHPStan docker compose run --rm test cs-check # code style
CI runs on push/PR (PHP 8.1–8.3): code style (PSR-12), PHPStan, tests with coverage. See Actions.
Documentation
Official InPost Buy (inpsa) API docs: inpsa-api-portal.inpost-group.com
Support the project
This project is actively maintained: we keep it in sync with InPost API changes and welcome issues and pull requests on GitHub.
If this library helps you, consider buying me a coffee — it allows me to maintain and update the library alongside InPost API changes.
License
MIT