# Working with the Telgea Open API

Get started with the Telgea API in under 5 minutes.

> Partner API · https://developer.telgea.com/docs/partner/guides

## Telgea Open API [#telgea-open-api]

**The Telgea Open API follows conventional REST API standards, aligned around various resources that can be managed
through the API.**

### Prerequisites [#prerequisites]

To work with the Open API, ensure you have:

* A Telgea organization account
* An API key: see [Authentication](https://developer.telgea.com/docs/partner/concepts/authentication) for how you get one

### Make Your First Request [#make-your-first-request]

The quickest first call needs no code: open the [playground](https://developer.telgea.com/playground#tag/users/GET/v1/users), paste your API key, and send **List users**. It is a read, so it changes nothing.

Or from a terminal:

```bash
curl -X GET https://api.telgea.com/v1/users \
  -H "x-api-key: $TELGEA_API_KEY"
```

The response includes a paginated list of users in your organization.

<Callout type="warn" title="There is no sandbox">
  Every call — from the playground or from your code — runs against your own live organization. Creating a user creates a real user, and provisioning creates a real line.
</Callout>

## Authentication [#authentication]

All API requests require an API key passed in the `x-api-key` header. Each key holds a set of
permissions that decide which operations it can call, and every operation in the
[API reference](https://developer.telgea.com/docs/partner/api) names the permission it needs. How you receive a key is described under
[Authentication](https://developer.telgea.com/docs/partner/concepts/authentication).

Include the key in every request:

```bash
curl -X GET https://api.telgea.com/v1/users \
  -H "x-api-key: $TELGEA_API_KEY"
```

Missing or invalid API keys return a `401 Unauthorized` status code. Insufficient permissions return `403 Forbidden`.

To check a key before writing any code, paste it into the [playground](https://developer.telgea.com/playground) and send a read such as **List users**. A `403` there names the permission the key is missing.

## Error Handling [#error-handling]

The Telgea Open API uses standard HTTP status codes and returns every failure as
[RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457), served as `application/problem+json`.
Success responses come in the form of `200 OK`, `201 Created` or `204 No Content`.
The [error index](/errors) lists every problem type the API returns, each with its own page.

| Status | `code`                        | When                                                                                                                                                                                                                           |
| ------ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400`  | `VALIDATION_ERROR`            | The request does not match the endpoint's schema. Nothing was processed.                                                                                                                                                       |
| `400`  | `BAD_REQUEST`                 | The request was refused, and no more specific code describes why — for instance, the resource is not in a state that allows the action.                                                                                        |
| `400`  | `INVALID_REQUEST`             | The request combines parameters that cannot be used together, so nothing was processed. Returned by the user-plan endpoints of the Enterprise API; other endpoints return `BAD_REQUEST`, which new integrations should handle. |
| `401`  | `UNAUTHORIZED`                | The `x-api-key` header is missing or invalid.                                                                                                                                                                                  |
| `403`  | `FORBIDDEN`                   | The key is valid but lacks the permission this endpoint requires.                                                                                                                                                              |
| `403`  | `ORGANIZATION_ACCESS_REVOKED` | Your organization's access to the API has been revoked. Contact support to restore it.                                                                                                                                         |
| `403`  | `COUNTRY_NOT_SUPPORTED`       | The request names a country that is not enabled for your organization.                                                                                                                                                         |
| `404`  | `NOT_FOUND`                   | No matching resource exists in your organization.                                                                                                                                                                              |
| `405`  | `METHOD_NOT_ALLOWED`          | The path does not accept this method. The `Allow` response header lists the ones it does.                                                                                                                                      |
| `409`  | `CONFLICT`                    | The request conflicts with an existing resource. Nothing was created or changed.                                                                                                                                               |
| `422`  | `UNPROCESSABLE_ENTITY`        | The request was understood but cannot be processed with these values.                                                                                                                                                          |
| `429`  | `RATE_LIMIT_EXCEEDED`         | The key has used its quota for the current window. `Retry-After` says when it resets.                                                                                                                                          |
| `429`  | `TOO_MANY_REQUESTS`           | Requests for this action arrive faster than it allows, such as a second SIM replacement inside its cooldown. Retry with backoff.                                                                                               |
| `500`  | `INTERNAL_ERROR`              | Retry with exponential backoff, then contact Telgea with the `requestId`.                                                                                                                                                      |
| `503`  | `SERVICE_UNAVAILABLE`         | A system the request depends on is temporarily unavailable. Retry with exponential backoff; if the request creates or changes something, read the resource back first.                                                         |

### Error Response Format [#error-response-format]

Every error body is a flat JSON object with the content type `application/problem+json`. It always
carries these members:

* `type` — a URI naming the kind of failure. It opens that error's page on this site, which says when
  it occurs and what to do about it.
* `title` — a short summary of the problem type. It is the same on every occurrence.
* `status` — the HTTP status of the response.
* `detail` — what went wrong on this request, in one sentence.
* `code` — a stable machine-readable code. Branch on `code` or `type`, never on the `detail` text.
* `requestId` — identifies the request in Telgea's logs. Quote it when you contact support.

Some errors add extension members after these, named in `camelCase`, such as `fields` on a
validation error or `missingPermission` on a permission error. The error schemas in the
[API reference](https://developer.telgea.com/docs/partner/api) list the members each operation's errors can carry. Ignore any member you do
not recognize.

```json schema=NotFoundError
{
  "type": "https://developer.telgea.com/errors/http/NOT_FOUND",
  "title": "Not found",
  "status": 404,
  "detail": "No resource matching this request exists in your organization.",
  "code": "NOT_FOUND",
  "requestId": "req_123"
}
```

### Validation Errors [#validation-errors]

A `VALIDATION_ERROR` adds `fields`, listing one entry per rejected field (at most 20). Every entry
describes the same request location: query, path and body are validated in that order, and the first
that fails answers the request.

```json schema=ValidationError
{
  "type": "https://developer.telgea.com/errors/http/VALIDATION_ERROR",
  "title": "Validation error",
  "status": 400,
  "detail": "The request does not match this endpoint's schema, so nothing was processed.",
  "code": "VALIDATION_ERROR",
  "requestId": "req_123",
  "fields": [
    {
      "field": "fields.authorizationCode",
      "location": "body",
      "code": "REQUIRED",
      "message": "This field is required."
    }
  ]
}
```

`location` is one of `body`, `query`, `path`, `header` or `cookie`. `code` is one of `REQUIRED`,
`INVALID_TYPE`, `INVALID_FORMAT`, `INVALID_VALUE`, `TOO_SMALL`, `TOO_LARGE`, `UNKNOWN_FIELD` or
`INVALID`. `field` is a dotted path, with `[i]` for array indices, and is empty when the failure is
about the request as a whole rather than one field.

### Permission Errors [#permission-errors]

A `403` raised by a missing API-key permission adds `missingPermission`, naming the permission the
key lacks.

```json schema=ForbiddenError
{
  "type": "https://developer.telgea.com/errors/http/FORBIDDEN",
  "title": "Forbidden",
  "status": 403,
  "detail": "This API key is missing the users:read permission.",
  "code": "FORBIDDEN",
  "requestId": "req_123",
  "missingPermission": "users:read"
}
```

## Pagination [#pagination]

List endpoints paginate in one of two ways. Resource collections use page numbers; the usage feed
uses a cursor, because records keep arriving while you read.

### Page-numbered endpoints [#page-numbered-endpoints]

`/v1/users`, `/v1/numbers` and `/v1/port-ins/number/{numberId}` take `page` and `pageSize` query
parameters and return:

```json op="GET /v1/users" dir=response status=200
{
  "page": 1,
  "pageSize": 50,
  "itemsCount": 142,
  "items": []
}
```

* `itemsCount` is the total across all pages; the number of pages is `Math.ceil(itemsCount / pageSize)`
* An empty `items` array with `itemsCount: 0` means no results match your query
* `page` is 1-indexed, and the default and maximum `pageSize` differ per endpoint:

| Endpoint                          | Default `pageSize` | Maximum `pageSize` |
| --------------------------------- | ------------------ | ------------------ |
| `/v1/users`                       | `10`               | `50`               |
| `/v1/teams` (Enterprise API)      | `50`               | `50`               |
| `/v1/numbers` (Partner API)       | `50`               | `100`              |
| `/v1/user-plans` (Enterprise API) | `50`               | `100`              |
| `/v1/port-ins/number/{numberId}`  | `50`               | `100`              |

### The usage feed [#the-usage-feed]

`/v1/usage` takes `pageSize` (default `50`, maximum `100`) and an opaque `cursor`, and returns:

```json op="GET /v1/usage" dir=response status=200
{
  "items": [],
  "nextCursor": "eyJjIjoiMjAyNi0wOS0xMVQwODozMDowMC4xMjM0NTZaIiwicyI6ImRhdGEiLCJpIjoiY2RyXzEifQ",
  "hasMore": true
}
```

Omit `cursor` for the first page, then pass the previous response's `nextCursor` to fetch the next
one. Stop when `hasMore` is `false`; `nextCursor` is `null` once the feed is exhausted. The feed
takes no `page` parameter and reports no total.

## Rate Limits [#rate-limits]

Each API key allows 1000 requests per 24 hours, shared across every endpoint. A request over the
budget returns `429` with the code `RATE_LIMIT_EXCEEDED`; wait for the window to reset before retrying.
When polling usage, walk the whole organization with the default page size rather than issuing one
request per subscriber.

## Retries and Idempotency [#retries-and-idempotency]

Every `POST`, `PATCH` and `DELETE` accepts an optional `Idempotency-Key` header: a value unique to the
request, of 1 to 255 printable ASCII characters. A UUID is the usual choice.

```bash
curl -X POST https://api.telgea.com/v1/numbers \
  -H "x-api-key: $TELGEA_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"userId": "9c1f2a6e-3b7d-4e0a-8f21-5d6c7b8a9e01", "countryCode": "DK"}'
```

If the connection drops before the response reaches you, send the request again with the same key:
rather than provisioning a second line or failing with a conflict, the API returns the original
response.

* A key identifies **one request to one endpoint**, and is remembered for **24 hours**.
* Within that window the same key replays the original outcome, including a failure, which is not
  re-attempted. Change the key whenever you mean to send a new request, including after correcting a
  rejected one.
* Keys are private to your API key and organization.
* The header is ignored on `GET`, and a malformed one is rejected with `400` before the request has any
  effect.

## Next Steps [#next-steps]

* [Number Provisioning](https://developer.telgea.com/docs/partner/guides/number-provisioning) — Give your users new numbers
* [API Reference](https://developer.telgea.com/docs/partner/api) — Explore all available endpoints
