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

# Resellers

> Automate your wholesale reseller program from the Developer API.

These endpoints let a merchant read and steer their own wholesale reseller program: list
reseller accounts, invite new ones, suspend and reinstate them, and read or update the program
settings.

They are part of the merchant [Developer API](/developers/api-overview) and authenticate
with a normal `shx_` API key. The API resellers themselves use to buy wholesale stock is a
different API. See the [Reseller API](/developers/reseller-api).

<Note>
  Tier authoring, product enrollment, and per-product price overrides are dashboard-only. They are
  configuration for the price cascade, not automation, and a second write path for them creates
  a second source of truth. `GET /dev/v1/reseller-tiers` is read-only for exactly that reason.
</Note>

***

## Scopes

| Scope             | Grants                                                                                                                                            |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `resellers.read`  | `GET /dev/v1/resellers`, `GET /dev/v1/resellers/{id}`, `GET /dev/v1/reseller-program`, `GET /dev/v1/reseller-tiers`                               |
| `resellers.write` | `POST /dev/v1/resellers/invite`, `POST /dev/v1/resellers/{id}/suspend`, `POST /dev/v1/resellers/{id}/reinstate`, `PATCH /dev/v1/reseller-program` |

A key with only `resellers.read` gets `403 FORBIDDEN` on any of the write routes. Reseller
scopes are not implied by `customers.*`, even though the dashboard puts Resellers under
Customers.

***

## Endpoints

### GET /dev/v1/resellers

Page-based list, newest first.

| Query      | Type                                                    | Default |
| ---------- | ------------------------------------------------------- | ------- |
| `status`   | `INVITED`, `APPLIED`, `ACTIVE`, `SUSPENDED`, `REJECTED` | all     |
| `search`   | matches the reseller email, max 128 chars               | —       |
| `page`     | integer ≥ 1                                             | `1`     |
| `per_page` | integer 1–100                                           | `25`    |

```bash theme={"system"}
curl "https://api.shoppex.io/dev/v1/resellers?status=ACTIVE&per_page=50" \
  -H "Authorization: Bearer shx_your_key_here"
```

```json theme={"system"}
{
  "data": {
    "items": [
      {
        "id": "019cb402-1111-7000-8000-000000000001",
        "customer_id": "019cb402-2222-7000-8000-000000000002",
        "email": "wholesale@example.com",
        "status": "ACTIVE",
        "tier": {
          "id": "019cb402-3333-7000-8000-000000000003",
          "name": "Gold",
          "discount_percent": 15
        },
        "application_note": null,
        "lifetime_revenue": "1250.00",
        "lifetime_orders": 7,
        "approved_at": "2026-07-01T10:00:00.000Z",
        "suspended_at": null,
        "rejected_at": null,
        "created_at": "2026-06-01T10:00:00.000Z",
        "updated_at": "2026-07-01T10:00:00.000Z"
      }
    ],
    "total": 1,
    "page": 1,
    "per_page": 50
  }
}
```

### GET /dev/v1/resellers/{id}

Returns `{ "data": { "reseller": { … } } }` with the same object. `404 RESELLER_NOT_FOUND` when
the ID belongs to another shop or does not exist.

### POST /dev/v1/resellers/invite

Sends the wholesale invitation email and creates the relationship in `INVITED` state.

```json theme={"system"}
{ "email": "new@example.com", "tier_id": "019cb402-3333-7000-8000-000000000003" }
```

`tier_id` is optional. Omit it to use the program's default tier. If neither is set the call
fails with `422` and the message asks you to pick a tier. Requires an enabled program.

Inviting an email that is already `INVITED`, `APPLIED`, `ACTIVE`, or `SUSPENDED` returns
`409 CONFLICT`. A previously `REJECTED` email can be re-invited.

### POST /dev/v1/resellers/{id}/suspend

Blocks wholesale pricing and revokes Reseller API access for that account. Only an `ACTIVE`
reseller can be suspended. Anything else returns `422`. The row is never deleted, so the status
history stays intact.

### POST /dev/v1/resellers/{id}/reinstate

Returns a `SUSPENDED` reseller to `ACTIVE`. Only a `SUSPENDED` reseller can be reinstated.

<Note>
  Reinstating sends no webhook. There is no `reseller:reinstated` event. Shoppex only sends
  `reseller:suspended` on the way out.
</Note>

### GET /dev/v1/reseller-program

```json theme={"system"}
{
  "data": {
    "program": {
      "id": "019cb402-8888-7000-8000-000000000008",
      "enabled": true,
      "enrollment_mode": "APPLICATION",
      "application_message": "Tell us about your business.",
      "require_application_note": true,
      "default_tier_id": "019cb402-3333-7000-8000-000000000003",
      "created_at": "2026-06-01T10:00:00.000Z",
      "updated_at": "2026-07-01T10:00:00.000Z"
    }
  }
}
```

The row is created lazily. The first read on a shop that never opened the Resellers page
returns a disabled program in `MANUAL` mode rather than a 404.

### PATCH /dev/v1/reseller-program

Partial update. Every field is optional.

```json theme={"system"}
{ "enabled": true, "enrollment_mode": "OPEN", "default_tier_id": "019cb402-3333-7000-8000-000000000003" }
```

| Field                      | Notes                                                                                                |
| -------------------------- | ---------------------------------------------------------------------------------------------------- |
| `enabled`                  | Master switch for the whole program                                                                  |
| `enrollment_mode`          | `MANUAL` (invite only), `APPLICATION` (customers apply, you approve), `OPEN` (customers self-enroll) |
| `application_message`      | Shown to applicants, max 2000 chars, nullable                                                        |
| `require_application_note` | Forces applicants to write a note                                                                    |
| `default_tier_id`          | Tier used when an invite does not name a tier, and for open enrollment                               |

`enrollment_mode: "OPEN"` without a `default_tier_id` returns `422`. Open enrollment turns
customers into resellers without review, so the tier they land in has to be decided up front.
An unknown `default_tier_id` returns `404 RESELLER_TIER_NOT_FOUND`.

### GET /dev/v1/reseller-tiers

Read-only, ordered by `sort_order`.

```json theme={"system"}
{
  "data": {
    "tiers": [
      {
        "id": "019cb402-3333-7000-8000-000000000003",
        "name": "Gold",
        "discount_percent": 15,
        "auto_advance_revenue": "5000.00",
        "auto_advance_orders": 20,
        "auto_advance_requires_both": false,
        "sort_order": 1,
        "member_count": 3,
        "is_default": true,
        "created_at": "2026-06-01T10:00:00.000Z",
        "updated_at": "2026-07-01T10:00:00.000Z"
      }
    ]
  }
}
```

`auto_advance_revenue` and `auto_advance_orders` are the thresholds that promote a reseller into
this tier automatically after an order completes. Either being `null` means it is not part of the
condition. `auto_advance_requires_both: false` means hitting either threshold is enough.

***

## Idempotency

All three write routes accept the standard `Idempotency-Key` header described in
[Errors](/developers/errors). It is optional here, unlike on the reseller-facing order
endpoint where it is mandatory.
