# API tokens for CI and automation

> Give each pipeline, cron job and monitor its own VPSNine token with the smallest scope, an address limit where it fits, an expiry, and a plan for rotating it.

Source: https://vpsnine.com/help/api-tokens-for-automation · Updated: 2026-10-21

For automation, make one VPSNine API token per job, with read-only scope unless the job changes something, an expiry, and an address limit when the job runs from fixed addresses. Store it in your CI's secret store, never in the repository, and replace it before it expires.

This page is about choosing those settings. [Create and use API tokens](https://vpsnine.com/help/api-tokens) covers the form itself, step by step.

## What you need

- A VPSNine account with a [confirmed email address](https://vpsnine.com/help/verify-your-email).
- Your account password (the token form asks for it).
- Somewhere to keep secrets: GitHub Actions secrets, GitLab CI/CD variables, a password manager, or a file only one user can read.

## Pick the scope for each job

A token can do anything its scope allows, as you. Match the scope to what the job does:

| Job | Scope | Why |
| --- | --- | --- |
| Monitoring, dashboards, an [Ansible inventory](https://vpsnine.com/tutorials/manage-vpsnine-servers-with-ansible) | read only | It only lists servers, invoices or snapshots. |
| [Deploy on push](https://vpsnine.com/tutorials/deploy-on-push-github-actions-vpsnine-api) with a snapshot first | read and write | Taking a snapshot changes something. |
| [Preview servers per pull request](https://vpsnine.com/tutorials/preview-servers-per-pull-request) | read and write | It orders and deletes servers. |
| The [vps9 CLI](https://vpsnine.com/help/install-the-vps9-cli) on your laptop | read and write | You'll use it for everything. |

There is no per-server scope yet: a read-and-write token can reboot or delete any server on the account. Keep those tokens to jobs you trust, and give everything else read-only.

On a [team account](https://vpsnine.com/help/teams), a token also never does more than its maker's role. A token made by a Technical member can't touch billing, whatever its scope, and if that person's role is lowered, their tokens follow at once. For a shared pipeline, make the token from a login whose role fits the job.

## Limit where it works from

**Only from these addresses** takes one address or range per line, IPv4 or IPv6:

```text
198.51.100.0/24
2001:db8:4::/48
```

From anywhere else the API answers `403 ip_not_allowed` and names the address it saw. Use it when the job runs from addresses you know: a self-hosted runner, one of your own servers, an office network. List both the IPv4 and the IPv6 range if the machine has both, because it may reach us over either.

Don't use it for GitHub-hosted runners or other shared CI machines: their addresses change from run to run and belong to the provider, so a limit either breaks the job or allows everyone on that provider. For those, rely on the secret store, a short expiry and a narrow scope instead.

## Set an expiry, and rotate before it

Pick 30 days, 90 days (the default) or a year. "never" is there for the rare job you can't touch, and it's the token most likely to outlive the person who made it.

To replace a token without downtime:

1. Make the new token with the same settings and a name that says which one it is, like `github-actions deploy 2027-01`.
2. Put it in the CI secret store in place of the old one.
3. Run the job once, or wait for its next run, and check that **last used** on the new token has a time.
4. Revoke the old token.

We email you when a token is created, so a token you didn't make shows up in your inbox.

## Store it as a secret

In **GitHub Actions**, add it under the repository's **Settings > Secrets and variables > Actions** as `VPS9_TOKEN`, and pass it to only the steps that need it:

```yaml
- name: Snapshot the server first
  env:
    VPS9_TOKEN: ${{ secrets.VPS9_TOKEN }}
  run: scripts/snapshot.sh web-1
```

Secrets aren't given to workflows triggered from forks, which is what you want for a token that can change servers.

In **GitLab CI**, add a CI/CD variable called `VPS9_TOKEN`, masked and protected so only protected branches get it.

For a **cron job** on a server, keep the token in a file only that user can read, and read it at run time:

```bash
install -m 600 /dev/null ~/.vps9-token
nano ~/.vps9-token
export VPS9_TOKEN="$(cat ~/.vps9-token)"
```

The vps9 CLI and the [TypeScript SDK](https://vpsnine.com/tutorials/script-vpsnine-with-the-typescript-sdk) both read `VPS9_TOKEN` from the environment, so nothing else needs the token.

## Check it worked

Run this from the machine that will use the token. It shows the account and your role, and fails with the error code if the scope or address limit is wrong:

```bash
curl -sS --fail-with-body -H "Authorization: Bearer $VPS9_TOKEN" https://my.vpsnine.com/api/v1/me
```

In the panel, **last used** on the token now has a time.

## Troubleshooting

**`403 ip_not_allowed`: "This token only works from 198.51.100.0/24; this request came from 203.0.113.7."** The job reached us from the address in the message. If that address is yours, add it (or its range) to a new token. If it changes from run to run, the job shouldn't use an address limit.

**`401 unauthorized` in CI but not on your machine.** The secret usually has a trailing newline or quotes pasted in, or the step doesn't get the secret (a fork's pull request, or a job that doesn't set `env`). Print its length, never its value: `echo "${#VPS9_TOKEN}"` should say 57.

**`429 rate_limited` from parallel jobs.** All tokens on an account share 120 requests a minute. See [Rate limits and request ids](https://vpsnine.com/help/api-rate-limits-and-request-ids).

## Doing it on VPSNine

The jobs these tokens drive run on your own servers; [compare the plans](https://vpsnine.com/plans) for build boxes and runners. The [developer hub](https://vpsnine.com/help/developers) links everything else for automation.
