mobieusKnow mobieusCore API — Authentication, scopes, rate limits History #60
Author
Patrick Bass
Submitted
May 28, 2026 6:25pm
Reviewed
May 28, 2026 6:25pm
Summary
Seeded by API feature deploy
# Authentication, scopes, and rate limits
## API keys
Every API request must send an `Authorization: Bearer <key>` header. Keys look like:
```
mc_live_YJs9gvYaRhL-6An-hdHlmQb0pTqfGC38hbAT3WwGrs4
mc_test_abcdefghijklmnopqrstuvwxyz0123456789ABCD
```
The prefix tells you (and any code reviewer) whether the key is live or test data.
### How keys are stored
- Mobieus stores only a **SHA-256 hash** of the key — the plaintext is shown **once** at creation and is unrecoverable.
- Lookup uses constant-time comparison so attackers can't grind hashes by timing.
- The first ~12 characters are stored separately so the admin UI can identify the key by prefix without ever seeing the secret.
### Managing keys
+ Tenant super-admins manage keys at **`/admin/api-keys`**:
Tenant super-admins manage keys at **`/admin/api-keys`**. Settings live at **`/admin/api/settings`**.
| Action | What happens |
|---|---|
| **Create key** | Generates a fresh key. The plaintext is shown once on the next page — copy it now, you cannot recover it. |
| **Edit** | Change the label or set a per-key rate-limit override. |
| **Revoke** | Immediate. The next request with that key returns `401 invalid_api_key`. Not reversible. |
| **Expire** | Optional `expires_at`. Keys past their expiry start returning `401` automatically. |
+ Every create and revoke is audit-logged.
Every create, edit, and revoke is audit-logged.
## Scopes
Every key has one or more scopes. Endpoints check the scope before processing — a key without `events:read` calling `GET /api/v1/events` gets `403 insufficient_scope`.
### Core, webhooks, marketplace, files, moderation
| Scope | Lets the key… |
|---|---|
+ | `events:read` | Read `/api/v1/events` |
+ | `users:read` | Read `/api/v1/users` and `/api/v1/users/{id}` |
+ | `posts:read` | Read `/api/v1/posts` and `/api/v1/posts/{id}` |
+ | `webhooks:read` | List webhook endpoints + delivery history *(Phase 2)* |
+ | `webhooks:manage` | Create / update / delete webhook endpoints *(Phase 2)* |
| `events:read` | Read `/api/v1/events`. |
| `users:read` | Read `/api/v1/users` and `/api/v1/users/{id}`. |
| `posts:read` | Read `/api/v1/posts` and `/api/v1/posts/{id}`. |
| `listings:read` | Read `/api/v1/listings` and `/api/v1/listings/{id}`. |
| `files:read` | Read `/api/v1/files` and `/api/v1/files/{id}`. |
| `reports:read` | Read `/api/v1/reports`, `/reports/{id}`, and `/moderation/actions`. |
| `reports:manage` | Dismiss or resolve reports via `POST /api/v1/reports/{id}/dismiss` and `/resolve`. |
| `webhooks:read` | List webhook endpoints + delivery history. |
| `webhooks:manage` | Create / update / delete webhook endpoints. |
### mobieusHelp
| Scope | Lets the key… |
|---|---|
| `helpdesk:read` | Read tickets, queues, agents, canned responses, tags, help topics, notification prefs, and the read-side AI hooks. |
| `helpdesk:write` | Create + update tickets, reply, change status, manage canned responses + tags, and the write-side AI hooks. |
| `helpdesk:admin` | Set notification prefs, read the helpdesk audit log, and the admin-only AI hooks. |
### mobieusLearn
| Scope | Lets the key… |
|---|---|
| `learn:read` | Read every Learn resource (courses, enrollments, attempts, certificates, templates, SCORM packages). |
| `learn:write` | Create / update / publish / archive courses, enroll users, cancel enrollments, revoke + regenerate certificates. |
| `learn:cohorts:grant` | Grant cohort access via `POST /api/v1/learn/cohorts/grant`. |
| `learn:xapi:read` | Query the native xAPI statement store. |
| `learn:xapi:write` | Store xAPI statements. |
### mobieusKnow
| Scope | Lets the key… |
|---|---|
| `know:read` | List pages, get a page or revision, run search. |
| `know:write` | Create / update / delete pages, approve / reject revisions. |
**Principle of least privilege**: only grant the scopes the integration actually needs.
## Plan gate
The public REST API is gated to **Pro**, **Creator Plus**, and **Sovereign** tenants. Tenants below Pro get `403 plan_gated`:
```json
{ "error": { "code": "plan_gated",
"message": "The public REST API is available on Pro and higher plans. Upgrade in /admin/billing.",
"current_plan": "starter",
"required_plans": ["pro", "creator-plus", "sovereign"],
"request_id": "req_…" } }
```
Upgrade in `/admin/billing` and the API surface lights up immediately — no key re-mint needed.
## Tenant isolation
+ A key is bound to the tenant it was minted in. A `mc_live_…` key from `support.mobieus.io` will not authenticate against `fort-smith-live.mobieus.io`, and there is no API surface to query a different tenant's data with it. Every endpoint mounts under the tenant subdomain; tenant scoping happens at the database connection layer, so cross-tenant queries are structurally impossible.
A key is bound to the tenant it was minted in. A `mc_live_…` key from one community will not authenticate against another, and there is no API surface to query a different tenant's data with it. Tenant scoping happens at the **database connection layer**, so cross-tenant queries are structurally impossible.
## Rate limits
+ Default: **600 requests per minute per key**. Every response carries:
Three layers, evaluated in order:
+ - `X-RateLimit-Limit` — the ceiling
+ - `X-RateLimit-Remaining` — what's left in the current window
1. **Per-key override** — `api_keys.rate_limit_per_minute` set on `/admin/api-keys/{id}/edit`. Highest precedence.
2. **Tenant default** — `api_settings.default_rate_limit_per_minute` set on `/admin/api/settings`. Applies when the per-key override is blank.
3. **Platform default** — `api.rate_limit_per_minute` in `app.ini` (currently **600**). Fallback when both overrides are blank.
Every response carries:
- `X-RateLimit-Limit` — the ceiling actually applied to this request
- `X-RateLimit-Remaining` — what's left in the current 60-second window
- `X-RateLimit-Reset` — Unix timestamp when the window resets
On exceed you get `429 Too Many Requests` plus a `Retry-After: <seconds>` header. Back off, then retry.
+
+ If you need a higher limit, raise it for your tenant by adjusting `api.rate_limit_per_minute` in `config/app.ini` or contact ops.
## Request IDs
Every response carries `X-Request-Id: req_xxxxxxxxxxxxxxxx`. The same id appears in the JSON body as `request_id`. Include it whenever you contact support — it lets us correlate your call with the server log instantly.
## Error envelope
Every error response uses the same JSON shape:
```json
{
"error": {
"code": "invalid_api_key",
"message": "The API key is invalid, revoked, or expired.",
"request_id": "req_3f2b8d9c10ab7c92"
}
}
```
Common codes:
| HTTP | `error.code` | Meaning |
|---|---|---|
| 401 | `missing_authorization` | No `Authorization` header. |
| 401 | `invalid_authorization` | Header wasn't a Bearer token. |
+ | 401 | `invalid_api_key` | Bad key, revoked, or expired. |
+ | 403 | `insufficient_scope` | Key valid but missing the required scope. |
| 401 | `invalid_api_key` | Bad key, revoked, or expired. |
| 403 | `plan_gated` | Tenant is on a plan below Pro. |
| 403 | `insufficient_scope` | Key valid but missing the required scope. |
| 400 | `invalid_since` / `invalid_until` | Date filter wasn't ISO-8601. |
+ | 404 | `user_not_found` / `post_not_found` | Resource doesn't exist. |
+ | 429 | `rate_limited` | Slow down — see `Retry-After`. |
+ | 500 | `internal_error` | Server error. Include the `request_id` when reporting. |
| 400 | `already_resolved` | `POST /reports/{id}/dismiss` or `/resolve` on a report that is already resolved. |
| 404 | `user_not_found` / `post_not_found` / `listing_not_found` / `file_not_found` / `report_not_found` | Resource doesn't exist or isn't visible. |
| 429 | `rate_limited` | Slow down — see `Retry-After`. |
| 500 | `internal_error` | Server error. Include the `request_id` when reporting. |

Authentication, scopes, and rate limits

API keys

Every API request must send an Authorization: Bearer <key> header. Keys look like:

mc_live_YJs9gvYaRhL-6An-hdHlmQb0pTqfGC38hbAT3WwGrs4
mc_test_abcdefghijklmnopqrstuvwxyz0123456789ABCD

The prefix tells you (and any code reviewer) whether the key is live or test data.

How keys are stored

  • Mobieus stores only a SHA-256 hash of the key — the plaintext is shown once at creation and is unrecoverable.
  • Lookup uses constant-time comparison so attackers can't grind hashes by timing.
  • The first ~12 characters are stored separately so the admin UI can identify the key by prefix without ever seeing the secret.

Managing keys

Tenant super-admins manage keys at /admin/api-keys:

Action What happens
Create key Generates a fresh key. The plaintext is shown once on the next page — copy it now, you cannot recover it.
Revoke Immediate. The next request with that key returns 401 invalid_api_key. Not reversible.
Expire Optional expires_at. Keys past their expiry start returning 401 automatically.

Every create and revoke is audit-logged.

Scopes

Every key has one or more scopes. Endpoints check the scope before processing — a key without events:read calling GET /api/v1/events gets 403 insufficient_scope.

Scope Lets the key…
events:read Read /api/v1/events
users:read Read /api/v1/users and /api/v1/users/{id}
posts:read Read /api/v1/posts and /api/v1/posts/{id}
webhooks:read List webhook endpoints + delivery history (Phase 2)
webhooks:manage Create / update / delete webhook endpoints (Phase 2)

Principle of least privilege: only grant the scopes the integration actually needs.

Tenant isolation

A key is bound to the tenant it was minted in. A mc_live_… key from support.mobieus.io will not authenticate against fort-smith-live.mobieus.io, and there is no API surface to query a different tenant's data with it. Every endpoint mounts under the tenant subdomain; tenant scoping happens at the database connection layer, so cross-tenant queries are structurally impossible.

Rate limits

Default: 600 requests per minute per key. Every response carries:

  • X-RateLimit-Limit — the ceiling
  • X-RateLimit-Remaining — what's left in the current window
  • X-RateLimit-Reset — Unix timestamp when the window resets

On exceed you get 429 Too Many Requests plus a Retry-After: <seconds> header. Back off, then retry.

If you need a higher limit, raise it for your tenant by adjusting api.rate_limit_per_minute in config/app.ini or contact ops.

Request IDs

Every response carries X-Request-Id: req_xxxxxxxxxxxxxxxx. The same id appears in the JSON body as request_id. Include it whenever you contact support — it lets us correlate your call with the server log instantly.

Error envelope

Every error response uses the same JSON shape:

{
  "error": {
    "code": "invalid_api_key",
    "message": "The API key is invalid, revoked, or expired.",
    "request_id": "req_3f2b8d9c10ab7c92"
  }
}

Common codes:

HTTP error.code Meaning
401 missing_authorization No Authorization header.
401 invalid_authorization Header wasn't a Bearer token.
401 invalid_api_key Bad key, revoked, or expired.
403 insufficient_scope Key valid but missing the required scope.
400 invalid_since / invalid_until Date filter wasn't ISO-8601.
404 user_not_found / post_not_found Resource doesn't exist.
429 rate_limited Slow down — see Retry-After.
500 internal_error Server error. Include the request_id when reporting.
# Authentication, scopes, and rate limits

## API keys

Every API request must send an `Authorization: Bearer <key>` header. Keys look like:

```
mc_live_YJs9gvYaRhL-6An-hdHlmQb0pTqfGC38hbAT3WwGrs4
mc_test_abcdefghijklmnopqrstuvwxyz0123456789ABCD
```

The prefix tells you (and any code reviewer) whether the key is live or test data.

### How keys are stored

- Mobieus stores only a **SHA-256 hash** of the key — the plaintext is shown **once** at creation and is unrecoverable.
- Lookup uses constant-time comparison so attackers can't grind hashes by timing.
- The first ~12 characters are stored separately so the admin UI can identify the key by prefix without ever seeing the secret.

### Managing keys

Tenant super-admins manage keys at **`/admin/api-keys`**:

| Action | What happens |
|---|---|
| **Create key** | Generates a fresh key. The plaintext is shown once on the next page — copy it now, you cannot recover it. |
| **Revoke** | Immediate. The next request with that key returns `401 invalid_api_key`. Not reversible. |
| **Expire** | Optional `expires_at`. Keys past their expiry start returning `401` automatically. |

Every create and revoke is audit-logged.

## Scopes

Every key has one or more scopes. Endpoints check the scope before processing — a key without `events:read` calling `GET /api/v1/events` gets `403 insufficient_scope`.

| Scope | Lets the key… |
|---|---|
| `events:read` | Read `/api/v1/events` |
| `users:read` | Read `/api/v1/users` and `/api/v1/users/{id}` |
| `posts:read` | Read `/api/v1/posts` and `/api/v1/posts/{id}` |
| `webhooks:read` | List webhook endpoints + delivery history *(Phase 2)* |
| `webhooks:manage` | Create / update / delete webhook endpoints *(Phase 2)* |

**Principle of least privilege**: only grant the scopes the integration actually needs.

## Tenant isolation

A key is bound to the tenant it was minted in. A `mc_live_…` key from `support.mobieus.io` will not authenticate against `fort-smith-live.mobieus.io`, and there is no API surface to query a different tenant's data with it. Every endpoint mounts under the tenant subdomain; tenant scoping happens at the database connection layer, so cross-tenant queries are structurally impossible.

## Rate limits

Default: **600 requests per minute per key**. Every response carries:

- `X-RateLimit-Limit` — the ceiling
- `X-RateLimit-Remaining` — what's left in the current window
- `X-RateLimit-Reset` — Unix timestamp when the window resets

On exceed you get `429 Too Many Requests` plus a `Retry-After: <seconds>` header. Back off, then retry.

If you need a higher limit, raise it for your tenant by adjusting `api.rate_limit_per_minute` in `config/app.ini` or contact ops.

## Request IDs

Every response carries `X-Request-Id: req_xxxxxxxxxxxxxxxx`. The same id appears in the JSON body as `request_id`. Include it whenever you contact support — it lets us correlate your call with the server log instantly.

## Error envelope

Every error response uses the same JSON shape:

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The API key is invalid, revoked, or expired.",
    "request_id": "req_3f2b8d9c10ab7c92"
  }
}
```

Common codes:

| HTTP | `error.code` | Meaning |
|---|---|---|
| 401 | `missing_authorization` | No `Authorization` header. |
| 401 | `invalid_authorization` | Header wasn't a Bearer token. |
| 401 | `invalid_api_key` | Bad key, revoked, or expired. |
| 403 | `insufficient_scope` | Key valid but missing the required scope. |
| 400 | `invalid_since` / `invalid_until` | Date filter wasn't ISO-8601. |
| 404 | `user_not_found` / `post_not_found` | Resource doesn't exist. |
| 429 | `rate_limited` | Slow down — see `Retry-After`. |
| 500 | `internal_error` | Server error. Include the `request_id` when reporting. |