AI Hub API
All AI Hub endpoints live on the agentic-ai backend, but the frontend never talks to it directly. Calls go through the admon proxy mounted at /ai-hub/* (handler in backend/controllers/aiHub.controller.js, routes in backend/routes/aiHub.routes.js).
Two layers of auth are stacked:
- admon — the request must carry a valid admon JWT (cookie) and the user must have the
menu-ai-hub-configpermission. Both are enforced by Hapi'stoken_adminstrategy plus thepermissionMiddleware.canByNamepre-handler on every route inaiHub.routes.js. - agentic-ai — the proxy injects the shared
x-admin-tokenserver-side (read fromprocess.env.BOT_AI_TOKENon admon, validated againstprocess.env.ADMIN_API_TOKENon agentic-ai — same value, different variable name per side). The token never reaches the browser bundle.
The frontend therefore calls a single host (admon) and never needs to know agentic-ai's URL or share its secret.
Countries
List countries with settings
GET /v1/countriesReturns the list of active countries together with their current business_hours and holidays. Used to populate the country selector and the editable settings panel in /ai/config.
Auth: upstream agentic-ai treats this as public (no x-admin-token required). Browsers, however, must still hit it through the admon /ai-hub proxy, which enforces the admon session (token_admin cookie) plus the menu-ai-hub-config permission like every other endpoint in this module.
Response 200:
{
"status": "success",
"countries": [
{
"code": "mexico",
"isoCode": "MX",
"name": "México",
"businessHours": {
"weekday": { "start": 9, "end": 18 },
"weekend": { "start": 10, "end": 14 }
},
"holidays": [
{ "date": "12-25", "name": "Navidad" }
]
}
]
}| Field | Type | Description |
|---|---|---|
code | string | Internal slug used as the Redis cache key (e.g. mexico, colombia). |
isoCode | string | Two-letter ISO 3166-1 alpha-2 code (e.g. MX). |
name | string | Display name. |
businessHours | object | null | { weekday?, weekend? } where each slot is { start, end } in 24h local time. null when not configured. |
holidays | array | Array of { date, name }. date is MM-DD (recurring) or YYYY-MM-DD (one-off, year is silently ignored at runtime). |
Update country settings
PATCH /v1/countries/{code}Persists business_hours and/or holidays to the countries table. Send only the fields you want to change — the other column is left untouched.
Auth: x-admin-token required.
Path parameters:
| Parameter | Type | Description |
|---|---|---|
code | string | Internal slug (mexico) or ISO code (MX). The handler matches code = $1 OR iso_code = $1. |
Request body:
| Field | Type | Description |
|---|---|---|
businessHours | object | { weekday?: { start, end }, weekend?: { start, end } } where each start/end is 0–23 or null (closed). At least one of weekday/weekend is required when sending the field. |
holidays | array | [{ date, name }] with date matching MM-DD or YYYY-MM-DD and name non-empty ≤120 chars. Max 100 entries. |
At least one of businessHours / holidays must be present.
Example request:
{
"businessHours": {
"weekday": { "start": 9, "end": 18 },
"weekend": { "start": null, "end": null }
},
"holidays": [
{ "date": "12-25", "name": "Navidad" },
{ "date": "01-01", "name": "Año nuevo" }
]
}Response 200:
{
"status": "success",
"country": {
"code": "mexico",
"isoCode": "MX",
"name": "México",
"businessHours": { "weekday": { "start": 9, "end": 18 }, "weekend": { "start": null, "end": null } },
"holidays": [
{ "date": "12-25", "name": "Navidad" },
{ "date": "01-01", "name": "Año nuevo" }
]
}
}Error responses:
| Status | Reason |
|---|---|
| 400 | Invalid payload (range, format, length, or empty body). |
| 401 | Missing or invalid x-admin-token. |
| 404 | No active country matches the supplied code / isoCode. |
| 500 | Database unavailable. |
Documents
All document endpoints scope by country code, and uploads are partitioned into country-documents/{COUNTRY}/raw/{filename} in Supabase Storage.
List documents for a country
GET /api/documents/{country_code}Returns documents indexed for the given country. The admon filters out rows where metadata.global = true before rendering, but the backend still returns them.
Auth: x-admin-token required.
Path parameters:
| Parameter | Type | Description |
|---|---|---|
country_code | string | ISO code or internal slug. |
Response 200:
{
"documents": [
{
"id": "9c0a5d2e-…",
"country_code": "MX",
"file_name": "plantilla-envia-mx.md",
"file_type": "md",
"file_size_bytes": 4221,
"processing_status": "completed",
"metadata": { "uploaded_by": "axel.iparrea" },
"created_at": "2026-05-12T17:14:22.000Z"
}
]
}| Field | Type | Description |
|---|---|---|
processing_status | string | pending / processing / completed / failed. Drives the badge in the documents panel. |
metadata | object | Free-form. The admon filters on metadata.global. |
Get document status
GET /api/documents/status/{id}Returns just the processing status of a single document. Useful for polling.
Auth: x-admin-token required.
Path parameters:
| Parameter | Type | Description |
|---|---|---|
id | string (uuid) | Document id. |
Response 200:
{ "id": "9c0a5d2e-…", "processing_status": "completed" }Get document preview (signed URL)
GET /api/documents/{id}/previewGenerates a short-lived signed URL to view/download the underlying file from Supabase Storage. The URL is generated on-demand — never cached, never logged.
Auth: x-admin-token required.
Path parameters:
| Parameter | Type | Description |
|---|---|---|
id | string (uuid) | Document id. |
Response 200:
{ "previewUrl": "https://…/object/sign/country-documents/MX/raw/plantilla.md?token=…" }| Field | Type | Description |
|---|---|---|
previewUrl | string | Signed URL, valid for 3600 seconds (1 hour). |
Notes:
- Server logs strip the
bucket_pathfrom any error message — only the document id is logged. - The admon opens the URL with
window.open(url, '_blank', 'noopener,noreferrer')to preventRefererleakage andwindow.openeraccess from the destination tab. - Content types
application/pdf,text/markdownare returned with proper inline disposition headers.
Upload a document
POST /api/documents/uploadUploads a file (PDF or Markdown) for a given country. The file is stored in Supabase Storage and the metadata row is inserted with processing_status='pending'. A background job processes the file (extraction → chunking → embeddings → document_chunks).
Auth: x-admin-token required.
Content-Type: multipart/form-data.
Form fields:
| Field | Type | Description |
|---|---|---|
file | file | Required. Allowed extensions: pdf, md. |
country_code | string | Required. ISO code or internal slug. |
metadata | string (JSON) | Optional. Stringified JSON merged into the metadata column. |
Response 200:
{
"status": "success",
"documentId": "9c0a5d2e-…",
"processing_status": "pending"
}Error responses:
| Status | Reason |
|---|---|
| 400 | Missing file, unsupported extension, missing country_code, file too large. |
| 401 | Missing or invalid x-admin-token. |
| 413 | File exceeds the configured size limit. |
| 500 | Storage or DB error. |
Delete a document
DELETE /api/documents/{id}Deletes the document row, cascades into document_chunks, and removes the object from Supabase Storage.
Auth: x-admin-token required.
Path parameters:
| Parameter | Type | Description |
|---|---|---|
id | string (uuid) | Document id. |
Response 200:
{ "status": "success", "deletedId": "9c0a5d2e-…" }Chat (Sandbox)
Send a sandbox message
POST /v1/chatUsed by the AgentSandbox panel inside /ai/config to exercise the unified support graph in real time. It is the same endpoint that production channels hit — the sandbox just identifies itself with the http channel and a deterministic userId so its state does not collide with real users.
Auth: x-admin-token required.
Request body:
| Field | Type | Description |
|---|---|---|
userId | string | Required. Stable per-conversation id (e.g. admin-sandbox-${conversationId}). |
country | string | Required. Internal country slug. |
channel | string | Required. One of http, respondio, whatsapp, telegram, enviaclaw, clients_portal. The sandbox always uses http. |
message | string | Required. The user message. |
metadata | object | Optional. Free-form metadata. The sandbox sets metadata.source = 'admon-sandbox' for traceability. |
Example request:
{
"userId": "admin-sandbox-3a4f1c",
"country": "mexico",
"channel": "http",
"message": "¿Cómo cotizo un envío MX → US?",
"metadata": { "source": "admon-sandbox" }
}Response 200:
{
"status": "success",
"response": "...",
"metadata": {
"thread_id": "…",
"intent": "quote_request",
"tools_used": ["createQuote"]
}
}| Field | Type | Description |
|---|---|---|
response | string | The agent's reply. |
metadata | object | Internal context: thread_id, intent, tools_used, etc. Used by the sandbox to display the conversation trace. |
Error responses:
| Status | Reason |
|---|---|
| 400 | Invalid payload (missing required field, invalid channel enum). |
| 401 | Missing or invalid x-admin-token. |
| 500 | Internal agent error (graph crash, downstream timeout). |
Permissions Summary
The admon enforces a single permission for the entire module before serving the UI. The agentic-ai backend itself does not consume admon permissions — it relies on the shared x-admin-token.
| Permission key | Used by | Access type |
|---|---|---|
menu-ai-hub-config | Sidebar entry, route guard for /ai/config | Read/Write — gates the entire AI Hub UI |
When this permission is missing for a user, the admon hides the AI Hub menu entry and the route is blocked. Granting it via role assignment or per-user override in admin_permissions_overrides unlocks the surface.
Notes
- All endpoints are subject to the rate limits configured on the agentic-ai server (Hapi
@hapi/inertdefaults plus custom middleware where applicable). - Times in responses (e.g.
created_at) are ISO 8601 strings in UTC. - Frontend callers hit admon at
/ai-hub/.... Admon rewrites the path to${ENVIA_AGENT_API_URL}/...and adds thex-admin-tokenserver-side. BothENVIA_AGENT_API_URL(upstream host, same variable the existing agent integration uses) andBOT_AI_TOKEN(shared secret) must be set in admon's.env. - See
./index.mdfor the architecture overview, data flow diagrams, and key design decisions.
