# Authentication

Every request to a `/v1/api` route is authenticated with a tenant API key. The key is a bearer token that belongs to a single store (tenant): it both authenticates the request and selects the store, so no separate tenant identifier or header is ever needed. This page covers how keys work, which scopes exist, the identity endpoint, and what happens when a store is suspended.

## API keys

An API key is a long-lived secret token issued for one store. Keys do not expire; they remain valid until the merchant revokes them. The plaintext key is shown exactly once, at creation. Store it in a secret manager immediately; it cannot be retrieved again.

Merchants create and revoke keys in the client dashboard (app.borkol.com) under Settings > API keys. At creation the merchant picks a name and a set of scopes for the key. The dashboard lists each key's name, scopes, last-used time, and creation time, and can revoke a key at any point. A revoked key immediately stops working: requests with it return `401 {"message": "Unauthenticated."}`.

Send the key on every request in the `Authorization` header:

```
Authorization: Bearer {api_key}
```

There is no `X-Tenant-Id` header on `/v1/api` routes and none is read. The store context derives entirely from the key.

## Scopes

Each key carries a set of scopes granted at creation. Scopes gate endpoints: a route that requires a scope the key does not carry returns `403 {"message": "Invalid ability provided."}`.

Scope names follow the pattern `{module}:{read|write}`. The full set:

| Scope | Grants |
| --- | --- |
| `tenant:read` | Basic store identity access. Always available to grant. |
| `commerce:read` | Read access to the commerce module (products, orders, configurator). |
| `commerce:write` | Write access to the commerce module. |
| `stock:read` | Read access to the stock module. |
| `stock:write` | Write access to the stock module. |
| `customers:read` | Read access to the customers module. |
| `customers:write` | Write access to the customers module. |
| `objects:read` | Read access to the objects module. |
| `objects:write` | Write access to the objects module. |

A merchant can only grant scopes for modules currently enabled on their store: the grantable set is always `tenant:read` plus the `{module}:read` and `{module}:write` pair of each enabled module. Requesting an unavailable scope at key creation fails with a 422 in the dashboard flow.

Scopes and module enablement are enforced independently. Even when a key carries `commerce:read`, calling a commerce route while the commerce module is disabled for the store returns `403 {"error": "Module not enabled: commerce"}`. Read endpoints require the module's `read` scope; create, update, and delete endpoints require the module's `write` scope. Each endpoint's reference page states the scope it requires.

## Request processing order

Middleware runs in a fixed order on every `/v1/api` request. Knowing the order helps interpret error responses:

1. **Authentication**: the bearer token is resolved to a store. Missing, malformed, or revoked token: `401 {"message": "Unauthenticated."}`.
2. **Tenancy and suspension**: the store context is initialized from the key. Suspended store: `403 {"message": "Tenant suspended.", "code": "tenant_suspended"}`.
3. **Rate limiting**: 120 requests per minute per store. Exceeded: `429 {"message": "Too Many Attempts."}`.
4. **Module gate and scope check** (module routes only): both run after the steps above, but their relative order varies by route. Disabled module: `403 {"error": "Module not enabled: {key}"}`. Missing ability: `403 {"message": "Invalid ability provided."}`. The checks are independent; when both would fail, either error may surface first.

## Tenant suspension

Borkol can suspend a store. While suspended, every `/v1/api` request with that store's keys, regardless of scope or endpoint, returns:

```json title="Response (403, suspended)"
{
  "message": "Tenant suspended.",
  "code": "tenant_suspended"
}
```

with HTTP status 403. The `code` field is stable and machine-readable; check for `tenant_suspended` to distinguish this 403 from scope and module errors. Suspension does not invalidate keys: when the suspension is lifted, existing keys work again unchanged.

## GET /v1/api/me

Returns the identity of the store the presented API key belongs to, plus the scopes granted to that key. This is the canonical first call to verify a key works and to inspect its abilities. It requires no scope; any valid key can call it.

### Parameters

None. Only the `Authorization: Bearer {api_key}` header is required.

### Response fields

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `tenant` | object | always | The store the key belongs to. |
| `tenant.id` | string (UUID) | always | Store ID. |
| `tenant.name` | string | always | Store display name. |
| `tenant.slug` | string | always | URL-safe store identifier. |
| `scopes` | array of strings | always | The scopes granted to this key, for example `["tenant:read", "commerce:read"]`. |

```bash title="Request"
curl https://api.borkol.com/v1/api/me \
  -H "Authorization: Bearer {api_key}"
```

```javascript title="Request (JS)"
const res = await fetch("https://api.borkol.com/v1/api/me", {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const me = await res.json();
```

```json title="Response"
{
  "tenant": {
    "id": "9c4e7f0a-2b31-4d6e-8f0a-1d2c3b4a5e6f",
    "name": "Example Store",
    "slug": "example-store"
  },
  "scopes": ["tenant:read", "commerce:read", "commerce:write", "stock:read"]
}
```

### Errors

| Status | Body | When |
| --- | --- | --- |
| 401 | `{"message": "Unauthenticated."}` | Bearer token missing, malformed, or revoked. |
| 403 | `{"message": "Tenant suspended.", "code": "tenant_suspended"}` | The store is suspended. |
| 429 | `{"message": "Too Many Attempts."}` | More than 120 requests in the current minute. |

## GET /v1/ping

Unauthenticated health check for API v1. No credentials, no store context, no scopes, and no tenant rate limit apply. Use it for uptime monitoring and to verify connectivity and the base URL before wiring credentials.

### Parameters

None.

### Response fields

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | string | always | Always `"ok"`. |
| `version` | string | always | Always `"v1"`. |

```bash title="Request"
curl https://api.borkol.com/v1/ping
```

```json title="Response"
{
  "status": "ok",
  "version": "v1"
}
```

### Errors

None specific to this route.
