> ## Documentation Index
> Fetch the complete documentation index at: https://muveya.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Get an API key for your dental clinic, send it as a bearer token, keep it on your server, rotate or revoke it, and understand every authentication failure.

Every `/v1` request authenticates with an **API key**: a secret machine credential that belongs to exactly one dental clinic and carries an explicit list of [scopes](/docs/en/api-reference/scopes). The separate MCP endpoint `/mcp` uses OAuth sign-in and authorization in Console.

## How the key is sent

Send the key in the `Authorization` header with the `Bearer` scheme, on every request:

```http theme={null}
GET /v1/me HTTP/1.1
Host: api.muveya.com
Authorization: Bearer mvy_test_EXAMPLE...
```

* The key is an opaque string, not a JWT. Do not decode it.
* `Authorization: Bearer` is the only accepted transport. There is no `X-API-Key` header and no query parameter for the key.
* The scheme name is not case sensitive, but the key itself must be sent exactly as issued, with no quotes and no line breaks.

## What a key looks like

```text theme={null}
mvy_test_<24 letters and digits>_<4 letters and digits>
```

| Part | Meaning |
| - | - |
| `mvy` | The muveya prefix. It lets secret scanners recognize a leaked key. |
| `test` | The key environment. It is the only environment issued in v1, and it does **not** mean sandbox data: a `mvy_test_` key reads the real data of the dental clinic it belongs to. `GET /v1/me` reports it as `"environment": "test"`. |
| 24 characters | The random secret. |
| 4 characters | A checksum. A mistyped or truncated key is rejected before muveya looks it up. |

muveya stores only a SHA-256 hash of the key and its first 12 characters (the **prefix**, for example `mvy_test_ab1`). The full key is shown once, when it is created, and cannot be recovered afterwards. If it is lost, request a new one.

## One key, one dental clinic

A key is bound to one dental clinic when it is created, on the server. That has three consequences:

1. **No request names a dental clinic.** No `/v1` path, query parameter or header accepts a tenant or workspace id. `GET /v1/me` tells you which dental clinic the key reads, as `workspaceId`.
2. **A key reads the whole dental clinic.** It is not limited by the location and warehouse access that applies to team members: within its scopes, it reads every location and every warehouse.
3. **To integrate two dental clinics, use two keys.** A record of another dental clinic answers `404`, exactly as if it did not exist.

A key is not a person. It cannot approve orders, confirm deliveries or do anything else that the product reserves for a team member.

## Get a key

<Warning>
  There is no console screen to create, list or revoke API keys yet. Keys are issued by the muveya team on request.
</Warning>

<Steps>
  <Step title="Ask from an account that manages integrations">
    A member of your dental clinic who holds the permission **Manage integrations and API keys** (`integrations.manage`) writes to [team@muveya.com](mailto:team@muveya.com). See [Roles and permissions](/docs/en/account/roles-and-permissions) for how that permission is granted.
  </Step>

  <Step title="Say what the key is for">
    Include a name for the integration (for example "ERP stock sync") and the exact scopes it needs. Choose them with the least-privilege recipes on [Scopes](/docs/en/api-reference/scopes). A key must have at least one scope.
  </Step>

  <Step title="Store the key the moment you receive it">
    The key is created for that member and your dental clinic with exactly the scopes you asked for. Put it straight into your secret manager: it is shown only once.
  </Step>

  <Step title="Check it">
    Call `GET /v1/me` (below) and confirm that `workspaceId` and `scopes` are what you expect.
  </Step>
</Steps>

Rules that apply to every key:

| Rule | Value |
| - | - |
| Scopes | At least one; see [Scopes](/docs/en/api-reference/scopes). Scopes cannot be changed on an existing key: request a new key instead. |
| Active keys per dental clinic | At most 10 |
| Expiry | Keys issued today have no expiry date. They work until they are revoked or one of the conditions below applies. |

### The key depends on its member and its dental clinic

A key keeps working only while the member it was created for is an active member of the dental clinic:

* If that member is **suspended** or **removed** from the dental clinic, the key stops working immediately.
* **Reactivating** the member does not bring the key back. The console says so when you reactivate someone: API keys from before the suspension stay closed. Request a new key.
* If the dental clinic itself is suspended, every one of its keys is refused.

Ask for keys from a member who will stay in charge of the integration, such as an owner or administrator who has been granted **Manage integrations and API keys** (no role includes it).

## Your first call: `GET /v1/me`

`GET /v1/me` requires no scope. Use it to check a key before anything else.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.muveya.com/v1/me \
    -H "Authorization: Bearer $MUVEYA_API_KEY"
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch("https://api.muveya.com/v1/me", {
    headers: { Authorization: `Bearer ${process.env.MUVEYA_API_KEY}` },
  });
  console.log(response.status, await response.json());
  ```

  ```python Python theme={null}
  import os
  import requests

  response = requests.get(
      "https://api.muveya.com/v1/me",
      headers={"Authorization": f"Bearer {os.environ['MUVEYA_API_KEY']}"},
      timeout=30,
  )
  print(response.status_code, response.json())
  ```
</CodeGroup>

```json 200 OK theme={null}
{
  "workspaceId": "66d0a1f0c0ffee0000000001",
  "environment": "test",
  "scopes": ["clinics:read", "catalog:read", "inventory:read"]
}
```

<ResponseField name="workspaceId" type="string" required>
  The dental clinic the key belongs to, decided by the server.
</ResponseField>

<ResponseField name="environment" type="string" required>
  The key environment. Always `test` in v1.
</ResponseField>

<ResponseField name="scopes" type="string[]" required>
  The scopes the key carries, in the colon form used across the API (`catalog:read`).
</ResponseField>

The response never contains the key, its hash or any other secret.

## Keep keys on your server

* Keep the key in a secret manager or in a server environment variable, and read it at runtime.
* Never put it in browser code, a mobile app, a shared spreadsheet, a code repository, a URL, a log line or an error report.
* Use one key per integration, so that you can revoke one without stopping the others.
* When you talk to support about a key, identify it by its name or its prefix (the first 12 characters), never by the full key.
* If a key may have leaked, ask for it to be revoked right away, then replace it.

The request builder in the **Endpoints** pages never sends requests and never keeps your key, but you should still not paste a key into any web page.

## Rotate a key

<Steps>
  <Step title="Get a second key">
    Request a new key with the same scopes (see [Get a key](#get-a-key)). Both keys work at the same time, and both count toward the limit of 10 active keys.
  </Step>

  <Step title="Deploy it">
    Replace the old key in every system that uses it.
  </Step>

  <Step title="Verify">
    Call `GET /v1/me` from each system with the new key.
  </Step>

  <Step title="Revoke the old key">
    Ask for the old key to be revoked (below).
  </Step>
</Steps>

## Revoke a key

A member with **Manage integrations and API keys** (`integrations.manage`) writes to [team@muveya.com](mailto:team@muveya.com) with the key name or prefix.

* Revocation takes effect on the very next request. There is no cache to wait for.
* It cannot be undone: a revoked key never works again. Request a new key if you still need access.
* The key record is kept for the audit trail of your dental clinic, and so are the creation and revocation events.

## Authentication failures

| Status | `code` | When |
| - | - | - |
| `401` | `api_keys.invalid` | The `Authorization` header is missing, uses another scheme or is empty; the key is malformed or its checksum does not match; the key is unknown or revoked; its member was suspended or removed; or its dental clinic is suspended. |
| `403` | `api_keys.scope_missing` | The key is valid but lacks a scope the operation requires. |

```json 401 Unauthorized theme={null}
{
  "type": "https://docs.muveya.com/errors/api_keys.invalid",
  "title": "API key invalid",
  "status": 401,
  "detail": "The API key is missing, malformed, revoked, or expired. Send a valid key as a Bearer token and try again.",
  "instance": "/v1/me",
  "code": "api_keys.invalid",
  "requestId": "8d0e5b8a-2f4c-4e1b-9a7d-0c3b5e6f7a81"
}
```

* The `401` is identical for every cause, on purpose: it does not reveal whether a key exists. Check the header format first, then check with the member who requested the key whether it was revoked or whether that member is still active.
* The `403` does not name the missing scope. Call `GET /v1/me`, compare the `scopes` with the table on [Scopes](/docs/en/api-reference/scopes), and request a key with the right scopes.
* Requests refused with `401` or `403` do not use your [rate limit](/docs/en/api-reference/rate-limits) budget and carry no rate-limit headers.
* A record that belongs to another dental clinic answers `404`, never `403`.

The full list of codes is on [Errors](/docs/en/api-reference/errors).

## MCP uses OAuth

An API key cannot authenticate `https://api.muveya.com/mcp`. Connect an MCP client through [Console sign-in and authorization](/docs/en/mcp/connect).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.