---
title: "API"
description: "Create and scope API keys, call the REST API, use the Tailscale-compatible API with OAuth clients, Terraform and Kubernetes, and run the CLI remotely."
---

Slopscale provides a [HTTP REST API](#rest-api) which drives the built-in [admin console](/slopscale/ref/console), may be used to
[remote control Slopscale](#remote-control) or provide a base for custom
integration and tooling.

The API requires a valid API key before use. To create an API key, log into your Slopscale server and generate
one with the default expiration of 90 days:

```shell
slopscale apikeys create
```

Copy the output of the command and save it for later. An API key cannot be retrieved again. If the API
key is lost, expire the old one, and create a new one.

A key created this way is all-access. To hand out less, create the key for a user, so it is bounded by the user's
[role](/slopscale/ref/roles):

```shell
slopscale apikeys create --user <USER>
```

A key can also be limited to some operations with scopes, the same vocabulary the OAuth clients use, and carry a
description saying what it is for. The scopes never reach past the caller that mints the key or the role of the
user that owns it: a scope the caller cannot delegate is dropped, and a caller that can delegate none of them is
refused. A key without scopes keeps its owner's whole role, unless the caller is itself a scoped key, in which
case the new key inherits the caller's scopes. An OAuth access token cannot mint API keys at all: a token is
bounded by tags as well as scopes, and an API key has no tags to be bounded by.

```shell
slopscale apikeys create --user <USER> --scope dns --scope devices:core:read --description "Resolver sync"
```

The console's _Keys_ page offers the same when creating an API key, and lists each key's scopes and description.

To list the API keys currently associated with the server:

```shell
slopscale apikeys list
```

and to expire an API key:

```shell
slopscale apikeys expire --prefix <PREFIX>
```

A key can be rotated instead of replaced. Rotating mints a new secret for the
same key and prints it once; the key keeps its id, owner, scopes, description
and expiry, and the old secret stops working immediately:

```shell
slopscale apikeys rotate --prefix <PREFIX>
```

Pass `--expiration` to give the rotated key a new expiry; without it the key
keeps the one it has. An expired key cannot be rotated, because expiring a key
is how it is revoked, so create a new key instead. A key that carries its own
scopes may only rotate a key whose scopes it could have minted itself, so
rotation never widens a credential. The console's _Keys_ page offers the same
action, and every rotation is recorded in the audit log as `apikey.rotate`,
naming the prefix the key had and the one it now answers to.

## REST API

- API endpoint: `/api/v1`, e.g. `https://slopscale.example.com/api/v1`
- Documentation: `/api/v1/docs`, e.g. `https://slopscale.example.com/api/v1/docs`
- Slopscale Version: `/version`, e.g. `https://slopscale.example.com/version`
- Authenticate using HTTP Bearer authentication by sending the [API key](/slopscale/ref/api) with the HTTP `Authorization: Bearer <API_KEY>` header.
- The OpenAPI 3.1 document is at `/api/v1/openapi.yaml`.
- Errors are [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807) problem details, `application/problem+json`, with
  `status`, `title` and `detail`, and the usual meaning of the status: `400` for malformed input, `401` and `403`
  for a missing or insufficient credential, `404` for an unknown user or node, `409` for a duplicate name, `412`
  for a stale `If-Match`. An unexpected failure answers `500` with an id that finds it in the server log.

Start by [creating an API key](/slopscale/ref/api) and test it with the examples below. Read the API documentation provided by your
Slopscale server at `/api/v1/docs` for details.

**Get details for all users**

```console
curl -H "Authorization: Bearer <API_KEY>" \
    https://slopscale.example.com/api/v1/user
```

**Get details for user 'bob'**

```console
curl -H "Authorization: Bearer <API_KEY>" \
    https://slopscale.example.com/api/v1/user?name=bob
```

**Register a node**

```console
curl -H "Authorization: Bearer <API_KEY>" \
    --json '{"user": "<USER>", "authId": "<AUTH_ID>"}' \
    https://slopscale.example.com/api/v1/auth/register
```

## Tailscale-compatible API

Alongside `/api/v1`, Slopscale serves a second API at `/api/v2` that reuses
**Tailscale's** wire shapes, so tooling written against Tailscale's control
plane works against Slopscale unchanged: the
[Terraform/OpenTofu provider](https://registry.terraform.io/providers/tailscale/tailscale/latest),
[tscli](https://github.com/jaxxstorm/tscli), the official
[Go client](https://pkg.go.dev/tailscale.com/client/tailscale/v2) and the
[Kubernetes operator](https://tailscale.com/kb/1236/kubernetes-operator).

- API endpoint: `/api/v2`, e.g. `https://slopscale.example.com/api/v2`
- Documentation: `/api/v2/docs`
- The tailnet in every path is `-` or the tailnet ID; Slopscale serves one
  tailnet, and any other name is a `404`. The tailnet ID is made on the
  server's first start and never changes. It has the shape of a Tailscale
  tailnet ID, such as `T4bXk29QaZmCNTRL`. It is shown on the console's
  _Settings › Tailnet_ page and as `tailnetId` in `GET /api/v1/server`. A
  machine whose client knows the field (capability version 148 and later)
  reports it as `CurrentTailnet.StableID` in `tailscale status --json`.
- Authenticate with an [API key](#rest-api), which is all-access, or with
  [OAuth 2.0 client credentials](#oauth-clients), which are limited to the
  scopes and tags they were created with. Point the Terraform provider at
  Slopscale with its `base_url`, and the operator with its
  `LOGIN_SERVER`/`--login-server`.

### OAuth clients

Most of the ecosystem accepts an API key or OAuth client credentials; the
Kubernetes operator accepts only the latter. An OAuth client is a key with
`keyType: "client"`, created with `slopscale oauth-clients create`, on the
console's _Keys_ page, or through the API. Its secret is shown once. The client
exchanges it for a one-hour access token at `POST /api/v2/oauth/token`, and the
token carries the client's scopes and tags:

```console
$ slopscale oauth-clients create --scope devices:core --scope auth_keys \
    --tag tag:k8s-operator --description "Kubernetes operator"
```

The scopes bound which operations the token may perform, and the tags bound
which machines it may create: an auth key minted with the token may only carry
tags the token holds, or tags those own through the policy's `tagOwners`. A
token cannot mint API keys, and it can never be given more than the caller that
created the client could delegate.

### Join nodes with an OAuth client

The Tailscale client can use an OAuth client secret in place of an auth key: it
exchanges the secret for an access token, mints a single-use tagged auth key and
registers with it. This works anywhere the client takes an auth key:
`tailscale up`, the container image, `tsnet` and the
[`tailscale/github-action`](https://github.com/tailscale/github-action). One
long-lived secret joins any number of nodes.

Create an OAuth client with the `auth_keys` scope and the tags its nodes get.
The secret is shown once:

```shell
slopscale oauth-clients create --scope auth_keys --tag tag:ci
```

Build the auth key from the secret:

1. Swap the prefix: `hskey-client-…` becomes `tskey-client-…`. The client only
   runs the exchange for `tskey-client-`; Slopscale accepts both.
1. Append `?baseURL=<your Slopscale URL>`.
1. Set `--advertise-tags`. Each tag must exist in the policy's `tagOwners` and be
   one of the OAuth client's tags, or owned by one of them.

```text
tskey-client-<id>-<secret>?baseURL=https://slopscale.example.com
```

:::warning[Always set baseURL]

Without `baseURL` the client sends the secret to `https://api.tailscale.com`.
:::

These are all the attributes the client understands; any other is an error.
Order does not matter and an empty value means the default.

| Attribute       | Default                     | Effect                                                                                                                         |
| --------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `baseURL`       | `https://api.tailscale.com` | Where the exchange and key creation go. Your Slopscale URL, no trailing slash.                                                  |
| `ephemeral`     | `true`                      | The node is removed after it goes offline (`node.ephemeral.inactivity_timeout`). `false` keeps it.                              |
| `preauthorized` | `false`                     | While [device approval](/slopscale/ref/approval) is on, a node joined with `false` waits for approval and one joined with `true` does not. |

Booleans take any Go `strconv.ParseBool` value (`true`, `false`, `1`, `0`, …).

`tailscale up`, either as the auth key or through `--client-secret` (which also
takes `file:/path/to/secret`):

```shell
tailscale up --login-server https://slopscale.example.com --advertise-tags tag:ci \
  --auth-key 'tskey-client-<id>-<secret>?baseURL=https://slopscale.example.com&ephemeral=false'
```

The container image:

```shell
docker run -d --name tailscale \
  -e TS_AUTHKEY='tskey-client-<id>-<secret>?baseURL=https://slopscale.example.com' \
  -e TS_EXTRA_ARGS='--login-server=https://slopscale.example.com --advertise-tags=tag:ci' \
  tailscale/tailscale
```

`tsnet` (`TS_CLIENT_SECRET` works too), with `tailscale.com/feature/oauthkey`
imported:

```go
srv := &tsnet.Server{
    ControlURL:    "https://slopscale.example.com",
    AuthKey:       "tskey-client-<id>-<secret>?baseURL=https://slopscale.example.com",
    AdvertiseTags: []string{"tag:ci"},
}
```

The GitHub Action, with the whole string stored as a repository secret:

```yaml
- uses: tailscale/github-action@v4
  with:
    authkey: ${{ secrets.SLOPSCALE_AUTHKEY }}
    args: --login-server=https://slopscale.example.com --advertise-tags=tag:ci
```

Use `authkey` even though the action marks it deprecated: `oauth-secret`
appends its own `?…` to the secret, which corrupts `baseURL`.

### Terraform provider support

Every resource and data source the provider offers, and how far Slopscale
carries it.

| Resource                          | Support     | Notes                                                                                                                                                                         |
| --------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tailscale_acl`                   | Supported   | Reads and writes the policy as HuJSON, comments and all, with an `ETag` for the conditional write. The plan-time validation call is supported, including running policy tests. |
| `tailscale_aws_external_id`       | Unsupported | Slopscale has no AWS log integration, so there is no external ID to hand out. Returns `404`.                                                                                    |
| `tailscale_contacts`              | Unsupported | Slopscale sends no account, support or security email of its own, so it stores no contact addresses. Returns `404`.                                                             |
| `tailscale_device_authorization`  | Supported   | Maps onto [device approval](/slopscale/ref/approval). Note that approval must be switched on for the resource to mean anything.                                                          |
| `tailscale_device_key`            | Supported   | Sets whether a machine's key expires.                                                                                                                                          |
| `tailscale_device_subnet_routes`  | Supported   | Approves the routes a machine advertises.                                                                                                                                      |
| `tailscale_device_tags`           | Supported   | Tags and users are exclusive here, so tagging a user's machine hands it over to the tags.                                                                                       |
| `tailscale_dns_configuration`     | Partial     | Every field is applied, including `useWithExitNode` and `overrideLocalDNS`. `magicDNS` comes from the configuration file, so a plan that changes it is refused with a `400`.    |
| `tailscale_dns_nameservers`       | Supported   |                                                                                                                                                                                |
| `tailscale_dns_preferences`       | Partial     | The only preference is `magicDNS`, which is set in the configuration file; a plan that repeats the value in force succeeds, one that changes it is refused.                     |
| `tailscale_dns_search_paths`      | Supported   |                                                                                                                                                                                |
| `tailscale_dns_split_nameservers` | Supported   |                                                                                                                                                                                |
| `tailscale_federated_identity`    | Supported   | See [workload identity federation](#workload-identity-federation).                                                                                                             |
| `tailscale_logstream_configuration` | Partial   | `configuration` streams the [audit log](/slopscale/ref/audit). `network` is refused with `404`: network flow logs are not streamed, they are read from `GET /api/v2/tailnet/-/logging/network` (see the [traffic monitor](/slopscale/ref/traffic#tailscale-compatible-api)). Destinations are `http`, `splunk`, `elastic`, `datadog`, `axiom` and `loki`; S3 and Cloud Storage are refused by name. |
| `tailscale_oauth_client`          | Supported   | Create, read, update in place and revoke.                                                                                                                                      |
| `tailscale_posture_integration`   | Partial     | `falcon`, `sentinelone`, `intune`, `jamfpro`, `kandji` and `kolide` are wired to Slopscale's [posture integrations](/slopscale/ref/device-trust). `fleet` and `huntress` are refused by name. |
| `tailscale_service`               | Supported   | Maps onto [Tailscale Services](/slopscale/ref/services).                                                                                                                                 |
| `tailscale_tailnet_key`           | Supported   | Slopscale's pre-auth keys. `DELETE` revokes; the key stays readable as `invalid` until it is reaped.                                                                            |
| `tailscale_tailnet_settings`      | Partial     | `devicesApprovalOn`, `usersApprovalOn`, `postureIdentityCollectionOn` and `devicesKeyDurationDays` can be changed. The rest are read-only here because they come from the configuration file or do not exist. |
| `tailscale_webhook`               | Supported   | See [Webhooks](/slopscale/ref/webhooks).                                                                                                                                                 |

| Data source         | Support   | Notes                                                                                     |
| ------------------- | --------- | ----------------------------------------------------------------------------------------- |
| `tailscale_4via6`   | Supported | Computed by the provider itself with no server call, so it works against anything.        |
| `tailscale_acl`     | Supported |                                                                                            |
| `tailscale_device`  | Supported | Reading a device with all fields adds its distribution, connectivity, tailnet lock key and collected serial numbers. |
| `tailscale_devices` | Supported |                                                                                            |
| `tailscale_service` | Supported |                                                                                            |
| `tailscale_user`    | Supported |                                                                                            |
| `tailscale_users`   | Supported |                                                                                            |

### Kubernetes operator

The operator authenticates only with OAuth client credentials, so create a
client with the `auth_keys`, `devices:core` and `services` scopes and the tag
the operator runs as, and let that tag own the tag its proxies use:

```json
{
  "tagOwners": {
    "tag:k8s-operator": ["alice@"],
    "tag:k8s": ["tag:k8s-operator"]
  }
}
```

The operator then mints its own auth keys for the proxies it creates, registers
a Service per ingress and deletes a proxy's device when the workload goes away.
All of it is covered by an end-to-end test in Slopscale's own suite.

### Workload identity federation

A CI job or cloud workload can trade the OIDC token its own platform signs for
a Slopscale access token, so no long-lived secret has to live in a pipeline.
Register the workload as a key with `keyType: "federated"`, naming the `issuer`,
`audience` and `subject` its tokens carry, optionally with `customClaimRules`
it must also satisfy, and the `scopes` and `tags` it should get:

```console
curl -H "Authorization: Bearer <API_KEY>" \
    --json '{"keyType": "federated", "scopes": ["devices:core"], "tags": ["tag:ci"],
             "issuer": "https://token.actions.githubusercontent.com",
             "audience": "slopscale", "subject": "repo:acme/app:ref:refs/heads/main",
             "customClaimRules": {"repository": "acme/app"}}' \
    https://slopscale.example.com/api/v2/tailnet/-/keys
```

The workload exchanges its token for a one-hour access token with exactly that
grant:

```console
curl --data-urlencode "client_id=<CLIENT_ID>" --data-urlencode "jwt=$ACTIONS_ID_TOKEN" \
    https://slopscale.example.com/api/v2/oauth/token-exchange
```

The presented token is checked against the issuer's published keys and against
every condition the identity carries, with a minute of clock leeway. Every
exchange is recorded in the [audit log](/slopscale/ref/audit) as `oauth.token.exchange`,
refusals included, so a run of failures against one identity is visible.

The console and the CLI manage identities next to OAuth clients, so neither
needs the Tailscale-compatible API. The console's _OAuth clients_ page creates
one and edits its trust conditions in place, and so does the CLI.

```console
$ slopscale oauth-clients create --federated --scope devices:core --tag tag:ci \
    --issuer https://token.actions.githubusercontent.com --audience slopscale \
    --subject "repo:acme/app:ref:refs/heads/main" --claim repository=acme/app

$ slopscale oauth-clients update --id <CLIENT_ID> \
    --subject "repo:acme/app:ref:refs/heads/release"
```

`slopscale oauth-clients list` shows both kinds with the subject each identity
trusts. On `/api/v1` the same operations are `POST /api/v1/oauth-client` with
`"keyType": "federated"`, `PATCH /api/v1/oauth-client/{clientId}`, where a
field left out keeps its value and a trust condition sent for a plain client is
refused, and `DELETE /api/v1/oauth-client/{clientId}` to revoke.

## Remote control

The `slopscale` binary can control a Slopscale instance from a remote machine over the HTTP API.

### Prerequisite

- A workstation to run `slopscale` (any supported platform, e.g. Linux).
- The Slopscale server reachable over HTTP(S).
- An [API key](/slopscale/ref/api) to authenticate with the Slopscale server.

### Setup remote control

1. Download the [`slopscale` binary from GitHub's release page](https://github.com/aislopware/slopscale/releases). Make
   sure to use the same version as on the server.

1. Put the binary somewhere in your `PATH`, e.g. `/usr/local/bin/slopscale`

1. Make `slopscale` executable: `chmod +x /usr/local/bin/slopscale`

1. [Create an API key](/slopscale/ref/api) on the Slopscale server.

1. Provide the connection parameters for the remote Slopscale server either via a minimal YAML configuration file or
   via environment variables:

    **Minimal YAML configuration file**

    ```yaml title="config.yaml"
    cli:
        address: <SLOPSCALE_URL>
        api_key: <API_KEY>
    ```

    **Environment variables**

    ```shell
    export SLOPSCALE_CLI_ADDRESS="<SLOPSCALE_URL>"
    export SLOPSCALE_CLI_API_KEY="<API_KEY>"
    ```

    This instructs the `slopscale` binary to connect to a remote instance at `<SLOPSCALE_URL>` (e.g.
    `https://slopscale.example.com`), instead of connecting to the local instance. A bare host without a scheme is
    assumed to be `https`.

1. Test the connection by listing all nodes:

    ```shell
    slopscale nodes list
    ```

    You should now be able to see a list of your nodes from your workstation, and you can
    now control the Slopscale server from your workstation.

### Behind a proxy

The remote CLI uses the same HTTP API as everything else, so it works through the reverse proxy already in front of
Slopscale with no extra setup.

### Troubleshooting

- Make sure you have the _same_ Slopscale version on your server and workstation.
- Verify that your TLS certificate is valid and trusted.
- If you don't have access to a trusted certificate (e.g. from Let's Encrypt), either:
    - Add your self-signed certificate to the trust store of your OS _or_
    - Disable certificate verification by either setting `cli.insecure: true` in the configuration file or by setting
      `SLOPSCALE_CLI_INSECURE=1` via an environment variable. We do **not** recommend to disable certificate validation.
