Getting Started
Authentication
Tenant API keys, the bearer header, scopes, the /v1/api/me endpoint, and tenant suspension.
View as MarkdownEvery 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.
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:
There is no X-Tenant-Id header on /v1/api routes and none is read. The store context derives entirely from the key.
Authorization: Bearer {api_key}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.
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:
- Authentication: the bearer token is resolved to a store. Missing, malformed, or revoked token:
401 {"message": "Unauthenticated."}. - Tenancy and suspension: the store context is initialized from the key. Suspended store:
403 {"message": "Tenant suspended.", "code": "tenant_suspended"}. - Rate limiting: 120 requests per minute per store. Exceeded:
429 {"message": "Too Many Attempts."}. - 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.
Request processing order
Middleware runs in a fixed order on every /v1/api request. Knowing the order helps interpret error responses:
- Authentication: the bearer token is resolved to a store. Missing, malformed, or revoked token:
401 {"message": "Unauthenticated."}. - Tenancy and suspension: the store context is initialized from the key. Suspended store:
403 {"message": "Tenant suspended.", "code": "tenant_suspended"}. - Rate limiting: 120 requests per minute per store. Exceeded:
429 {"message": "Too Many Attempts."}. - 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:
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.
{
"message": "Tenant suspended.",
"code": "tenant_suspended"
}Tenant suspension
Borkol can suspend a store. While suspended, every /v1/api request with that store's keys, regardless of scope or endpoint, returns:
{
"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"]. |
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. |
curl https://api.borkol.com/v1/api/me \
-H "Authorization: Bearer {api_key}"const res = await fetch("https://api.borkol.com/v1/api/me", {
headers: { Authorization: `Bearer ${apiKey}` },
});
const me = await res.json();{
"tenant": {
"id": "9c4e7f0a-2b31-4d6e-8f0a-1d2c3b4a5e6f",
"name": "Example Store",
"slug": "example-store"
},
"scopes": ["tenant:read", "commerce:read", "commerce:write", "stock:read"]
}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"]. |
curl https://api.borkol.com/v1/api/me \
-H "Authorization: Bearer {api_key}"const res = await fetch("https://api.borkol.com/v1/api/me", {
headers: { Authorization: `Bearer ${apiKey}` },
});
const me = await res.json();{
"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". |
Errors
None specific to this route.
curl https://api.borkol.com/v1/ping{
"status": "ok",
"version": "v1"
}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". |
curl https://api.borkol.com/v1/ping{
"status": "ok",
"version": "v1"
}Errors
None specific to this route.