> Source: Onevium official documentation
> Article: Application keys, scopes, and business-user isolation
> Original URL: https://onevium.com/docs/developers/authentication
> Language: English
> Updated: 2026-09-09
> Applies to: 1.1.23+

---

# Application keys, scopes, and business-user isolation

Choose the seven scopes against actual UI and Gateway routes, derive business subjects correctly, and replace, rotate, or revoke keys.

## What an application key authenticates

A business backend calls the Gateway with an **App Key**. Desktop login, device authentication, and inbound Webhook signatures are separate identity channels.

Each application request carries:

```text
Authorization: Bearer <application key>
X-Onevium-Subject: <business subject verified by your backend>
```

The key determines owner, app, and allowed scopes. `X-Onevium-Subject` identifies the business user within that app. The Gateway does not authenticate that user's login for your system, nor accept request-body `owner_id` or `app_id` as authorization.

## Create and obtain a key

1. Open **Channels → External access** and create or select an application. The initial application key is shown once and contains all seven scopes.
2. Choose **Create API key** on the selected application to issue a smaller credential for a particular backend.
3. Select operations, click **Create**, and save the displayed key to the backend credential store before closing the dialog.
4. Under **Credentials and history → API keys**, inspect the prefix, Active/Revoked state, and rotation/revocation actions. The original full key is not displayed again.

Put the full once-displayed application key in Bearer, not its list prefix or `key_id`. A desktop login token or private control token is not an App Key.

The **Conversation access** preset selects all seven scopes. **Read only** selects the four `:read` scopes. Neither preset calculates the minimum permissions for your use case; review its individual selections. At least one scope is required, and unknown or duplicate scopes are rejected.

![Read-only API key example: query status and history without sending messages or starting tasks.](https://onevium.com/docs/external-access/3cc390708/en/15-key-permissions.png "Read-only API key example: query status and history without sending messages or starting tasks.")

This is a read-only permission example in the preview interface with isolated data, not evidence that a real key was created or business authorization was validated.

## The seven actual scopes

| UI label                                | API value             | Allowed operations                                                                 |
| --------------------------------------- | --------------------- | ---------------------------------------------------------------------------------- |
| View published services                 | `endpoints:read`      | List this app's published services                                                 |
| Read conversation history               | `conversations:read`  | List conversations, read snapshots/history, conversation SSE, and query operations |
| Send messages and restart conversations | `conversations:write` | Create conversations, send messages, reset                                         |
| Read task status                        | `runs:read`           | Query runs and run SSE                                                             |
| Start and stop tasks                    | `runs:write`          | Start a run directly from a service and cancel runs                                |
| Download attachments                    | `resources:read`      | Download content accessible to this subject                                        |
| Upload attachments                      | `resources:write`     | Reserve a resource and upload content                                              |

There is no App Key `*`, `admin`, or `webhooks:write` scope. `conversations:write` includes reset, whose `cancel_and_reset` policy can stop this generation's work; it is not a “messages only, no stopping” permission.

Scopes govern API operation categories. They do not expand a service's published tools, directories, Skills, or MCP capabilities. Adding unknown fields such as `model`, `tools`, or `working_directory` cannot grant more access.

## Required scope by public endpoint

These are Gateway business routes. Except `/health` and independently signed Webhooks, they also require an App Key and nonempty subject.

| Method and path                             | Required scope        | Notes                                                                                                       |
| ------------------------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------- |
| `GET /api/v1/agent-endpoints`               | `endpoints:read`      | This app's published services                                                                               |
| `POST /api/v1/agent-endpoints/{id}/runs`    | `runs:write`          | Task-oriented submission                                                                                    |
| `POST /api/v1/conversations`                | `conversations:write` | Create a business conversation                                                                              |
| `GET /api/v1/conversations?endpoint_id=...` | `conversations:read`  | endpoint\_id selects the device                                                                             |
| `GET /api/v1/conversations/{id}`            | `conversations:read`  | Snapshot                                                                                                    |
| `GET /api/v1/conversations/{id}/messages`   | `conversations:read`  | Paginated history                                                                                           |
| `POST /api/v1/conversations/{id}/messages`  | `conversations:write` | Submit a message                                                                                            |
| `GET /api/v1/conversations/{id}/events`     | `conversations:read`  | Conversation SSE                                                                                            |
| `POST /api/v1/conversations/{id}/reset`     | `conversations:write` | Restart the context                                                                                         |
| `GET /api/v1/runs/{id}`                     | `runs:read`           | State and result                                                                                            |
| `GET /api/v1/runs/{id}/events`              | `runs:read`           | One run's SSE                                                                                               |
| `POST /api/v1/runs/{id}/cancel`             | `runs:write`          | Cancel a run with body `{}`                                                                                 |
| `GET /api/v1/operations/{id}`               | `conversations:read`  | Also applies to an operation created by run cancellation                                                    |
| `POST /api/v1/resources`                    | `resources:write`     | Declare endpoint\_id, content\_type, and size                                                               |
| `PUT /api/v1/resources/{id}/content`        | `resources:write`     | Upload bytes with matching type, length, and `X-Content-SHA256`                                             |
| `GET /api/v1/resources/{id}`                | `resources:read`      | Returns file content, not JSON metadata                                                                     |
| `GET /health`                               | No App Key            | Gateway health, not proof of device/model execution                                                         |
| `POST /api/v1/webhooks/{integration_id}`    | Webhook signature     | See [Webhooks](https://onevium.com/docs/developers/webhooks); an App Key alone cannot replace the signature |

Creation, message submission, direct run submission, cancel, reset, and resource reservation each require an operation-specific `Idempotency-Key`. Resource-content PUT uses the content hash and immutable-resource rules; it is not a new resource reservation.

## How app and subject isolation work

Different applications under one owner retain separate services, conversations, runs, and resources. Existing-resource reads check both app and subject. Reading another subject's resource normally returns **404**, rather than that user's data.

However, an App Key is **not bound to one subject**. The backend holding it must supply the correct one. Do not forward arbitrary browser `X-Onevium-Subject` headers, URL user IDs, or form values. Derive a stable internal identifier from an authenticated session, including tenant identity in a multitenant system.

This is backend pseudocode, not a Onevium SDK:

```typescript
const user = requireAuthenticatedUser(request); // Your existing login validation
const subject = `tenant:${user.tenantId}:user:${user.id}`;
const gatewayHeaders = {
  Authorization: `Bearer ${serverSecrets.oneviumAppKey}`,
  "X-Onevium-Subject": subject
};
```

A subject must be nonempty, at most 256 characters, and have no surrounding whitespace. Display names and email addresses can change, making them poor persistent associations. Use the same subject for reads, SSE, downloads, and controls.

Different keys for the same app differ in scopes, not automatically in subject or endpoint access. If two business systems must remain isolated even from each other's key holders, use separate applications rather than merely issuing two keys under one app.

## Start with the permissions the feature needs

| Backend use case                                       | Starting scope set                                       |
| ------------------------------------------------------ | -------------------------------------------------------- |
| Query a known run                                      | `runs:read`                                              |
| Submit an independent task and read its result         | `runs:write`, `runs:read`                                |
| Create/continue conversations and read history/results | `conversations:write`, `conversations:read`, `runs:read` |
| Cancel individual runs in that conversation flow       | Add `runs:write`                                         |
| Upload or download attachments                         | Add `resources:write` or `resources:read` respectively   |
| Let users select published services first              | Add `endpoints:read`                                     |

Querying `operations/{id}` needs `conversations:read`. A key with only `runs:write + runs:read` can request cancellation and inspect the run, but cannot query the control operation; add `conversations:read` for that complete confirmation flow.

## Verify with the intended identity

Load `APP_KEY` from your credential store and configure the test address and subject. This read-only request lists the app's services and needs `endpoints:read`:

**Set `GATEWAY_URL` to the gateway origin only, without a trailing `/api/v1`.** For example, if the UI displays `http://127.0.0.1:48541/api/v1`, use `http://127.0.0.1:48541`; the commands below append `/api/v1` themselves.

```bash
: "${APP_KEY:?Load APP_KEY from your credential store first}"
export GATEWAY_URL='http://127.0.0.1:REPLACE_WITH_API_PORT'
export SUBJECT='docs-test-user'
curl --fail-with-body -sS "$GATEWAY_URL/api/v1/agent-endpoints" \
  -H "Authorization: Bearer $APP_KEY" \
  -H "X-Onevium-Subject: $SUBJECT"
```

Expect 200 with an `endpoints` array. An empty array means this app has no visible published service; it does not require administrator scope.

Use a separately issued read-only test key to confirm a write request returns 403. Then follow the [conversation guide](https://onevium.com/docs/developers/conversations) with two test subjects to verify isolation. These are checks to perform in your environment, not tests already completed by this page.

**Illustrative scope-error response, not a live capture:**

```json
{
  "error": {
    "code": "FORBIDDEN",
    "message": "Application scope is not allowed."
  }
}
```

## Replace, rotate, and revoke credentials

Under **Credentials and history → API keys**, identify the correct prefix, choose **Rotate key → Confirm rotation**, and save the newly displayed key. Rotation keeps its scopes and revokes the previous key; **there is no old-key grace period**.

For a smoother handover, use two keys:

1. Choose **Create API key** with the scopes the updated backend needs.
2. Deploy it to the backend and verify required operations using the existing subjects.
3. After all callers switch, choose **Revoke key → Confirm revoke** for the old prefix.
4. Confirm new requests using the old key fail. Reducing scopes also requires a new key; rotation does not reduce the previous permission set.

Revocation affects subsequent authentication; it is not cancellation of an executing task. Existing SSE connections do not reauthenticate the key for every event, so callers should close them and reconnect with the new credential. Use explicit cancellation for a task. Disabling the entire application is a separate management operation affecting the whole app.

## Business credentials cannot administer the Gateway

V1 is a backend API. Requests carrying a browser `Origin` or cross-site `Sec-Fetch-Site` are rejected. Call from an authenticated business backend instead of sending the App Key to a browser.

Keep credentials out of URLs; `token`, `key`, and `ticket` query parameters are rejected. The desktop's `/api/integrations/cloud/...` paths are management-bridge routes, not this public Gateway API. The Node Gateway returns 404 for public `/api/v1/management/**`; adding App Key scopes cannot expose it.

Remote management has a separate private control channel/token. Devices authenticate with device identity, and Webhooks use signatures. Do not interchange these credentials. See [remote gateway setup](https://onevium.com/docs/developers/remote-gateway).

## Errors and acceptance checks

| Symptom                               | First checks                                                                         |
| ------------------------------------- | ------------------------------------------------------------------------------------ |
| 401 `UNAUTHENTICATED`                 | Bearer format, correct environment key, rotation/revocation, and active application  |
| 403 `FORBIDDEN`                       | Required scope, owner/device state, or an accidental direct browser request          |
| 400 `INVALID_REQUEST`                 | Missing/invalid subject, credentials in the URL, or invalid request fields           |
| Known ID returns 404                  | Correct app, subject, device routing, and actual resource ID                         |
| Old stream still emits after rotation | Close it and reconnect with the new key; stream output is not a fresh authentication |

Validate minimum scopes, cross-app/subject isolation, caller migration during rotation, old-key rejection, device offline/recovery, and the HTTPS/SSE/callback paths required by your deployment.

## Next steps

Return to [publishing and first requests](https://onevium.com/docs/developers/external-access), implement [business conversations](https://onevium.com/docs/developers/conversations), or configure [Webhooks and callbacks](https://onevium.com/docs/developers/webhooks).
