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.
| Auth | JWT bearer or X-API-Key (see API Keys) |
| Scoping | Staff see all sessions. All other callers see only sessions for AI characters they created — results are auto-filtered, no extra params needed. |
| Pagination | Page-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
| Param | Type | Description |
|---|---|---|
character_slug | string | Filter by character. Returns only sessions for this agent slug. |
tenant_id | string | Filter by tenant. |
start | ISO datetime | Lower bound on created_at (inclusive). |
end | ISO datetime | Upper bound on created_at (inclusive). |
page | int | 1-based page number (default 1). |
page_size | int | Results 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=100Response
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.
| Field | Meaning |
|---|---|
total_sessions | Count of matching sessions |
total_llm_input_tokens / total_llm_output_tokens | Sum across all matching sessions |
total_stt_audio_seconds | Total seconds of audio transcribed |
total_tts_characters | Total characters spoken by the agent |
total_estimated_cost_usd | Sum 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:
| Field | Use |
|---|---|
session_id | Unique ID; pass to chat-history endpoint for a single-session view |
created_at | When the session started |
finalized_at | When it ended (null for in-progress) |
session_duration_seconds | Length of the call |
turn_count | Conversation depth |
transcript / transcript_text | Full conversation, inline |
metadata | The metadata you passed at session start |
origin_url | Page the call came from |
recording_presigned_url | Time-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
| Status | Cause |
|---|---|
401 Unauthorized | Missing / expired JWT, or invalid / inactive X-API-Key |
400 Bad Request | Malformed 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());Related
- Single session detail / transcript → Chat History
- Starting new sessions → Sessions
- Form submissions per session → Form Responses