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

# API introduction

> Getting started with the Shoppex Developer API

<Note>
  This page is the **Dev API overview**, not the best first page for every workflow.

  Use this quick routing:

  * if you want your **first generic Dev API call**, start with [Quick start](/developers/quickstart)
  * if you want to **build or customize a theme with AI**, start with [Editing with AI](/storefront/editing-with-ai)
  * if you want **theme automation endpoints** specifically, read [Visual themes](/storefront/visual-themes) first and use this API section as reference after that
</Note>

## Guide vs reference

* `/themes/*` explains the ThemeDocument workflow and when to use settings,
  document, preview, and publish operations
* `/api-reference/*` explains the Dev API shape, auth, scopes, and endpoint details

Use the theme pages to understand the workflow. Use the API reference when you already know the workflow and need the exact request shape.

## Official SDKs

If you do not want to hand-write requests, use an official SDK for your language.

<Card title="SDKs & libraries" icon="box-open" href="/developers/sdks">
  Official Shoppex SDKs, install commands, public repos, and language examples.
</Card>

## Base URL

All API requests must be made to:

```
https://api.shoppex.io/dev/v1
```

## Authentication

The Shoppex API uses Bearer token authentication. You can authenticate with:

* an API key like `shx_...`, for server-to-server integrations you manage directly
* an OAuth2 access token like `shpat_...`, for installable third-party apps

An internal sync worker uses `Authorization: Bearer shx_your_api_key_here`. An installable ERP app uses `Authorization: Bearer shpat_your_access_token_here`.

Include the token in the `Authorization` header:

```bash theme={"system"}
Authorization: Bearer shx_your_api_key_here
```

<Warning>
  Keep your API keys secure. Never expose them in client-side code or public repositories.
</Warning>

API keys and OAuth2 access tokens can both be limited with scopes, for example `products.read` or `themes.write`. See [Authentication](/developers/authentication) for the full scope catalog, how to create and rotate keys, and the OAuth2 authorization flow.

Use `GET /me/capabilities` to inspect an active key's scopes, enabled payment methods, and (for OAuth2) issuing client at runtime. See [Authentication](/developers/authentication#inspecting-the-active-key) for the full response shape.

## Request format

All requests must:

* Use `Content-Type: application/json`
* Send JSON-encoded request bodies
* Include the Authorization header

<CodeGroup>
  ```bash cURL theme={"system"}
  curl https://api.shoppex.io/dev/v1/invoices \
    -H "Authorization: Bearer shx_your_api_key_or_shpat_access_token" \
    -H "Content-Type: application/json"
  ```
</CodeGroup>

## OAuth2 flow

For third-party apps, Shoppex also supports an OAuth2 authorization-code flow. The merchant browser is redirected to `GET /dev/v1/oauth/authorize` and approves on a Shoppex-hosted screen. Your backend then exchanges the returned `shoa_...` code at `POST /dev/v1/oauth/token` for a `shpat_...` access token.

The token endpoint returns standard OAuth2 JSON, not the normal Shoppex API envelope. Every other `/dev/v1/*` endpoint you call afterward still uses the standard Shoppex response format. See [Authentication](/developers/authentication) for the full flow with request and response examples.

## Payment retries and gateway switches

Shoppex can create more than one internal payment attempt for the same invoice. A customer can start with PayPal, close the checkout, reopen the invoice, and finish with Stripe.

This catches most people off guard. Treat the Shoppex invoice or payment ID as the source of truth, not a single provider session or order ID.

### Orders & disputes

* use `GET /orders` for your operational order queue
* use `POST /orders` when your backend wants to create a pending order from catalog line items
* use `POST /orders/:id/fulfill` for a server-side fulfillment trigger on a pending order
* use `POST /orders/:id/refund` for merchant-driven refunds without the dashboard
* use `GET /disputes` for chargebacks and payment-risk review

### Invoices & payments

* use `GET /invoices` when you need the lower-level invoice resource directly
* use `POST /payment-links` or `POST /payment-links/:id/toggle` for sales-link workflows
* use `GET /coupons/code/:code` for server-side coupon validation by code

### Subscriptions & customers

* use `POST /subscriptions/:id/pause` or `PATCH /subscriptions/:id/custom-fields` for recurring billing workflows
* use `POST /customers/:id/wallet/credit` or `POST /customers/:id/wallet/debit` for store-credit adjustments
* use `GET /customers/:id?include_affiliate=true` when you need customer detail plus affiliate summary in one response

### Products & variants

* use `GET /products/:id/variants/fields`, `POST /products/:id/variants/fields`, or `POST /products/:id/variants/fields/:fieldId/options` for variant configuration tooling
* use `GET /products?include_variants=true` when you need product reads with inline variant prices

### Analytics & affiliates

* use `GET /analytics/reports` or `POST /analytics/reports/:id/generate` for scheduled exports
* use `GET /affiliates/customers` or `POST /affiliates/applications/:id/approve` for affiliate program automation

### Store & themes

* use `GET /store/branding`, `POST /store/domains/additional`, `GET /store/domains/additional/:id/verification`, or `PUT /store/layout` for storefront automation
* use `GET /themes/:id/control/document/draft`, `PUT /themes/:id/control/document`, and `POST /themes/:id/control/publish` for revision-gated ThemeDocument automation
* use `GET/POST /themes/:id/control/settings` for settings-derived themes and `POST /themes/:id/control/preview` for previews

### Licenses & webhooks

* use `GET /licenses`, `POST /licenses`, `PATCH /licenses/:id`, or `DELETE /licenses/:id` for license support workflows
* use `POST /webhooks/logs/:id/retry` or `POST /webhooks/:id/rotate-secret` for webhook ops workflows

### Security

* use `GET /security/audit-trail` or `GET /security/session-trail/:session_id` for security and support workflows

Coupon note:

* the public storefront flow already had `POST /v1/storefront/coupons/check`
* `GET /coupons/code/:code` is the server-to-server Dev API equivalent

The reliable pattern: use Shoppex invoice/payment responses, listen to Shoppex webhooks, and make your fulfillment logic idempotent. Do not assume one invoice always maps to exactly one provider-side payment session.

## Theme automation shortcut

If you came here for themes, the usual endpoint family is:

* `GET /themes`
* `POST /themes/control/create`
* `GET /themes/{id}/control/settings`
* `POST /themes/{id}/control/settings`
* `GET /themes/{id}/control/document`
* `GET /themes/{id}/control/document/draft`
* `PUT /themes/{id}/control/document`
* `GET /themes/{id}/control/document/schema`
* `POST /themes/{id}/control/preview`
* `DELETE /themes/{id}/control/preview/{sessionId}`
* `POST /themes/{id}/control/publish`

These endpoints inspect, revision-save, preview, and publish validated
ThemeDocuments and their settings. For document-authoritative themes, read the
draft first. Send its `revision` back as `expected_revision` when saving, and
send the new revision returned by the save as `expected_revision` when
publishing. The MCP tools wrap this exact read → save → publish contract.

`POST /themes/control/create` accepts these document-native bases: `default`,
`classic`, `pulse`, `starlight`, and `clean-minimal`. Every accepted base maps
to a deployed ThemeDocument composition. Retired schemes (`apex`, `nebula`,
`phantom`, `shadow`, `vault`) are no longer valid bases. Existing themes on
them keep rendering and stay exportable and importable.

Best reading order:

1. [Editing with AI](/storefront/editing-with-ai)
2. [Visual themes](/storefront/visual-themes)
3. this API reference for the exact endpoint details

## Response format

All responses return JSON with a consistent structure:

<Tabs>
  <Tab title="Success Response">
    ```json theme={"system"}
    {
      "data": {
        "id": "abc123",
        "status": "COMPLETED"
      }
    }
    ```
  </Tab>

  <Tab title="List Response">
    ```json theme={"system"}
    {
      "data": [
        { "id": "abc123" },
        { "id": "def456" }
      ],
      "pagination": {
        "next_cursor": "eyJpZCI6MTIzfQ",
        "has_more": true
      }
    }
    ```
  </Tab>

  <Tab title="Error Response">
    ```json theme={"system"}
    {
      "error": {
        "code": "VALIDATION_ERROR",
        "message": "Invalid email address",
        "details": [
          { "field": "email", "message": "must be a valid email" }
        ],
        "doc_url": "https://docs.shoppex.io/api-reference/errors"
      }
    }
    ```
  </Tab>
</Tabs>

## HTTP status codes

<Note>
  All error responses follow the standard error envelope format. See the [Error handling guide](/developers/errors) for the full status-code table and detailed error codes.
</Note>

## Request tracing

Use `X-Request-Id` to correlate your logs with Shoppex responses. Shoppex returns the same header on both success and error responses.

```http theme={"system"}
X-Request-Id: sync-run-42
```

If you send `X-Request-Id: sync-run-42`, Shoppex echoes that same id back. If you do not send one, Shoppex generates one for you.

## Rate limiting

API requests are limited to protect the service. If you go over the limit, Shoppex returns a `429 Too Many Requests` response.

Authenticated Dev API requests return `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and (when blocked) `Retry-After` headers. If you are rate limited, wait until `X-RateLimit-Reset` or `Retry-After` before retrying. See the [Error handling guide](/developers/errors#rate-limiting) for a header example and the `RATE_LIMITED` error shape.

## Idempotency

All current state-changing `/dev/v1` endpoints support idempotent retries.

This covers product, category, group, coupon, variant, customer, webhook, ticket, review, blacklist, license, escrow, invoice, and payment writes. Retrying `POST /customers` after a timeout will not create the same customer twice, and retrying `POST /webhooks/:id/test` will not spam duplicate test deliveries.

Use one of these headers:

* `Idempotency-Key`
* `X-Idempotency-Key`

<CodeGroup>
  ```bash cURL theme={"system"}
  curl https://api.shoppex.io/dev/v1/payments \
    -X POST \
    -H "Authorization: Bearer shx_your_api_key" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: payment-create-123" \
    -d '{"title":"Order #1001","email":"buyer@example.com","value":49.99,"currency":"USD"}'
  ```
</CodeGroup>

Responses include an `Idempotency-Status` header:

* `created` when the request created a new payment
* `cached` when Shoppex replayed the stored response
* `processing` when the same key is still in flight
* `bypassed` when idempotency was intentionally skipped

Important behavior:

* the replay key is scoped to your authenticated key, the route, and the idempotency key
* Shoppex also checks the request body for the same method and path
* the replay window is currently 24 hours
* `5xx` responses are not cached for replay

In other words:

* same key + same body -> cached response
* same key + different body -> validation error
* same key while the first request is still in flight -> `processing`

## Pagination

The Dev API uses two pagination models. Cursor-based pagination covers most resource lists, including products, invoices, and orders. Page-based pagination covers operational lists, such as webhook delivery logs, where page navigation is clearer.

<Tabs>
  <Tab title="Cursor-based">
    Use cursor-based pagination for feed-style lists like products, invoices, orders, customers, and subscriptions.

    ### Parameters

    | Parameter | Type    | Default | Description                       |
    | --------- | ------- | ------- | --------------------------------- |
    | `limit`   | integer | 50      | Number of items per page (1-100)  |
    | `cursor`  | string  | -       | Cursor from the previous response |

    ### Basic usage

    #### First request

    ```bash theme={"system"}
    GET /products?limit=25
    ```

    **Response:**

    ```json theme={"system"}
    {
      "data": [
        { "uniqid": "prod_001", "title": "Product A" },
        { "uniqid": "prod_002", "title": "Product B" },
        // ... 23 more items
      ],
      "pagination": {
        "next_cursor": "eyJpZCI6MjUsImNyZWF0ZWRfYXQiOiIyMDI0LTAxLTAxIn0",
        "has_more": true
      }
    }
    ```

    #### Next page

    Use the `next_cursor` value from the previous response.

    ```bash theme={"system"}
    GET /products?limit=25&cursor=eyJpZCI6MjUsImNyZWF0ZWRfYXQiOiIyMDI0LTAxLTAxIn0
    ```

    #### Final page

    The response looks like this when no items remain.

    ```json theme={"system"}
    {
      "data": [
        { "uniqid": "prod_099", "title": "Product Y" },
        { "uniqid": "prod_100", "title": "Product Z" }
      ],
      "pagination": {
        "next_cursor": null,
        "has_more": false
      }
    }
    ```

    ### Code examples

    #### Fetch all pages

    <CodeGroup>
      ```typescript TypeScript theme={"system"}
      async function fetchAllProducts(): Promise<Product[]> {
        const allProducts: Product[] = [];
        let cursor: string | null = null;

        do {
          const url = new URL('https://api.shoppex.io/dev/v1/products');
          url.searchParams.set('limit', '100');
          if (cursor) url.searchParams.set('cursor', cursor);

          const response = await fetch(url.toString(), {
            headers: { 'Authorization': `Bearer ${API_KEY}` }
          });

          const { data, pagination } = await response.json();

          allProducts.push(...data);
          cursor = pagination.next_cursor;
        } while (cursor);

        return allProducts;
      }
      ```

      ```python Python theme={"system"}
      def fetch_all_products():
          all_products = []
          cursor = None

          while True:
              params = {'limit': 100}
              if cursor:
                  params['cursor'] = cursor

              response = requests.get(
                  'https://api.shoppex.io/dev/v1/products',
                  headers={'Authorization': f'Bearer {API_KEY}'},
                  params=params
              )

              result = response.json()
              all_products.extend(result['data'])

              if not result['pagination']['has_more']:
                  break

              cursor = result['pagination']['next_cursor']

          return all_products
      ```
    </CodeGroup>

    #### Async generator (streaming)

    ```typescript TypeScript theme={"system"}
    async function* fetchProductsStream(): AsyncGenerator<Product> {
      let cursor: string | null = null;

      do {
        const url = new URL('https://api.shoppex.io/dev/v1/products');
        url.searchParams.set('limit', '100');
        if (cursor) url.searchParams.set('cursor', cursor);

        const response = await fetch(url.toString(), {
          headers: { 'Authorization': `Bearer ${API_KEY}` }
        });

        const { data, pagination } = await response.json();

        for (const product of data) {
          yield product;
        }

        cursor = pagination.next_cursor;
      } while (cursor);
    }

    // Usage
    for await (const product of fetchProductsStream()) {
      console.log(product.title);
    }
    ```

    ### Filtering with pagination

    Filters work alongside pagination.

    ```bash theme={"system"}
    # Get completed invoices, paginated
    GET /invoices?filters=status:COMPLETED&limit=50

    # Continue with cursor
    GET /invoices?filters=status:COMPLETED&limit=50&cursor=eyJ...
    ```

    <Warning>
      Always include the same filters and sorts when you follow a cursor. Changing them invalidates the cursor.
    </Warning>

    ```bash theme={"system"}
    GET /orders?filters=status:COMPLETED,customer_email:buyer@example.com&sorts=-created_at
    GET /customers?filters=city:Berlin&sorts=created_at
    ```

    ### Cursor-based endpoints

    | Endpoint             | Default limit | Max limit |
    | -------------------- | ------------- | --------- |
    | `GET /products`      | 50            | 100       |
    | `GET /invoices`      | 50            | 100       |
    | `GET /customers`     | 50            | 100       |
    | `GET /coupons`       | 50            | 100       |
    | `GET /subscriptions` | 50            | 100       |
    | `GET /reviews`       | 50            | 100       |
    | `GET /tickets`       | 50            | 100       |
    | `GET /webhooks`      | 50            | 100       |
    | `GET /blacklist`     | 50            | 100       |
  </Tab>

  <Tab title="Page-based">
    Use page-based pagination for operational lists where integrators often jump to a specific page.

    ### Parameters

    | Parameter | Type    | Default | Description                |
    | --------- | ------- | ------- | -------------------------- |
    | `page`    | integer | 1       | Page number, starting at 1 |
    | `limit`   | integer | 25      | Number of items per page   |

    ### Basic usage

    ```bash theme={"system"}
    GET /webhooks/logs?page=2&limit=10
    ```

    ```json theme={"system"}
    {
      "data": [
        { "uniqid": "log_001", "status": "success" }
      ],
      "pagination": {
        "total": 37,
        "page": 2,
        "limit": 10,
        "total_pages": 4,
        "has_more": true
      }
    }
    ```

    ### Page-based endpoints

    | Endpoint                       | Default limit |
    | ------------------------------ | ------------- |
    | `GET /webhooks/logs`           | 25            |
    | `GET /affiliates/customers`    | 25            |
    | `GET /affiliates/applications` | 25            |
  </Tab>
</Tabs>

### Pagination best practices

<CardGroup cols={2}>
  <Card title="Use reasonable limits" icon="gauge">
    For a UI, 20-25 items is plenty. For a background sync job, raise the limit to 100. Larger limits increase response time.
  </Card>

  <Card title="Do not store cursors" icon="clock">
    Cursors work for immediate sequential use. They expire after 24 hours. They also become invalid if the underlying data changes significantly.
  </Card>

  <Card title="Handle empty results" icon="inbox">
    An empty `data` array with `has_more: false` is valid. No items match your query.
  </Card>

  <Card title="Respect rate limits" icon="shield">
    When you fetch all pages, add delays between requests to avoid rate limiting.
  </Card>
</CardGroup>

## Filtering

Core list endpoints now support a shared filtering and sorting contract:

```bash theme={"system"}
# Filter by exact fields
GET /orders?filters=status:COMPLETED,customer_email:john@example.com

# Sort oldest first
GET /customers?sorts=created_at

# Sort newest first (default)
GET /invoices?sorts=-created_at
```

Supported list endpoints for this shared contract:

* `GET /customers`
* `GET /orders`
* `GET /invoices`

`filters` uses `field:value` pairs separated by commas.

```bash theme={"system"}
filters=status:COMPLETED,customer_email:buyer@example.com
sorts=created_at     # oldest first
sorts=-created_at    # newest first (default)
```

Legacy single-field query params like `status` or `customerEmail` still work where already documented, but new integrations use `filters` and `sorts` instead.
