When to use an API key

Use an API key when:
  • Your backend needs to call Oshara endpoints (sync transcripts into your CRM, build a customer-facing dashboard, automate reports).
  • You want to authenticate without managing a user’s JWT refresh cycle.
  • You need to revoke access independently — disable one key without rotating user passwords.
For browser-side calls (the widget, session-start from a logged-in user) keep using JWT. Don’t expose API keys to the browser.

What a key can do

An API key inherits the permissions of the user who created it. It can read any data the owning user can read:
EndpointWhat the key can access
Session HistoryGET /api/billing/usage/Sessions for any AI character the key’s owner created
Chat HistoryGET /api/billing/sessions/<id>/Full transcript + recording URL for owned-character sessions
Form ResponsesGET /api/agents/<slug>/form-responses/Form submissions for owned characters
Non-owned data returns 404 (or empty results on list endpoints) — keys never see another user’s sessions.

Sending the header

X-API-Key: sk_<32-byte hex>
curl https://api.oshara.ai/api/billing/sessions/sess_a1b2c3/ \
  -H "X-API-Key: sk_a1b2c3d4e5f6..."
Both X-API-Key and x-api-key work (HTTP headers are case-insensitive); use the capitalised form in docs and code. When a request carries both a valid X-API-Key and a JWT, the API key wins — auth resolves to the API key’s user.

Create a key

POST /api/auth/api-keys/
Authorization: Bearer <jwt>
Content-Type: application/json
curl -X POST https://api.oshara.ai/api/auth/api-keys/ \
  -H "Authorization: Bearer <jwt>" \
  -H "Content-Type: application/json" \
  -d '{"name": "transcript-sync-prod"}'

Request body

FieldTypeRequiredDescription
namestringHuman-readable label. Pick something that tells your future self where the key is used ("crm-sync", "staging-dashboard").
is_activebooleanDefaults to true. Set false to create a disabled key.

Response (201 Created)

The full key value is returned only once — store it immediately.
{
  "success": true,
  "message": "Request successful",
  "data": {
    "message": "API key created successfully. Save this key safely — you won't see it again!",
    "api_key": {
      "id": 17,
      "name": "transcript-sync-prod",
      "key": "sk_a1b2c3d4e5f6789...",
      "created_at": "2026-05-28T14:23:00Z",
      "is_active": true
    }
  },
  "errors": null
}

List your keys

GET /api/auth/api-keys/
Authorization: Bearer <jwt>
curl https://api.oshara.ai/api/auth/api-keys/ \
  -H "Authorization: Bearer <jwt>"
The full key value is not included — only a masked preview. If you lost a key, create a new one and revoke the old.

Response

{
  "data": [
    {
      "id": 17,
      "name": "transcript-sync-prod",
      "key_preview": "sk_a1...f6789",
      "created_at": "2026-05-28T14:23:00Z",
      "last_used_at": "2026-05-28T18:42:11Z",
      "is_active": true
    },
    {
      "id": 16,
      "name": "old-zapier-key",
      "key_preview": "sk_z9...4231",
      "created_at": "2026-03-14T09:00:00Z",
      "last_used_at": null,
      "is_active": false
    }
  ]
}
FieldDescription
key_previewFirst 5 + last 4 chars of the key, e.g. sk_a1...f6789. Useful for matching keys to environments.
last_used_atTimestamp of the most recent successful request. null if never used. Updated on every authenticated call.
is_activeIf false, the key is rejected.

Revoke a key

Disable a key without deleting it (preferred — keeps the audit row):
curl -X PATCH https://api.oshara.ai/api/auth/api-keys/17/ \
  -H "Authorization: Bearer <jwt>" \
  -H "Content-Type: application/json" \
  -d '{"is_active": false}'
Permanently delete a key (loses history):
curl -X DELETE https://api.oshara.ai/api/auth/api-keys/17/ \
  -H "Authorization: Bearer <jwt>"
A revoked key returns 401 Unauthorized on subsequent requests.

Rename a key

curl -X PATCH https://api.oshara.ai/api/auth/api-keys/17/ \
  -H "Authorization: Bearer <jwt>" \
  -H "Content-Type: application/json" \
  -d '{"name": "transcript-sync-prod-v2"}'

Security guidance

  • One key per integration. If staging-dashboard is compromised, you revoke only that key — crm-sync keeps working.
  • Never embed in browser code. Keys give full read access to all your sessions/transcripts. Keep them server-side.
  • Store as a secret. Use a secret manager (AWS Secrets Manager, HashiCorp Vault, GitHub Actions secrets). Don’t commit to git.
  • Rotate periodically. Create the new key, deploy it, then revoke the old one — overlapping rotation, no downtime.
  • Monitor last_used_at. A key with last_used_at going stale or appearing from an unexpected IP is a signal to investigate.

Errors

StatusCauseFix
401 UnauthorizedMissing header, unknown key, or is_active: falseCheck the header is exactly X-API-Key and the key is active
404 Not Found on a session/character endpointKey’s owner doesn’t own that characterUse a key owned by the character’s creator, or grant access via the dashboard
200 with empty resultsSame as above, on list endpointsConfirm character_slug filter matches a character the owner created

Code examples

Node.js (server-side)

const OSHARA = "https://api.oshara.ai";

async function fetchSession(sessionId) {
  const r = await fetch(`${OSHARA}/api/billing/sessions/${sessionId}/`, {
    headers: { "X-API-Key": process.env.OSHARA_API_KEY }
  });
  if (!r.ok) throw new Error(`Oshara ${r.status}`);
  return (await r.json()).data;
}

Python (server-side)

import os, httpx

OSHARA = "https://api.oshara.ai"
HEADERS = {"X-API-Key": os.environ["OSHARA_API_KEY"]}

def fetch_session(session_id: str) -> dict:
    r = httpx.get(f"{OSHARA}/api/billing/sessions/{session_id}/", headers=HEADERS)
    r.raise_for_status()
    return r.json()["data"]

Sync new sessions hourly

const since = new Date(Date.now() - 3600_000).toISOString();
let url =
  `${OSHARA}/api/billing/usage/` +
  `?character_slug=support-bot&start=${since}`;

while (url) {
  const { data } = await fetch(url, {
    headers: { "X-API-Key": process.env.OSHARA_API_KEY }
  }).then(r => r.json());

  for (const s of data.sessions.data) {
    await db.sessions.upsert(s);
  }
  url = data.sessions.pagination.next;
}