Skip to content

SDK and examples

Everything you need to call Shara from your code: Bearer token authentication, the endpoint an API key can use and ready-to-copy examples in curl, TypeScript (fetch) and Python. The API is plain REST over HTTPS, with JSON bodies and responses, so any standard HTTP client works. There is no public SDK yet: the official SDKs arrive with general availability (GA) and the internal package is private.

The host is https://api.sharasaas.com. Transport is HTTPS only (TLS 1.2+), request and response bodies are JSON (Content-Type: application/json) and streaming mode returns Server-Sent Events (text/event-stream). Inference and the database are hosted in the European Union.

Property Value
Endpoint POST https://api.sharasaas.com/v1/inference
Protocol HTTPS (TLS 1.2+)
Format application/json · streaming in text/event-stream
Authentication Authorization: Bearer <api-key>
Rate limit 60 req/min per IP and route · 600 req/min per workspace and route (X-RateLimit-* headers)
Maximum body size 512 KB
Traceability X-Request-Id header on every response

Integrations authenticate with a Shara API key. You generate it from Settings → Security and send it on every request with the standard Authorization header:

Authorization: Bearer shara_sk_live_YOUR_API_KEY

The key has the fixed prefix shara_sk_live_ followed by a secret part. It is shown in full only once, at the moment you create it: save it in your secrets manager. After that, the dashboard only shows the prefix so you can identify it. Each key carries scopes (minimum permissions; to invoke agents it needs amadeus:invoke, or amadeus:invoke:agent:<slug> for a single agent) and expires after 90 days by default (365 at most).

Rotating a key revokes the old one first and then creates the new one, so there is a cut. To avoid it: create the new key, deploy it and only then revoke the old one. Key management routes, scopes and expiry are in the REST API page.

Treat the API key like a server password: never embed it in the browser, in mobile apps or in public repositories. It lives in backend environment variables. If you suspect it has leaked, revoke it from Settings → Security and issue a new one.

An API key works on POST /v1/inference (and on the MCP bridge, with the mcp:tenant scope). The workspace orchestrator is Amadeus: it receives your request, decides which departmental agent handles it and coordinates the work. You can address Amadeus (and let it route) or a specific agent by its slug in the agentSlug field.

Method Path What it does
POST /v1/inference Sends the turn to an agent and returns the result in the same response (normal or SSE).

Routes such as /runs and /usage/current only work with a signed-in app session, not with an API key. Shara does not offer inbound webhooks yet. The full list of routes is in the REST API page.

Every agent has an API slug, and the API always uses it, whatever name pack your company has chosen. The full table (public name, department and slug) is in the agent catalog. The slug is the agent’s public name in lowercase (amadeus, wanamaker, patterson, graham, holmes, kerckhoffs…).

The agents available to your company depend on your plan and on the agents you chose or hired. An agent that is not available to you answers 403.

A minimal request sends a model alias and the list of messages. The alias is the only thing accepted to select the model: Prelude (agile and economical, on every plan), Sonata (cost/quality balance, from Pro) and Symphony (the most capable, from Max) are the cloud models, in growing order of capacity; Concerto is the local model of the Concerto Local plan. Your plan sets the ceiling: asking for an alias above it returns 422 alias_plan_mismatch.

curl -sS https://api.sharasaas.com/v1/inference \
  -H "Authorization: Bearer shara_sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "alias": "Sonata",
    "agentSlug": "amadeus",
    "messages": [
      { "role": "user", "content": "Summarize this week’s support news." }
    ],
    "effort": "medium"
  }'

The call is synchronous: the response (200) carries the alias used, the content (already redacted), the usage breakdown in tokens and how the turn ended in status (completed, failed, blocked or canceled; read it, because a failed or blocked turn is still HTTP 200):

{
  "alias": "Sonata",
  "content": "This week support closed 34 tickets…",
  "usage": {
    "inputTokens": 184,
    "outputTokens": 412
  },
  "status": "completed",
  "runId": "3f8a1c52-9d4e-4b7a-a1c3-5e2f7d9b0a64"
}

Depending on the turn, it can also carry toolOutcome, blockedReason and motivoDeCorte. All fields are described in REST API.

With "stream": true the response arrives as text/event-stream: fragments as data: {"delta":"…"} and a closing end event with status, runId and the token counts. Use curl -N to see it live:

curl -sS -N https://api.sharasaas.com/v1/inference \
  -H "Authorization: Bearer shara_sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "alias": "Prelude",
    "stream": true,
    "messages": [{ "role": "user", "content": "Write a two-line greeting." }]
  }'
data: {"delta":"Hello, "}

data: {"delta":"welcome."}

event: end
data: {"done":true,"status":"completed","runId":"3f8a1c52-9d4e-4b7a-a1c3-5e2f7d9b0a64","alias":"Prelude","usage":{"inputTokens":21,"outputTokens":9}}

The stream may carry other named events (for example usage or error); ignore the ones you don’t use.

Equivalent in Node.js / TypeScript with native fetch. Always branch on the error field (a stable string), never on the text of message:

const BASE = 'https://api.sharasaas.com';
const API_KEY = process.env.SHARA_API_KEY; // never on the client
if (API_KEY === undefined) throw new Error('Missing SHARA_API_KEY');

interface InferenceResponse {
  alias: string;
  content: string;
  usage: { inputTokens: number; outputTokens: number };
  status: 'completed' | 'failed' | 'blocked' | 'canceled';
  runId?: string;
  toolOutcome?: { rejected: string[]; expired: string[] };
  blockedReason?: string;
  motivoDeCorte?: 'tope_de_pasos' | 'tope_de_gasto' | 'sin_cierre';
}

const res = await fetch(`${BASE}/v1/inference`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    alias: 'Sonata',
    agentSlug: 'amadeus',
    messages: [
      { role: 'user', content: 'Summarize this week’s support news.' },
    ],
    effort: 'medium',
  }),
});

if (!res.ok) {
  const { error } = (await res.json()) as { error: string };
  throw new Error(`Shara API ${res.status}: ${error}`);
}

const data = (await res.json()) as InferenceResponse;
console.log(data.status, data.content, data.usage);

And reading the stream:

const stream = await fetch(`${BASE}/v1/inference`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    alias: 'Prelude',
    stream: true,
    messages: [{ role: 'user', content: 'Write a two-line greeting.' }],
  }),
});
if (!stream.ok || stream.body === null) throw new Error(`Shara API ${stream.status}`);

const reader = stream.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
for (;;) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  const blocks = buffer.split('\n\n');
  buffer = blocks.pop() ?? '';
  for (const block of blocks) {
    const event = /^event: (.+)$/m.exec(block)?.[1] ?? 'message';
    const raw = /^data: (.+)$/m.exec(block)?.[1];
    if (raw === undefined) continue;
    let payload: { delta?: string; status?: string; runId?: string };
    try {
      payload = JSON.parse(raw);
    } catch {
      continue; // not a JSON event: ignore it
    }
    if (event === 'message' && payload.delta !== undefined) process.stdout.write(payload.delta);
    if (event === 'end') console.log('\n', payload.status, payload.runId);
  }
}
import json
import os

import requests

BASE = "https://api.sharasaas.com"
HEADERS = {"Authorization": f"Bearer {os.environ['SHARA_API_KEY']}"}  # server side only

# Normal mode: the result comes in this same response.
resp = requests.post(
    f"{BASE}/v1/inference",
    headers=HEADERS,
    json={
        "alias": "Sonata",
        "agentSlug": "amadeus",
        "messages": [{"role": "user", "content": "Summarize this week’s support news."}],
        "effort": "medium",
    },
)
if not resp.ok:
    raise RuntimeError(f"Shara API {resp.status_code}: {resp.json().get('error')}")
data = resp.json()
print(data["status"], data["content"])

# Streaming mode (SSE).
with requests.post(
    f"{BASE}/v1/inference",
    headers=HEADERS,
    json={
        "alias": "Prelude",
        "stream": True,
        "messages": [{"role": "user", "content": "Write a two-line greeting."}],
    },
    stream=True,
) as stream:
    stream.raise_for_status()
    stream.encoding = "utf-8"
    event = "message"
    for line in stream.iter_lines(decode_unicode=True):
        if not line:
            event = "message"
            continue
        if line.startswith("event: "):
            event = line[len("event: "):]
        elif line.startswith("data: "):
            try:
                payload = json.loads(line[len("data: "):])
            except json.JSONDecodeError:
                continue  # not a JSON event: ignore it
            if event == "message" and "delta" in payload:
                print(payload["delta"], end="", flush=True)
            elif event == "end":
                print("\n", payload.get("status"), payload.get("runId"))

Responses never reveal the real model behind an alias. A redactor processes every output (content, error messages and tool dumps) before it crosses the network, so you will only see Prelude · Sonata · Symphony · Concerto. Don’t depend on provider names: they won’t appear.

The limit uses a fixed 60-second window with a counter per route: 60 requests per IP and route and 600 requests per workspace and route. Every response carries headers so your client can regulate itself; use them instead of guessing:

  • X-RateLimit-Limit: request cap for the current window.
  • X-RateLimit-Remaining: requests you have left; slow down before reaching 0.
  • X-RateLimit-Reset: epoch (seconds) when the window resets.
  • Retry-After: only on 429, seconds to wait before retrying.

When you exceed the limit you get 429 with { "error": "rate_limit_exceeded", "retryAfterSec": <n> }; respect Retry-After with exponential backoff. Also keep in mind two cuts distinct from the request limit: quota_exceeded when your STU quota reaches the blocking level (200% of the quota), and kill_switch when a spending cap trips (by default 1M STU per run, 10M per hour and 80M per day; an owner or administrator can adjust or disable them). Both are also 429, but they are resolved by waiting for the quota reset or reviewing consumption, not by retrying in a loop.

With general availability (GA) we will publish official SDKs for Node.js / TypeScript and Python that wrap authentication, retries and streaming. In the meantime, the examples on this page cover the endpoint an API key can use: it is plain REST, with standard headers and JSON, and works with any HTTP client.

Want early access to the API or to raise your request limit per workspace? Write to us at api@aiginer.com with your use case, or to support@aiginer.com for any integration question.