---
title: "Create a personal access token"
method: POST
path: "/personal-access-tokens"
tags: ["Personal Access Tokens"]
---

# Create a personal access token

`POST /personal-access-tokens`

Mints a new personal access token for the caller. Deliberately not
admin-gated — any authenticated org member may create their own,
unlike OAuth app registration.

The token is minted against a lazily-created, per-org synthetic
OAuth app (`clientId: pat-system:<orgId>`) shared by every PAT in
that org — the same signing, hashing, and revocation machinery as
`/oauth2/token`, reused rather than duplicated.

`scopes` is validated against the org's configured `MCP_SCOPES`
env var, not the full role-aware OAuth-app scope catalog — a
non-admin can request any scope in that set.

The response's `accessToken` is shown **once**; only its SHA-256
hash is stored. It's prefixed `phpat_` (see the `bearerAuth`
security scheme).

## Request body

- CreatePatRequest — Request to create a personal access token (`createPatTokenSchema` in `pat.validators.ts`).
  - `name` string, required — Label to help you recognize the token later
  - `scopes` string[] — Scopes to grant, validated against the org's configured `MCP_SCOPES` (not the full role-aware OAuth-app scope set). Defaults to every scope in `MCP_SCOPES` if omitted.
  - `expiryDays` union — Token lifetime. Defaults to `30` if omitted — a token minted without an explicit choice shouldn't default to the longest lifetime. `"never"` is stored as a ~100-year expiry (the underlying schema field is required and TTL-indexed, so there's no literal null option).
    - 30 | 90 | 365
    - 'never'

## Response `201`

Personal access token created successfully

- CreatePatResponse — Response body for `POST /personal-access-tokens` (`pat.controller.ts` `createToken`).
  - `message` string, required
  - `token` PatWithSecret, required — A personal access token as seen by its own creator (one element of `GET /personal-access-tokens`'s `tokens` array, or the `token` object returned by `POST /personal-access-tokens` before `accessToken` is added).
    - `id` string, required — Token ID
    - `name` string, required — Token name
    - `scopes` string[], required — Granted scopes
    - `createdAt` string, date-time, required
    - `expiresAt` string, date-time, required — Expiry timestamp. A `"never"`-expiry token is stored as a ~100-year-out date, not a literal null — treat anything decades out as "never" rather than a real deadline.
    - `lastUsedAt` string, date-time — Last time this token successfully authenticated a request. Throttled server-side to update at most once per 5 minutes per token; absent if the token has never been used.
    - `accessToken` string, required — The raw token, `phpat_`-prefixed. Returned **only** in this creation response — only its hash is stored server-side, so it cannot be retrieved again later.

## Other responses

- `400` — Invalid request (validation error, or a requested scope isn't in the org's configured `MCP_SCOPES`)
- `401` — Unauthorized
- `429` — Rate limit exceeded

---

[API](https://skmtc.net/pipeshub-ai/apis/pipeshub-api.md) · [All operations](https://skmtc.net/pipeshub-ai/apis/pipeshub-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/pipeshub-ai/pipeshub-api/revisions/64bfe76b130e/schema)
