ucubix/php-client

UCubix Distribution API PHP Client with built-in rate limiting and DTO support

Maintainers

Package info

github.com/a2zwebltd/ucubix-php-client

pkg:composer/ucubix/php-client

Statistics

Installs: 10

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-04-09 03:29 UTC

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.