xGestion · referencia técnica

Comandos de api.php

Qué hace cada comando del endpoint genérico de la raíz del repo, con qué token se llama, qué parámetros recibe, qué devuelve y qué efectos tiene. Para consultas técnicas: integraciones, n8n, IAs externas, landing y bot de WhatsApp.

13 comandos vivos 14 retirados 5 tokens en system.apis (local) Revisado contra master f3db239a + informe del agente · 2026-10-17

Cómo llamar

Un solo archivo, ./api.php, recibe todos los comandos. El campo execute elige la acción.

EntornoURL
Producciónhttps://www.xgestion.com.ar/api.php
Producción (dominio Verena)https://www.verena.com.ar/api.php
Localhttp://localhost/xgestion/api.php

Petición

  • Autenticación: header Authorization: Bearer <token>. El token se busca en system.apis.api_token. Cada entorno tiene sus propios tokens.
  • Cuerpo: JSON (recomendado). También acepta form POST, query string GET o x-www-form-urlencoded crudo. Si llegan archivos, quedan en _FILES.
  • Alcance del token: si metadata.allowed_execute es un array, el token solo puede llamar esos comandos. Sin esa clave, el token puede llamar todo.
curl -s -X POST "$API" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"execute":"get_status"}'

Lo que pasa antes de cada comando

  1. Valida el token y su alcance. Si falla, responde 403 y registra el rechazo en pay.log como API Denied: <motivo>, con IP, comando pedido, api_id y los primeros 4 caracteres del token.
  2. Registra la entrada en pay.log dos veces: el cuerpo crudo (API Input (Raw)) y el detalle con método, IP, api_id (id del token en system.apis), headers y payload (API Input: <execute>). Los headers con credenciales (Authorization, Cookie, API keys) se guardan como [redacted] (LOG_SAFE_HEADERS).
  3. Fuerza sys_id = DB_MAINHOMO_ID (1, Verena Vending). Cualquier sys_id que mande el cliente se pisa.
  4. Abre sesión de sistema en esa empresa ($_SESSION[keySys]) y resuelve suc_id con la sucursal por defecto.

Respuestas

Siempre JSON. No hay un sobre único: según el comando la respuesta trae ok, status, error o err (la constante keyErr vale 'err'). Las claves escritas con constantes salen con el valor del define: prdName → prd_name, psSerie → serie, pgId → pg_id.

HTTPCuerpoCausa
403{"error":"API token missing"}No vino el header Authorization.
403{"error":"API token bad format"}El header no tiene la forma Bearer <token>.
403{"error":"API token invalid"}El token no existe, o su metadata no es JSON válido.
403{"error":"API token not allowed for this command"}El comando no está en allowed_execute del token.
400varía por comandoFalta un parámetro requerido.
200{"status":"API command not recognized"}execute vacío o inexistente. Ojo: devuelve 200, no 404.

Tokens y alcance

Estado de system.apis en la base local. Producción tiene sus propios tokens con otros ids y valores; los valores nunca se copian a este documento. Para ver o cambiar un token, consultar la tabla directamente.

idUsoallowed_execute
1Diagnóstico (Claude Code). En producción es el id 2: era el token general de n8n, rotado y restringido el 2026-10-15. En local sigue siendo el token general sin restricción.get_status, debug_log (PRD)
2IA externa, análisis de zonificación comercial. Solo lectura.zonificacion_consignaciones
3Landing pública home.html. Queda expuesto en el JS del sitio.landing_clientes, landing_equipos
4Bot WhatsApp desde Claude Code: sesiones, configuración, simulacro y revisión de calidad. En PRD es otro token con el mismo alcance.bot_sessions, bot_config_get,
bot_config_set, bot_dry_run, bot_qa
5Control de drift de esquema entre bases db_* (autoconfig). Solo lectura.schema_diff
6Controles diarios: el agente de evaluación (rutina de Claude) lee los controles y entrega su informe. En producción con otro id.controls, controls_report

Para dar acceso a un tercero se crea un token nuevo con allowed_execute limitado a lo que necesita, y una note en metadata que diga para qué es:

INSERT INTO system.apis (api_token, metadata) VALUES (
  SHA2(UUID(), 256),
  '{"sys_id":"1","allowed_execute":["landing_clientes"],"note":"para qué y quién lo usa"}'
);

Hallazgos a revisar

Detectados al relevar el código para este documento (2026-10-13). Los resueltos quedan anotados con su fecha.

Resuelto Los tokens quedaban guardados en pay.log

El log de entrada de api.php y mcp.php guardaba todos los headers, con Authorization. Desde el deploy 0da67742 (2026-10-15) se enmascaran con LOG_SAFE_HEADERS, y los registros anteriores se limpiaron en producción ese mismo día.

Resuelto El token general no tenía alcance y su valor era predecible

Rotado en producción el 2026-10-15 y limitado a get_status y debug_log. Antes de rotar se confirmó en pay.log que nadie más lo usaba en el último mes. El valor viejo también se sacó de la página de wiki "Chatbot WhatsApp — Arquitectura".

Resuelto Nueve comandos legados sin consumidor y unas 980 líneas de código muerto

Los comandos del bot en n8n (wsp_*, create_machine_ticket, create_refund_ticket) y los bloques if (false) de los ya eliminados se retiraron el 2026-10-16. Antes se verificó en pay.log que nadie los había llamado en el último mes. Ver "Eliminados".

Media zonificacion_consignaciones no hace lo que dice su comentario

Ignora zona_id, devuelve un objeto con accounts/locations/kpis en vez de la lista plana documentada, incluye CUIT y KPIs financieros por cuenta, y escribe statics (STATICS_UPSERT) aunque el token sea "solo lectura".

Baja Comando desconocido responde 200 y los tokens restringidos no pueden probar conexión

Un execute mal escrito devuelve HTTP 200. Y como get_status no está en ningún allowed_execute, un consumidor con token restringido no tiene un ping propio.

Diagnóstico

Salud del endpoint y lectura del log de eventos.

get_status

api.php:201
ActivoLecturatoken 1

Confirma que la API responde. No toca la base.

Parámetros

Ninguno.

Respuesta

{ "status": "ok", "timestamp": "2026-10-13 10:15:02", "message": "API is running smoothly" }

debug_log

api.php:933
ActivoLecturaDatos sensiblestoken 1

Devuelve los últimos registros de pay.log (lo que escribe LOG_EVENT). Sirve para depurar producción sin entrar a la base.

Parámetros

NombreTipoDescripción
limitintopcionalDefault 20, máximo 100.
namestringopcionalFiltro parcial (LIKE) por nombre del evento. Ej: WAPI_CALL_LLM, API Input: bot_dry_run.
typeintopcionalFiltro exacto por tipo. El comentario del código dice info|error|warning, pero la columna guarda el número; pasar el valor numérico.
sinceunix tsopcionalSolo registros posteriores.

Respuesta

{ "count": 2, "rows": [ { "id": 812345, "timestamp": "2026-10-13 09:58:11", "type": 1, "stx": 0,
                          "name": "API Input: bot_qa", "description": "...", "caller": "..." } ] }

Notas

  • El log contiene payloads completos de la API (con headers y tokens) y conversaciones del bot. Tratar la salida como confidencial.
  • pay.log retiene aproximadamente un mes.

Datos públicos e IA

Consultas de solo lectura sobre consignaciones de Verena Vending, para la landing y para análisis externos. Las tres salen de SQL_CONSIGNACIONES().

landing_clientes

api.php:255
ActivoLecturatoken 3

Lista de clientes para mostrar en home.html: cuentas con al menos una máquina activa en consignación y el flag KACC_IS_PUBLIC ("Mostrar en ambientes públicos") encendido en la ficha de cuenta.

Parámetros

Ninguno.

Respuesta

{ "status": 1, "clientes": [ { "nombre": "…", "logo": "https://…/img.png", "website": "https://…" } ] }

Notas

  • Qué cuentas aparecen se decide con el flag de la cuenta, no en el código.
  • logo sale de FILE_IMG_GET(FILE_TYPE_ACC_IMG). Sin logo cargado devuelve el placeholder NOT_IMG; el front lo trata como "sin logo".
  • website sale de preferences (preference = 'website') y puede ser null.
  • Consumidor: home.html (el token está en el JS público).

landing_equipos

api.php:321
ActivoLecturatoken 3

Sin pgId: catálogo de modelos de máquina con instalaciones activas hoy. Con pgId: datos del simulador de costo de ese modelo.

Parámetros

NombreTipoDescripción
pgIdintopcionalModelo a simular. Se envía como pgId, pero la respuesta lo devuelve como pg_id.

Respuesta (catálogo)

{ "status": 1, "equipos": [ { "pg_id": 690, "nombre": "…", "imagen": "https://…", "instalaciones": 14, "tieneContenedores": true } ] }

Respuesta (simulador)

{ "status": 1, "pg_id": 690, "pg_name": "…", "canon": 0.0, "capacity": 0, "variance": 0, "iva": 1.21, "imagen": "https://…",
  "insumos": [ { "nombre": "…", "pUnit": 0.0, "min": 0.0, "max": 0.0, "rinde": 0, "fraccion": 0, "umed": "…" } ] }

Notas

  • El catálogo filtra por familias de equipo pf_id IN (19, 20, 25, 38, 52). Si se crea una familia de equipo nueva, hay que agregarla en el código.
  • tieneContenedores es verdadero solo si todos los insumos de la receta tienen rinde > 1 (separa café de snacks y bebidas). El front lo usa para decidir qué modelos ofrecen simulador.
  • El simulador reusa PRD() + ARRAY_PG_CONTAINER_DETAIL(), las mismas funciones que pgs/insumos.php. pgId inexistente devuelve 404.
  • Consumidor: home.html.

zonificacion_consignaciones

api.php:226
ActivoLecturaEscribe staticsDatos sensiblestoken 2

Clientes y puntos con consignaciones activas, con coordenadas y máquinas, para que una IA externa analice zonas de densificación comercial. Delega en RESUMEN_DE_CONSIGNACIONES().

Parámetros

Ninguno efectivo. El comentario del código menciona zona_id, pero no se pasa a la función.

Respuesta (real)

{ "sys_id": 1, "soloseries": true,
  "accounts":  { "123": { "acc_id": 123, "acc_name": "…", "cuit": "…", "preferences": { … }, "kpis": { … } } },
  "locations": { "456": { "acc_id": 123, "acc_name": "…", "loc_id": 456, "loc_name": "…", "loc_addr": "…",
                          "loc_lat": -34.4, "loc_lng": -58.6, "fecha_alta": "2024-03-01",
                          "machines": { "690": { "pg_id": 690, "pg_name": "…", "valor": 0.0, "cant": 1 } } } },
  "kpis": { "<kpi>": { "name": "…", "description": "…", "unit": "…", "direction": "…", "title": "…" } } }

Notas

  • Antes de responder ejecuta STATICS_UPSERT(kpis_financieros), que escribe en statics.
  • Expone CUIT y KPIs financieros promedio por cuenta. Revisar si la IA externa necesita esos datos.
  • Errores de base devuelven 500 con {"error": "…"}.

Bot WhatsApp: administración

Leer y ajustar el bot de producción sin bajar la base. Las escrituras en PRD se muestran antes y se aplican con el OK de quien opera. Las conversaciones tienen datos personales: no copiarlas al repo.

bot_sessions

api.php:426
ActivoLecturaDatos personalestoken 4

Sesiones del bot (system.wax) con sus mensajes (system.wax_messages), de la más nueva a la más vieja.

Parámetros

NombreTipoDescripción
sinceunix tsopcionalActividad mínima. Default: últimos 7 días.
untilunix tsopcionalActividad máxima.
wa_idstringopcionalSolo ese número.
wax_idintopcionalUna sesión puntual. Ignora los demás filtros.
limitintopcionalSesiones. Default 20, máximo 100.
msg_limitintopcionalMensajes por sesión. Default 100, máximo 500.
include_testsboolopcionalIncluir los números de prueba 5491100009900 a 5491100009999.

Respuesta

{ "count": 1, "sessions": [ { "wax_id": 1369, "wa_id": "549…", "usr_name": "…", "acc_id": 0, "loc_id": 0, "usr_id": 0,
    "last_activity": "2026-10-12 18:40:03", "closed": true, "wax_facts": { … },
    "messages": [ { "role": "user", "content": "…", "at": "2026-10-12 18:39:51" } ] } ] }

bot_config_get

api.php:499
ActivoLecturatoken 4

Configuración vigente del bot: prompt maestro y sus últimas 30 versiones, contextos (system.bot_contexts) y acciones (system.bot_actions cruzadas con XG()).

Parámetros

NombreTipoDescripción
prompt_idintopcionalAgrega el contenido de esa versión del prompt en prompt_version_content.

Respuesta

{ "prompt": { "id": 3, "prompt_key": "wsp_master", "updated_at": "…", "updated_by": 0, "content": "…" },
  "prompt_versions": [ { "id": 3, "prompt_key": "wsp_master", "updated_at": "…", "updated_by": 0, "length": 5120 } ],
  "contexts": [ { "ctx_id": 4, "name": "Pagos y devoluciones", "scope": "global", "active": 1, "sort_order": 0, "content": "…" } ],
  "actions":  [ { "bot_id": 21764, "xg_key": "2710", "api_action": "human_handoff", "name": "…", "description": "…",
                  "params": { … }, "active": 1, "sort_order": 0, "required_rol": 0, "llm_hint": "…", "llm_system": "…" } ],
  "context_scopes": [ "global", "create_machine_ticket", "…" ] }

Notas

  • api_action: null en una acción significa que su xg_key ya no existe en XG().

bot_config_set

api.php:583
ActivoEscribetoken 4

Cambia un elemento de la configuración del bot por llamada. Devuelve el registro anterior y el nuevo, y deja ambos en pay.log como BOT CONFIG SET para poder volver atrás.

Parámetros

targetCamposEfecto
promptcontent (string) o restore_id (int)Inserta una versión nueva de wsp_master. Con restore_id copia el contenido de esa versión anterior. Nunca borra versiones.
contextctx_id (sin él crea uno), name, content, scope, active, sort_orderCrea o modifica un contexto. scope es global o un api_action del bot; otro valor da error con la lista válida. Crear exige name y content.
actionbot_id o xg_key; llm_hint, llm_system, active, sort_orderModifica una acción existente. No crea acciones (eso sale de XG()).

Solo se modifican los campos enviados.

Respuesta

{ "ok": true, "target": "context", "before": { … }, "after": { … } }
{ "ok": false, "target": "action", "err": [ "Acción inexistente (bot_id / xg_key)." ], "before": null, "after": null }   // HTTP 400

bot_dry_run

api.php:730
ActivoLecturaConsume OpenAItoken 4

Corre el pipeline del bot hasta la decisión del modelo, sin ejecutar la acción ni mandar WhatsApp. Sirve para probar un prompt, contexto o instrucción candidata antes de guardarla. Con simulate corre el pipeline completo con un número de prueba y guarda la sesión, para conversar varios turnos.

Parámetros

NombreTipoDescripción
msg_textstringrequeridoMensaje del usuario. Para tocar un botón, mandar su id.
wa_idstringrequeridoNúmero real: usa su identidad, acciones y sesión. Un número no registrado simula un desconocido.
contact_namestringopcionalNombre de perfil de WhatsApp.
historyarrayopcional[{role, content}]; reemplaza el historial. Ignorado con simulate.
wax_factsobjectopcionalReemplaza el estado transitorio. Ignorado con simulate.
promptstringopcionalPrompt maestro candidato (no se guarda).
contextsstring[]opcionalContextos candidatos (reemplazan a los guardados, no se guardan).
actionsobjectopcional{ api_action: { llm_hint, llm_system } } candidatos.
full_promptboolopcionalIncluye el system_prompt completo.
simulateboolopcionalPipeline completo con sesión guardada. Solo números 5491100009900 a 5491100009999. Los efectos de negocio no ocurren: vuelven en simulated_effects y simulated_mails.
resetboolopcionalCon simulate: cierra la sesión activa del número antes del mensaje.

Respuesta

{ "oai_http": 200, "oai_error": null, "action": "create_machine_ticket", "params": { "ps_id": 1234 },
  "params_missing": null, "response_text": "…", "shown": { "buttons": { "si": "✅ Sí, registrar" } },
  "simulated_effects": null, "simulated_mails": null, "interactive_payload": { … }, "note": "…",
  "identity": { "usr_id": 0, "usr_name": "", "acc_id": 0, "acc_name": "", "loc_id": 0, "wax_id": null },
  "available_actions": [ "…" ], "wax_facts": { … }, "key_info": { … }, "system_prompt_len": 9000, "user_msg": "…" }

Notas

  • Sin simulate no crea reclamos, leads ni tickets, no guarda la sesión y no toca system.users ni system.wa. Sí valida parámetros con lecturas (por ejemplo, que la máquina exista).
  • Cada llamada es una consulta real a OpenAI.

bot_qa

api.php:845
ActivoLecturaConsume OpenAIEnvía mail si sendtoken 4

Revisión de calidad de las conversaciones de un día: la misma que corre el cron BOT_QA_DAILY. Hace una consulta a OpenAI cada 15 conversaciones.

Parámetros

NombreTipoDescripción
fechaY-m-dopcionalDía a revisar. Default: ayer.
sendboolopcionaltrue: igual que el cron, registra el resumen en pay.log y encola el mail a BOT_QA_EMAILS. Default false: solo devuelve el análisis.

Respuesta (send = false)

{ "fecha": "2026-10-12", "kpis": { … }, "sessions": [ { … } ], "trend": [ … ], "mail_html": "<html>…" }

Controles diarios

El estado de la empresa ya evaluado por xGestion: máquinas, cobranza, facturación por contador, entregas y tareas. Es lo mismo que llega en el resumen diario por mail. El cálculo vive en fns/controls.php; quien consume solo muestra o prioriza.

controls

api.php:899
ActivoLecturaDatos de clientestoken 6

Corre los controles diarios (CTL_RUN) y devuelve, por control, su nivel, un resumen, el criterio aplicado y los casos para revisar. No manda el mail: eso lo hace el cron CTL_DAILY_MAIL_SEND.

Parámetros

NombreTipoDescripción
checkstring | arrayopcionalUno o varios controles. Sin check, todos. Un id inexistente responde 400 con la lista válida.
as_ofY-m-d H:i:sopcionalEvalúa como si fuera ese momento. Sirve para probar contra una copia de la base que no está al día. Las tareas usan siempre la fecha real.
htmlintopcional1: agrega mail_html, el cuerpo del resumen tal como se mandaría.

Controles

checkQué evalúaCriterio por defecto
chk_maquinas_sin_cobrosMáquinas con QR que llevan sin cobrar más de lo normal para esa máquina, según su ritmo por día y hora de las últimas 8 semanas.Aviso si lo normal eran 3 ventas en ese lapso; crítico desde 6, o sin cobros en 8 semanas.
chk_reintegros_por_maquinaReintegros de Mercado Pago por máquina, contra sus cobros QR.3 en 7 días; crítico con 6.
chk_deuda_vencidaClientes con saldo vencido, con la misma vista que Saldos Generales.Desde $ 10.000; crítico desde $ 1.000.000.
chk_ocex_contadoresCuentas con facturación por contador cuya OCEX venció o está por vencer.Crítico si venció y hubo tomas en 90 días; aviso a 15 días.
chk_entregas_pendientesÓrdenes para reparto con cantidades sin entregar, con el criterio de la pantalla de repartos.Más de 3 días; crítico con más de 7.
chk_tareasTareas atrasadas o pendientes de aceptar, tomadas del resumen diario de tareas.Atrasada es crítico; sin aceptar es aviso.

Los umbrales son constantes CTL_* en constants.php.

Respuesta

{ "as_of": "2026-10-13 09:00:00", "status": "crit",
  "checks": { "chk_ocex_contadores": { "check": "chk_ocex_contadores", "title": "Facturación por contador detenida",
      "area": "Facturación", "status": "crit", "summary": "1 cuentas con la OCEX de contadores vencida o por vencer.",
      "threshold": "…", "count": 1, "err": [], "seconds": 0.003,
      "items": [ { "level": "crit", "title": "…", "detail": "La OCEX 131096 venció el 30/09/2026. …",
                   "acc_id": 0, "orden": 131096, "vence": "2026-09-30", "days_left": -13, "readings_after": 0 } ] } },
  "err": [] }

Notas

  • status de cada control y del total: ok, warn, crit o error (no se pudo calcular).
  • Cada caso trae los ids para ubicarlo (acc_id, loc_id, ps_id, orden) y datos propios de su control.
  • Devuelve nombres de clientes y saldos. Tratar la salida como confidencial.

controls_report

api.php:919
ActivoEnvía mailtoken 6

Recibe el informe del agente de evaluación (la rutina diaria de Claude que analiza controls) y lo manda por mail al equipo con CTL_AGENT_REPORT_SEND.

Parámetros

NombreTipoDescripción
report_htmlstringrequeridoCuerpo en HTML básico: h3, h4, p, ul, ol, li, b, i, br, table. Todo lo demás se quita, incluidos atributos, links e imágenes.
summarystringopcionalUna línea para el asunto; se corta en 110 caracteres.
as_ofY-m-d H:i:sopcionalMomento de los controles analizados; sale en el encabezado del mail.

Respuesta

{ "ok": true, "err": [], "to": "…" }
{ "ok": false, "err": [ "Límite diario de informes alcanzado (3)." ] }   // HTTP 429

Notas

  • Los destinatarios son fijos (CTL_DAILY_EMAILS) y hay un máximo de 3 informes por día (CTL_AGENT_MAX_PER_DAY): el token no sirve para escribirle a terceros ni para inundar la casilla.
  • Cada envío queda en pay.log como CTL AGENT REPORT, sin el contenido.
  • Un informe vacío o de menos de 20 caracteres responde 400.

Infraestructura

Control de las bases de datos.

schema_diff

api.php:875
ActivoLecturatoken 5

Compara la estructura de bases de empresa (tablas, columnas, índices, FKs) vía SCHEMA_DIFF(). Se usa después de cada cambio de esquema para confirmar que db_1 (donde en local los nodos de autoconfig se aplican a mano) quedó igual que las demás.

Parámetros

NombreTipoDescripción
sourcestringopcionalBase de referencia. Default DB_NAME_HOMO (db_1).
targetstring | arrayopcionalBase o bases a comparar, o all (todas las db_* menos source). Default db_0.
only_diffintopcional1: omite los targets sin diferencias.

Respuesta

{ "source": "db_1", "targets": [ { "db": "db_139", "total": 2, … } ], "err": [] }

Notas

  • Solo lee information_schema. Una base inexistente responde 400 con el detalle en err.

Eliminados

Comandos del bot cuando corría en n8n. Hoy el bot corre en site/wsp/webhook.php y llama a las funciones WAPI_* directamente. Llamarlos devuelve "API command not recognized". El código está en el historial de git: el último commit que lo contiene es el anterior al retiro del 2026-10-16.

ComandoRetiradoQué hacía · reemplazo
wsp_contextantes de oct 2026Identificaba al usuario y armaba el menú. Hoy: WAPI_BUILD_CONTEXT.
wsp_sendantes de oct 2026Enviaba un WhatsApp. Hoy: WAPI_SEND.
wsp_state_getantes de oct 2026Leía la sesión en system.wax. Hoy: WAPI_LOAD_SESSION.
wsp_state_setantes de oct 2026Guardaba la sesión. Hoy: WAPI_SAVE_SESSION.
wsp_state_patchantes de oct 2026Modificaba parte de la sesión. Ya nadie toca wax desde afuera.
wsp_create_sys_ticket2026-10-16Creaba un ticket de soporte en system.tkt (TKT_ADD_NEW).
wsp_pending_tasks2026-10-16Listaba las tareas pendientes como mensaje de WhatsApp (TASKS_LIST_FULL).
wsp_create_lead2026-10-16Registraba un prospecto como orden de consulta comercial.
wsp_check_machine2026-10-16Validaba un número de máquina y devolvía su consignación.
wsp_check_payment2026-10-16Validaba un pago en Mercado Pago y lo cruzaba con psr.
wsp_create_ticket2026-10-16Creaba un reclamo genérico (CS_INSERT_CLAIM).
create_machine_ticket2026-10-16Reclamo por falla de máquina. El bot actual tiene una acción con el mismo nombre, que resuelve WAPI_EXECUTE_ACTION.
create_refund_ticket2026-10-16Reclamo por pago sin entrega. El bot actual tiene una acción con el mismo nombre, que resuelve WAPI_EXECUTE_ACTION.

Mantenimiento

Este documento se actualiza en el mismo cambio que toca api.php. La fuente está en el repo, en docs/api/api-reference.html, y se republica sobre el mismo link.

Al agregar un comando

  1. Agregar el bloque if ($data['execute'] == '…') { … exit; } en api.php, con el comentario de cabecera estándar (abajo).
  2. Si lo usa un tercero o queda expuesto, crear un token con allowed_execute limitado y una note.
  3. Agregar la tarjeta del comando en su grupo, actualizar la tabla de tokens y los contadores del encabezado.
  4. Republicar el artifact.

Cabecera estándar en api.php

#===============================================================================================================================
#	nombre_del_comando — qué hace, en una línea.
#	Consumidor: quién lo llama (n8n, home.html, IA externa, Claude Code…). Token: id / alcance.
#	Efectos: lectura | escribe <tablas> | consume OpenAI | envía mail.
#	Parámetros:
#	  param   (tipo, requerido|opcional)  descripción
#	Output: { … }

Al modificar o retirar un comando, actualizar su tarjeta (o moverla a "Eliminados") y la fecha y commit del encabezado.