List sessions

GET /api/billing/usage/
Returns a paginated list of AgentSessionSummary objects plus aggregate totals. Filter by character to get the session history of a specific AI character.
AuthJWT bearer or X-API-Key (see API Keys)
ScopingStaff see all sessions. All other callers see only sessions for AI characters they created — results are auto-filtered, no extra params needed.
PaginationPage-based, default 50 / page, max 200
# Using an API key (recommended for backends)
curl "https://api.oshara.ai/api/billing/usage/?character_slug=support-bot" \
  -H "X-API-Key: sk_..."

# Or using a JWT
curl "https://api.oshara.ai/api/billing/usage/?character_slug=support-bot" \
  -H "Authorization: Bearer <token>"

Query parameters

ParamTypeDescription
character_slugstringFilter by character. Returns only sessions for this agent slug.
tenant_idstringFilter by tenant.
startISO datetimeLower bound on created_at (inclusive).
endISO datetimeUpper bound on created_at (inclusive).
pageint1-based page number (default 1).
page_sizeintResults per page (default 50, max 200).

Examples

# All sessions for one character
GET /api/billing/usage/?character_slug=support-bot

# Sessions in May 2026 for one character
GET /api/billing/usage/?character_slug=support-bot&start=2026-05-01T00:00:00Z&end=2026-05-31T23:59:59Z

# Second page, 100 per page
GET /api/billing/usage/?character_slug=support-bot&page=2&page_size=100

Response

Wrapped in the standard envelope ({ success, message, data, errors }). The data field contains:
{
  "total_sessions": 1284,
  "total_llm_input_tokens": 1840234,
  "total_llm_output_tokens": 612480,
  "total_stt_audio_seconds": 18420.6,
  "total_tts_characters": 1284512,
  "total_estimated_cost_usd": "18.250400",

  "sessions": {
    "data": [
      {
        "id": 142,
        "session_id": "sess_a1b2c3",
        "tenant_id": "acme",
        "character_slug": "support-bot",
        "user": 7,
        "room_name": "oshara-voice-support-bot-user123-abc12",
        "origin_url": "https://yoursite.com/contact",
        "metadata": { "user_id": "u_42" },
        "session_duration_seconds": 184.7,
        "turn_count": 14,
        "interruption_count": 2,
        "llm_input_tokens": 1840,
        "llm_output_tokens": 612,
        "llm_model": "gpt-4o-mini",
        "stt_audio_seconds": 92.3,
        "tts_characters": 1284,
        "estimated_cost_usd": "0.014200",
        "finalized_at": "2026-05-28T14:23:48Z",
        "recording_status": "READY",
        "recording_url": "s3://oshara-recordings/sess_a1b2c3.mp4",
        "recording_presigned_url": "https://s3.amazonaws.com/...?X-Amz-Signature=...",
        "transcript": [
          { "role": "assistant", "text": "Hi! How can I help you today?", "turn_index": 0, "ts": 0.42 },
          { "role": "user",      "text": "I want to book a demo.",         "turn_index": 1, "ts": 3.18 }
        ],
        "transcript_text": "Assistant: Hi!...\nUser: I want to book a demo.",
        "created_at": "2026-05-28T14:20:43Z"
      }
    ],
    "pagination": {
      "count": 1284,
      "next": "https://api.oshara.ai/api/billing/usage/?character_slug=support-bot&page=2",
      "previous": null
    }
  }
}

Aggregate totals

The top-level fields reflect the filtered queryset — totals for sessions matching your character_slug / date range, not the whole tenant.
FieldMeaning
total_sessionsCount of matching sessions
total_llm_input_tokens / total_llm_output_tokensSum across all matching sessions
total_stt_audio_secondsTotal seconds of audio transcribed
total_tts_charactersTotal characters spoken by the agent
total_estimated_cost_usdSum of estimated_cost_usd
Use these for billing dashboards without having to fetch every page.

Per-session fields

Each entry in sessions.data is a full AgentSessionSummary — same shape as Chat History. That means you get the transcript inline for every session in the list, no follow-up calls needed. Notable fields for character history:
FieldUse
session_idUnique ID; pass to chat-history endpoint for a single-session view
created_atWhen the session started
finalized_atWhen it ended (null for in-progress)
session_duration_secondsLength of the call
turn_countConversation depth
transcript / transcript_textFull conversation, inline
metadataThe metadata you passed at session start
origin_urlPage the call came from
recording_presigned_urlTime-limited URL to play the audio recording

Pagination

sessions.pagination.next and previous are absolute URLs you can fetch directly. count is the total before pagination.
async function* iterAllSessions(slug) {
  let url = `https://api.oshara.ai/api/billing/usage/?character_slug=${slug}`;
  while (url) {
    const { data } = await fetch(url, {
      headers: { "X-API-Key": process.env.OSHARA_API_KEY }
    }).then(r => r.json());

    for (const session of data.sessions.data) yield session;
    url = data.sessions.pagination.next;
  }
}

for await (const s of iterAllSessions("support-bot")) {
  console.log(s.session_id, s.session_duration_seconds, s.transcript.length, "turns");
}

Errors

StatusCause
401 UnauthorizedMissing / expired JWT, or invalid / inactive X-API-Key
400 Bad RequestMalformed start / end datetime
Note: callers never get a 403 here. If you filter by a character_slug you don’t own, the response is a 200 with an empty sessions.data array — the scoping is enforced silently at the queryset level.

Common patterns

Daily report for one character

const today    = new Date().toISOString().slice(0, 10);
const tomorrow = new Date(Date.now() + 86_400_000).toISOString().slice(0, 10);

const stats = await fetch(
  `https://api.oshara.ai/api/billing/usage/?character_slug=support-bot` +
  `&start=${today}T00:00:00Z&end=${tomorrow}T00:00:00Z`,
  { headers: { "X-API-Key": process.env.OSHARA_API_KEY } }
).then(r => r.json());

console.log(`Today: ${stats.data.total_sessions} calls, ` +
            `${stats.data.total_estimated_cost_usd} USD`);

Compare characters

const slugs = ["support-bot", "sales-agent", "onboarding-bot"];
const results = await Promise.all(
  slugs.map(slug =>
    fetch(`https://api.oshara.ai/api/billing/usage/?character_slug=${slug}`, {
      headers: { "X-API-Key": process.env.OSHARA_API_KEY }
    }).then(r => r.json()).then(j => ({ slug, ...j.data }))
  )
);

console.table(results.map(r => ({
  slug:     r.slug,
  sessions: r.total_sessions,
  cost:     r.total_estimated_cost_usd,
  avg_len:  r.total_stt_audio_seconds / r.total_sessions,
})));

Sync new sessions hourly into your DB

const since = await db.kv.get("oshara:last_synced") ?? new Date(Date.now() - 3600_000).toISOString();

for await (const s of iterAllSessions("support-bot", { start: since })) {
  await db.sessions.upsert({
    session_id:    s.session_id,
    started_at:    s.created_at,
    duration:      s.session_duration_seconds,
    cost:          s.estimated_cost_usd,
    transcript:    s.transcript_text,
    user_metadata: s.metadata,
  });
}

await db.kv.set("oshara:last_synced", new Date().toISOString());