# REST API and personal access tokens

> Submit query requests, follow them and read masked results from scripts and CI with the /api/v1 REST API. Token scopes, expiry, rate limits, curl examples.

- Documentation: Guides
- Updated: 2026-11-04
- Source: https://queryproxy.com/docs/api/
- Language: en-US
- Author: Muhammet Şafak

---
The REST API gives scripts and CI pipelines the same workflow the web UI has. A
request filed through the API passes the same [SQL guards](/docs/sql-guards/),
waits for the same human approval, and lands in the same audit trail as one filed
in the browser. Nothing runs until a reviewer approves it.

The endpoint reference is the OpenAPI 3.1 description in the repository,
[`docs/openapi.yaml`](https://github.com/QueryProxy/QueryProxy/blob/main/docs/openapi.yaml).
This page explains how tokens work and walks through a complete flow.

If you only need to file a request, wait for it and fetch the result from a
terminal or a pipeline, the [`qp` command-line client](/docs/cli/) does the
polling for you.

## Personal access tokens

Every call carries a personal access token as a bearer token:

```text
Authorization: Bearer <id>|qp_<secret>
```

- A token **belongs to one user** and never grants more than that user may do in
  the web UI. Its scopes can only narrow that.
- A token **always expires**. You choose the lifetime when you create it, up to
  `QUERYPROXY_API_TOKEN_MAX_DAYS` days (default `90`).
- A token **stops working** when its owner is suspended or deleted. There are no
  service accounts: automation uses the token of a real user.
- A user the `QUERYPROXY_REQUIRE_2FA` policy covers, who has not enrolled in
  two-factor authentication yet, gets no API access: a token must not stand in
  for the second factor. The token works again once the user has enrolled.

### Create and revoke a token

Open **Profile**, then **API tokens**. Name the token, pick its scopes and set the
number of days it lasts, then create it. The value is shown **once**, with a Copy
button; QueryProxy stores only its hash, and a reload hides the value for good.
Revoking a token stops it at once. Creating and revoking are audited as
`token.created` and `token.revoked`.

Creating a token asks you to confirm first:

- With a local password, enter it, or a code from your authenticator app when
  two-factor authentication is on. Failed attempts are rate-limited.
- An account without a local password (SSO only) must have signed in through SSO
  in the last 15 minutes. Otherwise the page sends you through SSO again and
  brings you back.

### Scopes

| Scope | Allows | Who can hold it |
| :-- | :-- | :-- |
| `requests:submit` | Submit a request, cancel your own pending request | DBA or developer in a team |
| `requests:read` | Read a request, list connections | Every role |
| `results:read` | Read the stored result of a completed request | Not offered to auditors |
| `approvals:write` | Approve or reject requests you may review | DBA in a team |

The Profile page offers only the scopes your roles can use.

## Rules the API keeps

- **No approval bypass.** A request waits for a human decision exactly as in the
  web UI.
- **No overrides.** An approval that would be an override (the reviewer is a
  system administrator who is not a DBA of the request's team, or is the requester) needs a
  written reason. The API refuses it with `403` and an `override_url` that points
  to the web form. The API has no override of its own.
- **A token can never approve its owner's own request.**
- **Results are masked** by the connection's masking rules, as everywhere else.
  Column names of a result are result data: an auditor does not get them, and an
  auditor cannot read a result at all.
- **Auditors see redacted SQL.** `sql_original` and `sql_prepared` are `null`
  for a viewer who may not see literal values; `sql_redacted` carries the
  statement with the literals replaced.

A leaked token held by a reviewer can still approve other users' requests inside
its `approvals:write` scope, so give that scope sparingly and keep the lifetime
short.

## Rate limits

Each token may make `QUERYPROXY_API_RATE_LIMIT` calls per minute (default `60`).
Above that the API answers `429` with a `Retry-After` header. Failed
authentications are limited per address to the same number. Responses carry
`X-RateLimit-Limit` and `X-RateLimit-Remaining`.

## Walkthrough with curl

The examples assume two variables. `QP` is your installation, and `TOKEN` holds
the token value.

```bash
export QP=https://queryproxy.example.com
export TOKEN='7|qp_...'
```

**1. Find the connection.** `GET /api/v1/connections` lists the connections you
can use, without hosts or credentials.

```bash
curl -s -H "Authorization: Bearer $TOKEN" -H 'Accept: application/json' \
  "$QP/api/v1/connections"
```

**2. Submit the request.** Needs `requests:submit`. `connection_id` and `sql` are
required; `title` (up to 150 characters) and `reason` (up to 1000) are optional.
The answer is `201`, with the request in status `pending`.

```bash
curl -s -X POST "$QP/api/v1/requests" \
  -H "Authorization: Bearer $TOKEN" -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{"connection_id": 3, "sql": "SELECT id, status FROM orders", "title": "Orders check", "reason": "Support ticket"}'
```

The SQL guard can refuse the statement with `422` and a list of `violations`. A
connection you cannot see is reported as `404`; one you can see but may not submit
to (an auditor, or a `read` grant for a write) is refused with `403`.

**3. Follow the request.** Needs `requests:read`. Poll until `status` is
`completed` (or `failed`, `rejected`, `cancelled`). The statuses are `pending`,
`approved`, `rejected`, `queued`, `running`, `completed`, `failed` and
`cancelled`.

```bash
curl -s -H "Authorization: Bearer $TOKEN" -H 'Accept: application/json' \
  "$QP/api/v1/requests/42"
```

**4. Approval.** A DBA of the team (not the requester) approves in the web UI, in
Slack or Teams, or through the API with the reviewer's own token that
has `approvals:write` (`REVIEWER_TOKEN` below):

```bash
curl -s -X POST -H "Authorization: Bearer $REVIEWER_TOKEN" -H 'Accept: application/json' \
  "$QP/api/v1/requests/42/approve"

curl -s -X POST "$QP/api/v1/requests/42/reject" \
  -H "Authorization: Bearer $REVIEWER_TOKEN" -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{"reason": "Add a WHERE clause"}'
```

A decided request answers `409`. The requester can withdraw a pending request
with `POST /api/v1/requests/42/cancel`.

**5. Read the result.** Needs `results:read`. The default is NDJSON, one JSON
array per row with values in column order (the column names are in
`result.columns` of the request). Send `Accept: text/csv` for CSV with a header
row.

```bash
curl -s -H "Authorization: Bearer $TOKEN" "$QP/api/v1/requests/42/result"

curl -s -H "Authorization: Bearer $TOKEN" -H 'Accept: text/csv' \
  "$QP/api/v1/requests/42/result" -o result.csv
```

The response headers `X-QueryProxy-Row-Count` and `X-QueryProxy-Truncated` tell
you how many rows were stored and whether the result was cut at the row ceiling.
A request that has not completed, has failed, or whose result has been pruned
answers `409`; `404` means the stored file is missing from the result disk. Every read is audited as `result.downloaded`.

## Errors

Errors are JSON with a `message`. Validation errors (`422`) add `errors`, keyed
by field.

| Status | Meaning |
| :-- | :-- |
| `401` | The token is missing, unknown, expired or revoked, its owner is suspended or deleted, or its owner must enrol in two-factor authentication first |
| `403` | The token lacks the scope, or its owner may not do this. An override adds `override_url` |
| `404` | No such request or connection, or a connection that is not visible to you. A request you may not view answers `403` |
| `409` | The request is no longer pending, or has no stored result (not completed, failed, or pruned) |
| `422` | The body is invalid, or the SQL guard refused the statement |
| `429` | The token used its budget for this minute; wait `Retry-After` seconds |

## In the audit log

API submissions, approvals, rejections, cancellations and result reads carry
`channel = api`. Token creation and revocation are `token.created` and
`token.revoked`. See [Audit chain and SIEM export](/docs/audit-export/).

## Settings

| Variable | Default | Meaning |
| :-- | :-- | :-- |
| `QUERYPROXY_API_TOKEN_MAX_DAYS` | `90` | Longest lifetime a token can be given, in days |
| `QUERYPROXY_API_RATE_LIMIT` | `60` | Calls per minute for each token, and failed authentications per address |

## Next steps

- [The `qp` client](/docs/cli/) for a ready-made command-line client of this API.
- [Approval workflow](/docs/approval-workflow/) for who may approve what.
- [Single sign-on](/docs/sso-oidc/) if your users sign in through a provider.
- [Production hardening](/docs/production-hardening/) for token hygiene.
