> ## Documentation Index
> Fetch the complete documentation index at: https://andcze.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# PHP SDK

> itemshop/sdk (Composer) — storefront, the API-key client, errors and webhook verification for PHP 8.1+.

The official PHP SDK (≥ 8.1): any PSR-18 HTTP client (Guzzle by default), PSR-4, readonly DTOs, PHPStan level 8. Mirrors `@itemshop/sdk` in TypeScript.

| | Auth | Use it in |
| - | - | - |
| `ItemShop\Storefront` | none (public Storefront API v1) | WordPress themes, Laravel views, cron jobs |
| `ItemShop\Client` | API key (`X-API-Key`) | your backend only — **never** the browser |

## Installation

```bash theme={null}
composer require itemshop/sdk
```

## Storefront (public, no secrets)

```php theme={null}
use ItemShop\Storefront;

$sf = new Storefront(shopId: '664f1a2b3c4d5e6f7a8b9c0d', locale: 'pl');   // id, uniqueName or domain; or $ITEMSHOP_SHOP_ID
// $sf = Storefront::fromDomain('sklep.mojserwer.pl');

$shop = $sf->shop->get();
echo $shop->displayName, ' — ', $shop->currency, PHP_EOL;
foreach ($shop->servers as $server) {
    printf("%s: %s (%d/%d)\n", $server->name, $server->isOnline ? 'online' : 'offline', $server->playersOnline, $server->maxPlayers);
}

$catalog = $sf->products->list();                    // ->categories (= the shop's servers) + ->products
foreach ($sf->products->list(category: $catalog->categories[0]->id)->products as $product) {
    echo $product->name, ': ', $product->price, ' ', $product->currency, PHP_EOL;
}

$vip = $sf->products->get('vip');                    // slug or id
$methods = $sf->paymentMethods->list();              // one entry per channel: code, channel, limits
$quote = $sf->discounts->validate('LATO10', [['productId' => $vip->id]]);   // $quote->valid / ->reason
```

### Checkout

Your site never touches payment data: `checkout->create()` returns a `redirectUrl` (the gateway, or the shop's status page for a free order). The SDK adds a random `Idempotency-Key` and retries network failures **with the same key**, so a flaky connection never creates two orders. A cart has 1–20 lines; two or more form one payment group (`->groupId`, `->orderIds`).

```php theme={null}
$result = $sf->checkout->create([
    'items' => [['productId' => $vip->id, 'quantity' => 1, 'variant' => '30 dni']],
    'nick' => 'Steve',
    'email' => 'steve@example.com',
    'consents' => [
        'terms' => true,
        'immediateDelivery' => true,
        'legalVersion' => $shop->legal->version,      // the legal documents the buyer was shown
    ],
    'paymentMethodCode' => $methods[0]->code,
    'paymentChannel' => $methods[0]->channel,
    'returnUrl' => 'https://sklep.mojserwer.pl/dziekujemy',
], captchaToken: $_POST['cf-turnstile-response'] ?? null);

header('Location: ' . $result->redirectUrl);

// On the return page:
$returned = Storefront::returnedOrderFromReturnUrl($currentUrl);   // ?itemshop_order=<number>&itemshop_key=<key> → ['number' => …, 'key' => …] or null
if ($returned !== null) {
    $status = $sf->orders->statusByNumber($returned['number'], $returned['key']);   // pending → paid → completed
    if ($status->delivery?->ready) {
        $delivery = $sf->orders->delivery($returned['number'], $returned['key']);   // keys, text, download link per line
    }
}
// $result->orderNumber and $result->statusKey work the same way right after checkout
```

Typed models work too (`new StorefrontCheckoutDto(items: [new StorefrontCartItemDto(productId: …)], …)`). Keep your own key (`idempotencyKey:`) with the cart in the session to stay idempotent across requests; `Storefront::newIdempotencyKey()` generates a valid one.

### Vouchers and widgets

```php theme={null}
$sf->vouchers->redeem('ABCD-1234', 'Steve', turnstileToken: $token);   // ->orderNumber, ->statusKey, ->productName

$sf->widgets->recentPurchases(limit: 5);   // [nick, productName, serverName, amount, createdAt]
$sf->widgets->topSpenders(limit: 10);      // [nick, totalSpent, orderCount]
$sf->widgets->monthlyGoal();               // enabled, percent
$sf->widgets->onlinePlayers();             // count, players: ['Steve', …]
```

A widget the shop owner switched off returns empty data.

## Client (API key, server-side)

Create a key in the panel under **Settings → API keys** with only the permissions you need. See [API with a key](https://andcze.mintlify.site/en/developers/api-keys.md).

```php theme={null}
use ItemShop\Client;

$client = new Client(apiKey: getenv('ITEMSHOP_API_KEY'));   // or just new Client() with $ITEMSHOP_API_KEY set
$shopId = '664f1a2b3c4d5e6f7a8b9c0d';

// every page, lazily (a generator)
foreach ($client->orders->list($shopId, status: 'completed', fromDate: '2026-10-01') as $order) {
    echo $order->orderNumber, ' ', $order->customer?->nickname, PHP_EOL;
}

$page = $client->orders->list($shopId, limit: 50);   // just the first page
$page->items; $page->total; $page->pages; $page->hasNext;
$next = $page->nextPage();

$product = $client->products->create($shopId, [
    'name' => 'VIP', 'price' => 19.99, 'serverId' => '…', 'commands' => ['lp user {PLAYER} parent set vip'],
]);
$client->products->update($shopId, $product->id, ['price' => 14.99]);
$client->orders->fulfill($shopId, $orderId);
```

Resources: `shops`, `team`, `servers`, `products`, `paymentMethods`, `blacklist`, `discounts`, `vouchers`, `stats`, `uploads`, `orders`, `paymentErrors`, `site` (`files`, `draft`, `revisions`, `builds`, `deployments`), `templates`, `agents`, `skills`. AI Workspace settings and tokens are panel-only, so they are not part of this client.

### Site: deploy from CI

```php theme={null}
$client->site->publish($shopId, __DIR__ . '/dist', message: 'CI build');   // upload → build → deploy (production)
$client->site->upload($shopId, 'site.zip');                                // just a new revision (a directory needs ext-zip)

$client->site->files->list($shopId);                                       // draft; rev: '<id>' for a revision
$client->site->files->write($shopId, 'src/site.config.ts', $source);
$client->site->files->read($shopId, 'src/App.tsx');
$client->site->draft->commit($shopId, ['message' => 'New colors']);
$build = $client->site->builds->create($shopId, ['revisionId' => 'draft']);
$build = $client->site->builds->wait($shopId, $build->id);
echo $client->site->builds->log($shopId, $build->id);
$client->site->deployments->promote($shopId, $olderDeploymentId);          // rollback
```

## Errors

Every exception extends `ItemShop\Exception\ItemShopException` and carries an always-set `$errorCode` (UPPER\_SNAKE\_CASE): the backend's code when it sent one, otherwise one derived from the status, otherwise an SDK code (`NETWORK_ERROR`, `TIMEOUT`, `CONFIG_ERROR`, `VALIDATION_FAILED`). `$status` is 0 when no response arrived.

```php theme={null}
use ItemShop\ErrorCode;
use ItemShop\Exception\{ApiException, ConnectionException, RateLimitException, ValidationException};

try {
    $sf->checkout->create($input);
} catch (ValidationException $e) {            // rejected before sending, e.g. LEGAL_CONSENT_REQUIRED
} catch (RateLimitException $e) {
    sleep((int) ($e->retryAfter() ?? 5));
} catch (ApiException $e) {
    if ($e->is(ErrorCode::LEGAL_DOCUMENTS_CHANGED)) { /* re-fetch $shop->legal and ask again */ }
    if ($e->is(ErrorCode::CAPTCHA_REQUIRED)) { /* show Turnstile, retry with captchaToken: */ }
} catch (ConnectionException $e) {            // network down / TimeoutException
}
```

| Exception | Status |
| - | - |
| `BadRequestException` | 400 |
| `AuthenticationException` | 401 |
| `PermissionDeniedException` | 403 (key lacks a permission) |
| `NotFoundException` | 404 |
| `ConflictException` | 409 (`IDEMPOTENCY_KEY_REUSED`, `CHECKOUT_IN_PROGRESS`…) |
| `UnprocessableEntityException` | 422 |
| `RateLimitException` | 429 (`retryAfter()`) |
| `ServerException` | 5xx |

## Retries, timeouts, HTTP client

GET requests and writes carrying an `Idempotency-Key` (checkout) are retried on network errors and 408/425/429/500/502/503/504; any other write only on 429. Backoff is exponential from 300 ms, capped at 5 s, with jitter, honouring `Retry-After`.

```php theme={null}
new Client(apiKey: $key, maxRetries: 4, timeout: 10.0);
new Client(apiKey: $key, httpClient: $anyPsr18Client, requestFactory: $psr17, streamFactory: $psr17);
new Client(apiKey: $key, apiBase: 'http://localhost:3000');     // local backend
```

## Webhooks

The backend does not send signed outgoing webhooks yet (orders reach Discord and the game plugin directly) — see [Webhooks](https://andcze.mintlify.site/en/developers/webhooks.md). `Webhook` implements the planned [Standard Webhooks](https://www.standardwebhooks.com) scheme (`webhook-id`, `webhook-timestamp`, `webhook-signature: v1,<base64 HMAC-SHA256>`), so you can write the receiver today.

```php theme={null}
use ItemShop\Webhook;
use ItemShop\Exception\WebhookVerificationException;

$webhook = new Webhook(getenv('ITEMSHOP_WEBHOOK_SECRET'));              // "whsec_…"
try {
    $event = $webhook->verify(file_get_contents('php://input'), getallheaders());   // raw body!
    // or $webhook->verifyRequest($psr7Request)
} catch (WebhookVerificationException) {
    http_response_code(400);
    exit;
}
```

## Models

Models live in `ItemShop\Model` as `final` classes with `public readonly` properties, generated from OpenAPI. `Model::fromArray()` hydrates the camelCase wire format; `->toArray()` / `json_encode($model)` turns it back. Fields unknown to the SDK are kept in `$model->extra`.

> **[Storefront API](https://andcze.mintlify.site/en/developers/storefront-api.md)**
>
> The endpoints under the SDK.

> **[Errors and limits](https://andcze.mintlify.site/en/developers/errors-and-limits.md)**
>
> Error codes and rate limits.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.