Tanyalah UstazFor Business

API reference

Every endpoint is prefixed with https://api-v2.tanyalahustaz.com/api/v1.

GET/healthPublic

Health check. No authentication required.

Response

{ "status": "ok", "version": "v1", "timestamp": "..." }
GET/meAuth required

Returns the authenticated account profile and API key metadata.

Response

{
  "partner": { "id": "uuid", "email": "...", "company_name": "..." },
  "api_key": { "id": "uuid", "name": "Production", "last_used_at": null }
}
POST/chatAuth required

Main integration endpoint. Sends a user message, retrieves relevant knowledge from the Tanyalah Ustaz library, and returns an AI reply with sources.

Request body

{
  "message": "Can a traveler combine Dhuhr and Asr?",
  "category": "fiqh",
  "session_id": "your-user-session-id"
}

Response

{
  "reply": "Travelers may combine Dhuhr with Asr...",
  "session_id": "your-user-session-id",
  "sources": [{ "slug": "jamak-solat-musafir", "title": "...", "category": "fiqh" }]
}

// Stream tokens as they arrive (text/event-stream):
//   { "message": "...", "stream": true }
data: {"type":"meta","session_id":"...","sources":[...]}
data: {"type":"text","content":"Travelers "}
data: {"type":"text","content":"may combine..."}
data: {"type":"done"}
GET/chat/sessionsAuth required

List conversation sessions for your account, newest activity first. Supports ?limit and ?offset.

Response

{
  "data": [{
    "session_id": "your-user-session-id",
    "turn_count": 3,
    "created_at": "...",
    "updated_at": "...",
    "last_user_message": "Can a traveler combine Dhuhr and Asr?"
  }],
  "pagination": { "limit": 20, "offset": 0, "total": 1 }
}
DELETE/chat/sessionsAuth required

Delete all chat history for your account. Requires ?confirm=all.

Response

{ "deleted": true, "deleted_turns": 12 }
GET/chat/sessions/:sessionIdAuth required

Get chronological chat history for a session. Pagination limit/offset apply to conversation turns (each turn is one user + one assistant message).

Response

{
  "session_id": "your-user-session-id",
  "data": [
    { "id": "...-user", "role": "user", "content": "...", "created_at": "..." },
    { "id": "...-assistant", "role": "assistant", "content": "...", "sources": [...], "created_at": "..." }
  ],
  "pagination": { "limit": 20, "offset": 0, "total": 3 }
}
DELETE/chat/sessions/:sessionIdAuth required

Delete one chat session and all of its turns.

Response

{
  "deleted": true,
  "session_id": "your-user-session-id",
  "deleted_turns": 3
}
GET/usageAuth required

Usage stats for the current API key over the last 30 days.

Response

{
  "period_days": 30,
  "total_requests": 12,
  "top_endpoints": [{ "endpoint": "/api/v1/chat", "count": 8 }],
  "recent_requests": [...]
}
GET/openapi.jsonPublic

OpenAPI 3.1 specification for this API. No authentication required.

Response

{ "openapi": "3.1.0", "info": { "title": "Tanyalah Ustaz Developer API", "version": "1.0.0" }, ... }

Streaming responses

Add "stream": true to POST /chat to receive the reply as server-sent events (SSE) instead of waiting for the full JSON. The first event carries the session_id and sources; text events follow as tokens arrive. This is the recommended way to make your UI feel fast.

Event shapes

data: {"type":"meta","session_id":"...","sources":[...]}
data: {"type":"text","content":"Travelers "}
data: {"type":"text","content":"may combine..."}
data: {"type":"done"}

Browser example

Read the stream and append each text chunk:

const response = await fetch("https://api-v2.tanyalahustaz.com/api/v1/chat", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer tlh_live_...",
  },
  body: JSON.stringify({ message: "Can a traveler combine prayers?", stream: true }),
});

const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = "";

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });

  const parts = buffer.split("\n\n");
  buffer = parts.pop() ?? "";

  for (const part of parts) {
    const line = part.split("\n").find((entry) => entry.startsWith("data:"));
    if (!line) continue;
    const event = JSON.parse(line.slice(5).trim());
    if (event.type === "text") appendToUi(event.content);
    if (event.type === "done") finish();
  }
}