Skip to content

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.

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.

PropertyValue
Hosthttps://api.aiginer.com
ProtocolHTTPS required (TLS 1.2+)
Formatapplication/json · streaming over text/event-stream
Service authenticationAPI key via Authorization: Bearer <api-key>
Rate limit60 req/min per IP · 600 req/min per workspace (see X-RateLimit-* headers)
TraceabilityX-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 /v1 running through the announced deprecation period.

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:

bash
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.

Each API key carries one or more scopes that bound what it can do. Always grant the minimum necessary (principle of least privilege):

ScopeAllows
amadeus:invokeInvoke 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:readRead workspace metadata.
departments:readRead the departments and agents catalog.
usage:readRead the period’s (STU) consumption.
webhooks:ingestAssociated with ingesting inbound events.
mcp:tenantAccess 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 get 403 scope_required and the message field indicates which one is missing.

Keys expire after 365 days by default (configurable maximum: 730). Manage them from these endpoints (they require an admin session, not an API key):

MethodRouteWhat it does
GET/api-keysLists the keys (no secret or hash).
POST/api-keysCreates a key; returns the secret once only.
POST/api-keys/{id}/rotateIssues 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.

The standard limit is 60 requests per minute (60 s sliding window). Every response includes headers so your client can self-regulate:

HeaderMeaning
X-RateLimit-LimitCap of requests for the current window.
X-RateLimit-RemainingRequests left in the window.
X-RateLimit-ResetMoment (epoch in seconds) the window resets.
Retry-AfterOnly on 429: seconds to wait before retrying.
X-Request-IdUnique 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.

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).

MethodRouteAuthWhat it does
POST/v1/inferenceAPI keyInvokes an agent (Amadeus by default, or whichever you pass in agentSlug) and gets a run_id.
GET/runs/{run_id}API keyStatus and result of an execution.
GET/runsAPI keyPaginated list of executions.
GET/usage/currentAPI keyPeriod consumption (STU): quota, consumed, rollover.
POST/webhooks/inbound/{slug}HMACIngests a signed inbound event.
GET/v1/mcp/tenant/sseMCP tokenSSE transport for the per-tenant MCP bridge.
POST/v1/mcp/tenant/rpcMCP tokenJSON-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 agentSlug you can address Amadeus (the default, if you omit it) or a specific agent by its slug. There’s no public agent catalog endpoint yet; check this documentation’s agent catalog for the available slugs.

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:

FieldTypeNotes
aliasstring · requiredOne of Prelude · Sonata · Symphony · Concerto.
messagesarray (1 to 100) · required{ "role": "user"|"assistant"|"system", "content": "..." }; content 1 to 500,000 characters.
agentSlugstring · optionalTarget agent for the turn. Defaults to amadeus. Your API key needs the scope for that agent.
powerModestring · optionalnormal (default) or low.
maxTokensinteger · optionalBetween 1 and 32,000.
effortstring · optionallow · medium · high · x-high · max.
streamboolean · optionalIf true, the response arrives via SSE.
conversationIdstring · optionalGroups 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:

json
{
  "alias": "Sonata",
  "content": "Texto de la respuesta del agente…",
  "usage": {
    "inputTokens": 184,
    "outputTokens": 412
  }
}

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:

bash
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}}

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:

json
{
  "period": "2026-06",
  "quotaStu": 100000,
  "rolloverStu": 12500,
  "consumedStu": 41820,
  "pctConsumed": 37.2,
  "projectedStu": 96400,
  "level": "green",
  "resetAt": "2026-07-01T00:00:00.000Z"
}

A non-streaming invocation follows the send → poll pattern. The full flow is:

  1. Send: POST /v1/inference with your message. The response includes a run_id.
  2. Poll: GET /runs/{run_id} every few seconds until status is terminal.
  3. Read the result: when status is completed, the detail carries the steps, the tool calls and the (redacted) content the agent produced.

The status field takes one of these values:

statusMeaning
in_progressThe run is still executing. Keep polling.
completedFinished successfully; the result is available.
failedFinished with an error. The detail carries the (redacted) reason.
canceledCanceled 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, use stream mode instead of polling.

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:

json
{
  "error": "string_en_snake_case",
  "message": "Human-readable description (optional).",
  "requestId": "req-a1b2c3"
}

Catalog of verified public codes:

errorHTTPWhen
missing_bearer401Missing Authorization: Bearer … header.
not_authenticated401No valid session/credential on a route that requires one.
api_key_not_found401API key with an unknown, revoked or expired prefix.
api_key_invalid401API key whose hash fails to verify.
payment_not_completed402The checkout payment hasn’t been completed.
tenant_forbidden403Authenticated, but no membership in the requested tenant.
forbidden / role_forbidden403The action isn’t allowed for your role/key.
scope_required403The API key doesn’t have the necessary scope (message indicates it).
agent_disabled403The agent is paused. Extra: agentSlug, reason.
agent_unknown403The agentSlug doesn’t exist. Extra: agentSlug.
agent_not_in_plan403The agent isn’t included in your plan. Extra: agentSlug.
agent_not_allowed403The agent doesn’t belong to your department. Extra: agentSlug.
not_a_member403You’re not a member of the agent’s tenant. Extra: agentSlug.
not_found404The resource doesn’t exist or isn’t visible to you.
expired410Single-use token/link already expired.
invalid_request / invalid_body400Body or parameters that fail validation.
quota_exceeded429STU quota exhausted. Extra: policy, pctConsumed, resetAt.
kill_switch429Active spending cut. Extra: kind, activeSince?.
rate_limit_exceeded429Request limit exceeded. Extra: retryAfterSec + Retry-After.
too_many_attempts429Too many failed attempts; wait before retrying.
internal_error500Unexpected 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_found as “resource doesn’t exist”.

Several errors carry extra fields so your client can react without a second call. Exhausted quota:

json
{
  "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):

json
{
  "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.

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:

bash
POST /webhooks/inbound/{slug}
Content-Type: application/json

Headers you must send:

HeaderValueNotes
X-SHARA-Signature<hex> HMAC of the raw bodyDefault name; configurable per webhook.
X-SHARA-Timestamp<unix-seconds> or ISO 8601Closes the anti-replay window. Required if the webhook declares it.
X-SHARA-Event-Idunique event idIdempotency. Accepted alternatives: X-Event-Id, X-Request-Id, X-Webhook-Id, X-Hook-Id.
Content-Typeapplication/jsonThe 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:

json
{
  "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:

json
{ "ok": true, "code": "ok", "eventId": "8b1d…", "runId": "run_…" }

{ "ok": true, "code": "duplicate", "eventId": "8b1d…" }
HTTPerrorCause
400payload_invalid / invalid_bodyInvalid JSON or the template doesn’t render.
401signature_invalidMissing/incorrect signature, unsupported scheme or timestamp out of window.
404webhook_not_found / webhook_unavailableNon-existent, paused or revoked slug.
413payload_too_largeBody larger than 1 MB.
429rate_limitedPer-IP limit exceeded (RPM configurable per webhook).

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:

python
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):

javascript
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);
}

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).

Complete recipes on the https://api.aiginer.com host. Replace shara_sk_live_… with your API key.

bash
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"
  }'
python
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": {...} }
javascript
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());

When working in async mode, save the run_id and poll it until a terminal status:

python
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"])
javascript
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`);
}
bash
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:

javascript
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.com with your use case, or to support@aiginer.com to raise your per-workspace request limit.

Questions about the API? Write to us at api@aiginer.com.