Token lifecycle
Two things expire on different clocks: the API key you create in the UI lasts months, and the access token you exchange it for lasts an hour.
Create key (Settings → MCP) ← one time
│
▼
Exchange: POST /api/v1/oauth/token ← every ~1 hour
│ client_id + client_secret → JWT
▼
Use JWT for API / MCP requests ← every request
│ Authorization: Bearer <jwt>
│
├─ Token expires (1 hour)
│ → Exchange again with same credentials
│
├─ Key revoked by admin
│ → All requests fail immediately (401)
│
└─ Key expired (30–180 days from creation)
→ Exchange fails, create a new key
Access tokens#
| Property | Value |
|---|---|
| Lifetime | 1 hour (expires_in: 3600) |
| Format | Signed JWT |
| Refresh | Exchange the same client ID and secret again — there is no refresh token |
| Scope | Fixed at exchange time; to change scopes, exchange a new token |
Cache the token for its full hour instead of exchanging per request, or you'll hit the 60/min exchange limit. A practical pattern is to store the token with its expiry and re-exchange when fewer than ~5 minutes remain, so a long-running import doesn't fail mid-flight.
An expired token returns 401. Exchange a new one and retry the request.
API keys#
| Property | Value |
|---|---|
| Lifetime | 30–180 days from creation, depending on how the key was configured |
| Secret | Shown once at creation and never again |
| Revocation | Immediate — in-flight tokens from that key stop working |
When a key expires or is revoked, the token exchange fails with invalid_client rather than a distinct error, so treat that response as "create a new key" once you've ruled out a wrong secret. Rotate by creating the new key first, deploying it, then revoking the old one.
Presigned upload URLs#
Upload URLs handed back by the import endpoints are a third, much shorter clock: they expire 1 hour after they're issued, independently of your access token. If an upload URL expires before you use it:
- CSV parts — call retry a part for a fresh URL.
- Survey media files — call retry a media file.
- Files and insights — call initialize again (files, insights). This creates a new
wav_keyor insight, since neither has a retry endpoint.