Ir al contenido

API REST

Shara expone una API REST sobre HTTPS para integraciones programáticas. El host es https://api.aiginer.com, 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.

La API es REST sobre HTTPS. El host es https://api.aiginer.com. No todos los endpoints cuelgan de un prefijo /v1 uniforme: la invocación de agentes y el bridge MCP sí lo llevan (/v1/inference, /v1/mcp/…), mientras que la gestión de claves, los runs, el consumo y los webhooks entrantes viven en la raíz (/api-keys, /runs, /usage/current, /webhooks/inbound/{slug}). Esta página usa siempre la ruta completa y real de cada endpoint. 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
Hosthttps://api.aiginer.com
ProtocoloHTTPS obligatorio (TLS 1.2+)
Formatoapplication/json · streaming en text/event-stream
Autenticación de servicioAPI key vía Authorization: Bearer <api-key>
Rate limit60 req/min por IP · 600 req/min por workspace (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.

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.

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
amadeus:invokeInvocar agentes vía POST /v1/inference, el endpoint único de invocación (dirige al orquestador Amadeus o a un agente concreto según agentSlug).
amadeus:invoke:agent:<slug>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.

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/api-keysLista las claves (sin secreto ni hash).
POST/api-keysCrea una clave; devuelve el secreto una sola vez.
POST/api-keys/{id}/rotateEmite una clave nueva con los mismos scopes y revoca la anterior.
DELETE/api-keys/{id}Revoca de inmediato (soft-delete).

Cuerpo de creación: { "name": "...", "scopes": ["amadeus:invoke"], "expiresInDays": 365 } (name de 1 a 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.

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": <n> } 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.

Resumen de la superficie pública. Rutas completas sobre el host https://api.aiginer.com (algunas llevan /v1, otras no; usa siempre la ruta tal cual aparece aquí).

MétodoRutaAuthQué hace
POST/v1/inferenceAPI keyInvoca un agente (Amadeus por defecto, o el que indiques en agentSlug) y obtiene un run_id.
GET/runs/{run_id}API keyEstado y resultado de una ejecución.
GET/runsAPI keyLista paginada de ejecuciones.
GET/usage/currentAPI keyConsumo (STU) del periodo: cuota, consumido, rollover.
POST/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. Con agentSlug puedes dirigirte a Amadeus (por defecto, si lo omites) o a un agente concreto por su slug. No hay un catálogo público de agentes vía API todavía; consulta el catálogo de agentes de esta documentación para ver los slugs disponibles.

POST /v1/inference crea una ejecución. Es el único endpoint de invocación: no hay una ruta por agente, sino un campo agentSlug en el cuerpo. Campos del cuerpo:

CampoTipoNotas
aliasstring · obligatorioUno de Prelude · Sonata · Symphony · Concerto.
messagesarray (1 a 100) · obligatorio{ "role": "user"|"assistant"|"system", "content": "..." }; content de 1 a 500.000 caracteres.
agentSlugstring · opcionalAgente objetivo del turno. Por defecto amadeus. Tu API key necesita el scope para ese agente.
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.
conversationIdstring · opcionalAgrupa varios turnos en un mismo hilo (hasta 200 caracteres).

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

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

GET /usage/current 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"
}

Una invocación no-streaming sigue el patrón enviar → consultar (polling). El flujo completo es:

  1. Enviar: POST /v1/inference con tu mensaje. La respuesta incluye un run_id.
  2. Consultar: GET /runs/{run_id} cada pocos segundos hasta que status sea terminal.
  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:

statusSignificado
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 /runs lista ejecuciones con filtros status, agent_slug (≤64 caracteres), limit (1 a 200) y offset (≥0); la respuesta incluye total para paginar. GET /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 a 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.

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:

errorHTTPCuá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.

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 /webhooks/inbound/{slug}
Content-Type: application/json

Cabeceras que debes enviar:

CabeceraValorNotas
X-SHARA-Signature<hex> HMAC del cuerpo crudoNombre por defecto; configurable por webhook.
X-SHARA-Timestamp<unix-seconds> 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…" }
HTTPerrorCausa
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).

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

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

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

Recetas completas sobre el host https://api.aiginer.com. Sustituye shara_sk_live_… por tu 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());

Cuando trabajas en modo asíncrono, guarda el run_id y consúltalo hasta un estado terminal:

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 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.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 hasta 10 s
  }
  throw new Error(`run ${runId} no terminó a tiempo`);
}
bash
curl -sS https://api.aiginer.com/usage/current \
  -H "Authorization: Bearer shara_sk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

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

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.

¿Dudas sobre la API? Escríbenos a api@aiginer.com.