# Refresh tokens, narrow scopes and end a connection

> How long Zorlya tokens last, how refresh rotation and replay detection work, how scopes narrow, and how a person or a client ends a connection.

Source: https://zorlya.com/guides/developers/tokens-scopes-and-revocation

Updated 2026-10-25.

A Zorlya access token lasts 1 hour and a refresh token 30 days, and every refresh replaces both. Scopes only narrow, and a person, a client or an account change can end a connection at any time. Read this before you store tokens.

## The lifetimes

| Item | Lasts | Notes |
| --- | --- | --- |
| Authorization code | 60 seconds | Works once |
| Connect request | 10 minutes | The time a person has to finish the Connect page and the sign-in after you send them. It works once |
| Access token | 1 hour | Sent on every call |
| Refresh token | 30 days | Rotates every time it is used |

Every token is for one audience, the resource it was issued for: `https://mcp.zorlya.com`. The MCP server refuses a token for any other audience, and a refresh cannot move a grant to another one. Tokens also carry the account type (job seeker, hiring, support or staff), the role and, for hiring, the company.

Zorlya stores only a SHA-256 digest of each code and token. A database dump cannot be used to present any of them, and the server cannot show you a token it issued earlier. Lose a token and you sign in again.

## Refresh tokens rotate

Trade a refresh token for a new pair:

```bash
curl -s -X POST https://api.zorlya.com/oauth/token \
  -d grant_type=refresh_token \
  -d "refresh_token=$REFRESH_TOKEN" \
  -d "client_id=$CLIENT_ID"
```

Send this to `api.zorlya.com`, the authorization server. The MCP host does not serve `/oauth/*`.

The answer has a new `access_token` and a new `refresh_token`. Three things follow, and each one was checked against a local server:

- **The old refresh token is dead at once.** Using it again answers `invalid_grant` with "refresh token does not exist".
- **The old access token is dead too.** A call with it answers `401` straight after the refresh, even though its hour had not run out.
- **Store the new pair before you use it.** If your process stops between the answer and the write, you hold two dead tokens and the person must sign in again. Refreshing from two processes at the same time has the same effect, so refresh from one place.

A refresh also resets the 30 day clock, so a client that refreshes at least once a month stays signed in.

## Scopes only narrow

Scope names are `area:action`, such as `applications:write`. The action is `read`, `write` or `delete`. `zorlya:read`, `zorlya:write` and `zorlya:delete` are umbrellas: each means every area of that action that the account may use. [What the Zorlya MCP server offers](/guides/developers/what-the-mcp-server-offers) lists the areas.

Seven controls decide what a connection gets, and it gets what all of them allow:

1. An administrator's switch for each action (read, write, delete) in each app. Delete starts off.
2. An administrator's switch for each area.
3. The ceiling set by your AI app's entry on Zorlya's list.
4. The person's choice on the Connect page: Read only, Read and change, or Read, change and delete once a delete tool exists. The person can only lower what you asked for.
5. The areas the person switched off for this one connection, on the Connect page (Advanced: choose areas) or later with Lower access on `mcp.zorlya.com/connections`. A tool in a switched-off area is not listed, and a call to it is refused as not available. This control only removes tools, and an area comes back only when the person connects again.
6. The account's own rights, checked on every call. Leaving a company or losing a staff role ends access at once.
7. Per-call safeguards: separate rate limits for read, write and delete, and a request log.

A grant carries the scopes that come out of that. On refresh you may send `scope` to ask for fewer:

```bash
-d "scope=zorlya:read"
```

You cannot ask for more than the grant holds. A refresh that names a scope the grant lacks answers `invalid_scope`. To widen access, send the person through the Connect page again and ask for the larger scope. A refresh is also held to the list entry as it is now: if the entry was switched off, lost the account's app or got a lower ceiling, the refresh answers `invalid_grant` and the person connects again.

What a connection can do follows from its scopes:

- `tools/list` shows only the tools the grant can call.
- A write tool called on a read grant answers `403` with `WWW-Authenticate: Bearer error="insufficient_scope", scope="…"` and a body naming the scope needed. The client keeps its session and asks the person again with the wider scope.
- A tool that is switched off, or that the account may never use, answers "Tool not available".
- `zorlya:read` is also what a client gets when it asks for no scope.

## Ending a connection

There are four ways, and they end different things.

**The client revokes.** `POST /oauth/revoke` with the token and your `client_id`:

```bash
curl -s -X POST https://api.zorlya.com/oauth/revoke \
  -d "token=$REFRESH_TOKEN" -d "client_id=$CLIENT_ID"
```

It answers `200` and an empty body, whether or not the token was valid, so a caller cannot probe for live tokens. Revoking a **refresh token** ends the whole grant: that token and its access token. Revoking an **access token** ends only that access token. Prefer revoking the refresh token on sign-out. Use the hint `token_type_hint=refresh_token` to say which you are sending.

**The person revokes.** A job seeker sees and ends their connections in the Zorlya app under Settings, Integrations, AI clients. The email sent when a connection is made also links to `mcp.zorlya.com/connections`, which lists a person's connections for every kind of account and lets them lower one or end it. Your next call answers `401`, and a refresh answers `invalid_grant`.

**A used code is replayed.** Present an authorization code a second time and the server refuses it with "This authorization code has already been used." It also ends the tokens the first exchange issued. A call with the first access token answers `401` afterwards.

**The account changes.** The server checks on every call that the account may still connect and still holds the role the token was issued for. A suspended account, a member removed from the company or a staff role taken away gets `401`, and a password reset ends every session, AI clients included. An administrator can also switch MCP off for everyone, and every route then answers 404.

## What to do on each answer

| You get | Do |
| --- | --- |
| `401` on the MCP server | Refresh once and retry. If the refresh fails, sign in again |
| `403` with `insufficient_scope` | Ask the person again with the scope named in the answer |
| `invalid_grant` on refresh | The grant is over. Discard your tokens and start the Connect page again |
| `invalid_scope` on refresh | Ask for no more than you hold, or start the Connect page again with the wider scope |
| `invalid_client` on `/oauth/token` | The `client_id` is unknown. Register again |
| `invalid_target` | The `resource` is not the one the grant was issued for |
| `429` on `/oauth/register`, `/oauth/authorize`, `/oauth/token` or `/oauth/revoke` | Wait. Registration is limited to 10 a minute per address, `/oauth/authorize` to 30, `/oauth/token` to 60 and `/oauth/revoke` to 20; on the MCP server a `429` means the 120 reads a minute are used |

## What we checked

Rotation, the dead access token, scope narrowing, revocation by refresh token and code replay were run against a local copy of the server on 2026-10-17. The lifetimes come from the server's source. On 2026-10-18 the audience, the controls, the step-up `403` and the connections page were added from the source and the design, and not run. The request log and the connections page on `mcp.zorlya.com` are merged. On 2026-10-25 the text was checked against the repository again. None of it was run against the production server.
