# API de control

> La referencia REST de todo lo que no es una tool: catálogo, conexiones, sesiones, sincronizaciones y webhooks.



Ejecutar una tool es siempre `POST /v1/tools/{id}/execute`, y su contrato completo vive en [la referencia](/docs/referencia) y en el [OpenAPI](/docs/api). Alrededor de eso el gateway expone una superficie de **control**: qué puedes llamar, qué tienes conectado, en qué va una sincronización y a dónde se entregan los eventos. Esta página documenta esa superficie completa.

<Callout type="info" title="Estos endpoints no están en openapi.json">
  Los contratos OpenAPI publicados cubren la ejecución de tools. Los endpoints de control de esta página todavía no entran en esos archivos; mientras tanto, esta página es su referencia.
</Callout>

## Convenciones [#convenciones]

La base es `https://connect.emisso.ai/api/v1`. Salvo `GET /v1/health`, `GET /v1` y `GET /v1/openapi.json`, todos los endpoints exigen `Authorization: Bearer connect_sk_…` y responden `401 unauthorized` con el challenge `WWW-Authenticate` si el token falta o no sirve. Si la infraestructura de autenticación está caída, la respuesta es `503 service_unavailable` (retryable): una caída nuestra nunca se disfraza de «tu clave está mala».

Reglas que aplican a toda la superficie:

* Todo cuerpo de petición está limitado a **256 KB**: por encima responde `413 payload_too_large`, y un JSON inválido responde `400 malformed_request`.
* Toda respuesta de éxito envuelve el resultado en `{ "data": … }`; todo error viene como `{ "error": { "code", "message", "request_id", "suggested_fix" } }` con el código del [catálogo de errores](/docs/operar/errores).
* Los `GET` de esta página no escriben en la bitácora: no ejecutan ninguna tool. Solo `execute` audita.

## Descubrimiento [#descubrimiento]

### `GET /v1/health` [#get-v1health]

Sin credencial. Dice si el gateway está arriba y cuántas tools tiene el registro.

```bash
curl https://connect.emisso.ai/api/v1/health
```

```json
{ "data": { "status": "ok", "registry": { "tools": 29 } } }
```

### `GET /v1` [#get-v1]

Sin credencial. El puntero raíz del API; a diferencia del resto, responde sin el envelope `data`.

```bash
curl https://connect.emisso.ai/api/v1
```

```json
{
  "data_url": "/api/v1",
  "openapi": "/api/v1/openapi.json",
  "docs": "https://connect.emisso.ai/docs"
}
```

### `GET /v1/tools` [#get-v1tools]

El catálogo **filtrado para tu organización**: solo los conectores que tienes habilitados y solo lo que cubren los scopes de tu clave. Dos organizaciones ven listas distintas. Cada entrada trae la ficha completa, incluidos los JSON-Schema de entrada y salida y, cuando la tool lo declara, un `example` ejecutable:

```bash
curl https://connect.emisso.ai/api/v1/tools \
  -H "Authorization: Bearer connect_sk_..."
```

```json
{
  "data": [
    {
      "id": "sii.rcv.consultar",
      "mcpName": "sii__rcv__consultar",
      "connector": "sii", "resource": "rcv", "verb": "consultar",
      "title": "Consultar RCV del SII",
      "description": "…",
      "plane": "action",
      "reads": "rcv",
      "scope": "sii:read",
      "auth": "none",
      "sensitive": true,
      "version": 4,
      "deprecated": false,
      "annotations": { "readOnly": true, "destructive": false, "idempotent": true, "openWorld": false, "title": "Consultar RCV del SII" },
      "inputSchema": { "…": "draft 2020-12" },
      "outputSchema": { "…": "draft 2020-12" },
      "example": { "input": { "…": "…" }, "output": { "…": "…" } }
    }
  ],
  "pagination": { "cursor": null, "hasMore": false }
}
```

`plane` dice si la tool persiste; `reads` aparece solo en las tools de consulta y nombra el **alcance** que leen. El modelo completo está en [sincronizar y consultar](/docs/conceptos/sincronizar-consultar).

### `GET /v1/tools/{id}` [#get-v1toolsid]

La misma ficha, para un id concreto. Este endpoint no filtra por organización: devuelve la ficha de cualquier tool del catálogo, esté o no conectado su sistema, porque descubrir no es ejecutar. Un id inexistente responde `404 tool_not_found`.

```bash
curl https://connect.emisso.ai/api/v1/tools/sii.rcv.consultar \
  -H "Authorization: Bearer connect_sk_..."
```

```json
{ "data": { "id": "sii.rcv.consultar", "…": "la misma ficha de GET /v1/tools" } }
```

### `GET /v1/connectors` [#get-v1connectors]

Los conectores de uso general, con su estado de habilitación para tu organización. **Los sistemas
conectables (SII y bancos) no aparecen aquí**: este endpoint sirve solo el catálogo público. Para
obtener los códigos de conector sin adivinar, incluidos los conectables, usa la tool
[`conexiones.sistemas.listar`](/docs/referencia/conexiones/sistemas-listar), que además dice qué ya
está conectado.

```bash
curl https://connect.emisso.ai/api/v1/connectors \
  -H "Authorization: Bearer connect_sk_..."
```

```json
{ "data": [{ "code": "core", "name": "Core", "plan": "free", "enabled": true }] }
```

<Callout type="info" title="Este endpoint lista solo el catálogo público">
  `/v1/connectors` sirve los conectores de uso general (`core`, `echo`, `indicadores`, `conexiones`). Las fuentes private/tenant como el SII o los bancos **no** aparecen aquí aunque las tengas conectadas y aunque sus tools sí salgan en `/v1/tools`. Para ver lo que tu organización tiene conectado, usa `/v1/connections` o la tool `conexiones.estado.consultar`.
</Callout>

### `GET /v1/openapi.json` [#get-v1openapijson]

Sin credencial. El contrato OpenAPI 3.1 del catálogo público, servido por el propio runtime. El contrato del catálogo documentado completo (SII y bancos incluidos) es otro artefacto: [`/docs/openapi.json`](https://connect.emisso.ai/docs/openapi.json).

```bash
curl -s https://connect.emisso.ai/api/v1/openapi.json | jq .info
```

```json
{
  "title": "Emisso Connect API",
  "version": "0.1.0"
}
```

## Conexiones [#conexiones]

### `GET /v1/connections` [#get-v1connections]

Tus conexiones, cada una la unidad de facturación. De aquí salen los `conn_…` que necesitas para el header `X-Connect-Connection`.

```bash
curl https://connect.emisso.ai/api/v1/connections \
  -H "Authorization: Bearer connect_sk_..."
```

```json
{
  "data": [
    {
      "id": "conn_4ly7hnmw2tigryuxfjkr4",
      "connector": "sii",
      "display_name": "SII prod",
      "status": "active",
      "plan": "paid",
      "enabled_scopes": ["sii:read"]
    }
  ],
  "pagination": { "cursor": null, "hasMore": false }
}
```

Los conectores `open` y `platform` nunca aparecen: no hay nada que conectar.

<Callout type="info" title="Si eres un agente, prefiere la tool">
  `conexiones.estado.consultar` responde lo mismo y además: qué **alcances** tiene activos cada conexión, en qué van sus sincronizaciones, qué tools puedes invocar sobre ella y `datosListos`, la respuesta a «¿ya hay algo que leer?». Por ser una tool, se llama igual por MCP que por REST.
</Callout>

## Sesiones de conexión [#sesiones-de-conexión]

El flujo hosted completo (incrustar el iframe, `postMessage`, el webhook `connect_session.consumed`, el modelo de amenaza) vive en [el enlace hosted](/docs/operar/enlace-hosted). Aquí, el contrato de los dos endpoints.

### `POST /v1/connect_sessions` [#post-v1connect_sessions]

Crea un enlace de un solo uso para que un tercero entregue credenciales de una fuente concreta, sin cuenta y sin acceso al dashboard.

| Campo             | Tipo                   | Notas                                                                                                                                     |
| ----------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `mode`            | `"create" \| "reauth"` | `create` (default) emite una conexión nueva; `reauth` reemplaza la credencial de una existente y exige `connection_id`.                   |
| `connector_code`  | `string`               | Una fuente conectable: `sii`, `bci_pyme`, `banco_security`, `bice_empresas`, `bch_empresas`. El enlace queda fijado a esa fuente.         |
| `connection_id`   | `string`               | Obligatorio en `reauth`. Una conexión de otra organización degrada al mismo `400 validation_error`: nunca un 404 que confirme que existe. |
| `allowed_origins` | `string[]`             | Hasta 5 orígenes `https` exactos, sin path ni comodín. Allowlist del iframe y destino del `postMessage`.                                  |
| `redirect_uri`    | `string`               | `https`, sin fragmento y, si registraste orígenes, dentro de ese conjunto.                                                                |

El esquema es estricto: un campo desconocido responde `400 validation_error`. La emisión está limitada a **60 enlaces por hora por organización**; superado el tope, la respuesta es `429 rate_limited` con `Retry-After`. El enlace vence a las **24 horas** en `create` y a la **1 hora** en `reauth`, y admite **5 intentos** reales de credencial.

```bash
curl -X POST https://connect.emisso.ai/api/v1/connect_sessions \
  -H "Authorization: Bearer connect_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "create",
    "connector_code": "sii",
    "allowed_origins": ["https://app.tu-producto.cl"],
    "redirect_uri": "https://app.tu-producto.cl/conexiones/listo"
  }'
```

Respuesta (`201`):

```json
{
  "data": {
    "id": "cs_7f3ab9c1d2e4f5061728",
    "mode": "create",
    "connector_code": "sii",
    "connection_id": null,
    "token_prefix": "connect_cs_AbC…",
    "allowed_origins": ["https://app.tu-producto.cl"],
    "redirect_uri": "https://app.tu-producto.cl/conexiones/listo",
    "status": "pending",
    "attempts": 0,
    "max_attempts": 5,
    "expires_at": "2026-08-08T12:00:00.000Z",
    "consumed_at": null,
    "resulting_connection_id": null,
    "created_at": "2026-08-07T12:00:00.000Z",
    "url": "https://connect.emisso.ai/c/connect_cs_..."
  },
  "meta": { "request_id": "req_..." }
}
```

<Callout type="warn" title="El token aparece una sola vez">
  El token en claro viaja solo en esta respuesta: la base guarda su `sha256` y un prefijo visible para nombrarlo en una lista. Si lo pierdes, emite otra sesión y revoca la anterior.
</Callout>

### `GET /v1/connect_sessions` [#get-v1connect_sessions]

Lista las sesiones con su `token_prefix`, su estado (`pending`, `consumed`, `revoked`, `expired`) y sus intentos; nunca el token. Acepta `?connection_id=conn_…` para acotar a una conexión.

```bash
curl "https://connect.emisso.ai/api/v1/connect_sessions?connection_id=conn_..." \
  -H "Authorization: Bearer connect_sk_..."
```

```json
{
  "data": [
    {
      "id": "cs_7f3ab9c1d2e4f5061728",
      "mode": "reauth",
      "connector_code": "sii",
      "connection_id": "conn_4ly7hnmw2tigryuxfjkr4",
      "token_prefix": "connect_cs_AbC…",
      "allowed_origins": [],
      "redirect_uri": null,
      "status": "consumed",
      "attempts": 1,
      "max_attempts": 5,
      "expires_at": "2026-08-07T13:00:00.000Z",
      "consumed_at": "2026-08-07T12:14:09.000Z",
      "resulting_connection_id": "conn_4ly7hnmw2tigryuxfjkr4",
      "created_at": "2026-08-07T12:00:00.000Z"
    }
  ],
  "pagination": { "cursor": null, "hasMore": false }
}
```

## Sincronizaciones [#sincronizaciones]

`conexion.sincronizar` como tool corre en el momento y bloquea hasta terminar; en un banco eso ronda los 90 segundos por login. Esta vía asíncrona encola el trabajo y lo consulta después, y es la recomendada para varios períodos o para no sostener una petición HTTP larga.

### `POST /v1/connections/{conn}/syncs` [#post-v1connectionsconnsyncs]

| Campo      | Tipo       | Notas                                                                                                                    |
| ---------- | ---------- | ------------------------------------------------------------------------------------------------------------------------ |
| `periodos` | `string[]` | Opcional. Cada uno con forma `AAAA-MM`; sin él se encola el mes en curso. Máximo 24 períodos por petición.               |
| `alcances` | `string[]` | Opcional. Sin él se toman todos los alcances habilitados de la conexión. Cada uno debe estar habilitado en esa conexión. |

```bash
curl -X POST https://connect.emisso.ai/api/v1/connections/conn_.../syncs \
  -H "Authorization: Bearer connect_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"periodos":["2026-08"],"alcances":["rcv"]}'
```

Respuesta (`202`), un trabajo por período encolado:

```json
{ "data": { "job_ids": ["sjb_..."] }, "meta": { "request_id": "req_..." } }
```

Un `periodo` mal formado se rechaza con `400 validation_error` antes de ocupar un cupo. La cola admite hasta 50 trabajos pendientes por organización (`429 too_many_pending`), y un período que ya tiene un trabajo en vuelo responde `409 connection_sync_in_progress`.

### `GET /v1/syncs/{id}` [#get-v1syncsid]

El estado del trabajo. `status` es uno de `queued`, `running`, `succeeded`, `failed` o `partial`.

```bash
curl https://connect.emisso.ai/api/v1/syncs/sjb_... \
  -H "Authorization: Bearer connect_sk_..."
```

```json
{
  "data": {
    "job_id": "sjb_...",
    "status": "succeeded",
    "targets": ["rcv"],
    "outcomes": [{ "alcance": "rcv", "status": "ok", "recordsSynced": 12 }],
    "alcance": "rcv",
    "periodo": "2026-08",
    "trigger": "manual",
    "attempts": 1,
    "records_synced": 12,
    "last_error": null,
    "created_at": "2026-08-06T17:11:30.000Z",
    "updated_at": "2026-08-06T17:11:44.526Z"
  }
}
```

`outcomes` desglosa el resultado por alcance cuando el trabajo terminó bien (cada entrada con `status` `ok`, `partial` o `failed`); en un trabajo fallido o aún en curso viene `null`. Si prefieres que te avisen en vez de sondear, los eventos `sync.*` llegan por webhook firmado.

## Webhooks [#webhooks]

La firma de las entregas, los cuerpos de cada evento y las políticas de reintento están en [webhooks](/docs/operar/webhooks). El índice de la superficie:

| Método y ruta                                           | Qué hace                                                                               |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `POST /v1/webhooks`                                     | Crea un endpoint. La única respuesta que muestra el `secret`.                          |
| `GET /v1/webhooks`                                      | Lista tus endpoints.                                                                   |
| `GET /v1/webhooks/{id}`                                 | Un endpoint puntual.                                                                   |
| `PATCH /v1/webhooks/{id}`                               | Modifica `url`, `event_types` o `enabled`.                                             |
| `DELETE /v1/webhooks/{id}`                              | Deshabilita el endpoint. Es un apagado, no un borrado: reaparece con `enabled: false`. |
| `POST /v1/webhooks/{id}/roll-secret`                    | Rota el `secret` y devuelve el nuevo, una única vez.                                   |
| `GET /v1/webhooks/{id}/deliveries`                      | Las entregas del endpoint. `?limit=` entre 1 y 100, default 50.                        |
| `POST /v1/webhooks/{id}/deliveries/{deliveryId}/resend` | Reenvía una entrega puntual.                                                           |

Los eventos suscribibles son `sync.succeeded`, `sync.failed`, `sync.partial` y `connect_session.consumed`. Un `event_types` vacío suscribe a todos.

### `POST /v1/webhooks` [#post-v1webhooks]

| Campo           | Tipo       | Notas                                                                                   |
| --------------- | ---------- | --------------------------------------------------------------------------------------- |
| `url`           | `string`   | `https` y resoluble a una dirección pública; si no, `422 invalid_webhook_url`.          |
| `event_types`   | `string[]` | Opcional. Vacío = todos los eventos.                                                    |
| `connection_id` | `string`   | Opcional. Acota el endpoint a los eventos de una conexión; debe ser de tu organización. |

```bash
curl -X POST https://connect.emisso.ai/api/v1/webhooks \
  -H "Authorization: Bearer connect_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"url":"https://app.tu-producto.cl/hooks/connect","event_types":["sync.succeeded","sync.failed"]}'
```

Respuesta (`201`):

```json
{
  "data": {
    "id": "whk_...",
    "url": "https://app.tu-producto.cl/hooks/connect",
    "event_types": ["sync.succeeded", "sync.failed"],
    "connection_id": null,
    "enabled": true,
    "disabled_at": null,
    "last_success_at": null,
    "created_at": "2026-08-07T12:00:00.000Z",
    "secret": "whsec_..."
  },
  "meta": { "request_id": "req_..." }
}
```

Guarda el `secret` al recibirlo: las lecturas posteriores (`GET /v1/webhooks`, `GET /v1/webhooks/{id}`) devuelven el endpoint sin él.

### `GET /v1/webhooks/{id}/deliveries` [#get-v1webhooksiddeliveries]

```bash
curl "https://connect.emisso.ai/api/v1/webhooks/whk_.../deliveries?limit=2" \
  -H "Authorization: Bearer connect_sk_..."
```

```json
{
  "data": [
    {
      "id": "whd_...",
      "event_type": "sync.succeeded",
      "status": "delivered",
      "attempts": 1,
      "response_status": 200,
      "error": null,
      "created_at": "2026-08-07T12:10:00.000Z",
      "updated_at": "2026-08-07T12:10:01.000Z"
    },
    {
      "id": "whd_...",
      "event_type": "sync.failed",
      "status": "failed",
      "attempts": 6,
      "response_status": 500,
      "error": "HTTP 500",
      "created_at": "2026-08-06T09:00:00.000Z",
      "updated_at": "2026-08-06T18:02:11.000Z"
    }
  ],
  "pagination": { "cursor": null, "hasMore": false }
}
```

`status` es uno de `pending`, `delivering`, `delivered` o `failed`.

### `POST /v1/webhooks/{id}/deliveries/{deliveryId}/resend` [#post-v1webhooksiddeliveriesdeliveryidresend]

Reencola una entrega concreta, por ejemplo después de arreglar el receptor.

```bash
curl -X POST https://connect.emisso.ai/api/v1/webhooks/whk_.../deliveries/whd_.../resend \
  -H "Authorization: Bearer connect_sk_..."
```

```json
{ "data": { "resent": true } }
```

## Ejecutar una tool [#ejecutar-una-tool]

Es una sola forma: `POST /v1/tools/{id}/execute` con el cuerpo `{ "input": { … } }`, el header `X-Connect-Connection: conn_…` cuando el conector tiene conexión, y la respuesta `{ "data": …, "meta": { "request_id", "tool_id", "plane", "latency_ms", "audit_status" } }`. La ficha de cada tool, con su ejemplo, está en [la referencia](/docs/referencia).

## Errores frecuentes por superficie [#errores-frecuentes-por-superficie]

Todos salen del [catálogo](/docs/operar/errores) y traen `request_id` más `suggested_fix`.

| Superficie           | Códigos que vas a ver                                                                                                                                                                                     |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Toda la API          | `401 unauthorized` (falta o falla el bearer, con challenge `WWW-Authenticate`) · `503 service_unavailable` (falla transitoria nuestra, reintenta) · `413 payload_too_large` · `400 malformed_request`     |
| Tools                | `404 tool_not_found` (el id no existe, o el conector no está habilitado para tu organización) · `400 validation_error` · `403 scope_not_granted` · `428 connection_credential_required`                   |
| Sincronizaciones     | `403 connection_disabled` (la conexión no existe en tu organización o no está activa) · `403 alcance_not_enabled` · `409 connection_sync_in_progress` · `429 too_many_pending` · `404 sync_job_not_found` |
| Sesiones de conexión | `400 validation_error` (incluye `connection_id` ajeno o inconsistente con `mode`) · `429 rate_limited` con `Retry-After`                                                                                  |
| Webhooks             | `404 webhook_endpoint_not_found` · `404 webhook_delivery_not_found` · `422 invalid_webhook_url` · `403 feature_not_in_plan`                                                                               |

Los 404 de esta tabla nunca distinguen «no existe» de «existe en otra organización»: esa ambigüedad es deliberada.
