---
title: "List packages, GET /ext/packages"
description: "Returns every package the calling key is authorized to sell, in the OneStream flat-array shape."
---

# GET /ext/packages

`/ext/packages` returns every package the calling key can sell. Packages are the price sheet of the panel: each row describes a subscription (credit cost, duration, max connections) and, optionally, its trial variant. Storefronts, dashboards, and any flow that creates a line off a package name reads this list.

The response shape is the OneStream shape: a plain JSON array of package objects. There is no envelope. If your integration handles the native v1 API too, note that this endpoint corresponds to `GET /panel-api/v1/packages`, but the v1 flavor wraps the same rows in `{"items": [...]}`. Here you get the rows directly.

## Endpoint

`GET https://<your-panel-domain>/panel-api/onestream/ext/packages`

## Authentication

Send the API key in `X-Api-Key`, `X-Auth-User`, or `Authorization: Bearer`. See the [OneStream overview](/docs/?page=os-ref-overview#base-url-and-authentication).

## Required scope

`packages:read`.

## Query parameters

None. The full list of visible packages is returned in one call. There are typically fewer than a few hundred packages per panel, so pagination is not applied. The list is scoped silently by caller type: an admin key sees every package, a reseller key sees only packages whose `groups` list contains the reseller's `member_group_id`. No error is raised when the visible set is empty; the response is `[]`.

## Response

A plain JSON array. Each element has the following fields.

| Field | Type | Description |
| ----- | ---- | ----------- |
| `id` | int | Numeric package id. Stable across the panel's lifetime. Pass this as `package` when creating a line. |
| `package_name` | string | Human-readable name shown in the panel UI. |
| `is_trial` | bool | `true` when the package can only be sold as a trial. Paid fields will be zero. |
| `is_official` | bool | `false` marks the package as internal or hidden from public storefronts. |
| `official_credits` | float | Credits charged when the package is sold as a paid subscription. |
| `official_duration` | int | Length of the paid subscription in `official_duration_in` units. |
| `official_duration_in` | string | One of `hours`, `days`, `weeks`, `months`, `years`. |
| `trial_credits` | float | Credits charged when the caller requests a trial. `0` if trials are disabled. |
| `trial_duration` | int | Length of the trial in `trial_duration_in` units. `0` if trials are disabled. |
| `trial_duration_in` | string | Same allowed values as `official_duration_in`. |
| `max_connections` | int | Maximum concurrent devices a subscriber on this package can have logged in. |
| `is_restreamer` | bool | `true` when the package enables restreamer permissions. |
| `forced_country` | string | Two-letter ISO country code that forces every line created under this package to that country. Empty string means no restriction. |

Example response:

```json
[
  {
    "id": 76,
    "package_name": "Package 12M 1 Connection",
    "is_trial": false,
    "is_official": true,
    "official_credits": 12,
    "official_duration": 12,
    "official_duration_in": "months",
    "trial_credits": 0,
    "trial_duration": 0,
    "trial_duration_in": "hours",
    "max_connections": 1,
    "is_restreamer": false,
    "forced_country": ""
  },
  {
    "id": 67,
    "package_name": "Package 1M 1 Connection",
    "is_trial": false,
    "is_official": true,
    "official_credits": 1,
    "official_duration": 1,
    "official_duration_in": "months",
    "trial_credits": 0,
    "trial_duration": 0,
    "trial_duration_in": "hours",
    "max_connections": 1,
    "is_restreamer": false,
    "forced_country": ""
  }
]
```

## Examples

### cURL

```bash
curl -H "X-Api-Key: <your-api-key>" \
     https://<your-panel-domain>/panel-api/onestream/ext/packages
```

### PHP raw

```php
$ch = curl_init('https://<your-panel-domain>/panel-api/onestream/ext/packages');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-Key: <your-api-key>']);
$packages = json_decode(curl_exec($ch), true);
curl_close($ch);
foreach ($packages as $p) {
    echo $p['id'] . ' ' . $p['package_name'] . ' (' . $p['official_credits'] . " credits)\n";
}
```

### Python raw

```python
import requests

packages = requests.get(
    "https://<your-panel-domain>/panel-api/onestream/ext/packages",
    headers={"X-Api-Key": "<your-api-key>"},
    timeout=30,
).json()

for p in packages:
    print(p["id"], p["package_name"], p["official_credits"])
```

## Errors

| HTTP | Error slug | When it happens | How to fix |
| ---- | ---------- | --------------- | ---------- |
| 401 | `invalid_key` | Header is missing, the token is unknown, the key was disabled, expired, deleted, or the caller IP is not on the key's IP allow-list. | Check the header. If the key was rotated, mint a new one from the panel. |
| 403 | `insufficient_scope` | The key does not carry `packages:read`. | Regenerate the key with `packages:read`, or use a key that has it. |
| 429 | `rate_limited` | The per-key or per-IP rate limit was hit. Response carries `Retry-After` and `X-RateLimit-*` headers. | Back off for the number of seconds in `Retry-After`. Cache the response. |
| 403 | `api_disabled` | An admin has turned the Panel API off for this panel. | Ask the admin to re-enable it. |

## See also

- [List bouquets](/docs/?page=os-ref-bouquets)
- [OneStream overview](/docs/?page=os-ref-overview)
- [Native packages endpoint](/docs/?page=xai-ref-catalog-packages)
