HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json
WWW-Authenticate: Bearer error="invalid_token"
X-Request-Id: req_01HV9XK...

{
  "type": "https://docs.digitalseaservice.com/errors/invalid_token",
  "title": "Invalid or expired token",
  "status": 401,
  "detail": "Access token is expired or has been revoked.",
  "instance": "req_01HV9XK..."
}

What it means

We couldn’t authenticate the request. Either there’s no Authorization header, the bearer token we saw doesn’t match anything we issued, it’s expired, or it’s been revoked.

Common causes & fixes

CauseFix
Missing Authorization headerAdd Authorization: Bearer <access_token>.
Header format wrongMust be exactly Bearer <token>. Not bearer, no extra whitespace, no quoting.
Access token expired (1-hour TTL)Refresh via grant_type=refresh_token. See Authentication / refreshing.
Refresh token used after rotationThe previous refresh token is revoked the moment you refresh. Always store the new one immediately.
User disconnectedThe user clicked Disconnect on their DSS dashboard. Reconsent flow is needed — see user.consent.revoked.
Partner suspendedYour client_id has been suspended by DSS support. Contact us.

Distinguishing “expired” vs “revoked”

We don’t expose this in the detail for security reasons (you shouldn’t be able to probe which case you’re in to enumerate users). Treat them the same:
  1. Try a refresh. If it succeeds, you had a stale access token — store the new pair and retry the original request.
  2. If the refresh itself returns invalid_grant, the user has either disconnected or your refresh token has expired (90-day TTL). In either case, you need to send the user through the authorize flow again.
def request_with_auto_refresh(url, access_token, refresh_token):
    r = httpx.get(url, headers={"Authorization": f"Bearer {access_token}"})
    if r.status_code != 401:
        return r, access_token, refresh_token

    try:
        new_tokens = refresh(refresh_token)  # POST /oauth/token
    except httpx.HTTPStatusError as exc:
        if exc.response.status_code == 400:
            raise NeedsReconsent() from exc
        raise

    r = httpx.get(url, headers={"Authorization": f"Bearer {new_tokens['access_token']}"})
    return r, new_tokens["access_token"], new_tokens["refresh_token"]