REST API
Shara exposes a REST API over HTTPS for programmatic integrations. The host is https://api.aiginer.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, a run’s lifecycle, errors, webhooks and SDKs.
Introduction
Section titled “Introduction”The API is REST over HTTPS. The host is https://api.aiginer.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, usage and inbound webhooks live at the root (/api-keys, /runs, /usage/current, /webhooks/inbound/{slug}). 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).
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.aiginer.com |
| Protocol | HTTPS required (TLS 1.2+) |
| Format | application/json · streaming over text/event-stream |
| Service authentication | API key via Authorization: Bearer <api-key> |
| Rate limit | 60 req/min per IP · 600 req/min per workspace (see X-RateLimit-* headers) |
| 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 → API Keys (requires an admin role and active two-step verification) and it’s sent on every request via the standard header:
Authorization: Bearer shara_sk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Key format: fixed prefix shara_sk_live_ followed by 24 base62 characters (38 characters 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.
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):
| Scope | Allows |
|---|---|
amadeus:invoke | Invoke agents 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. |
tenant:read | Read workspace metadata. |
departments:read | Read the departments and agents catalog. |
usage:read | Read the period’s (STU) consumption. |
webhooks:ingest | Associated with ingesting inbound events. |
mcp:tenant | Access the per-tenant MCP bridge (tools via SSE/JSON-RPC). |
Wildcards per resource (
resource:*) are supported when creating a key. The full wildcard*:*is reserved and never issued to an API key. If a key lacks the scope a route requires, you get403 scope_requiredand themessagefield indicates which one is missing.
Rotation and revocation
Section titled “Rotation and revocation”Keys expire after 365 days by default (configurable maximum: 730). Manage them from these endpoints (they require an admin session, not an API key):
| Method | Route | What it does |
|---|---|---|
GET | /api-keys | Lists the keys (no secret or hash). |
POST | /api-keys | Creates a key; returns the secret once only. |
POST | /api-keys/{id}/rotate | Issues a new key with the same scopes and revokes the previous one. |
DELETE | /api-keys/{id} | Revokes it immediately (soft-delete). |
Creation body: { "name": "...", "scopes": ["amadeus:invoke"], "expiresInDays": 365 } (name 1 to 100 characters, at least one scope, expiresInDays between 1 and 730). Rotation is the recommended way to renew credentials with no downtime window: create the new one, deploy it and the previous one is revoked instantly.
Rate limit
Section titled “Rate limit”The standard limit is 60 requests per minute (60 s sliding window). Every response includes headers so your client can self-regulate:
| 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. 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 public surface. Full routes on the https://api.aiginer.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 | Invokes an agent (Amadeus by default, or whichever you pass in agentSlug) and gets a run_id. |
GET | /runs/{run_id} | API key | Status and result of an execution. |
GET | /runs | API key | Paginated list of executions. |
GET | /usage/current | API key | Period consumption (STU): quota, consumed, rollover. |
POST | /webhooks/inbound/{slug} | HMAC | Ingests a signed inbound event. |
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 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.
Sending a message to an agent
Section titled “Sending a message to an agent”POST /v1/inference creates an execution. 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": "..." }; content 1 to 500,000 characters. |
agentSlug | string · optional | Target agent for the turn. 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 four aliases describe a growing capacity/cost level, from Prelude (fast and cheap) to Concerto (maximum capacity). Choose based on task complexity; the alias is the only thing accepted to select a model (never a provider name). The synchronous response (200) has this shape:
{
"alias": "Sonata",
"content": "Texto de la respuesta del agente…",
"usage": {
"inputTokens": 184,
"outputTokens": 412
}
}
Streaming (SSE)
Section titled “Streaming (SSE)”With "stream": true the response is text/event-stream. You receive incremental fragments and a closing event with the consumption summary and the runId. Every fragment passes through the redactor before being emitted:
data: {"delta":"Hello, "}
data: {"delta":"how can I "}
data: {"delta":"help you?"}
event: end
data: {"done":true,"runId":"run_a1b2c3","usage":{"inputTokens":184,"outputTokens":412}}
Checking consumption
Section titled “Checking consumption”GET /usage/current returns the period status in STU (Shara’s consumption unit): base quota, accumulated rollover, consumed, percentage, projection, level and reset date. It’s open reading for any member and does not expose absolute money figures (those live behind the admin panel). Typical shape:
{
"period": "2026-06",
"quotaStu": 100000,
"rolloverStu": 12500,
"consumedStu": 41820,
"pctConsumed": 37.2,
"projectedStu": 96400,
"level": "green",
"resetAt": "2026-07-01T00:00:00.000Z"
}
A run’s lifecycle
Section titled “A run’s lifecycle”A non-streaming invocation follows the send → poll pattern. The full flow is:
- Send:
POST /v1/inferencewith your message. The response includes arun_id. - Poll:
GET /runs/{run_id}every few seconds untilstatusis terminal. - Read the result: when
statusiscompleted, the detail carries the steps, the tool calls and the (redacted) content the agent produced.
The status field takes one of these values:
status | Meaning |
|---|---|
in_progress | The run is still executing. Keep polling. |
completed | Finished successfully; the result is available. |
failed | Finished with an error. The detail carries the (redacted) reason. |
canceled | Canceled before completion. |
GET /runs lists executions with status, agent_slug (≤64 characters), limit (1 to 200) and offset (≥0) filters; 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 your credential, you get 404 not_found. Only redacted content is ever returned: never raw material from providers.
Polling recommendation: start with a 1 to 2 s interval and apply backoff up to ~10 s for long runs. Always respect
X-RateLimit-Remaining. If you need live results, usestreammode instead of polling.
Errors
Section titled “Errors”All errors are JSON with an error field (a stable snake_case string, meant for your logic) and, optionally, message (a human-readable text). On uncontrolled failures (5xx) the global handler adds requestId. Never parse message: branch on error. Canonical shape:
{
"error": "string_en_snake_case",
"message": "Human-readable description (optional).",
"requestId": "req-a1b2c3"
}
Catalog of verified public codes:
error | HTTP | When |
|---|---|---|
missing_bearer | 401 | Missing Authorization: Bearer … header. |
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_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. |
scope_required | 403 | The API key doesn’t have the necessary scope (message indicates it). |
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 included in your plan. 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. |
not_found | 404 | The resource doesn’t exist or isn’t visible to you. |
expired | 410 | Single-use token/link already expired. |
invalid_request / invalid_body | 400 | Body or parameters that fail validation. |
quota_exceeded | 429 | STU quota exhausted. Extra: policy, pctConsumed, resetAt. |
kill_switch | 429 | Active spending cut. Extra: kind, 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. |
Some
404s are more resource-specific (agent_not_found,webhook_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:
{
"error": "quota_exceeded",
"policy": "hardstop",
"pctConsumed": 100,
"resetAt": "2026-07-01T00:00:00.000Z"
}
policy can be green · warning · critical · overage · grace · hardstop. Spending cut (kill switch):
{
"error": "kill_switch",
"kind": "day",
"message": "Interruptor de seguridad de consumo activo.",
"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.
Webhooks
Section titled “Webhooks”Shara uses a signed inbound webhooks model: an external system (your CRM, a form, another automation) signs and sends an event to Shara, which verifies the signature and routes it to the configured agent. There’s no subscription to outbound events; the direction of traffic is towards Shara.
You create the webhook from the panel (Settings → Webhooks); each one has its own slug in the URL and its own signing secret (never shared between tenants), shown only once at creation. The ingestion endpoint is:
POST /webhooks/inbound/{slug}
Content-Type: application/json
Headers you must send:
| Header | Value | Notes |
|---|---|---|
X-SHARA-Signature | <hex> HMAC of the raw body | Default name; configurable per webhook. |
X-SHARA-Timestamp | <unix-seconds> or ISO 8601 | Closes the anti-replay window. Required if the webhook declares it. |
X-SHARA-Event-Id | unique event id | Idempotency. Accepted alternatives: X-Event-Id, X-Request-Id, X-Webhook-Id, X-Hook-Id. |
Content-Type | application/json | The body is signed as raw bytes. |
The body is free-form JSON from the sender: Shara verifies it, audits it and transforms it (via a configurable template) into what the agent receives. Example of an inbound lead:
{
"event": "lead.created",
"id": "evt_9f2c1ab7",
"occurred_at": "2026-06-16T09:30:00Z",
"contact": {
"name": "Marta Ruiz",
"email": "marta@example.com",
"phone": "+34600111222",
"company": "Example SL"
},
"source": "landing-form"
}
200 response on successful ingestion, and idempotent variant when you resend the same X-SHARA-Event-Id:
{ "ok": true, "code": "ok", "eventId": "8b1d…", "runId": "run_…" }
{ "ok": true, "code": "duplicate", "eventId": "8b1d…" }
| HTTP | error | Cause |
|---|---|---|
400 | payload_invalid / invalid_body | Invalid JSON or the template doesn’t render. |
401 | signature_invalid | Missing/incorrect signature, unsupported scheme or timestamp out of window. |
404 | webhook_not_found / webhook_unavailable | Non-existent, paused or revoked slug. |
413 | payload_too_large | Body larger than 1 MB. |
429 | rate_limited | Per-IP limit exceeded (RPM configurable per webhook). |
HMAC signature verification
Section titled “HMAC signature verification”The signature is HMAC-SHA256 (or HMAC-SHA512, configurable) calculated over the raw body in bytes, never over the re-serialized JSON. The result is lowercase hex (64 characters for SHA-256, 128 for SHA-512). Verification is fail-closed (with no secret, everything is rejected), compares in constant time (timingSafeEqual) and applies a ±300 s anti-replay window with 60 s clock tolerance. This is how you sign it, on the sender side:
import hmac, hashlib, time, json, requests
SECRET = "wsec_your_signing_secret"
SLUG = "incoming-leads"
BASE = "https://api.aiginer.com"
payload = {"event": "lead.created", "id": "evt_9f2c1ab7",
"contact": {"email": "marta@example.com"}}
raw = json.dumps(payload, separators=(",", ":")).encode("utf-8")
signature = hmac.new(SECRET.encode(), raw, hashlib.sha256).hexdigest()
timestamp = str(int(time.time()))
resp = requests.post(
f"{BASE}/webhooks/inbound/{SLUG}",
data=raw,
headers={
"Content-Type": "application/json",
"X-SHARA-Signature": signature,
"X-SHARA-Timestamp": timestamp,
"X-SHARA-Event-Id": payload["id"],
},
)
print(resp.status_code, resp.json())
And this is how you’d verify a signature at your own receiver, if you re-forward Shara events to your backend (same canonical scheme):
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifySharaSignature(rawBody, signatureHex, timestamp, secret) {
// 1) Anti-replay BEFORE the HMAC (±300 s window, 60 s skew)
const nowSec = Math.floor(Date.now() / 1000);
const ts = Number(timestamp);
const diff = nowSec - ts;
if (diff > 300) return false; // too old
if (diff < -60) return false; // too far in the future
// 2) Hex format (64 = sha256)
if (signatureHex.length !== 64 || !/^[0-9a-f]+$/i.test(signatureHex)) return false;
// 3) HMAC over the RAW body + constant-time comparison
const expected = createHmac('sha256', secret).update(rawBody).digest();
const received = Buffer.from(signatureHex, 'hex');
if (received.length !== expected.length) return false;
return timingSafeEqual(received, expected);
}
Idempotency and retries
Section titled “Idempotency and retries”Shara responds synchronously and doesn’t retry ingestion: the sender retries on a 5xx or a timeout. So those retries don’t duplicate executions, always send a unique X-SHARA-Event-Id per event: Shara guarantees idempotency by that identifier (a UNIQUE constraint) and resending the same event returns { "ok": true, "code": "duplicate" } without creating a new run. Ingestion also has a per-IP rate limit (60 RPM by default, configurable per webhook).
Code examples
Section titled “Code examples”Complete recipes on the https://api.aiginer.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.aiginer.com/v1/inference \
-H "Authorization: Bearer shara_sk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-d '{
"alias": "Sonata",
"agentSlug": "amadeus",
"messages": [
{ "role": "user", "content": "Resume las novedades de soporte de esta semana." }
],
"effort": "medium"
}'
import requests
BASE = "https://api.aiginer.com"
API_KEY = "shara_sk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
resp = requests.post(
f"{BASE}/v1/inference",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"alias": "Sonata",
"agentSlug": "amadeus",
"messages": [
{"role": "user", "content": "Resume las novedades de soporte de esta semana."}
],
"effort": "medium",
},
)
resp.raise_for_status()
print(resp.json()) # { "alias": "Sonata", "content": "…", "usage": {...} }
const BASE = 'https://api.aiginer.com';
const API_KEY = 'shara_sk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
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: 'Resume las novedades de soporte de esta semana.' },
],
effort: 'medium',
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${(await res.json()).error}`);
console.log(await res.json());
Polling a run
Section titled “Polling a run”When working in async mode, save the run_id and poll it until a terminal status:
import time, requests
BASE = "https://api.aiginer.com"
API_KEY = "shara_sk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
def wait_for_run(run_id, interval=2.0, timeout=300):
deadline = time.time() + timeout
while time.time() < deadline:
r = requests.get(f"{BASE}/runs/{run_id}", headers=HEADERS)
r.raise_for_status()
run = r.json()["run"]
if run["status"] in ("completed", "failed", "canceled"):
return run
time.sleep(interval)
interval = min(interval * 1.5, 10) # backoff up to 10 s
raise TimeoutError(f"run {run_id} did not finish in time")
run = wait_for_run("run_a1b2c3")
print(run["status"])
const BASE = 'https://api.aiginer.com';
const API_KEY = 'shara_sk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
async function waitForRun(runId, { interval = 2000, timeout = 300000 } = {}) {
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
const res = await fetch(`${BASE}/runs/${runId}`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { run } = await res.json();
if (['completed', 'failed', 'canceled'].includes(run.status)) return run;
await new Promise((r) => setTimeout(r, interval));
interval = Math.min(interval * 1.5, 10000); // backoff up to 10 s
}
throw new Error(`run ${runId} did not finish in time`);
}
Checking consumption
Section titled “Checking consumption”curl -sS https://api.aiginer.com/usage/current \
-H "Authorization: Bearer shara_sk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
Receiving and verifying an inbound webhook
Section titled “Receiving and verifying an inbound webhook”Example of a minimal server that receives the signed ingestion. Capture the raw body (not the already-parsed JSON) so the HMAC matches byte for byte:
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
const SECRET = process.env.SHARA_WEBHOOK_SECRET;
const app = express();
// RAW body so the HMAC can be verified over the exact bytes
app.use(express.raw({ type: 'application/json' }));
app.post('/hooks/shara', (req, res) => {
const sig = req.get('X-SHARA-Signature') ?? '';
const ts = req.get('X-SHARA-Timestamp') ?? '';
const diff = Math.floor(Date.now() / 1000) - Number(ts);
if (diff > 300 || diff < -60) return res.status(401).json({ error: 'signature_invalid' });
const expected = createHmac('sha256', SECRET).update(req.body).digest();
const received = Buffer.from(sig, 'hex');
const ok =
received.length === expected.length && timingSafeEqual(received, expected);
if (!ok) return res.status(401).json({ error: 'signature_invalid' });
const event = JSON.parse(req.body.toString('utf8'));
console.log('verified event:', event.event, event.id);
res.json({ ok: true });
});
app.listen(8080);
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.
Want early access to the API? Write to
api@aiginer.comwith your use case, or tosupport@aiginer.comto raise your per-workspace request limit.