---
name: Trip Ninja — rotate the 90-day access token
description: >-
  Exchange a refresh token for a fresh access token before the 90-day expiry, and read the
  environment flags on the response so you never point sandbox credentials at production (or
  DataStream credentials at SmartFlights).
api: openapi/webjet-tripninja-adminpanel-refresh-token-openapi.yml
operations:
  - 'POST /adminpanel/refresh-token/'
generated: '2026-07-28'
method: generated
source: >-
  openapi/webjet-tripninja-adminpanel-refresh-token-openapi.yml,
  https://devhub.tripninja.io/smartflights/authentication/,
  https://github.com/trip-ninja-inc/tn-sdk-python
---

# Trip Ninja — rotate the 90-day access token

> `operationId` is `""` in the published document. Anchor on METHOD + PATH.

This is a **credential-rotation** operation. Treat it as safety-critical: it mints new long-lived
secrets and invalidates the refresh token you used. Do not run it autonomously on a schedule without
a human-approved secret store to write into.

## Step 1 — POST /adminpanel/refresh-token/

Body is `RefreshTokenRequest` — both members required:

- `access_token` — the current (possibly expired) access token.
- `refresh_token` — the current refresh token.

## Step 2 — read `RefreshTokenResponse` and store it

- `access_token` — the new token. Present it as `Authorization: Token <access_token>` on every call.
- `refresh_token` — **a NEW refresh token is issued every time.** The old one is spent. Persist the
  new one atomically or you lock yourself out and have to go back to the Admin Panel.
- `expiry_date` — when the new access token stops working, `YYYY-MM-DDThh:mm:ss.SSSSSSZ` in UTC.
- `created_date` — issue time. The access token expires 90 days after this.
- `is_production_token` — `false` means these credentials are **sandbox only**.
- `is_datastream_token` — `false` means these credentials are for **SmartFlights**, not DataStream.

Check both booleans before using the token. Pointing a sandbox token at production, or a DataStream
token at SmartFlights, fails as an authorization error, not as a helpful message.

## Step 3 — handle failure

`401 Unauthorized` returns `{"status": "401", "message": "Invalid token"}`. There is no self-service
recovery: a burned or lost refresh token has to be regenerated by a human in the Admin Panel at
`https://app.tripninja.io` under Developer Resources, and Trip Ninja issues the Admin Panel account
itself — there is no signup form.

## Alternative: the SDK credential exchange

The first-party SDKs do not use this endpoint. They `POST /sdk/auth/` with `X-Client-ID` and
`X-Client-Secret` headers (from env `TN_SDK_CLIENT_ID` / `TN_SDK_CLIENT_SECRET`), receive a
credentials document keyed `sandbox_token` / `prod_token` / `data_stream_token`, cache it in
`credentials.json`, and re-fetch automatically on a `401` before replaying the request once.
`/sdk/auth/` appears in **no** published OpenAPI document — it is only visible in the SDK source at
`https://github.com/trip-ninja-inc/tn-sdk-python`.

## Rules an agent must respect

- Never log, echo or emit any token value. The examples in the published spec are illustrative
  strings, not live credentials.
- Rotate **before** `expiry_date`, not after — an expired access token cannot be presented alongside
  the refresh token in some client flows without a manual reset.
- IP allow-listing applies to this endpoint too: a non-allow-listed source IP gets `403` regardless
  of how valid the tokens are.
