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.

PropiedadValor
Base URLhttps://api.shara.aiginer.com/v1
ProtocoloHTTPS obligatorio (TLS 1.2+)
Formatoapplication/json · streaming en text/event-stream
Autenticación de servicioAPI key vía Authorization: Bearer
Rate limit estándar60 req/min (ver cabeceras X-RateLimit-*)
TrazabilidadCabecera X-Request-Id en toda respuesta
Versionado: la versión actual es v1. Cuando publiquemos cambios incompatibles lo haremos bajo un prefijo nuevo (/v2) y mantendremos /v1 en 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:

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

ScopePermite
zeus:invokeInvocar 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:readLectura de metadatos del workspace.
departments:readLectura del catálogo de departamentos y agentes.
usage:readLectura del consumo (STU) del periodo.
webhooks:ingestAsociado a la ingesta de eventos entrantes.
mcp:tenantAcceso 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, recibes 403 scope_required y el campo message indica 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étodoRutaQué hace
GET/v1/api-keysLista las claves (sin secreto ni hash).
POST/v1/api-keysCrea una clave; devuelve el secreto una sola vez.
POST/v1/api-keys/{id}/rotateEmite 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:

CabeceraSignificado
X-RateLimit-LimitTope de peticiones de la ventana actual.
X-RateLimit-RemainingPeticiones que te quedan en la ventana.
X-RateLimit-ResetMomento (epoch en segundos) en que se reinicia la ventana.
Retry-AfterSolo en 429: segundos a esperar antes de reintentar.
X-Request-IdIdentificador ú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étodoRutaAuthQué hace
GET/v1/agentsAPI keyLista los agentes activos de tu workspace.
POST/v1/agents/{slug}/messagesAPI keyEnvía un mensaje a un agente y obtiene un run_id.
POST/v1/amadeus/messagesAPI keyHabla directamente con el orquestador Amadeus.
GET/v1/runs/{run_id}API keyEstado y resultado de una ejecución.
GET/v1/runsAPI keyLista paginada de ejecuciones.
GET/v1/usageAPI keyConsumo (STU) del periodo: cuota, consumido, rollover.
POST/v1/webhooks/inbound/{slug}HMACIngesta de un evento entrante firmado.
GET/v1/mcp/tenant/sseToken MCPTransporte SSE del *bridge* MCP por tenant.
POST/v1/mcp/tenant/rpcToken MCPCanal 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:

CampoTipoNotas
aliasstring · obligatorioUno de Prelude · Sonata · Symphony · Concerto.
messagesarray (1–100) · obligatorio{ "role": "user"|"assistant"|"system", "content": "..." }; content de 1 a 500.000 caracteres.
powerModestring · opcionalnormal (defecto) o low.
maxTokensentero · opcionalEntre 1 y 32.000.
effortstring · opcionallow · medium · high · x-high · max.
streamboolean · opcionalSi 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:

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

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

json
{
  "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. 1 Enviar: POST /v1/agents/{slug}/messages con tu mensaje. La respuesta incluye un run_id.
  2. 2 Consultar: GET /v1/runs/{run_id} cada pocos segundos hasta que status sea terminal.
  3. 3 Leer resultado: cuando status es completed, 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_progressEl run sigue ejecutándose. Sigue consultando.
completedTerminó con éxito; el resultado está disponible.
failedTerminó con error. El detalle trae el motivo (redactado).
canceledSe 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 el X-RateLimit-Remaining. Si necesitas resultados en vivo, usa el modo stream en 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:

json
{
  "error": "string_en_snake_case",
  "message": "Descripción legible (opcional).",
  "requestId": "req-a1b2c3"
}

Catálogo de códigos públicos verificados:

`error`HTTPCuándo
missing_bearer401Falta la cabecera Authorization: Bearer ….
not_authenticated401Falta sesión/credencial válida en una ruta que la requiere.
api_key_not_found401API key con prefijo desconocido, revocada o caducada.
api_key_invalid401API key cuyo hash no verifica.
payment_not_completed402El pago del checkout no se ha completado.
tenant_forbidden403Autenticado, pero sin membresía en el tenant solicitado.
forbidden / role_forbidden403La acción no está permitida para tu rol/clave.
scope_required403La API key no tiene el scope necesario (lo indica message).
agent_disabled403El agente está pausado. Extra: agentSlug, reason.
agent_unknown403El agentSlug no existe. Extra: agentSlug.
agent_not_in_plan403El agente no está incluido en tu plan. Extra: agentSlug.
agent_not_allowed403El agente no pertenece a tu departamento. Extra: agentSlug.
not_a_member403No eres miembro del tenant del agente. Extra: agentSlug.
not_found404El recurso no existe o no es visible para ti.
expired410Token/enlace de un solo uso ya caducado.
invalid_request / invalid_body400Cuerpo o parámetros que no superan la validación.
quota_exceeded429Cuota STU agotada. Extra: policy, pctConsumed, resetAt.
kill_switch429Corte de gasto activo. Extra: kind, activeSince?.
rate_limit_exceeded429Límite de peticiones superado. Extra: retryAfterSec + Retry-After.
too_many_attempts429Demasiados intentos fallidos; espera antes de reintentar.
internal_error500Error inesperado del servidor. Incluye requestId.
Algunos 404 son 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_found como "recurso inexistente".

Varios errores traen campos extra para que tu cliente reaccione sin una segunda llamada. Cuota agotada:

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

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

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

Cabeceras que debes enviar:

CabeceraValorNotas
X-SHARA-Signature HMAC del cuerpo crudoNombre por defecto; configurable por webhook.
X-SHARA-Timestamp o ISO 8601Cierra la ventana anti-replay. Obligatoria si el webhook la declara.
X-SHARA-Event-Idid único del eventoIdempotencia. Alternativas aceptadas: X-Event-Id, X-Request-Id, X-Webhook-Id, X-Hook-Id.
Content-Typeapplication/jsonEl 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:

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

Respuesta 200 al ingerir correctamente, y variante idempotente cuando reenvías el mismo X-SHARA-Event-Id:

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

{ "ok": true, "code": "duplicate", "eventId": "8b1d…" }
HTTP`error`Causa
400payload_invalid / invalid_bodyJSON inválido o plantilla no renderiza.
401signature_invalidFirma ausente/incorrecta, esquema no soportado o timestamp fuera de ventana.
404webhook_not_found / webhook_unavailableSlug inexistente, pausado o revocado.
413payload_too_largeCuerpo mayor de 1 MB.
429rate_limitedExcedido 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:

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

javascript
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

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

python
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"])
javascript
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

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

javascript
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 a api@aiginer.com indicando tu caso de uso, o a soporte@aiginer.com para subir tu límite de peticiones por workspace.
¿Algo que no encuentras? Escríbenos a hola@aiginer.com.