owlette docs
api

authentication

Owlette public API calls authenticate with scoped API keys. Dashboard and setup flows may also use first-party sessions or Firebase ID tokens, but external integrations should use owk_live_* or owk_test_* keys.

Last updated: 2026-04-28


bearer header

Send API keys in the standard bearer header:

Authorization: Bearer owk_live_...

Compatibility paths remain available on selected routes during developer preview:

  • x-api-key: owk_live_...
  • ?api_key=owk_live_...

New clients should use Authorization: Bearer. Query-string keys are easy to leak through logs and should be avoided outside compatibility cases such as EventSource/SSE clients that cannot set headers.


key format

prefixenvironmentuse
owk_live_liveproduction fleets, production audit records, billable usage
owk_test_testsandbox/dev integrations and non-production automation

The raw key suffix is a 32-byte random value encoded as base64url, currently 43 characters. Treat the full key as opaque. The API only returns the raw key once, at creation.

Stored and list responses expose a keyPrefix for display and audit correlation; they never return the full raw key again.


creating keys

dashboard

Use Settings -> API Keys in the dashboard for normal key creation. Choose:

  • name
  • environment: live or test
  • scopes
  • expiration

Copy the raw key immediately into a secrets manager.

API creation split

POST /api/keys creates scoped user API keys and requires a signed-in user session or Firebase ID token. You cannot bootstrap a new key with an API key.

/api/account/api-keys is a superadmin/platform convenience surface. It is not the normal self-service key bootstrap path for public integrations.

This split is intentional: a leaked API key should not be able to mint a broader replacement for itself.


verifying identity

GET /api/whoami returns the resolved caller, active key context, scope list, rate-limit summary, quota summary, and primary site when available. The rateLimit block is a metadata hint, not the active enforcement counter. Use RateLimit-* response headers on actual API calls for enforced limits and live counters.

curl -fsS "https://owlette.app/api/whoami" \
  -H "Authorization: Bearer $OWLETTE_API_KEY" | jq

Representative API-key response:

{
  "userId": "user_01HWABCD1234EFGH5678IJKL90",
  "email": "ops@example.com",
  "role": "admin",
  "key": {
    "keyId": "9e05dd1e-47ac-4a59-a8bf-00b38faea1b4",
    "name": "ci preview",
    "keyPrefix": "owk_live_kB8n3p",
    "scopes": [
      { "resource": "site", "id": "kiosk-fleet-01", "permissions": ["read"] },
      { "resource": "machine", "id": "*", "permissions": ["read", "write"] }
    ],
    "environment": "live",
    "expiresAt": 1798049400000,
    "lastUsedAt": 1777408200000,
    "isLegacy": false
  },
  "rateLimit": {
    "tier": "api",
    "limitPerMinute": 600,
    "note": "metadata hint only; use RateLimit-* response headers on actual API calls for enforced limits and live counters"
  },
  "quota": {
    "siteId": "kiosk-fleet-01",
    "tier": "pro",
    "usedBytes": 23456789012,
    "pendingBytes": 104857600,
    "limitBytes": 107374182400
  },
  "primarySiteId": "kiosk-fleet-01"
}

When the request is authenticated by a session or Firebase ID token instead of an API key, key is null.


scopes

Each key has a scopes[] array. A request is allowed only when the key has a matching resource, id, and permission, and the owning user still has access to the resource.

[
  { "resource": "site", "id": "kiosk-fleet-01", "permissions": ["read"] },
  { "resource": "machine", "id": "*", "permissions": ["read", "write"] },
  { "resource": "chat", "id": "kiosk-fleet-01", "permissions": ["read", "write"] }
]

Scope fields:

fieldmeaning
resourceOne of roost, site, machine, chat, deploy, process, user, or installer.
idSpecific resource id, or * for every resource of that type the owning user can access.
permissionsNon-empty list of exact permissions.

Permissions are exact. write does not imply read, deploy does not imply write, and admin does not imply every other permission. Include every permission the integration needs.

Supported permissions:

permissioncommon use
readlist and detail reads
writecreate, update, publish, or queue writable operations
deployrollout or deployment actions where separately modeled
rollbackrollback actions
adminadministrative operations such as webhook management, log clearing, or platform resources

user and installer scopes are superadmin-only at key creation time.


presets

The dashboard exposes presets as a starting point:

presetgrants
readonlyread on common resources
publisherread and write on common resources
operatorread, write, deploy, and rollback on common resources
adminread, write, deploy, rollback, and admin on common resources

Narrow presets before use when possible. Prefer one key per integration and one environment per key.


expiration, rotation, and revocation

Every scoped key expires. Current defaults are:

  • default TTL: 90 days
  • maximum TTL: 365 days
  • rotation grace: 24 hours for the old key value

Rotate a key when the secret might have leaked or when your normal rotation schedule requires it. Rotation returns a new raw key once and leaves the key's scope policy intact.

Revoke a key when the integration is retired or when rotation is not enough. Revocation is permanent; create a new key if access is needed again.

Self-service lifecycle endpoints require a signed-in user session or Firebase ID token:

  • POST /api/keys/{keyId}/rotate issues a replacement key with the same scopes and environment, returns the new raw key once, and leaves the previous key valid only for the rotation grace window.
  • DELETE /api/keys/{keyId} revokes the key immediately by deleting its user record and lookup entry.

auth errors

statuscodemeaning
401unauthorizedMissing, malformed, unknown, or unsupported credential.
401token_expiredThe API key's expiresAt has passed. A rotated old key after its grace window is treated as 401 unauthorized / invalid API key.
403scope_insufficientCredential is valid but lacks the exact resource/id/permission needed.
403forbiddenCaller lacks the required role, site membership, or platform capability.
404not_foundResource is absent or intentionally hidden from this caller.

All public errors use the problem envelope. Do not branch on error prose; branch on code.


key handling

  • Store keys in a secrets manager, not source code.
  • Never log raw keys.
  • Use separate keys for CI, local development, monitoring, and each external integration.
  • Use test keys for development automation and live keys only for production work.
  • Keep scopes narrow and add missing permissions intentionally after a 403 scope_insufficient.
  • Revoke unused keys promptly.

see also

on this page