API Reference
Authentication

Authentication

The PurpletGo API supports two authentication methods: API keys (for server-to-server integrations - this is what the rest of this reference assumes) and session cookies (used by the web app itself).


API keys

Generating an API key

  1. Go to Settings → API Keys
  2. Click Generate New Key
  3. Give the key a name and select its scopes
  4. Copy the key immediately - it is only shown once, in the response of the create call
⚠️

The full key value is only ever returned at creation time. PurpletGo doesn't store it in plaintext and can't show it to you again - only a short prefix is kept for identification. If you lose it, rotate the key (see below) or revoke it and generate a new one.

How keys are stored

PurpletGo never stores the key itself. What is stored is an HMAC-SHA256 of the key under a server-side pepper - a secret that lives only in the server's environment, not in the database - plus the first 12 characters (pgo_live_a1b) so the key can be recognised in the dashboard. A copy of the database on its own is therefore not enough to check whether a guessed or leaked key is valid. Keys stored before the pepper was introduced are moved under it the first time they are used; nothing changes on your side.

Authenticating requests

Include the API key in the Authorization header as a Bearer token:

Authorization: Bearer pgo_live_••••••••••••••••••••••••••••••••

Example:

curl https://app.purpletgo.com/api/v1/employees \
  -H "Authorization: Bearer pgo_live_••••••••••••••••••••••••••••••••"

Key format

Keys are generated as pgo_live_ followed by 32 hex characters. There is no separate test/sandbox key type today - every key is a live, production-capable credential, so treat every generated key with the same care as a production secret.

Keys created before the rename to PurpletGo start with fnl_live_ instead. They are still valid and need no action: the prefix is part of the key itself, so an existing key keeps working exactly as issued, and only newly created keys carry pgo_live_.

A request with no Authorization: Bearer header gets 401 with Missing or invalid Authorization header. Requests with a malformed key, or a key that doesn't match a stored hash, get back 401 Unauthorized with a generic Invalid or unauthorized API key message - PurpletGo deliberately doesn't distinguish "key not found" from "key revoked" from "key expired" in the error text, to avoid leaking which keys exist.


Scopes

Each API key is created with one or more scopes that limit what it can do:

ScopeDescription
offboardings:readList and view offboardings
offboardings:writeCreate offboardings and update their status
employees:readList employees
webhooks:readList registered webhooks
webhooks:writeCreate and delete webhooks. Subscribing to an event also needs the scope that reads its data: employees:read for asset.* events, offboardings:read for the rest

A request to an endpoint whose scope isn't on the key gets a 403 Forbidden naming the missing scope.


Rotating keys

Rotating a key issues a new secret for the same key: its name, scopes, expiry and usage history stay as they are, only the value you send in the Authorization header changes. To rotate: Settings → API Keys → Rotate next to the key, or call POST /api/keys/{keyId}/rotate. The new secret is shown once, like a newly created key.

When you rotate you choose a grace period of none, 1 hour, 24 hours or 7 days (the default). During it both the old and the new secret authenticate, so you can deploy the new one without an outage; when it ends, the old secret gets the same generic 401 as any invalid key. With no grace period the old secret stops working the moment the rotation is confirmed. Rotating again during a grace period replaces the previous secret, so at most two secrets ever work for one key. Revoking a key ends both at once.

Rotate a key whenever its secret may have been exposed, and on a schedule if your security policy asks for one - there is no need to create a new key and re-point integrations at it.

Revoking keys

To revoke a key: Settings → API Keys → click Revoke next to the key.

Revoked keys are immediately invalid, including a previous secret still inside its rotation grace period. Any in-flight requests using a revoked key will receive 401 Unauthorized. Revocation can't be undone - you'll need to generate a new key.


Session cookies (web app)

The PurpletGo web app authenticates with a JWT stored in an HttpOnly access_token cookie, set when you log in. This is managed automatically by the app - you don't need to handle it manually, and it isn't the recommended way to integrate with PurpletGo from your own systems (use an API key instead).

If you're building a browser-based tool and need to authenticate the same way the web app does, you can log in via POST /api/auth/login below and the cookie will be set on the response. The same endpoints will also accept the token as a Bearer header if you'd rather manage it yourself.


Auth endpoints

Login

POST /api/auth/login

Authenticate with email and password. On success, sets the access_token session cookie and returns the user in the response body - there's no token in the JSON response, it only ever travels in the cookie.

Request body

{
  "email": "admin@acme.com",
  "password": "••••••••"
}

Response 200 OK

{
  "user": {
    "id": "usr_abc123",
    "email": "admin@acme.com",
    "fullName": "Jane Smith",
    "orgId": "org_xyz",
    "role": "admin"
  }
}

If the account has two-factor authentication enabled, login instead returns a short-lived challenge and no cookie is set yet:

{ "require2fa": true, "challengeToken": "eyJ..." }

If the organisation enforces SSO, login is rejected with 403 and a redirect URL to the identity provider instead of a normal error.

A small number of legacy SSO-bypass accounts get a different success shape entirely - { "message": "Login successful", "role": "...", "orgId": "..." } with no user object. Don't assume user is always present; check for it before reading nested fields.

Limited to 10 attempts per 15 minutes per IP.


Register

POST /api/auth/register

Create a new account and organisation. Self-service registration only works for the very first organisation on a PurpletGo instance - once one exists, new users must be invited from Settings → Team.

Request body

{
  "fullName": "Jane Smith",
  "email": "jane@acme.com",
  "password": "min8chars",
  "orgName": "Acme Corp",
  "acceptPrivacy": true
}

Response 202 Accepted

Registration doesn't create the account immediately - it sends a verification email and the account is created only once the link is clicked.

{
  "pending": true,
  "message": "Check your email to verify your address and activate your account."
}

Limited to 5 registrations per hour per IP.


Logout

POST /api/auth/logout

Revokes the current session and clears the auth cookie.

Response 200 OK

{ "success": true, "ssoLogoutUrl": null }

ssoLogoutUrl is only populated when the session was established via SAML SSO and the identity provider supports Single Logout - otherwise it's null.


Get current user

GET /api/auth/me

Returns the currently authenticated user.

Response 200 OK

{
  "id": "usr_abc123",
  "email": "admin@acme.com",
  "fullName": "Jane Smith",
  "orgId": "org_xyz",
  "role": "admin",
  "theme": "system",
  "language": "en",
  "isPlatformAdmin": false
}

Other account endpoints

Beyond the four above, /api/auth also exposes session-based account management that isn't part of the public API surface but is used by the web app itself: email verification (GET /verify-email), password reset (POST /forgot-password, POST /reset-password), profile updates (PATCH /profile, PATCH /password, PATCH /theme, PATCH /language), Google OAuth (GET /google, GET /google/callback), multi-org support (GET /my-orgs, POST /switch-org), invite acceptance (GET /invite-preview, POST /accept-invite, GET /my-pending-invites), active-session management (GET /sessions, DELETE /sessions/:id, DELETE /sessions), and notification preferences (GET /notification-preferences, PATCH /notification-preferences). These aren't scoped for API-key access and their shapes may change without notice - build integrations against the four endpoints above instead.