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

# PHP

> Every route of the API as a method, for your own server

`shoplygg/shoply-php` is the API for a store's own server: every route of this reference is a method, with array shapes your editor and PHPStan read, retries that are safe to repeat, lists a page at a time or all at once, and a check that a webhook came from Shoply. Each route's page shows its call.

```sh theme={null}
composer require shoplygg/shoply-php
```

PHP 8.1 or later, with the curl extension. It has no other dependencies.

```php theme={null}
<?php

require 'vendor/autoload.php';

$shoply = new Shoply\Client(['apiKey' => getenv('SHOPLY_API_KEY')]);

$order = $shoply->orders->show('ORDER_ID');
echo $order['email'], $order['amount'], $order['status'];
```

Make the key under **Developers**, **API Keys** in the dashboard, with the permissions your code needs ([Authentication](/api-reference/authentication.md)). Left out, `apiKey` is read from `SHOPLY_API_KEY`. A key is a secret: keep it on your server, never in a page. For a buyer's own browser, see [the Customer API](/guides/customer-api.md).

## A route is a method

A route's method is its name in this reference, and its page shows the call. **Get an order** is `orders.show`, so `$shoply->orders->show($orderId)`. A call takes:

1. The IDs in the route's address, in order. The store's own is filled in for you.
2. Its fields, as one array: what goes in the address and what goes in the body are sorted out for you.
3. Options, last: `idempotencyKey`, `timeout`, `maxRetries`, `headers`.

```php theme={null}
$coupon = $shoply->coupons->create(['code' => 'SPRING10', 'value' => 10, 'maxUses' => 100]);

$shoply->products->setVisibility($productId, ['visibility' => 'unlisted']);

$shoply->orders->markPaid($orderId);
```

A call answers the route's `data`, as arrays. The store is asked of the key once, the first time it is needed (`GET /key`). Pass `storeId` to skip that.

| Option | What it does |
| - | - |
| `apiKey` | The key. Left out, `SHOPLY_API_KEY`. |
| `storeId` | The store's ID. Left out, asked of the key once. |
| `timeout` | How long one try waits for an answer, in seconds. 60. |
| `maxRetries` | How many more tries a request that is safe to repeat is given. 2. |
| `baseUrl` | Where the API is. `https://api.shoply.gg/v1`. |

## Lists

A list that is paged answers a `Shoply\Page`, and a `foreach` over it walks every row of every page, asking for the next as it goes.

```php theme={null}
$page = $shoply->orders->list(['status' => 'completed', 'perPage' => 50]);

echo count($page->data), $page->total, $page->pages;

$next = $page->nextPage(); // null after the last

foreach ($shoply->customers->list(['search' => 'gmail.com']) as $customer) {
    echo $customer['emailAddress'], "\n";
}
```

A page has `data`, its rows, `page`, `pages`, `total`, and `meta`, whatever else the list answers beside its rows (counts, figures). A list that is not paged, such as **List products**, answers its rows.

## Errors

A refusal is a `Shoply\ShoplyException`.

| Property | What it is |
| - | - |
| `status` | The HTTP status. 0 when nothing answered. |
| `errorCode` | The word that does not change: `not_found`, `missing_scope`, `validation_failed`, `rate_limited`, and the route's own. `connection_error` or `timeout` when nothing answered. See [Errors](/api-reference/errors.md). |
| `getMessage()` | What is wrong, in words for a person. |
| `errors` | For `validation_failed`, what is wrong with each field, by its name. |
| `requestId` | The request's ID. Quote it to Shoply about it. |
| `retryAfter` | Seconds to wait, when told to slow down. |
| `body` | The whole answer. |

```php theme={null}
try {
    $shoply->coupons->create(['code' => 'SPRING10', 'value' => 10]);
} catch (Shoply\ShoplyException $e) {
    if ($e->errorCode !== 'validation_failed') {
        throw $e;
    }

    echo $e->errors['code'] ?? '';
}
```

## Trying again

Every change is sent with an [Idempotency-Key](/api-reference/idempotency.md), a new one for each call and the same on each of its tries. So a try whose answer was lost is answered as the first one was, never carried out twice. To make a change once however many times your own code runs it, pass your own. Options come after the fields, so a route whose fields you leave out takes `[]` before them:

```php theme={null}
$shoply->orders->markPaid($orderId, [], ['idempotencyKey' => "paid-{$invoiceNumber}"]);
```

A read, or a change with a key, is tried again up to twice more when nothing answered, or the answer was 408, 429, 500, 502, 503 or 504, or 409 `request_in_progress`. It waits as long as `Retry-After` says, or half a second, then a second, then two, a little more or less. Told to wait more than a minute, it does not, and throws with `retryAfter`. An upload is not tried again, since it carries no key.

Pass `['maxRetries' => 0]` to a call, or to the client, to try once.

## Files

A route that answers a file, a CSV or a PDF, answers a `Shoply\File`: its `body` as it came, `contentType()`, and `save($path)`.

```php theme={null}
$shoply->orders->export(['status' => 'completed'])->save('orders.csv');
```

An upload takes `file` as a path on disk, or a `CURLFile`:

```php theme={null}
$shoply->productImages->upload($productId, ['file' => '/path/to/cover.png']);
```

What else the route takes goes beside `file`: a dispute's file says what it is.

```php theme={null}
$shoply->disputes->addFile($disputeId, ['file' => 'chat.pdf', 'kind' => 'communication']);
```

## A route newer than the package

`request()` reaches any route by its method and its address after `/v1`, with `{storeId}` filled in. It answers the whole envelope ([Conventions](/api-reference/conventions.md)).

```php theme={null}
$answer = $shoply->request('GET', '/stores/{storeId}/products', ['query' => ['search' => 'pro']]);
```

## Webhooks

`Shoply\Webhooks::verify` checks a delivery's signature with your endpoint's signing secret, refuses one signed more than five minutes ago, and answers the event as an array. Pass the raw body, as it arrived, and the request's headers: from `getallheaders()`, your framework's request, or `$_SERVER`. See [Webhooks](/api-reference/webhooks.md).

```php theme={null}
try {
    $event = Shoply\Webhooks::verify(
        (string) file_get_contents('php://input'),
        getallheaders(),
        (string) getenv('SHOPLY_WEBHOOK_SECRET'),
    );
} catch (Shoply\ShoplyException $e) {
    http_response_code(401);
    exit;
}

// Answer first, then do the work.
http_response_code(200);

switch ($event['event']) {
    case 'order.paid':
        // $event['data'] is the order, as Get an order answers it
        break;
}
```

`Shoply\Webhooks::sign($body, $secret)` makes the headers Shoply would send, to test your handler with.

## The Customer API

`Shoply\Customer` is [the Customer API](/guides/customer-api.md): a buyer signs in with a code emailed to them, and reads their own orders, keys, subscriptions and affiliate page at your store. It holds the buyer's token and no key.

```php theme={null}
$buyer = new Shoply\Customer(['storeId' => 'YOUR_STORE_ID']);

$buyer->login(['email' => $email]);
$buyer->verify(['email' => $email, 'code' => $code]); // keeps the token it answers

$me    = $buyer->show();
$order = $buyer->orders->show($me['orders'][0]['id']);

$buyer->logout(); // and forgets it
```

To keep a buyer signed in, keep `$buyer->token` and pass it back as `token` next time. A change made with a buyer's token is never sent again by itself.

## OAuth

An app that sellers connect to their own stores (see [Apps and OAuth](/guides/oauth.md)) passes the token it is handed as `apiKey`, and `Shoply\OAuth` has the steps that get one: `authorizeUrl()`, `state()`, `pkce()`, `exchange()` and `revoke()`.

```php theme={null}
use Shoply\OAuth;

$pkce = OAuth::pkce();
$url  = OAuth::authorizeUrl(['clientId' => $clientId, 'redirectUri' => $redirectUri, 'state' => $state, 'codeChallenge' => $pkce['challenge']]);

// Back at your redirect URL:
$token  = OAuth::exchange(['clientId' => $clientId, 'clientSecret' => $clientSecret, 'code' => $code, 'redirectUri' => $redirectUri, 'codeVerifier' => $pkce['verifier']]);
$shoply = new Shoply\Client(['apiKey' => $token['accessToken'], 'storeId' => $token['storeId']]);
```

A refusal from the token address is a `ShoplyException` whose `errorCode` is OAuth's word: `invalid_grant`, `invalid_client`. A notice that a seller disconnected your app is checked with `Shoply\Webhooks::verify` and your app's notice secret.

## Types

Each method's docblock says what it takes, as an array shape with each field's words, and what it answers. PhpStorm and PHPStan read them, so a missing field, a value a field does not take or a misspelt key is caught as you write it.

What a route answers is described from an answer that route gave, as its page's example is. A field that was text there may be null in another answer, and a route with no example answers `mixed`.


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