Search by

beziworld / laravel-inpost-pay

kamcw

Laravel client for the InPost Pay merchant API: basket binding, orders, refunds and signed InPost Pay callbacks

Package info

github.com/Beziworld/laravel-inpost-pay

Homepage

Issues

pkg:composer/beziworld/laravel-inpost-pay

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

v1.0.0 2026-11-04 18:53 UTC

This package is auto-updated.

Last update: 2026-11-04 18:55:34 UTC


README

tests Latest Version License

InPost Pay merchant integration for Laravel (the one-click checkout in the InPost Mobile app):

  • client for the InPost Pay (Basket App) API: OAuth2 client credentials with token caching, basket binding for Widget 2.0, basket updates, binding checks and desynchronisation, order updates (status, description, shipment numbers), basket and order listings, payment methods, transactions and signed refunds
  • Merchant Backend API routes called by InPost Pay (basket details, binding confirmation, basket events, basket removal, order creation, order details, order events) with RSA signature verification of every request (x-signature, public keys fetched by version and cached, 240 s timestamp window)
  • contracts the application implements: BasketProvider and OrderHandler; the package does the HTTP, JSON, signature and error format work
  • typed basket builder (Basket, Product, DeliveryOption, Consent, Price) producing the documented structure, and typed DTOs for everything InPost Pay sends
  • transaction webhooks v2 (payments, refunds, settlements) with SHA-512 signature verification
  • Laravel events for every callback, sandbox and production, runtime credentials with using()

Polska wersja dokumentacji

Why this package

Existing InPost Pay integrations are platform plugins: the official Magento 2 modules (inpost/magento2-module-inpost-pay and its Hyvä/GraphQL add-ons) and a Shopware 6 plugin (crehler/inpost-pay). There is no Laravel package and no framework-agnostic PHP client on Packagist. This package brings the complete merchant side of InPost Pay to any Laravel shop: both directions of the API, the RSA and SHA-512 signature schemes (verified against the official examples), and a basket model that matches the documentation field by field.

Installation

composer require beziworld/laravel-inpost-pay
php artisan vendor:publish --tag=inpost-pay-config   # optional
INPOST_PAY_CLIENT_ID=...          # merchant panel (production) or issued by InPost (sandbox)
INPOST_PAY_CLIENT_SECRET=...
INPOST_PAY_SANDBOX=true           # sandbox-api.inpost.pl / sandbox-login.inpost.pl
INPOST_PAY_MERCHANT_SECRET=...    # merchant panel, API Keys tab: signs refunds and webhooks

INPOST_PAY_ROUTE_PREFIX=inpost-pay          # Merchant Backend API base: https://shop.test/inpost-pay
INPOST_PAY_WEBHOOK_PATH=inpost-pay/events   # transaction webhooks URL

Register https://your-shop/inpost-pay as the merchant backend URL and https://your-shop/inpost-pay/events as the transaction webhook URL in the InPost Pay merchant panel. Sandbox credentials are requested through inpostpay.pl/kontakt.

Implement the merchant side

InPost Pay calls your shop to read baskets and create orders. Implement two contracts and bind them:

use Beziworld\InpostPay\Contracts\BasketProvider;
use Beziworld\InpostPay\Contracts\OrderHandler;

$this->app->bind(BasketProvider::class, App\InpostPay\ShopBasketProvider::class);
$this->app->bind(OrderHandler::class, App\InpostPay\ShopOrderHandler::class);
use Beziworld\InpostPay\Contracts\BasketProvider;
use Beziworld\InpostPay\Data\{Basket, BasketEvent, BindingConfirmation, Consent, DeliveryOption, Price, Product};
use Beziworld\InpostPay\Enums\{ConsentRequirement, DeliveryType};
use Beziworld\InpostPay\Exceptions\MerchantApiError;

final class ShopBasketProvider implements BasketProvider
{
    public function basket(string $basketId): Basket
    {
        $cart = Cart::find($basketId) ?? throw MerchantApiError::basketNotFound($basketId);

        return new Basket(
            basePrice: Price::fromGross($cart->total, 23),
            products: $cart->items->map(fn ($item) => new Product(
                productId: (string) $item->product_id,
                name: $item->name,
                basePrice: Price::fromGross($item->price, 23),
                quantity: (string) $item->quantity,
                availableQuantity: (string) $item->stock,
                image: $item->image_url,
                ean: $item->ean,
            ))->all(),
            delivery: [
                (new DeliveryOption(DeliveryType::Apm, Price::fromGross('12.99', 23), now()->addDays(2)))
                    ->withOption('Paczka w Weekend', 'PWW', Price::fromGross('5.00', 23)),
                new DeliveryOption(DeliveryType::Courier, Price::fromGross('16.99', 23), now()->addDays(1)),
            ],
            consents: [new Consent('terms', 'Regulamin sklepu', route('terms'), '2024-01', ConsentRequirement::RequiredAlways)],
            expiresAt: now()->addDays(7),
        );
    }

    public function confirmBinding(BindingConfirmation $confirmation): Basket
    {
        // $confirmation->phoneNumber, ->browserTrusted, ->browserId, ->isSuccessful()
        return $this->basket($confirmation->basketId);
    }

    public function handleEvent(BasketEvent $event): Basket
    {
        // $event->type (PRODUCTS_QUANTITY, PROMO_CODES, RELATED_PRODUCTS), ->quantities, ->promoCodes, ->relatedProducts
        Cart::find($event->basketId)->applyInpostChanges($event->quantities, $event->promoCodes);

        return $this->basket($event->basketId);
    }

    public function unbind(string $basketId): void
    {
        Cart::find($basketId)?->forgetInpostBinding();
    }
}

OrderHandler::create(OrderRequest $order) receives the customer, delivery (deliveryType, deliveryPoint, deliveryAddress, deliveryCodes), invoice data, accepted consents and InPost discounts, and returns the created order in the documented order_details structure. handleEvent(OrderEvent $event) receives payment updates ($event->isPaid() for AUTHORIZED).

Errors thrown as MerchantApiError (basketNotFound(), orderNotFound(), badRequest() or a custom code) are returned as {"error_code": ..., "error_message": ...} with their HTTP status; other exceptions are reported and answered with HTTP 500 INTERNAL_SERVER_ERROR.

Routes

Method Path (under INPOST_PAY_ROUTE_PREFIX) Contract method Event
GET /v1/izi/basket/{basketId} BasketProvider::basket
POST /v1/izi/basket/{basketId}/confirmation BasketProvider::confirmBinding BasketBindingConfirmed / BasketBindingRejected
POST /v1/izi/basket/{basketId}/event BasketProvider::handleEvent BasketChanged
DELETE /v1/izi/basket/{basketId}/binding BasketProvider::unbind BasketUnbound
POST /v1/izi/order OrderHandler::create OrderCreated
GET /v1/izi/order/{orderId} OrderHandler::find
POST /v1/izi/order/{orderId}/event OrderHandler::handleEvent OrderEventReceived, OrderPaid
POST INPOST_PAY_WEBHOOK_PATH TransactionWebhookReceived, PaymentAuthorized, PaymentDeclined, RefundCompleted, RefundDeclined, SettlementReceived

Every Merchant Backend API request is verified: the public key of the x-public-key-ver version is fetched from /v1/izi/signing-keys/public/{version} (cached for a day), its SHA-256 must match x-public-key-hash, the x-signature is checked with SHA256withRSA over base64(base64(sha256(body)),merchant_external_id,version,timestamp) and the timestamp must be within 240 seconds. Failures return HTTP 401 INVALID_SIGNATURE. INPOST_PAY_VERIFY_SIGNATURE=false disables the check for local debugging only.

Call InPost Pay

use Beziworld\InpostPay\Data\OrderUpdate;
use Beziworld\InpostPay\Facades\InpostPay;

// Widget 2.0: key for the basket binding widget
$bindingKey = InpostPay::bindBasket($cart->id);

// The basket changed in the shop: send the full current basket
InpostPay::updateBasket($cart->id, $basket);

$binding = InpostPay::basketBinding($cart->id, $browserId);   // ->linked, ->browserTrusted, ->maskedPhoneNumber
InpostPay::unbindBasket($cart->id, realized: true);            // basket turned into an order in the shop

// Orders: shipment numbers are mandatory, statuses and descriptions shown in the app
InpostPay::addShipments($orderId, ['520000011395200025754311'], 'Wysłane');
InpostPay::completeOrder($orderId, 'Zrealizowane');           // ORDER_COMPLETED
InpostPay::rejectOrder($orderId, 'Anulowane');                // ORDER_REJECTED
InpostPay::updateOrder($orderId, OrderUpdate::description('W trakcie realizacji'));

// Back office
InpostPay::baskets(pageIndex: 0, pageSize: 50);
InpostPay::orders(orderId: $orderId);
InpostPay::paymentMethods();                                  // ['CARD', 'BLIK_CODE', ...]

// Transactions and refunds (signed with INPOST_PAY_MERCHANT_SECRET)
$page = InpostPay::transactions(['order_id' => $orderId, 'status' => ['CAPTURED']]);
$refund = InpostPay::refund($transactionId, amount: '49.99', externalRefundId: 'RMA-17');
$refund->status;    // RefundStatus::Pending | Success | Failed
$refund->commandId; // X-Command-ID: pass it again to repeat a refund with an unknown outcome

Other stores or credentials at runtime:

InpostPay::using(['client_id' => $store->inpost_client_id, 'client_secret' => $store->inpost_secret, 'sandbox' => false])
    ->addShipments($orderId, [$trackingNumber]);

Errors

Exception When
ApiException InPost Pay rejected the request: httpStatus(), errorCode(), body()
AuthenticationException the token endpoint rejected the client credentials
RateLimitExceeded HTTP 429, retryAfter() in seconds
InvalidConfiguration missing client credentials or merchant secret, or contracts not bound
InvalidSignature a request or webhook failed signature verification (handled by the routes)
MerchantApiError thrown by your contracts to answer InPost Pay with an error code

Reads are retried twice on connection errors and HTTP 5xx; writes (basket updates, order updates, refunds) are never retried automatically. A request answered with HTTP 401 is repeated once with a fresh token.

Development

composer install
composer ci   # pint --test, phpstan (level 8), pest

Author

Created and maintained by Kamil Ćwiertnia, beziworld.eu.

Need an integration? Get in touch through beziworld.eu.

Other packages by the author

laravel-inpost · laravel-paynow · laravel-przelewy24 · laravel-payu · laravel-tpay

License

MIT. See LICENSE.md. This package is not affiliated with InPost S.A.