> ## 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.

# Python

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

`shoply` is the API for a store's own server in Python: every route of this reference is a method, with types your editor and mypy 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}
pip install shoply
```

Python 3.9 or later. It has no dependencies.

```python theme={null}
import shoply

client = shoply.Client()  # SHOPLY_API_KEY from the environment

order = client.orders.show("ORDER_ID")
print(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, the key 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 `client.orders.show(order_id)`. 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 keyword arguments named as the reference names them: what goes in the address and what goes in the body are sorted out for you. A name that is a Python keyword takes an underscore: `from_`, and **Import a storefront** is `storefronts.import_`.
3. Options, as keyword arguments too: `idempotency_key`, `timeout`, `max_retries`, `headers`.

```python theme={null}
coupon = client.coupons.create(code="SPRING10", value=10, maxUses=100)

client.products.setVisibility(product_id, visibility="unlisted")

client.orders.markPaid(order_id)
```

A call answers the route's `data`, as dicts and lists. A field left as `None` is not sent. The store is asked of the key once, the first time it is needed (`GET /key`). Pass `store_id` to skip that.

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

## Lists

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

```python theme={null}
page = client.orders.list(status="completed", perPage=50)

print(len(page.data), page.total, page.pages)

after = page.next_page()  # None after the last

for customer in client.customers.list(search="gmail.com"):
    print(customer["emailAddress"])
```

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.ShoplyError`.

| Attribute | What it is |
| - | - |
| `status` | The HTTP status. 0 when nothing answered. |
| `code` | 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). |
| `message` | What is wrong, in words for a person. It is the exception's text too. |
| `errors` | For `validation_failed`, what is wrong with each field, by its name. |
| `request_id` | The request's ID. Quote it to Shoply about it. |
| `retry_after` | Seconds to wait, when told to slow down. |
| `body` | The whole answer. |

```python theme={null}
try:
    client.coupons.create(code="SPRING10", value=10)
except shoply.ShoplyError as error:
    if error.code != "validation_failed":
        raise

    print((error.errors or {}).get("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:

```python theme={null}
client.orders.markPaid(order_id, idempotency_key=f"paid-{invoice_number}")
```

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 raises with `retry_after`. An upload is not tried again, since it carries no key.

Pass `max_retries=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 bytes, as it came, `content_type`, `text()`, and `save(path)`.

```python theme={null}
client.orders.export(status="completed").save("orders.csv")
```

An upload takes `file` as a path, the file's bytes, or a file opened for reading, with `filename` and `content_type` where you want to say them. What else the route takes goes beside it:

```python theme={null}
client.productImages.upload(product_id, file="/path/to/cover.png")

client.disputes.addFile(dispute_id, 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)).

```python theme={null}
answer = client.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. Pass the raw body, as it arrived, and the request's headers, or a WSGI environ. See [Webhooks](/api-reference/webhooks.md).

```python theme={null}
import os

import shoply
from flask import Flask, request

app = Flask(__name__)


@app.post("/webhooks/shoply")
def shoply_webhook() -> tuple[str, int]:
    try:
        event = shoply.webhooks.verify(request.get_data(), request.headers, os.environ["SHOPLY_WEBHOOK_SECRET"])
    except shoply.ShoplyError:
        return "", 401

    if event["event"] == "order.paid":
        ...  # event["data"] is the order, as Get an order answers it

    return "", 200
```

In Django, the body is `request.body` and the headers `request.headers`. `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.

```python theme={null}
buyer = shoply.Customer("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 its key, and `shoply.oauth` has the steps that get one: `authorize_url()`, `state()`, `pkce()`, `exchange()` and `revoke()`.

```python theme={null}
import shoply

pkce = shoply.oauth.pkce()
url = shoply.oauth.authorize_url(client_id, redirect_uri, state=state, code_challenge=pkce["challenge"])

# Back at your redirect URL:
token = shoply.oauth.exchange(client_id=client_id, client_secret=client_secret, code=code, redirect_uri=redirect_uri, code_verifier=pkce["verifier"])
client = shoply.Client(token["access_token"], store_id=token["store_id"])
```

A refusal from the token address is a `ShoplyError` whose `code` 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 says what it takes, with each field's words, and what it answers, as a `TypedDict`. VS Code, PyCharm and mypy 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 `None` in another answer, a whole number may have a fraction in another and is typed `float`, and a route with no example answers `Any`.


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