malpka32 / inpost-buy-sdk
PHP SDK for InPost Von Halsky (formerly InPost Buy, inpsa) marketplace API – offers, orders, returns, claims, refunds, price program, OAuth2 PKCE
Fund package maintenance!
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
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
PHP SDK for the InPost Von Halsky API (formerly InPost Buy, inpsa) — sell your products in the InPost Mobile app marketplace. Categories, offers, orders, returns, claims, refunds and the price program — all wrapped in a clean, type-safe interface.
Covers 100% of non-deprecated operations of InPost Von Halsky API 1.7.1 — see API coverage.
What is InPost Von Halsky?
InPost Von Halsky (launched as InPost Buy, API codename inpsa) is InPost's shopping platform inside the InPost Mobile app. Merchants integrate their product catalog and orders via REST API: publish offers, receive orders, handle returns and claims. This SDK handles authentication, serialization and mapping so you focus on business logic — whether you build a PrestaShop/WooCommerce/Magento module, an ERP connector or a custom shop integration.
Features
- Categories — fetch product categories as a tree (read-only), with details and attributes
- Offers — create single or batch offers; batch price/stock updates; attribute patch (upsert/remove); close/reopen; events; hints; deposit types
- Offer attachments — list, upload (with display priority), reorder, download, delete
- Price Program — opt-in/opt-out offers in InPost Attractive Prices, list declarations, track commands
- Orders — list, fetch, accept/refuse, events, refund payment (full or partial), delivery methods
- Returns — list (per organization or order), fetch, accept, reject
- Claims (complaints) — list with filters, fetch details with documents, reject / partial refund / full refund, claim types dictionary
- Organizations — list organizations available for the integration
- OAuth2 — client credentials grant (default) with in-memory caching; OAuth2 PKCE (Authorization Code flow) for merchant integrations (e.g. PrestaShop/WooCommerce modules)
- Custom token provider — use
InPostBuyClient::createWithTokenProvider()with anyAccessTokenProviderInterface - Accept-Language — set response language (Polish
plor Englishen) viaLanguageenum - Typed DTOs, enums and collections —
OfferDto,OrderDto,ReturnDto,ClaimDto, … no raw arrays in your code - Exceptions —
NotFoundException,BadRequestException,ServerExceptionetc. with HTTP status and error details
Why this SDK?
InPost also publishes an official, OpenAPI-generated PHP client. Choose what fits your project — this SDK focuses on:
- Hand-written, domain-oriented API —
$client->acceptReturn($id)instead of generic generated request objects - OAuth2 PKCE out of the box — the merchant authorization flow needed by multi-tenant modules (PrestaShop, WooCommerce, …)
- No Guzzle lock-in — built on
symfony/http-client-contracts, works with any compatible client - Strict quality — PHP 8.1+, PHPStan level 10, typed enums and collections, CI on PHP 8.1–8.3
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' ));
Refunds and delivery methods
use malpka32\InPostBuySdk\Dto\Offer\Core\MoneyDto; // Full or partial refund of order payment $refund = $client->refundOrder('order-uuid', new MoneyDto(amount: 49.99, currency: 'PLN')); echo $refund->status?->value; // PENDING | SUCCESS | FAILED // Delivery methods with localized names (v2) foreach ($client->getDeliveryMethods() as $method) { echo $method->code->value . ': ' . $method->name . "\n"; // APM: Paczkomat InPost }
Returns
use malpka32\InPostBuySdk\Dto\Order\Returns\ReturnStatus; $result = $client->getReturns(status: [ReturnStatus::NEW], limit: 30); echo "Total: {$result->page->total}\n"; foreach ($result->returns as $return) { echo $return->id . ' – order ' . $return->orderId . ' – ' . ($return->returnReason?->text ?? '-') . "\n"; foreach ($return->orderLines as $line) { echo ' ' . $line->offer->product?->name . "\n"; } } $client->getOrderReturns('order-uuid'); // returns of one order $client->acceptReturn('return-uuid'); // or ->rejectReturn('return-uuid')
Claims (complaints)
use malpka32\InPostBuySdk\Dto\Order\Claim\ClaimListFilter; use malpka32\InPostBuySdk\Dto\Order\Claim\ClaimSort; use malpka32\InPostBuySdk\Dto\Order\Claim\ClaimState; $claims = $client->getClaims(new ClaimListFilter( states: [ClaimState::RESOLUTION_IN_PROGRESS], submissionDateFrom: new DateTimeImmutable('-30 days'), sort: [ClaimSort::EXPIRES_AT_ASC], )); foreach ($claims->claims as $summary) { if ($summary->orderId === null) { continue; } $claim = $client->getClaim($summary->orderId, $summary->claimId); echo $claim->specification?->issueDescription . "\n"; foreach ($claim->specification?->supportingDocuments ?? [] as $document) { echo ' ' . $document->filename . ' (' . $document->type?->value . ")\n"; } } // Resolve – each can be performed once per claim $client->refundClaim('order-uuid', 'claim-uuid', 'Full refund granted'); $client->refundClaimPartially('order-uuid', 'claim-uuid', 'Partial refund – 25%'); $client->rejectClaim('order-uuid', 'claim-uuid', 'Damage caused by user'); $claimTypes = $client->getClaimTypes(); // localized dictionary
Price Program (InPost Attractive Prices)
use malpka32\InPostBuySdk\Collection\CampaignDeclarationProposalCollection; use malpka32\InPostBuySdk\Dto\Offer\Campaign\CampaignDeclarationProposalDto; $commands = $client->declareCampaignParticipation(CampaignDeclarationProposalCollection::fromProposals( CampaignDeclarationProposalDto::optIn('offer-uuid-1'), CampaignDeclarationProposalDto::optOut('offer-uuid-2'), )); foreach ($commands as $command) { $status = $client->getCampaignCommandStatus($command->commandId); foreach ($status->errors as $error) { echo $error->message . "\n"; } } $current = $client->getOfferCampaignDeclarations('offer-uuid-1'); echo $current->declaration?->participationMode?->value ?? 'no declaration';
Organizations
foreach ($client->getOrganizations() as $organization) { echo $organization->id . ' – ' . $organization->name . "\n"; }
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 Von Halsky API docs: inpsa-api-portal.inpost-group.com
API coverage
All non-deprecated operations of API 1.7.1 are supported. The deprecated GET /v1/orders/delivery-methods is intentionally skipped in favour of v2.
| Area | Client methods |
|---|---|
| Organizations | getOrganizations() |
| Categories | getCategories(), getCategory(), getCategoryAttributes() |
| Offers | getOffers(), putOffer(), putOffers(), updateOfferPrices(), updateOfferStocks(), patchOfferAttributes(), getOffer(), getOfferDetails(), closeOffer(), reopenOffer(), getOfferCommandStatus(), getOfferEvents(), getOfferHint(), getDepositTypes() |
| Offer attachments | getOfferAttachments(), createOfferAttachment(), updateOfferAttachmentPriorities(), downloadOfferAttachment(), deleteOfferAttachment() |
| Price Program | getCampaignDeclarations(), declareCampaignParticipation(), getOfferCampaignDeclarations(), getCampaignCommandStatus() |
| Orders | getOrders(), getOrder(), updateOrderStatus(), getOrderCommandStatus(), getOrderEvents(), refundOrder(), getDeliveryMethods() |
| Returns | getReturns(), getOrderReturns(), getReturn(), acceptReturn(), rejectReturn() |
| Claims | getClaims(), getClaim(), rejectClaim(), refundClaimPartially(), refundClaim(), getClaimTypes() |
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