# HTTP API

ShellHub’s HTTP API speaks JSON, and it is the same one the console uses. [API Reference](https://docs.shellhub.io/api/reference) lists every route; this page is how you get in and what a credential can do once you are.

## Authenticating

Two credentials work, and they are for different things.

An **API key** goes in the `X-API-Key` header:

```bash
$ curl -H "X-API-Key: <your-api-key>" https://cloud.shellhub.io/api/devices
```

It carries a namespace and a role. It belongs to the namespace rather than to anybody’s session, and any expiry it has is one you set at creation, not a session lifetime the server imposes.

A **bearer token** is the JWT a person gets from signing in:

```bash
$ curl -H "Authorization: Bearer <token>" https://cloud.shellhub.io/api/devices
```

The role behind it is read again on every request rather than trusted from the token, so a role change takes effect immediately.

**Anything unattended should hold an API key.** A script driving a bearer token has to handle sign-in and expiry, and if it does that with a person’s credentials it breaks the day that person leaves.

## Creating a key

From the console, under **Team → API Keys**. A key has:

| Field      | Meaning                                                |
| ---------- | ------------------------------------------------------ |
| **Name**   | Unique within the namespace. How you identify it later |
| **Role**   | `administrator`, `operator`, or `observer`             |
| **Expiry** | 30, 60 or 90 days, a year, or never                    |

Name keys for what they do: `ci-deploy`, `inventory-sync`, not `key1`. The key’s own routes address it by name. An access policy and an SSH identity refer to it by the id creation returns, so renaming a key breaks neither.

A key is scoped to the one namespace it was created in. It reaches nothing in another namespace, and it is not accepted on the instance administration surface under `/admin/api`, which takes a credential of its own.

> **Careful**
>
> A key’s role must be **equal to or less than the role of whoever created it**. An operator cannot mint an administrator key. That constraint is what stops key creation from being a privilege escalation path.

Give a key the least role that does the job, and set an expiry where you can. Most integrations only read, so an observer key is enough, and a key with a deadline stops mattering on its own if somebody forgets it exists. See [User Roles](https://docs.shellhub.io/manage/team/user-roles).

## What a key cannot do

Some routes refuse an API key whatever its role, because they act for a person:

- Creating, listing, updating or deleting API keys, its own included
- Managing provisioning keys
- Generating an invitation link, or accepting an invitation
- Creating, listing or leaving namespaces
- Changing a user’s own profile or password, and anything to do with MFA or the vault
- Approving an SSH login or pairing a device

The refusal is structural, not a permission you can grant.

## A key over SSH

An API key’s token never opens an SSH session, and an SSH identity never calls the API. See [SSH Access](https://docs.shellhub.io/api/ssh-access) for how the key connects over SSH, enrolling its public key and writing the policy that grants it.

## Rotating a key

Create the new key, move consumers to it, then delete the old one. Name them so you can tell them apart mid-rotation, `ci-deploy-2026` rather than editing in place. Deleting takes effect immediately, and anything still using the old key starts getting `401`.

## When a credential is refused

A protected route answers `401` whether the key does not exist, has expired, or was never valid, and does not tell an unauthenticated caller which of those it was. A `401` means “this credential is not usable”, not “this credential does not exist”.

## Paginating a response

List routes take `page` and `per_page`, and answer the total in the `X-Total-Count` header:

```bash
$ curl -H "X-API-Key: $KEY" \
    "https://cloud.shellhub.io/api/devices?page=1&per_page=100"
```

## The specification

[API Reference](https://docs.shellhub.io/api/reference) renders it in the browser. The files are the same thing, to diff or to hand to a generator:

| Edition        | Specification                                                                       |
| -------------- | ----------------------------------------------------------------------------------- |
| **Community**  | [community-openapi.yaml](https://docs.shellhub.io/openapi/community-openapi.yaml)   |
| **Cloud**      | [cloud-openapi.yaml](https://docs.shellhub.io/openapi/cloud-openapi.yaml)           |
| **Enterprise** | [enterprise-openapi.yaml](https://docs.shellhub.io/openapi/enterprise-openapi.yaml) |

**Take the one matching your edition.** Editions expose different routes: a Community server has no MFA, firewall rule, or web endpoint routes at all, so a client generated from the wrong file produces methods that answer 404. That is not a permission problem, because the route is not there. See [Editions](https://docs.shellhub.io/about/editions).

## Where to go next

- [API Reference](https://docs.shellhub.io/api/reference) — Every route, per edition.
- [MCP Server](https://docs.shellhub.io/api/mcp) — The same API, for AI agents.
- [SSH Access](https://docs.shellhub.io/api/ssh-access) — The key’s way in to devices.
