ux2dev / speedy
Framework-agnostic PHP SDK for Speedy courier (api.speedy.bg)
Requires
- php: ^8.2
- ext-json: *
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
Requires (Dev)
- guzzlehttp/guzzle: ^7.0
- orchestra/testbench: ^10.0
- pestphp/pest: ^4.0
Suggests
- guzzlehttp/guzzle: Supplies PSR-18 client + PSR-17 factories out of the box
- illuminate/support: Required for the Laravel facade and multi-account manager
This package is auto-updated.
Last update: 2026-04-30 13:26:17 UTC
README
Warning: Developer testing version of the library — use at your own risk.
Framework-agnostic PHP SDK for the Speedy courier API at https://api.speedy.bg/v1. Covers all ten Speedy service groups (Shipment, Print, Track, Pickup, Location, Calculate, Client, Validation, Services, Payments) as resource methods that return typed Response DTOs. Works with plain PHP or Laravel.
Requirements
- PHP 8.2 or higher
- JSON extension
- A PSR-18 HTTP client and PSR-17 request/stream factories (Guzzle provides both)
Installation
composer require ux2dev/speedy
Quick start — plain PHP
use GuzzleHttp\Client; use GuzzleHttp\Psr7\HttpFactory; use Ux2Dev\Speedy\Config\SpeedyConfig; use Ux2Dev\Speedy\Speedy; use Ux2Dev\Speedy\Dto\Request\Shipment\CreateShipmentRequest; $config = new SpeedyConfig( userName: 'your-username', password: 'your-password', language: 'EN', ); $factory = new HttpFactory(); $speedy = new Speedy($config, new Client(), $factory, $factory); $response = $speedy->shipment()->create(new CreateShipmentRequest( ref1: 'ORDER-1234', )); echo $response->id;
Quick start — Laravel
The package auto-registers SpeedyServiceProvider and a Speedy facade.
Publish the config:
php artisan vendor:publish --tag=speedy-config
Set credentials in .env:
SPEEDY_USERNAME=demo SPEEDY_PASSWORD=secret SPEEDY_LANGUAGE=EN
Use the facade:
use Ux2Dev\Speedy\Laravel\Facades\Speedy; use Ux2Dev\Speedy\Dto\Request\Shipment\CreateShipmentRequest; $response = Speedy::shipment()->create(new CreateShipmentRequest(ref1: 'ORDER-1234'));
Multiple accounts
return [ 'default' => 'main', 'accounts' => [ 'main' => ['user_name' => env('SPEEDY_USERNAME'), 'password' => env('SPEEDY_PASSWORD'), 'language' => 'EN'], 'second' => ['user_name' => env('SPEEDY_USERNAME_2'), 'password' => env('SPEEDY_PASSWORD_2'), 'language' => 'BG'], ], ];
Speedy::account('second')->shipment()->create($req);
account() returns an immutable clone — the default stays untouched.
How the SDK is organised
| Layer | Location | Purpose |
|---|---|---|
| Config | Ux2Dev\Speedy\Config\SpeedyConfig |
Base URL + credentials, validated, password redacted |
| Transport | Ux2Dev\Speedy\Http\SpeedyTransport |
PSR-18 dispatch, auth auto-injection, error mapping |
| Request DTOs | Ux2Dev\Speedy\Dto\Request\{Group}\*Request |
Generated, toArray() |
| Response DTOs | Ux2Dev\Speedy\Dto\Response\{Group}\*Response |
Generated, fromArray() |
| Model DTOs | Ux2Dev\Speedy\Dto\Model\* |
Shared entities (Address, Office, Site, ...) |
| Resources | Ux2Dev\Speedy\Resources\{Group} |
Generated, one method per operation |
| Root client | Ux2Dev\Speedy\Speedy |
Aggregator |
| Laravel | Ux2Dev\Speedy\Laravel\* |
Service Provider + multi-account Manager + Facade |
Generated from bin/endpoints.json (a hand-curated catalog of 45 operations) plus bin/schemas/ (a snapshot of https://api.speedy.bg/v1/schema) by bin/generate.php.
Resources
10 generated resource groups, mirroring Speedy's service organisation:
shipment print track pickup
location calculate clients validation
services payments
Every resource method takes a Request DTO plus optional $language and $clientSystemId overrides, and returns a typed Response DTO:
$speedy->shipment()->create($createShipmentRequest); $speedy->shipment()->cancel($cancelShipmentRequest); $speedy->location()->findOffice($findOfficeRequest); $speedy->track()->track($trackRequest); $speedy->print()->voucher($printVoucherRequest);
Print Service
Print endpoints currently follow Speedy's documented JSON envelope (PrintVoucherResponse carries the PDF as a pdf byte-array). When you need the binary path directly, the SDK exposes Ux2Dev\Speedy\Http\PrintResult with body, contentType, filename, plus bytes(), saveTo($path), isPdf(), and isZpl() helpers.
Errors
Successful return paths always carry a "good" Response DTO. When the API responds with a populated error field, the transport throws Ux2Dev\Speedy\Exception\ApiException with structured fields (apiCode, apiMessage, context, errorId, component, httpStatus). The raw response body is intentionally not retained on the exception — the structured fields cover every documented error attribute, and dropping the body avoids leaking PII or credentials into logs and traces.
| Exception | When |
|---|---|
ConfigurationException |
Invalid SpeedyConfig input or unknown Laravel account |
TransportException |
PSR-18 client failure (network, timeout) |
InvalidResponseException |
Empty body, malformed JSON, or unexpected envelope |
ApiException |
Speedy returned a non-null error field |
All extend SpeedyException.
Regenerating the SDK
composer speedy:fetch-schemas # snapshots /v1/schema into bin/schemas composer speedy:generate # rewrites src/Resources, src/Dto, and the Speedy/Facade method blocks vendor/bin/pest # snapshot test catches drift
Testing
composer install vendor/bin/pest XDEBUG_MODE=coverage vendor/bin/pest --coverage --min=100
The suite mocks a PSR-18 client to exercise every resource method end-to-end.
License
MIT