API tokens

Long-lived tokens used by CLI tools, MCP clients, and any non-browser integration. The op_ prefix is what makes the server recognize them on the Authorization header.

How they're stored. The full token (e.g. op_GgFnE3R8…) is shown only at creation time. The server stores the SHA-256 hash plus a 12-character display prefix, so administrators can identify a token without being able to recover it.

POST /api/tokens

Create a token. Authenticate with a JWT (cookie or bearer) — you cannot mint new tokens using another API token's session because the dashboard is the issuance surface, but in practice get_current_user will also accept an op_ token here.

Request

POST /api/tokens
Content-Type: application/json
Authorization: Bearer eyJhbGc...

{ "name": "Laptop CLI" }

Response 200

{
  "id": 7,
  "name": "Laptop CLI",
  "token_prefix": "op_GgFnE3R8",
  "status": "active",
  "created_at": "2026-05-03T17:30:00",
  "last_used_at": null,
  "token": "op_GgFnE3R8X4kP9bH...A2"
}
Save this token now. The token field is returned only on creation. Subsequent GET /api/tokens responses include the prefix only.

cURL

curl -X POST https://outpost.click/api/tokens \
  -H "Authorization: Bearer eyJhbGc..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Laptop CLI"}'

Validation

  • name — 1 to 100 characters.

GET /api/tokens

List all tokens for the current user, newest first. The full token value is never returned — only the display prefix.

Response 200

[
  {
    "id": 7,
    "name": "Laptop CLI",
    "token_prefix": "op_GgFnE3R8",
    "status": "active",
    "created_at": "2026-05-03T17:30:00",
    "last_used_at": "2026-05-03T18:12:55"
  },
  {
    "id": 4,
    "name": "Old token",
    "token_prefix": "op_8Hk2ksv1",
    "status": "active",
    "created_at": "2026-04-12T11:01:22",
    "last_used_at": null
  }
]

cURL

curl https://outpost.click/api/tokens \
  -H "Authorization: Bearer eyJhbGc..."
last_used_at is updated on every successful authentication via the token, so you can see at a glance which tokens are still in active use.

PUT /api/tokens/{id}

Rename a token. Only the name can be modified; the secret itself is immutable (delete and recreate to rotate).

Request

PUT /api/tokens/7
Content-Type: application/json
Authorization: Bearer eyJhbGc...

{ "name": "Laptop CLI (mac mini)" }

Response 200

{
  "id": 7,
  "name": "Laptop CLI (mac mini)",
  "token_prefix": "op_GgFnE3R8",
  "status": "active",
  "created_at": "2026-05-03T17:30:00",
  "last_used_at": "2026-05-03T18:12:55"
}

cURL

curl -X PUT https://outpost.click/api/tokens/7 \
  -H "Authorization: Bearer eyJhbGc..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Laptop CLI (mac mini)"}'

Errors

  • 404 Token not found (also returned when the token belongs to another user).

DELETE /api/tokens/{id}

Permanently revoke a token. After deletion any request bearing it will get 401 Invalid API token.

curl -X DELETE https://outpost.click/api/tokens/7 \
  -H "Authorization: Bearer eyJhbGc..."

200 OK
{ "deleted": 7 }

Token format

Tokens are generated by:

op_ + secrets.token_urlsafe(32)

which yields ~43 base64url characters of entropy after the prefix. The storage shape (in api_tokens):

ColumnDescription
token_hashSHA-256 of the full token, hex.
token_prefixFirst 12 characters of the full token (e.g. op_GgFnE3R8).
statusCurrently always "active"; only active tokens are accepted on auth.
last_used_atUpdated on every successful auth via the token.

Using an API token

Every endpoint that accepts JWTs also accepts an API token in the same Authorization: Bearer header.

Bash function

outpost() {
  curl -sS -H "Authorization: Bearer $OUTPOST_TOKEN" "$@"
}

outpost https://outpost.click/auth/me
outpost https://outpost.click/pages

Python helper

import os, requests

BASE = "https://outpost.click"
SESSION = requests.Session()
SESSION.headers["Authorization"] = f"Bearer {os.environ['OUTPOST_TOKEN']}"

def list_pages():
    r = SESSION.get(f"{BASE}/pages")
    r.raise_for_status()
    return r.json()

def upload(name, zip_path, visibility="public"):
    with open(zip_path, "rb") as fh:
        r = SESSION.post(
            f"{BASE}/api/pages",
            data={"name": name, "visibility": visibility},
            files={"file": fh},
        )
    r.raise_for_status()
    return r.json()

Node.js (fetch)

import fs from "node:fs";
import FormData from "form-data";
import fetch from "node-fetch";

const TOKEN = process.env.OUTPOST_TOKEN;

const fd = new FormData();
fd.append("name", "My Site");
fd.append("visibility", "public");
fd.append("file", fs.createReadStream("./site.zip"));

const res = await fetch("https://outpost.click/api/pages", {
  method: "POST",
  headers: { Authorization: `Bearer ${TOKEN}` },
  body: fd,
});
console.log(await res.json());