API REST
SHARA expone una API REST sobre HTTPS para integraciones programáticas. Todas las rutas viven bajo https://api.shara.aiginer.com/v1, las respuestas son JSON y la autenticación de servicio se hace con una API key con scopes. Está en beta privada y se abre con la disponibilidad general (GA). Esta página es la referencia técnica de autenticación, endpoints, ciclo de vida de un run, errores, webhooks y SDKs.
Introducción
La API es REST sobre HTTPS. La Base URL es https://api.shara.aiginer.com/v1 y todos los endpoints documentados aquí cuelgan de ese prefijo /v1. El cuerpo de las peticiones y las respuestas son JSON (Content-Type: application/json), salvo el modo *streaming*, que devuelve Server-Sent Events (text/event-stream).
Tres principios atraviesan toda la superficie pública: (1) los modelos se referencian siempre por alias musical (Prelude, Sonata, Symphony, Concerto) y nunca por su proveedor; (2) toda respuesta se pasa por un redactor antes de cruzar la red, de modo que ni los mensajes de error ni los volcados de herramientas filtran detalles internos; (3) la identidad de quien llama (tenant, usuario, agente) se deriva siempre de la credencial, nunca del cuerpo de la petición.
| Propiedad | Valor |
|---|---|
| Base URL | https://api.shara.aiginer.com/v1 |
| Protocolo | HTTPS obligatorio (TLS 1.2+) |
| Formato | application/json · streaming en text/event-stream |
| Autenticación de servicio | API key vía Authorization: Bearer |
| Rate limit estándar | 60 req/min (ver cabeceras X-RateLimit-*) |
| Trazabilidad | Cabecera X-Request-Id en toda respuesta |
Versionado: la versión actual esv1. Cuando publiquemos cambios incompatibles lo haremos bajo un prefijo nuevo (/v2) y mantendremos/v1en funcionamiento durante el periodo de deprecación anunciado.
Autenticación
Las integraciones de servidor se autentican con una API key de SHARA. La generas en Ajustes → API Keys (requiere rol de administración y verificación en dos pasos activa) y se envía en cada petición con la cabecera estándar:
Authorization: Bearer shara_sk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX Formato de la clave: prefijo fijo shara_sk_live_ seguido de 24 caracteres base62 (38 caracteres en total). Solo se muestra el valor completo una única vez, en el momento de la creación: guárdalo en tu gestor de secretos. Después, el panel solo enseña el prefijo visible (shara_sk_live_ + 6 caracteres) para que la identifiques sin poder recuperar el secreto.
La verificación es robusta por diseño: la clave se busca por prefijo (solo se consideran claves activas, no expiradas y no revocadas) y el secreto se compara con su hash mediante una función de hashing robusta y comparación en tiempo constante. Una caché breve evita rehasheos en ráfaga sin debilitar la revocación.
Scopes
Cada API key lleva uno o más scopes que delimitan lo que puede hacer. Concede siempre el mínimo necesario (principio de menor privilegio):
| Scope | Permite |
|---|---|
zeus:invoke | Invocar agentes vía el orquestador Amadeus (POST /v1/inference). |
zeus:invoke:agent: | Variante granular: restringe la clave a un único agente por su slug. |
tenant:read | Lectura de metadatos del workspace. |
departments:read | Lectura del catálogo de departamentos y agentes. |
usage:read | Lectura del consumo (STU) del periodo. |
webhooks:ingest | Asociado a la ingesta de eventos entrantes. |
mcp:tenant | Acceso al *bridge* MCP por tenant (herramientas vía SSE/JSON-RPC). |
Se admiten comodines por recurso (recurso:*) al crear una clave. El comodín total*:*está reservado y nunca se emite a una API key. Si una clave no tiene el scope requerido para una ruta, recibes403 scope_requiredy el campomessageindica cuál falta.
Rotación y revocación
Las claves caducan a los 365 días por defecto (máximo configurable: 730). Gestiónalas desde estos endpoints (requieren sesión de administración, no API key):
| Método | Ruta | Qué hace |
|---|---|---|
GET | /v1/api-keys | Lista las claves (sin secreto ni hash). |
POST | /v1/api-keys | Crea una clave; devuelve el secreto una sola vez. |
POST | /v1/api-keys/{id}/rotate | Emite una clave nueva con los mismos scopes y revoca la anterior. |
DELETE | /v1/api-keys/{id} | Revoca de inmediato (soft-delete). |
Cuerpo de creación: { "name": "...", "scopes": ["zeus:invoke"], "expiresInDays": 365 } (name 1–100 caracteres, al menos un scope, expiresInDays entre 1 y 730). La rotación es la vía recomendada para renovar credenciales sin ventana de corte: crea la nueva, despliégala y la anterior queda revocada en el acto.
Límite de peticiones (rate limit)
El límite estándar es de 60 peticiones por minuto (ventana deslizante de 60 s). Cada respuesta incluye cabeceras para que tu cliente se autorregule:
| Cabecera | Significado |
|---|---|
X-RateLimit-Limit | Tope de peticiones de la ventana actual. |
X-RateLimit-Remaining | Peticiones que te quedan en la ventana. |
X-RateLimit-Reset | Momento (epoch en segundos) en que se reinicia la ventana. |
Retry-After | Solo en 429: segundos a esperar antes de reintentar. |
X-Request-Id | Identificador único de la petición; se propaga en todas las respuestas. |
Si superas el límite recibes 429 con { "error": "rate_limit_exceeded", "retryAfterSec": y la cabecera Retry-After. Respeta ese valor con *backoff* exponencial. Si tu integración necesita un techo mayor por workspace, escríbenos a soporte@aiginer.com. La cabecera X-Request-Id viaja en cada respuesta (si la envías tú, se reutiliza; si no, se genera): inclúyela siempre que abras una incidencia con soporte.
Referencia de endpoints
Resumen de la superficie pública. Todas las rutas cuelgan de https://api.shara.aiginer.com/v1.
| Método | Ruta | Auth | Qué hace |
|---|---|---|---|
GET | /v1/agents | API key | Lista los agentes activos de tu workspace. |
POST | /v1/agents/{slug}/messages | API key | Envía un mensaje a un agente y obtiene un run_id. |
POST | /v1/amadeus/messages | API key | Habla directamente con el orquestador Amadeus. |
GET | /v1/runs/{run_id} | API key | Estado y resultado de una ejecución. |
GET | /v1/runs | API key | Lista paginada de ejecuciones. |
GET | /v1/usage | API key | Consumo (STU) del periodo: cuota, consumido, rollover. |
POST | /v1/webhooks/inbound/{slug} | HMAC | Ingesta de un evento entrante firmado. |
GET | /v1/mcp/tenant/sse | Token MCP | Transporte SSE del *bridge* MCP por tenant. |
POST | /v1/mcp/tenant/rpc | Token MCP | Canal JSON-RPC del *bridge* MCP por tenant. |
El orquestador del workspace se llama Amadeus: recibe tu petición, decide qué agente departamental la atiende y coordina el trabajo. Puedes dirigirte a Amadeus (y dejar que enrute) o a un agente concreto por su slug.Enviar un mensaje a un agente
POST /v1/agents/{slug}/messages (y su atajo POST /v1/amadeus/messages) crea una ejecución. Campos del cuerpo:
| Campo | Tipo | Notas |
|---|---|---|
alias | string · obligatorio | Uno de Prelude · Sonata · Symphony · Concerto. |
messages | array (1–100) · obligatorio | { "role": "user"|"assistant"|"system", "content": "..." }; content de 1 a 500.000 caracteres. |
powerMode | string · opcional | normal (defecto) o low. |
maxTokens | entero · opcional | Entre 1 y 32.000. |
effort | string · opcional | low · medium · high · x-high · max. |
stream | boolean · opcional | Si es true, la respuesta llega por SSE. |
Los cuatro alias describen un nivel de capacidad/coste creciente, de Prelude (rápido y económico) a Concerto (máxima capacidad). Elige según la complejidad de la tarea; el alias es lo único que se acepta para seleccionar modelo (nunca un nombre de proveedor). La respuesta síncrona (200) tiene esta forma:
{
"alias": "Sonata",
"content": "Texto de la respuesta del agente…",
"usage": {
"inputTokens": 184,
"outputTokens": 412
}
} Streaming (SSE)
Con "stream": true la respuesta es text/event-stream. Recibes fragmentos incrementales y un evento de cierre con el resumen de consumo y el runId. Cada fragmento pasa por el redactor antes de emitirse:
data: {"delta":"Hola, "}
data: {"delta":"¿en qué "}
data: {"delta":"puedo ayudarte?"}
event: end
data: {"done":true,"runId":"run_a1b2c3","usage":{"inputTokens":184,"outputTokens":412}} Listar agentes
GET /v1/agents devuelve los agentes activos de tu workspace, identificados por su slug y el alias que tienen asignado. Úsalo para descubrir qué agentes departamentales puedes invocar antes de enviar mensajes.
Consultar el consumo
GET /v1/usage devuelve el estado del periodo en STU (la unidad de consumo de SHARA): cuota base, *rollover* acumulado, consumido, porcentaje, proyección, nivel y fecha de reinicio. Es lectura abierta a cualquier miembro y no expone cifras económicas absolutas (esas viven detrás del panel de administración). Forma típica:
{
"period": "2026-06",
"quotaStu": 100000,
"rolloverStu": 12500,
"consumedStu": 41820,
"pctConsumed": 37.2,
"projectedStu": 96400,
"level": "green",
"resetAt": "2026-07-01T00:00:00.000Z"
} Ciclo de vida de un run
Una invocación no-streaming sigue el patrón enviar → consultar (polling). El flujo completo es:
- 1 Enviar:
POST /v1/agents/{slug}/messagescon tu mensaje. La respuesta incluye unrun_id. - 2 Consultar:
GET /v1/runs/{run_id}cada pocos segundos hasta questatussea terminal. - 3 Leer resultado: cuando
statusescompleted, el detalle trae los pasos, las llamadas a herramientas y el contenido (redactado) producido por el agente.
El campo status toma uno de estos valores:
| `status` | Significado |
|---|---|
in_progress | El run sigue ejecutándose. Sigue consultando. |
completed | Terminó con éxito; el resultado está disponible. |
failed | Terminó con error. El detalle trae el motivo (redactado). |
canceled | Se canceló antes de completarse. |
GET /v1/runs lista ejecuciones con filtros status, agent_slug (≤64 caracteres), limit (1–200) y offset (≥0); la respuesta incluye total para paginar. GET /v1/runs/{run_id} devuelve { run, steps[], toolCalls[] }; si el run no existe o no es visible para tu credencial, recibes 404 not_found. Solo se devuelve contenido redactado: nunca material crudo de proveedores.
Recomendación de *polling*: arranca con un intervalo de 1–2 s y aplica *backoff* hasta ~10 s para runs largos. Respeta siempre elX-RateLimit-Remaining. Si necesitas resultados en vivo, usa el modostreamen lugar de consultar.
Errores
Todos los errores son JSON con un campo error (cadena estable en snake_case, pensada para tu lógica) y, opcionalmente, message (texto orientativo). En fallos no controlados (5xx) el manejador global añade requestId. Nunca parsees message: ramifica por error. Forma canónica:
{
"error": "string_en_snake_case",
"message": "Descripción legible (opcional).",
"requestId": "req-a1b2c3"
} Catálogo de códigos públicos verificados:
| `error` | HTTP | Cuándo |
|---|---|---|
missing_bearer | 401 | Falta la cabecera Authorization: Bearer …. |
not_authenticated | 401 | Falta sesión/credencial válida en una ruta que la requiere. |
api_key_not_found | 401 | API key con prefijo desconocido, revocada o caducada. |
api_key_invalid | 401 | API key cuyo hash no verifica. |
payment_not_completed | 402 | El pago del checkout no se ha completado. |
tenant_forbidden | 403 | Autenticado, pero sin membresía en el tenant solicitado. |
forbidden / role_forbidden | 403 | La acción no está permitida para tu rol/clave. |
scope_required | 403 | La API key no tiene el scope necesario (lo indica message). |
agent_disabled | 403 | El agente está pausado. Extra: agentSlug, reason. |
agent_unknown | 403 | El agentSlug no existe. Extra: agentSlug. |
agent_not_in_plan | 403 | El agente no está incluido en tu plan. Extra: agentSlug. |
agent_not_allowed | 403 | El agente no pertenece a tu departamento. Extra: agentSlug. |
not_a_member | 403 | No eres miembro del tenant del agente. Extra: agentSlug. |
not_found | 404 | El recurso no existe o no es visible para ti. |
expired | 410 | Token/enlace de un solo uso ya caducado. |
invalid_request / invalid_body | 400 | Cuerpo o parámetros que no superan la validación. |
quota_exceeded | 429 | Cuota STU agotada. Extra: policy, pctConsumed, resetAt. |
kill_switch | 429 | Corte de gasto activo. Extra: kind, activeSince?. |
rate_limit_exceeded | 429 | Límite de peticiones superado. Extra: retryAfterSec + Retry-After. |
too_many_attempts | 429 | Demasiados intentos fallidos; espera antes de reintentar. |
internal_error | 500 | Error inesperado del servidor. Incluye requestId. |
Algunos404son más específicos por recurso (agent_not_found,webhook_not_found,member_not_found…), todos con la misma forma. Para tu lógica de cliente puedes tratar cualquier*_not_foundcomo "recurso inexistente".
Varios errores traen campos extra para que tu cliente reaccione sin una segunda llamada. Cuota agotada:
{
"error": "quota_exceeded",
"policy": "hardstop",
"pctConsumed": 100,
"resetAt": "2026-07-01T00:00:00.000Z"
} policy puede ser green · warning · critical · overage · grace · hardstop. Corte de gasto (kill switch):
{
"error": "kill_switch",
"kind": "day",
"message": "Interruptor de seguridad de consumo activo.",
"activeSince": "2026-06-16T08:12:00.000Z"
} kind indica el ámbito del interruptor: run · hour · day · manual. Ante 429, respeta Retry-After/resetAt y reintenta con *backoff*; ante 5xx, reintenta con *backoff* e incluye el requestId si abres una incidencia.
Webhooks
SHARA usa un modelo de webhooks entrantes firmados: un sistema externo (tu CRM, un formulario, otra automatización) firma y envía un evento a SHARA, que verifica la firma y lo enruta al agente configurado. No hay suscripción a webhooks salientes; el sentido del tráfico es hacia SHARA.
Creas el webhook desde el panel (Ajustes → Webhooks); cada uno tiene un slug propio en la URL y su propio secreto de firma (nunca compartido entre tenants), que se muestra una sola vez al crearlo. El endpoint de ingesta es:
POST /v1/webhooks/inbound/{slug}
Content-Type: application/json Cabeceras que debes enviar:
| Cabecera | Valor | Notas |
|---|---|---|
X-SHARA-Signature | HMAC del cuerpo crudo | Nombre por defecto; configurable por webhook. |
X-SHARA-Timestamp | o ISO 8601 | Cierra la ventana anti-replay. Obligatoria si el webhook la declara. |
X-SHARA-Event-Id | id único del evento | Idempotencia. Alternativas aceptadas: X-Event-Id, X-Request-Id, X-Webhook-Id, X-Hook-Id. |
Content-Type | application/json | El cuerpo se firma como bytes crudos. |
El cuerpo es JSON libre del emisor: SHARA lo verifica, lo audita y lo transforma (vía plantilla configurable) en lo que recibe el agente. Ejemplo de un lead entrante:
{
"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"
} Respuesta 200 al ingerir correctamente, y variante idempotente cuando reenvías el mismo X-SHARA-Event-Id:
{ "ok": true, "code": "ok", "eventId": "8b1d…", "runId": "run_…" }
{ "ok": true, "code": "duplicate", "eventId": "8b1d…" } | HTTP | `error` | Causa |
|---|---|---|
400 | payload_invalid / invalid_body | JSON inválido o plantilla no renderiza. |
401 | signature_invalid | Firma ausente/incorrecta, esquema no soportado o timestamp fuera de ventana. |
404 | webhook_not_found / webhook_unavailable | Slug inexistente, pausado o revocado. |
413 | payload_too_large | Cuerpo mayor de 1 MB. |
429 | rate_limited | Excedido el límite por IP (RPM configurable por webhook). |
Verificación de firma HMAC
La firma es HMAC-SHA256 (o HMAC-SHA512, configurable) calculada sobre el cuerpo crudo en bytes, nunca sobre el JSON re-serializado. El resultado es hex en minúsculas (64 caracteres para SHA-256, 128 para SHA-512). La verificación es fail-closed (sin secreto, se rechaza todo), compara en tiempo constante (timingSafeEqual) y aplica una ventana anti-replay de ±300 s con tolerancia de reloj de 60 s. Así firmas tú, lado emisor:
import hmac, hashlib, time, json, requests
SECRET = "wsec_tu_secreto_de_firma"
SLUG = "leads-entrantes"
BASE = "https://api.shara.aiginer.com/v1"
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()) Y así verificarías una firma en tu propio receptor, si reenvías eventos de SHARA a tu backend (mismo esquema canónico):
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifySharaSignature(rawBody, signatureHex, timestamp, secret) {
// 1) Anti-replay ANTES del HMAC (ventana ±300 s, skew 60 s)
const nowSec = Math.floor(Date.now() / 1000);
const ts = Number(timestamp);
const diff = nowSec - ts;
if (diff > 300) return false; // demasiado antiguo
if (diff < -60) return false; // demasiado en el futuro
// 2) Formato del hex (64 = sha256)
if (signatureHex.length !== 64 || !/^[0-9a-f]+$/i.test(signatureHex)) return false;
// 3) HMAC sobre el cuerpo CRUDO + comparación en tiempo constante
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);
} Idempotencia y reintentos
SHARA responde de forma síncrona y no reintenta la ingesta: el reintento lo hace el emisor ante un 5xx o un *timeout*. Para que esos reintentos no dupliquen ejecuciones, envía siempre un X-SHARA-Event-Id único por evento: SHARA garantiza idempotencia por ese identificador (restricción UNIQUE) y un reenvío del mismo evento devuelve { "ok": true, "code": "duplicate" } sin crear un run nuevo. La ingesta tiene además *rate limit* por IP (60 RPM por defecto, configurable por webhook).
Ejemplos de código
Recetas completas que usan la Base URL /v1. Sustituye shara_sk_live_… por tu API key.
Enviar un mensaje a un agente
curl -sS https://api.shara.aiginer.com/v1/agents/amadeus/messages \
-H "Authorization: Bearer shara_sk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-d '{
"alias": "Sonata",
"messages": [
{ "role": "user", "content": "Resume las novedades de soporte de esta semana." }
],
"effort": "medium"
}' import requests
BASE = "https://api.shara.aiginer.com/v1"
API_KEY = "shara_sk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
resp = requests.post(
f"{BASE}/agents/amadeus/messages",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"alias": "Sonata",
"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.shara.aiginer.com/v1';
const API_KEY = 'shara_sk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
const res = await fetch(`${BASE}/agents/amadeus/messages`, {
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
alias: 'Sonata',
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 de un run
Cuando trabajas en modo asíncrono, guarda el run_id y consúltalo hasta un estado terminal:
import time, requests
BASE = "https://api.shara.aiginer.com/v1"
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 hasta 10 s
raise TimeoutError(f"run {run_id} no terminó a tiempo")
run = wait_for_run("run_a1b2c3")
print(run["status"]) const BASE = 'https://api.shara.aiginer.com/v1';
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 hasta 10 s
}
throw new Error(`run ${runId} no terminó a tiempo`);
} Consultar el consumo
curl -sS https://api.shara.aiginer.com/v1/usage \
-H "Authorization: Bearer shara_sk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" Recibir y verificar un webhook entrante
Ejemplo de servidor mínimo que recibe la ingesta firmada. Captura el cuerpo crudo (no el JSON ya parseado) para que el HMAC cuadre byte a byte:
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
const SECRET = process.env.SHARA_WEBHOOK_SECRET;
const app = express();
// cuerpo CRUDO para verificar el HMAC sobre los bytes exactos
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('evento verificado:', event.event, event.id);
res.json({ ok: true });
});
app.listen(8080); SDKs
Con la disponibilidad general (GA) publicaremos SDKs oficiales para Node.js / TypeScript y Python, además de los ejemplos en curl de esta página. Hasta entonces, cualquier cliente HTTP estándar funciona: la API es REST plana, con cabeceras estándar y JSON.
¿Quieres acceso anticipado a la API? Escribe aapi@aiginer.comindicando tu caso de uso, o asoporte@aiginer.compara subir tu límite de peticiones por workspace.