REST API
Shara exposes a REST API over HTTPS for programmatic integrations. The host is https://api.sharasaas.com, responses are JSON and service-level authentication uses an API key with scopes. It’s in private beta and opens up at general availability (GA). This page is the technical reference for authentication, endpoints, errors, rate limits and SDKs.
Introduction
Section titled “Introduction”The API is REST over HTTPS. The host is https://api.sharasaas.com. Not every endpoint hangs off a uniform /v1 prefix: agent invocation and the MCP bridge do (/v1/inference, /v1/mcp/…), while key management, runs and usage live at the root (/tenants/me/api-keys, /me/api-keys, /runs, /usage/current) This page always uses each endpoint’s real, full route. Request and response bodies are JSON (Content-Type: application/json), except in streaming mode, which returns Server-Sent Events (text/event-stream).
An API key only works on two surfaces: POST /v1/inference and the MCP bridge. Every other route on this page (key management, runs, usage) needs the session of a signed-in member of the app; it does not accept an API key.
Three principles run through the whole public surface: (1) models are always referenced by a musical alias (Prelude, Sonata, Symphony, Concerto) and never by their provider; (2) every response passes through a redactor before crossing the network, so neither error messages nor tool dumps leak internal details; (3) the identity of the caller (tenant, user, agent) is always derived from the credential, never from the body of the request.
| Property | Value |
|---|---|
| Host | https://api.sharasaas.com |
| Protocol | HTTPS required (TLS 1.2+) |
| Format | application/json · streaming over text/event-stream |
| Service authentication | API key via Authorization: Bearer <api-key> (only POST /v1/inference and the MCP bridge) |
| Rate limit | Fixed 60 s window · 60 req per IP and route · 600 req per workspace and route (see X-RateLimit-* headers) |
| Maximum body size | 512 KB (413 request_too_large above that) |
| Traceability | X-Request-Id header on every response |
Versioning: the current version is
v1. When we publish breaking changes we’ll do it under a new prefix (/v2) and keep/v1running through the announced deprecation period.
Authentication
Section titled “Authentication”Server integrations authenticate with a Shara API key. You generate it at Settings → Security (requires an administrator role for company keys; if your account has two-step verification, it must be completed in that session) and it’s sent on every request via the standard header:
Authorization: Bearer shara_sk_live_YOUR_API_KEY
Key format: fixed prefix shara_sk_live_ followed by 24 base62 characters (38 characters in total). The full value is shown only once, at creation time: save it in your secrets manager. Afterwards, the panel only shows the visible prefix (shara_sk_live_ + 6 characters) so you can identify it without being able to recover the secret.
Verification is robust by design: the key is looked up by prefix (only active, non-expired and non-revoked keys are considered) and the secret is compared against its hash using a robust hashing function and constant-time comparison. A short cache avoids burst rehashing without weakening revocation.
There are two kinds of key. Company keys are a service principal of the workspace and are limited by their scopes. Personal keys act as the person who created them, with that person’s role and department, and never have more permissions than that person.
Scopes
Section titled “Scopes”Each API key carries one or more scopes that bound what it can do. Always grant the minimum necessary (principle of least privilege). These are the scopes a route checks today:
| Scope | Allows |
|---|---|
amadeus:invoke |
Invoke any agent via POST /v1/inference, the single invocation endpoint (routes to the Amadeus orchestrator or a specific agent depending on agentSlug). |
amadeus:invoke:agent:<slug> |
Granular variant: restricts the key to a single agent by its slug. You can add several. |
mcp:tenant |
Access to the per-tenant MCP bridge (tools via SSE/JSON-RPC). It must appear literally in the key; wildcards don’t grant it. |
workspace:all |
Shortcut available only on company keys: it expands to the whole company scope catalog. |
The key creator may list other scope names from its catalog, but no API-key route requires them today, so they grant nothing. The wildcard
resource:*expands to that resource’s scopes in the catalog;amadeus:*expands toamadeus:invoke(it does not add the per-agent scopes). The total wildcard*:*is not accepted when creating a key. If a key can’t invoke at all you get403 scope_required; if it can invoke but not the requested agent,403 agent_scope_required(withagentSlugand amessagenaming the missing scope). Company keys that invoke agents (amadeus:*,workspace:all) can only be created or rotated byowner,ceooradmin(ceois a legacy alias ofowner); otherwise you get403 scope_forbidden.
Managing keys
Section titled “Managing keys”Manage keys from these endpoints. They need a signed-in session, not an API key.
| Method | Route | What it does |
|---|---|---|
GET |
/tenants/me/api-keys |
Lists the company keys (no secret or hash). Roles owner, ceo, admin and admin_it. |
POST |
/tenants/me/api-keys |
Creates a company key; returns the secret once only. |
POST |
/tenants/me/api-keys/{id}/rotate |
Issues a new key with the same name and scopes and revokes the previous one. |
DELETE |
/tenants/me/api-keys/{id} |
Revokes it immediately (soft-delete). |
GET |
/tenants/me/api-keys/scopes |
Scope catalog for company keys. |
GET / POST |
/me/api-keys |
Lists / creates personal keys (any signed-in member, only their own; at most 20 active). |
POST / DELETE |
/me/api-keys/{id}/rotate · /me/api-keys/{id} |
Rotates / revokes a personal key. |
GET |
/me/api-keys/scopes |
Scope catalog for personal keys. |
/api-keys (and /api-keys/{id}/rotate, /api-keys/{id}) still exists as a deprecated alias of the company routes. Use /tenants/me/api-keys in new integrations.
Creation body: { "name": "...", "scopes": ["amadeus:invoke"], "expiresInDays": 90 } (name 1 to 100 characters, 1 to 100 scopes, expiresInDays between 1 and 365). Keys expire after 90 days by default and 365 days is the maximum. Only company keys can use "neverExpires": true (instead of expiresInDays); on a personal key it returns 400 never_expires_not_allowed. Other creation errors: 400 invalid_scope, 400 conflicting_expiry, 409 key_limit_reached.
Rotation is not zero-downtime. The server revokes the previous key first and only then creates the new one, so for a moment neither is valid, and the old key stops working at once. The new key keeps the same scopes and gets the default 90-day expiry (a company key that never expired stays that way). To avoid an interruption, create the new key, deploy it, then revoke the old one.
Rate limit
Section titled “Rate limit”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 once the caller is identified. Every response includes headers so your client can self-regulate (the ones for the tighter of the two limits):
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
Cap of requests for the current window. |
X-RateLimit-Remaining |
Requests left in the window. |
X-RateLimit-Reset |
Moment (epoch in seconds) the window resets. |
Retry-After |
Only on 429: seconds to wait before retrying. |
X-Request-Id |
Unique request identifier; propagated in every response. |
If you exceed the limit you get 429 with { "error": "rate_limit_exceeded", "retryAfterSec": <n> } and the Retry-After header. Respect that value with exponential backoff. The health probes (/livez, /healthz) are exempt from this limit and don’t carry X-RateLimit-* headers. If your integration needs a higher ceiling per workspace, write to support@aiginer.com. The X-Request-Id header travels in every response (if you send it, it’s reused; if not, it’s generated): always include it whenever you open a support ticket.
Endpoint reference
Section titled “Endpoint reference”Summary of the surface covered on this page. Full routes on the https://api.sharasaas.com host (some carry /v1, others don’t; always use the route as it appears here).
| Method | Route | Auth | What it does |
|---|---|---|---|
POST |
/v1/inference |
API key (or app session) | Invokes an agent (Amadeus by default, or whichever you pass in agentSlug). Synchronous: the response already carries the result. |
GET |
/runs/{run_id} |
App session only (no API key) | Status and detail of an execution. |
GET |
/runs |
App session only (no API key) | Paginated list of executions. |
GET |
/usage/current |
App session only (no API key) | Period consumption (STU): quota, consumed, rollover. |
GET |
/usage/timeseries · /usage/breakdown · /usage/by-agent · /usage/events |
App session only (no API key) | Consumption over time, by alias or tool, by agent, and the raw event list. |
POST |
/runs/{id}/cancel |
App session only (no API key) | Cancels a run that is waiting for approval. |
GET |
/v1/mcp/tenant/sse |
MCP token | SSE transport for the per-tenant MCP bridge. |
POST |
/v1/mcp/tenant/rpc |
MCP token | JSON-RPC channel for the per-tenant MCP bridge. |
The MCP bridge takes the key in the X-MCP-Tenant-Token header or in Authorization: Bearer, and the key must carry the scope mcp:tenant literally. Without a valid token it answers 401 unauthorized; it can also answer 429 rate_limit_exceeded or 429 too_many_connections.
The workspace orchestrator is called Amadeus: it receives your request, decides which departmental agent handles it and coordinates the work. With
agentSlugyou can address Amadeus (the default, if you omit it) or a specific agent by itsslug. There’s no public agent catalog endpoint yet; check this documentation’s agent catalog for the available slugs. The slug stays the same whatever name and image pack your company uses (agentSlug: "patterson"works with any pack). A selectable agent your company has not chosen, or an additional agent it has not hired, is not available to it.
Sending a message to an agent
Section titled “Sending a message to an agent”POST /v1/inference runs the turn and answers in the same call. It’s the only invocation endpoint: there’s no per-agent route, just an agentSlug field in the body. Body fields:
| Field | Type | Notes |
|---|---|---|
alias |
string · required | One of Prelude · Sonata · Symphony · Concerto. |
messages |
array (1 to 100) · required | { "role": "user"|"assistant"|"system", "content": "..." }; each content 1 to 500,000 characters (the whole body is also capped at 512 KB). |
agentSlug |
string · optional | Target agent for the turn (1 to 64 characters). Defaults to amadeus. Your API key needs the scope for that agent. |
powerMode |
string · optional | normal (default) or low. |
maxTokens |
integer · optional | Between 1 and 32,000. |
effort |
string · optional | low · medium · high · x-high · max. |
stream |
boolean · optional | If true, the response arrives via SSE. |
conversationId |
string · optional | Groups several turns into the same thread (up to 200 characters). |
The aliases Prelude, Sonata and Symphony are cloud models of growing capacity: Prelude (agile and economical), Sonata (balance) and Symphony (the most capable). Concerto is the local model of the Concerto Local plan and spends 0 STU. Choose based on task complexity; the alias is the only thing accepted to select a model (never a provider name). A body that fails validation returns 400 with error: "FST_ERR_VALIDATION".
The synchronous response (200) has this shape:
{
"alias": "Sonata",
"content": "The agent’s reply text…",
"usage": {
"inputTokens": 184,
"outputTokens": 412
},
"status": "completed",
"runId": "3f8a1c52-9d4e-4b7a-a1c3-5e2f7d9b0a64"
}
| Field | Meaning |
|---|---|
status |
How the turn ended: completed, failed, blocked or canceled. A failed or blocked turn is still HTTP 200: read this field, not only the status code. |
runId |
Optional. Id of the run created for the turn (only if it was recorded). |
toolOutcome |
Optional. { "rejected": [...], "expired": [...] } with the names of tools a person rejected or that expired in the turn. |
herramientasEjecutadas |
Optional. Number of tools that ran successfully in the turn (only when greater than 0). |
blockedReason |
Optional. Reason, when the turn didn’t complete. |
motivoDeCorte |
Optional. Present only when the run stopped at one of its own limits (steps or turn spend) and not because something broke: tope_de_pasos, tope_de_gasto or sin_cierre. |
Two field names are in Spanish.
herramientasEjecutadasandmotivoDeCortekeep their Spanish names in the API. They are stable: use them exactly as written.
Streaming (SSE)
Section titled “Streaming (SSE)”With "stream": true the response is text/event-stream. You receive incremental fragments as data: {"delta":"…"} and a closing end event with the final status, the runId and the token counts. Every fragment passes through the redactor before being emitted. The stream may also carry other named events (for example usage with progress, or error): ignore the ones you don’t use.
data: {"delta":"Hello, "}
data: {"delta":"how can I "}
data: {"delta":"help you?"}
event: end
data: {"done":true,"status":"completed","runId":"3f8a1c52-9d4e-4b7a-a1c3-5e2f7d9b0a64","alias":"Sonata","usage":{"inputTokens":184,"outputTokens":412}}
Checking consumption
Section titled “Checking consumption”GET /usage/current returns the period status in STU (Shara’s consumption unit). It works with an app session only (it does not accept an API key) and is open reading for any member of the workspace. It does not expose absolute money figures. Fields:
| Field | Meaning |
|---|---|
plan, planName |
Plan slug (first, pro, max, max_x5, max_x20, plan_local) and its display name. |
year, month |
Label of the billing period. |
stuBase, stuRollover |
Base quota (with additional agents included) and rollover carried over. |
stuConsumed, stuRemaining, pctConsumed |
Consumed, remaining and percentage over base plus rollover. |
daysElapsed, daysInMonth, projectedMonthEnd, projectedPct |
Linear projection to the end of the period. |
status |
Traffic light: green (under 70%), yellow (70 to 90%), red (90 to 100%), blown (100% or more). |
level |
Quota level: green, warning, critical, overage, grace, hardstop (see STU and quotas). |
bannerMessage |
Text for display. Branch on level, not on this text. |
allowInference, forceLowSpending, overageBilling |
What the current level implies. |
resetAt |
When the period ends. |
hasSnapshot |
Whether the period already has a usage record. |
activeAlerts |
Active alerts for the period. |
additionalAgents, addonAgentSlugs |
Additional agents purchased: how many and which. |
{
"plan": "pro",
"planName": "Pro",
"year": 2026,
"month": 6,
"stuBase": 140000000,
"stuRollover": 20000000,
"stuConsumed": 60000000,
"stuRemaining": 100000000,
"pctConsumed": 37.5,
"daysElapsed": 10,
"daysInMonth": 30,
"projectedMonthEnd": 180000000,
"projectedPct": 112.5,
"status": "green",
"level": "green",
"bannerMessage": "…",
"allowInference": true,
"forceLowSpending": false,
"overageBilling": false,
"resetAt": "2026-07-01T00:00:00.000Z",
"hasSnapshot": true,
"activeAlerts": [],
"additionalAgents": 0,
"addonAgentSlugs": []
}
Without an active subscription it answers 403 no_active_subscription.
More views of consumption
Section titled “More views of consumption”These routes also need an app session and are restricted to the people who can see the company’s figures (owners and administrators, and accounts with Amadeus enabled); others get 403 role_forbidden. Dates are YYYY-MM-DD; if you omit them the current period is used. Unlike /usage/current, they include billableCents (euro cents).
| Route | Query | Returns |
|---|---|---|
GET /usage/timeseries |
from, to, groupBy: day (default), alias or agent_slug |
{ from, to, groupBy, points: [{ day, key, stuConsumed, billableCents, eventCount }] } |
GET /usage/breakdown |
from, to, groupBy: alias or tool_name |
{ from, to, totalStu, items: [{ key, stuConsumed, billableCents, eventCount, pctOfTotal }] } |
GET /usage/by-agent |
from, to |
{ from, to, totalStu, totalCents, items: [{ agentSlug, stuConsumed, billableCents, runCount, pctOfTotal }] } |
GET /usage/events |
from, to, limit (1 to 10,000) |
{ from, to, events: [...] }, one entry per usage event (alias, token counts, STU, tool, power mode). Meant for exporting to CSV. |
Runs and their status
Section titled “Runs and their status”POST /v1/inference is synchronous, so you don’t need to poll to get the result. Runs are the history of those executions, and you can query them with an app session only (GET /runs and GET /runs/{run_id} do not accept an API key). A run’s status takes one of these values:
status |
Meaning |
|---|---|
in_progress |
The run is still executing. |
completed |
Finished successfully. |
failed |
Finished with an error. The detail carries the (redacted) reason. |
canceled |
Canceled before completion. |
suspended |
Parked, waiting for someone to approve an action. It can be canceled with POST /runs/{id}/cancel. |
GET /runs lists executions with these filters: status, agent_slug (≤64 characters), includeInternal (true also shows the system’s internal runs, which are hidden by default), limit (1 to 100) and offset (≥0); the response includes total for pagination. GET /runs/{run_id} returns { run, steps[], toolCalls[] }; if the run doesn’t exist or isn’t visible to you, you get 404 run_not_found. What you see depends on your role: members who can’t see the whole workspace only see their own runs. Only redacted content is ever returned: never raw material from providers.
POST /runs/{id}/cancel cancels a run that is waiting for approval (status suspended) and returns { ok: true, run }. A run in any other state answers 409 run_not_cancelable; one that does not exist or is not yours, 404 run_not_found.
Errors
Section titled “Errors”All errors are JSON with an error field (a stable string, meant for your logic) and, optionally, message (a human-readable text). Errors that go through the global handler (validation failures, an empty body, oversized chunked bodies, unexpected failures) also carry requestId; the X-Request-Id header is always present. Never parse message: branch on error. Canonical shape:
{
"error": "string_in_snake_case",
"message": "Human-readable description (optional).",
"requestId": "req-a1b2c3"
}
A route that doesn’t exist answers with the framework’s shape: { "message": "Route not found", "error": "Not Found", "statusCode": 404 }.
Catalog of verified public codes:
error |
HTTP | When |
|---|---|---|
missing_bearer |
401 | Missing Authorization: Bearer … header. |
invalid_token |
401 | Invalid or expired session token. |
unknown_user |
401 | Valid session token, but the user doesn’t exist. |
tenant_unresolved |
401 | Couldn’t resolve the caller’s workspace. |
not_authenticated |
401 | No valid session/credential on a route that requires one. |
api_key_not_found |
401 | API key with an unknown, revoked or expired prefix. |
api_key_not_found |
404 | On the key management routes: that key id does not exist or is not yours. |
already_revoked |
409 | The key you are revoking or rotating was already revoked. |
api_key_invalid |
401 | API key whose hash fails to verify. |
payment_not_completed |
402 | The checkout payment hasn’t been completed. |
tenant_forbidden |
403 | Authenticated, but no membership in the requested tenant. |
forbidden / role_forbidden |
403 | The action isn’t allowed for your role/key. |
no_active_subscription |
403 | The workspace has no active subscription. |
scope_required |
403 | The API key can’t invoke (message indicates the missing scope). |
agent_scope_required |
403 | The key can invoke, but not this agent. Extra: agentSlug. |
agent_disabled |
403 | The agent is paused. Extra: agentSlug, reason. |
agent_unknown |
403 | The agentSlug doesn’t exist. Extra: agentSlug. |
agent_not_in_plan |
403 | The agent isn’t available to your company on your plan. Extra: agentSlug. |
agent_not_purchased |
403 | An additional agent that isn’t purchased in this account. Extra: agentSlug. |
agent_not_allowed |
403 | The agent doesn’t belong to your department. Extra: agentSlug. |
not_a_member |
403 | You’re not a member of the agent’s tenant. Extra: agentSlug. |
agent_slug_required |
400 | A signed-in member who can’t use Amadeus must send agentSlug. |
FST_ERR_VALIDATION |
400 | The body or parameters fail validation (for example in /v1/inference). |
invalid_request / invalid_body |
400 | Body or parameters that fail validation on other routes. |
empty_body |
400 | A JSON request with an empty body. |
not_found |
404 | The resource doesn’t exist or isn’t visible to you. |
expired |
410 | Single-use token/link already expired. |
request_too_large |
413 | The body exceeds the limit (512 KB). Extra: limitBytes. |
alias_plan_mismatch |
422 | You asked for a model alias above the ceiling of your plan (for example Sonata on First or Symphony on Pro). Ask for a lower one. |
run_not_cancelable |
409 | Only a run waiting for approval can be canceled. |
quota_exceeded |
429 | STU quota exhausted (hardstop). Extra: policy, pctConsumed, resetAt. |
kill_switch |
429 | Active spending cut. Extra: kind, observedCents, thresholdCents, activeSince?. |
rate_limit_exceeded |
429 | Request limit exceeded. Extra: retryAfterSec + Retry-After. |
too_many_attempts |
429 | Too many failed attempts; wait before retrying. |
internal_error |
500 | Unexpected server error. Includes requestId. |
provider_unavailable |
503 | The model behind the alias is not available right now. Retry with backoff. |
kill_switch_engine_error |
503 | The spending check could not be completed, so the request was not run. Retry in a few seconds. |
Some
404s are more resource-specific (run_not_found,agent_not_found,member_not_found…), all with the same shape. For your client logic you can treat any*_not_foundas “resource doesn’t exist”.
Several errors carry extra fields so your client can react without a second call. Exhausted quota (it only happens at the hardstop level, from 200% of your quota):
{
"error": "quota_exceeded",
"policy": "hardstop",
"pctConsumed": 203.5,
"resetAt": "2026-07-01T00:00:00.000Z"
}
Spending cut (kill switch). The caps are set in STU: by default 1M per run, 10M per hour and 80M per day (at the €5 per million rate, 500, 5,000 and 40,000 euro cents). An owner or administrator can edit or switch off each one. observedCents and thresholdCents are strings with the billable amount in euro cents:
{
"error": "kill_switch",
"kind": "day",
"observedCents": "41250",
"thresholdCents": "40000",
"message": "…",
"activeSince": "2026-06-16T08:12:00.000Z"
}
kind indicates the switch’s scope: run · hour · day · manual. On 429, respect Retry-After/resetAt and retry with backoff; on 5xx, retry with backoff and include the requestId if you open a support ticket.
Code examples
Section titled “Code examples”Complete recipes on the https://api.sharasaas.com host. Replace shara_sk_live_… with your API key.
Sending a message to an agent
Section titled “Sending a message to an agent”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"
}'
import requests
BASE = "https://api.sharasaas.com"
API_KEY = "shara_sk_live_YOUR_API_KEY"
resp = requests.post(
f"{BASE}/v1/inference",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"alias": "Sonata",
"agentSlug": "amadeus",
"messages": [
{"role": "user", "content": "Summarize this week’s support news."}
],
"effort": "medium",
},
)
resp.raise_for_status()
data = resp.json()
print(data["status"], data["content"]) # the result is in this same response
const BASE = 'https://api.sharasaas.com';
const API_KEY = process.env.SHARA_API_KEY; // never in the browser
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) throw new Error(`HTTP ${res.status}: ${(await res.json()).error}`);
const data = await res.json();
console.log(data.status, data.content);
Shara has its own MCP server, at https://mcp.sharasaas.com, so you can use your company’s information from Cursor, Windsurf or any other MCP client. It exposes three things: your company’s indicators, the company memory and escalating a matter to an agent. Anything it writes waits for your approval, like any other action in Shara.
The owner, an admin or IT creates the token in Settings → MCP. Nobody else can. Then you add Shara to your client with the server address and the token in an Authorization header (the exact file depends on the client; in Cursor it is mcp.json):
{
"mcpServers": {
"shara": {
"url": "https://mcp.sharasaas.com",
"headers": {
"Authorization": "Bearer <token>"
}
}
}
}
Treat the token like a password: do not paste it into a repository or share it.
At general availability (GA) we’ll publish official SDKs for Node.js / TypeScript and Python, on top of the curl examples on this page. Until then, any standard HTTP client works: the API is plain REST, with standard headers and JSON. See SDK and examples.
Want early access to the API? Write to
api@aiginer.comwith your use case, or tosupport@aiginer.comto raise your per-workspace request limit.