ucubix / php-client
UCubix Distribution API PHP Client with built-in rate limiting and DTO support
Requires
- php: ^8.2
- guzzlehttp/guzzle: ^7.8
- spatie/laravel-data: ^4.0
Requires (Dev)
- phpunit/phpunit: ^10.5|^11.0
This package is not auto-updated.
Last update: 2026-04-09 14:26:52 UTC
README
PHP client for the UCubix Distribution API with built-in rate limiting, typed DTOs, and full endpoint coverage.
Requirements
- PHP 8.2+
- Guzzle 7.8+
Installation
composer require ucubix/php-client
Quick Start
use Ucubix\PhpClient\Client\UcubixClient; $client = new UcubixClient(apiKey: 'YOUR_API_KEY'); // Find a product $products = $client->getProducts(['search' => 'Cyberpunk']); $product = $client->getProduct($products->data[0]->id); // Check regional pricing foreach ($product->regional_pricing as $region) { echo "{$region->region_code} ({$region->reseller_wsp}% WSP)\n"; foreach ($region->countries as $country) { echo " {$country->country_name}: {$country->price} {$country->currency_code}\n"; } } // Create an order $order = $client->createOrder( productUuid: $product->id, quantity: 1, regionCode: $product->regional_pricing[0]->region_code, countryCode: $product->regional_pricing[0]->countries[0]->country_code, ); echo "Order {$order->id} — status: {$order->status}\n"; // Get license keys when order is fulfilled $items = $client->getOrderItems($order->id); foreach ($items->data as $item) { if ($item->hasLicenseKey()) { $key = $client->getLicenseKey($item->license_key_uuid); echo "Key: {$key->license_key}\n"; } }
Configuration
$client = new UcubixClient( apiKey: 'YOUR_API_KEY', baseUrl: 'https://ucubix.com/api/v1/', // default );
API Methods
Organisation Info
| Method | Returns |
|---|---|
getOrganisation() |
Organisation |
$org = $client->getOrganisation(); echo $org->summary->total_usd_equivalent;
Products
| Method | Returns |
|---|---|
getProducts(filters, page, perPage, sort) |
PaginatedResponse<Product> |
getProduct(id) |
Product |
getProductPhotos(id, page, perPage) |
PaginatedResponse<Media> |
getProductScreenshots(id, page, perPage) |
PaginatedResponse<Media> |
getProductCategories(id, page, perPage) |
PaginatedResponse<Category> |
getProductPublishers(id, page, perPage) |
PaginatedResponse<Publisher> |
getProductPlatforms(id, page, perPage) |
PaginatedResponse<Platform> |
getProductFranchises(id, page, perPage) |
PaginatedResponse<Franchise> |
getProductDevelopers(id, page, perPage) |
PaginatedResponse<Developer> |
getProducts() filters (validated, throws InvalidArgumentException on unknown keys):
| Key | Type | Description |
|---|---|---|
search |
string |
Full-text search |
category |
string |
Category UUID |
publisher |
string |
Publisher UUID |
developer |
string |
Developer UUID |
franchise |
string |
Franchise UUID |
platform |
string |
Platform UUID |
Sort options: name, -name, created_at, -created_at
$products = $client->getProducts( filters: ['search' => 'Game', 'platform' => 'platform-uuid'], page: 1, perPage: 15, sort: 'name', ); $product = $client->getProduct('product-uuid'); $photos = $client->getProductPhotos('product-uuid');
Orders
| Method | Returns |
|---|---|
getOrders(filters, page, perPage, sort) |
PaginatedResponse<Order> |
getOrder(id) |
Order |
getOrderItems(orderId, page, perPage) |
PaginatedResponse<OrderItem> |
createOrder(productUuid, quantity, regionCode, countryCode?) |
Order |
updateOrder(id, quantity) |
Order |
cancelOrder(id) |
bool |
getOrders() filters (validated, throws InvalidArgumentException on unknown keys):
| Key | Type | Description |
|---|---|---|
code |
string |
Filter by order code |
external_reference |
string |
Filter by external reference |
Sort options (default: -order_date): code, status, total_price, srp, currency_code, order_date, approved_at, rejected_at, delivered_at, distribution_model
$orders = $client->getOrders(filters: ['code' => 'ORD-001'], sort: '-order_date'); $order = $client->getOrder('order-uuid'); $items = $client->getOrderItems('order-uuid'); $order = $client->createOrder('product-uuid', 5, 'NorthAmerica', 'us'); $order = $client->updateOrder('order-uuid', quantity: 10); $client->cancelOrder('order-uuid');
License Keys
| Method | Returns |
|---|---|
getLicenseKey(id) |
LicenseKey |
getBulkLicenseKeys(ids) |
LicenseKey[] |
$key = $client->getLicenseKey('license-key-uuid'); $keys = $client->getBulkLicenseKeys(['uuid-1', 'uuid-2']); // up to 1000
Catalog Dictionaries
| Method | Returns |
|---|---|
getCategories(page, perPage, sort) |
PaginatedResponse<Category> |
getPublishers(page, perPage, sort) |
PaginatedResponse<Publisher> |
getPlatforms(page, perPage, sort) |
PaginatedResponse<Platform> |
getDevelopers(page, perPage, sort) |
PaginatedResponse<Developer> |
getFranchises(page, perPage, sort) |
PaginatedResponse<Franchise> |
$categories = $client->getCategories(sort: 'name'); $publishers = $client->getPublishers(); $platforms = $client->getPlatforms(); $developers = $client->getDevelopers(); $franchises = $client->getFranchises();
Pagination
All list endpoints return PaginatedResponse<T>:
$page = 1; do { $orders = $client->getOrders(page: $page, perPage: 50); foreach ($orders->data as $order) { // process order } $page++; } while ($orders->hasMorePages());
DTOs
All DTOs extend Spatie\LaravelData\Data. Properties are readonly.
Product
| Property | Type | Notes |
|---|---|---|
id |
string |
UUID |
name |
string |
|
summary |
?string |
|
description |
?string |
|
release_date |
?string |
ISO 8601 |
type |
?string |
e.g. "Game" |
created_at |
?string |
ISO 8601 |
regional_pricing |
RegionalPricing[] |
Only on single-resource requests |
metadata |
?ProductMetadata |
System requirements, SteamDB |
RegionalPricing
| Property | Type |
|---|---|
region_code |
string |
reseller_wsp |
float |
countries |
CountryPrice[] |
CountryPrice
| Property | Type |
|---|---|
country_name |
string |
country_code |
string |
price |
?float |
currency_code |
?string |
is_promotion |
bool |
original_price |
?float |
promotion_name |
?string |
promotion_end_date |
?string |
can_be_ordered |
bool |
in_stock |
bool |
ProductMetadata
| Property | Type |
|---|---|
minimum |
SystemRequirement[] |
recommended |
SystemRequirement[] |
steamdb |
?SteamdbInfo |
SystemRequirement
| Property | Type |
|---|---|
parameter |
?string |
value |
?string |
SteamdbInfo
| Property | Type |
|---|---|
id |
int |
type |
string |
url |
string |
Order
| Property | Type | Notes |
|---|---|---|
id |
string |
UUID |
code |
string |
|
external_reference |
?string |
|
external_reference_attempt |
?int |
|
status |
string |
See OrderStatus |
total_price |
float |
|
srp |
float |
|
estimated_cost |
?float |
|
items_count |
int |
|
currency_code |
?string |
ISO 4217 |
order_date |
string |
ISO 8601 |
approved_at |
?string |
|
rejected_at |
?string |
|
delivered_at |
?string |
|
distribution_model |
?string |
"sale", "consignment" |
rejection_note |
?string |
Helper: $order->getStatus() returns OrderStatus enum.
OrderItem
| Property | Type | Notes |
|---|---|---|
id |
string |
UUID |
price |
float |
|
country_code |
?string |
ISO 3166-1 alpha-2 |
license_key_uuid |
?string |
Only when order is fulfilled/delivered |
fulfilled_at |
?string |
|
created_at |
?string |
|
updated_at |
?string |
Helper: $item->hasLicenseKey() returns bool.
LicenseKey
| Property | Type |
|---|---|
id |
string |
license_key |
string |
created_at |
?string |
updated_at |
?string |
Organisation
| Property | Type |
|---|---|
uuid |
string |
name |
string |
summary |
OrganisationSummary |
credit_lines |
CreditLine[] |
OrganisationSummary
| Property | Type |
|---|---|
currencies |
int |
total_usd_equivalent |
string |
CreditLine
| Property | Type |
|---|---|
currency |
string |
balance |
string |
Category
| Property | Type |
|---|---|
id |
string |
name |
string |
parent_id |
?string |
child_ids |
string[] |
Publisher / Developer
| Property | Type |
|---|---|
id |
string |
name |
string |
website |
?string |
about |
?string |
created_at |
?string |
updated_at |
?string |
Platform
| Property | Type |
|---|---|
id |
string |
name |
string |
created_at |
?string |
updated_at |
?string |
Franchise
| Property | Type |
|---|---|
id |
string |
name |
string |
created_at |
?string |
Media
| Property | Type |
|---|---|
id |
string |
name |
string |
file_name |
string |
collection_name |
string |
mime_type |
string |
disk |
string |
size |
int |
order_column |
?int |
url |
string |
created_at |
?string |
updated_at |
?string |
PaginatedResponse
| Property | Type |
|---|---|
data |
T[] |
currentPage |
int |
perPage |
int |
total |
int |
lastPage |
int |
firstPageUrl |
?string |
lastPageUrl |
?string |
nextPageUrl |
?string |
prevPageUrl |
?string |
Helper: $response->hasMorePages() returns bool.
Enums
OrderStatus
use Ucubix\PhpClient\Enums\OrderStatus; OrderStatus::NEW; // 'new' OrderStatus::PENDING; // 'pending' OrderStatus::APPROVED; // 'approved' OrderStatus::REJECTED; // 'rejected' OrderStatus::FULFILLED; // 'fulfilled' OrderStatus::DELIVERED; // 'delivered' OrderStatus::CANCELLED; // 'cancelled'
Error Handling
All API errors throw typed exceptions:
use Ucubix\PhpClient\Exceptions\ApiException; use Ucubix\PhpClient\Exceptions\AuthenticationException; use Ucubix\PhpClient\Exceptions\RateLimitException; use Ucubix\PhpClient\Exceptions\ValidationException; try { $order = $client->createOrder($uuid, 5, 'InvalidRegion'); } catch (AuthenticationException $e) { // 401 Unauthorized or 403 Forbidden // Invalid API key or IP not whitelisted echo $e->getMessage(); echo $e->getCode(); // 401 or 403 } catch (ValidationException $e) { // 422 Unprocessable Entity echo $e->getMessage(); // e.g. "Quantity must not be greater than 1." echo $e->field; // field name if provided } catch (RateLimitException $e) { // 429 Too Many Requests (after all retries exhausted) echo $e->retryAfter; // seconds to wait } catch (ApiException $e) { // All other errors (400, 404, 500, etc.) echo $e->getMessage(); echo $e->getCode(); echo $e->errorKey; echo $e->errorDetail; }
Exception Hierarchy
ApiException
├── AuthenticationException (401, 403)
├── RateLimitException (429)
└── ValidationException (422)
Rate Limiting
The client has a dual-layer rate limiting system, matching the SharpAPI php-core pattern.
1. Client-side Sliding Window
Proactive throttling: the client tracks request timestamps in a sliding window and blocks (via usleep + 50ms buffer) before exceeding the configured requests per minute. This prevents hitting the server limit.
// Check/configure requests per minute (default: 100) $client->getRequestsPerMinute(); // 100 $client->setRequestsPerMinute(50); // slow down $client->setRequestsPerMinute(0); // disable client-side throttling // Direct access to the rate limiter $limiter = $client->getRateLimiter(); $limiter->canProceed(); // non-blocking check $limiter->remaining(); // slots left in current window
2. Server-side 429 Retry
Reactive handling: if the server returns 429 Too Many Requests, the client automatically retries up to 3 times, respecting the Retry-After header.
// Configure max retries (default: 3) $client->setMaxRetryOnRateLimit(5);
3. Server Header Tracking
After every response, the client reads X-RateLimit-Limit and X-RateLimit-Remaining headers.
$client->getRateLimitLimit(); // e.g. 100 $client->getRateLimitRemaining(); // e.g. 87 $client->canMakeRequest(); // true if remaining > 0
4. Server-side Limit Adaptation
If the server reports a higher limit via X-RateLimit-Limit header, the client automatically adapts its sliding window upward (one-way ratchet — never decreases).
5. State Persistence
Export/restore rate limit state for caching across requests:
// Export $state = $client->getRateLimitState(); // ['limit' => 100, 'remaining' => 87] // Restore (e.g. from Redis/session) $client->setRateLimitState($state);
API Rate Limits
- 100 requests per minute per API key
Authentication
The API uses Bearer Token authentication with IP whitelisting:
Authorization: Bearer YOUR_API_KEY
Accept: application/vnd.api+json
Content-Type: application/json
Requests from non-whitelisted IPs receive a 403 Forbidden error.
Testing
composer install vendor/bin/phpunit
License
MIT. See LICENSE.