Skip to content

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:

  1. admon — the request must carry a valid admon JWT (cookie) and the user must have the menu-ai-hub-config permission. Both are enforced by Hapi's token_admin strategy plus the permissionMiddleware.canByName pre-handler on every route in aiHub.routes.js.
  2. agentic-ai — the proxy injects the shared x-admin-token server-side (read from process.env.BOT_AI_TOKEN on admon, validated against process.env.ADMIN_API_TOKEN on 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/countries

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

json
{
  "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" }
      ]
    }
  ]
}
FieldTypeDescription
codestringInternal slug used as the Redis cache key (e.g. mexico, colombia).
isoCodestringTwo-letter ISO 3166-1 alpha-2 code (e.g. MX).
namestringDisplay name.
businessHoursobject | null{ weekday?, weekend? } where each slot is { start, end } in 24h local time. null when not configured.
holidaysarrayArray 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:

ParameterTypeDescription
codestringInternal slug (mexico) or ISO code (MX). The handler matches code = $1 OR iso_code = $1.

Request body:

FieldTypeDescription
businessHoursobject{ weekday?: { start, end }, weekend?: { start, end } } where each start/end is 023 or null (closed). At least one of weekday/weekend is required when sending the field.
holidaysarray[{ 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:

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

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

StatusReason
400Invalid payload (range, format, length, or empty body).
401Missing or invalid x-admin-token.
404No active country matches the supplied code / isoCode.
500Database 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:

ParameterTypeDescription
country_codestringISO code or internal slug.

Response 200:

json
{
  "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"
    }
  ]
}
FieldTypeDescription
processing_statusstringpending / processing / completed / failed. Drives the badge in the documents panel.
metadataobjectFree-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:

ParameterTypeDescription
idstring (uuid)Document id.

Response 200:

json
{ "id": "9c0a5d2e-…", "processing_status": "completed" }

Get document preview (signed URL)

GET /api/documents/{id}/preview

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

ParameterTypeDescription
idstring (uuid)Document id.

Response 200:

json
{ "previewUrl": "https://…/object/sign/country-documents/MX/raw/plantilla.md?token=…" }
FieldTypeDescription
previewUrlstringSigned URL, valid for 3600 seconds (1 hour).

Notes:

  • Server logs strip the bucket_path from any error message — only the document id is logged.
  • The admon opens the URL with window.open(url, '_blank', 'noopener,noreferrer') to prevent Referer leakage and window.opener access from the destination tab.
  • Content types application/pdf, text/markdown are returned with proper inline disposition headers.

Upload a document

POST /api/documents/upload

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

FieldTypeDescription
filefileRequired. Allowed extensions: pdf, md.
country_codestringRequired. ISO code or internal slug.
metadatastring (JSON)Optional. Stringified JSON merged into the metadata column.

Response 200:

json
{
  "status": "success",
  "documentId": "9c0a5d2e-…",
  "processing_status": "pending"
}

Error responses:

StatusReason
400Missing file, unsupported extension, missing country_code, file too large.
401Missing or invalid x-admin-token.
413File exceeds the configured size limit.
500Storage 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:

ParameterTypeDescription
idstring (uuid)Document id.

Response 200:

json
{ "status": "success", "deletedId": "9c0a5d2e-…" }

Chat (Sandbox)

Send a sandbox message

POST /v1/chat

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

FieldTypeDescription
userIdstringRequired. Stable per-conversation id (e.g. admin-sandbox-${conversationId}).
countrystringRequired. Internal country slug.
channelstringRequired. One of http, respondio, whatsapp, telegram, enviaclaw, clients_portal. The sandbox always uses http.
messagestringRequired. The user message.
metadataobjectOptional. Free-form metadata. The sandbox sets metadata.source = 'admon-sandbox' for traceability.

Example request:

json
{
  "userId": "admin-sandbox-3a4f1c",
  "country": "mexico",
  "channel": "http",
  "message": "¿Cómo cotizo un envío MX → US?",
  "metadata": { "source": "admon-sandbox" }
}

Response 200:

json
{
  "status": "success",
  "response": "...",
  "metadata": {
    "thread_id": "…",
    "intent": "quote_request",
    "tools_used": ["createQuote"]
  }
}
FieldTypeDescription
responsestringThe agent's reply.
metadataobjectInternal context: thread_id, intent, tools_used, etc. Used by the sandbox to display the conversation trace.

Error responses:

StatusReason
400Invalid payload (missing required field, invalid channel enum).
401Missing or invalid x-admin-token.
500Internal 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 keyUsed byAccess type
menu-ai-hub-configSidebar entry, route guard for /ai/configRead/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/inert defaults 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 the x-admin-token server-side. Both ENVIA_AGENT_API_URL (upstream host, same variable the existing agent integration uses) and BOT_AI_TOKEN (shared secret) must be set in admon's .env.
  • See ./index.md for the architecture overview, data flow diagrams, and key design decisions.

Envia Admin