← Todos los artículos/ blog ·

Cómo depurar llamadas a la API de Business Central en SaaS y on-premises

Del error HTTP al código AL. Cómo leer los errores de la API, aislar la llamada, usar la telemetría y adjuntar el depurador o hacer snapshot debugging en Business Central online y on-premises.

Una integración que falla con un 400 y un mensaje críptico es de las cosas que más tiempo hacen perder en un proyecto de Business Central. La buena noticia es que casi siempre se resuelve siguiendo el mismo orden: leer bien el error, aislar la llamada, mirar la telemetría y, si hace falta, ver el código AL que se ejecuta.

Los cuatro pasos sirven igual para Business Central online (SaaS) y on-premises. Lo que cambia son las herramientas disponibles en cada caso, y te lo indico en cada paso.

1. Lee bien el error

El código HTTP te dice en qué capa está el problema:

Código Qué suele significar
400 La petición está mal formada: un campo que no existe, un tipo incorrecto, un filtro mal escrito...
401 Autenticación o permisos: token no válido, aplicación no registrada en Business Central o sin permisos suficientes.
404 El endpoint no existe: entorno, empresa o entidad mal escritos, o la extensión que publica esa API no está instalada.
405 Método no permitido (por ejemplo, un POST contra una consulta, que es de solo lectura). En on-premises, también si ese tipo de servicio está desactivado.
409 Otro usuario ha cambiado el registro entre tu lectura y tu escritura.
429, 502, 503 Demasiadas peticiones o servicio saturado. Reintenta con espera; con 502 y 503 el servidor envía una cabecera retry-after.

Después, mira el cuerpo de la respuesta. Business Central devuelve un código de error cuyo prefijo, casi siempre, indica la categoría:

{
  "error": {
    "code": "Application_FieldValidationException",
    "message": "Customer Posting Group must have a value in Customer: No.=C00690. It cannot be zero or empty."
  }
}
Prefijo Qué ha pasado
BadRequest_* Error al construir la petición: URL, filtro, cuerpo o cabeceras.
Authentication_* No ha podido autenticarse con esas credenciales.
Authorization_* Se ha autenticado, pero no tiene permisos para eso.
Application_* Un error de la lógica de negocio: el mismo que vería un usuario en pantalla.
Internal_* Error interno o de integridad de datos en el servidor.

Hay tres que conviene reconocer a la primera:

  • Application_DialogException o Application_FieldValidationException: es un Error() o un TestField del código AL. Si haces lo mismo desde la pantalla, verás el mismo mensaje. El problema está en los datos, no en la integración.
  • BadRequest_InvalidToken (Could not validate the client concurrency token): falta la cabecera If-Match en un PATCH o DELETE, o la etag que envías no es válida.
  • Request_EntityChanged: alguien modificó el registro entre tu lectura y tu escritura. Vuelve a leerlo y repite el cambio.

2. Aísla la llamada

Antes de tocar código, reproduce la llamada fuera de tu integración, con un cliente REST como Bruno, Insomnia, la extensión REST Client de Visual Studio Code o simplemente curl. Mismo token, misma URL y mismo cuerpo:

curl -s "https://api.businesscentral.dynamics.com/v2.0/$TENANT/sandbox/api/v2.0/companies($COMPANY_ID)/salesOrders?\$top=1" \
  -H "Authorization: Bearer $TOKEN" \
  -H "client-request-id: $(uuidgen)" \
  -H "User-Agent: mi-integracion/1.4"

Tres comprobaciones que resuelven buena parte de los 401 y 404:

  • Decodifica el token (por ejemplo, en jwt.ms, el decodificador de Microsoft). aud tiene que ser https://api.businesscentral.dynamics.com, tid el tenant correcto y, en un token de aplicación, roles debe incluir API.ReadWrite.All. Mira también exp: un token caducado da 401.
  • Consulta el $metadata del endpoint (.../api/v2.0/$metadata, o .../api/{publisher}/{grupo}/v1.0/$metadata en una API propia). Ahí están los nombres reales de entidades y campos, y si tu API propia no aparece, la extensión no está instalada en ese entorno.
  • Envía siempre client-request-id con un GUID, y un User-Agent que identifique tu integración. Las dos cabeceras se guardan en la telemetría, y con el GUID encontrarás tu llamada exacta en el paso siguiente.

3. Telemetría: lo que vio el servidor

Si el entorno envía telemetría a Azure Application Insights, cada llamada que recibe Business Central queda registrada con el evento RT0008: endpoint, método, código HTTP, tiempo de ejecución, objeto AL y extensión que la atendieron y, desde la versión 22, el motivo del error (failureReason) tal como lo vio el servidor.

Cómo se activa:

  • SaaS: el administrador añade la cadena de conexión de Application Insights al entorno desde el Business Central Admin Center. Si publicas extensiones, también puedes ponerla en el app.json (applicationInsightsConnectionString) y recibir la telemetría de tus objetos.
  • On-premises: la misma cadena de conexión se configura en la instancia del servidor (ApplicationInsightsConnectionString).

Esta consulta KQL lista las llamadas fallidas del último día con lo necesario para entender qué ha pasado:

traces
| where timestamp > ago(1d)
| where customDimensions has "RT0008"
| where customDimensions.eventId == "RT0008"
| where toint(customDimensions.httpStatusCode) >= 400
| project timestamp
, endpoint = tostring(customDimensions.endpoint)
, httpMethod = tostring(customDimensions.httpMethod)
, httpStatusCode = tostring(customDimensions.httpStatusCode)
, failureReason = tostring(customDimensions.failureReason)
, alObjectName = tostring(customDimensions.alObjectName)
, extensionName = tostring(customDimensions.extensionName)
, executionTimeMs = toreal(totimespan(customDimensions.serverExecutionTime)) / 10000
| order by timestamp desc

Para encontrar una llamada concreta, filtra por el GUID que enviaste añadiendo | where tostring(customDimensions.httpHeaders) has "tu-guid" antes del project (después, customDimensions ya no existe).

Otras dimensiones que ayudan:

  • requestQueueTime: cuánto esperó la petición en cola antes de ejecutarse. Si sube, estás rozando los límites de peticiones simultáneas.
  • sqlExecutes y sqlRowsRead: si una llamada lee cientos de miles de filas, falta un filtro o sobra un campo calculado.
  • extensionPublisher: te dice de quién es el código que atendió la llamada. Muy útil cuando el error viene de una app de terceros.

Ten en cuenta que la telemetría llega a Application Insights con unos minutos de retraso, y que las llamadas SOAP no registran ni el código HTTP ni el motivo del error.

4. El depurador: ver el código AL que se ejecuta

Cuando el error viene de la lógica AL (los Application_*), lo más rápido es ver el código mientras atiende la llamada. Aquí es donde más cambia la cosa entre entornos.

Sandbox online: adjuntar a la siguiente llamada

En un sandbox puedes adjuntar el depurador de Visual Studio Code y decirle que se detenga en la siguiente sesión de servicio web:

{
  "name": "Attach: siguiente llamada web service (sandbox)",
  "type": "al",
  "request": "attach",
  "environmentType": "Sandbox",
  "environmentName": "sandbox",
  "breakOnNext": "WebServiceClient",
  "breakOnError": true
}
  1. Publica la versión actual de la extensión (Ctrl+F5) para que el código local coincida con el del servidor.
  2. Pon los puntos de interrupción y pulsa F5 para adjuntarte.
  3. Lanza la llamada desde tu cliente REST. El depurador se detiene en tus puntos de interrupción y, con breakOnError, también en la línea que lanza el error.

Por defecto, solo puedes depurar las llamadas que haga tu propio usuario, así que lanza la llamada con un token de tu usuario. Si la que falla es la de una aplicación de Entra (servidor a servidor), añade "userId" con el id del usuario que Business Central crea para esa aplicación (lo encuentras en la tabla de usuarios). Para depurar sesiones de otro usuario, el tuyo necesita el conjunto de permisos D365 ATTACH DEBUG.

Producción online: snapshot debugging

En producción no puedes adjuntar el depurador, pero sí grabar una ejecución y depurarla después sin conexión. Es el snapshot debugging:

{
  "name": "Snapshot: siguiente llamada web service (producción)",
  "type": "al",
  "request": "snapshotInitialize",
  "environmentType": "Production",
  "environmentName": "production",
  "breakOnNext": "WebServiceClient",
  "userId": "id-del-usuario-de-la-aplicacion"
}
  1. Marca los snappoints: se colocan igual que los puntos de interrupción, pero no detienen nada. Guardan el estado de las variables en ese punto. Los errores AL se graban siempre, aunque no pongas ninguno.
  2. Pulsa F7 para iniciar la grabación y reproduce la llamada que falla.
  3. Pulsa Alt+F7 para terminar. Visual Studio Code descarga el fichero .snapshot, que puedes depurar paso a paso como si fuera una sesión normal.

Lo que conviene saber:

  • Tu usuario necesita el conjunto de permisos D365 Snapshot Debug.
  • Los símbolos tienen que coincidir con los de producción. Si no, los puntos caen en líneas que no son.
  • Si nadie hace la llamada en 30 minutos, la sesión se cierra; una vez empieza a grabar, hay que terminarla en 10 minutos.
  • El snapshot puede contener datos personales de clientes. Trátalo como tal y bórralo cuando acabes.

Desde la versión 29 existe también, en preview, un servidor MCP de snapshot debugging para que un agente de IA prepare la grabación y la recoja por ti. El mecanismo de fondo es el mismo.

On-premises: adjuntar a tu servidor

En on-premises tienes las dos opciones (adjuntar y snapshot) contra tu propia instancia. La configuración cambia el entorno por el servidor:

{
  "name": "Attach: siguiente llamada web service (on-premises)",
  "type": "al",
  "request": "attach",
  "server": "https://bc-server",
  "serverInstance": "BC",
  "port": 7049,
  "authentication": "UserPassword",
  "breakOnNext": "WebServiceClient",
  "breakOnError": true
}

El puerto es el del endpoint de desarrollo de la instancia (7049 por defecto), que tiene que estar habilitado. Si el servidor usa autenticación de Windows, no puedes indicar userId: solo adjuntarte a la siguiente sesión o a una sesión concreta.

Cuando el depurador no se detiene

Hace poco me tocó depurar con un compañero una integración intercompany: al crear líneas en un pedido de venta de una empresa, Business Central llamaba a su propio servicio web para modificar el pedido de compra de otra empresa del mismo entorno. El servicio se ejecutaba (las líneas aparecían en la otra empresa, pero mal), y el depurador no se detenía en ningún punto. Estos fueron los problemas, por orden:

  • breakOnNext tiene que ser WebServiceClient. Con WebClient te adjuntas a la siguiente sesión de la interfaz web, y con Background a una sesión en segundo plano. Si el valor se ha cambiado sin querer, el depurador se queda esperando una sesión que nunca llega. Borra el valor y pulsa Ctrl+Espacio para ver las opciones.
  • La sesión del servicio web no es la tuya. Aunque tú lances el proceso desde la pantalla, la llamada la hace el usuario del servicio web (el de las credenciales que usa el código, a menudo guardadas en una tabla de configuración). Y por defecto solo te adjuntas a sesiones de tu propio usuario.
  • userId no existe en versiones antiguas. Adjuntarse a la sesión de otro usuario llegó en versiones recientes (hacia la 22). Aquella instalación era una 18, así que la propiedad no hacía nada. La solución fue autenticarse en Visual Studio Code con el usuario del servicio web: ejecutar AL: Clear credentials cache, pulsar F5 e introducir sus credenciales. A partir de ahí, la siguiente llamada se detuvo en el punto de interrupción.
  • Adjuntarse no publica. F5 con una configuración attach solo se engancha al servidor; no despliega tu código. Si lo que hay publicado no coincide con tu código local, los puntos de interrupción caen en líneas que no son o no se activan.
  • Sin punto de interrupción ni error, no se para nada. Pon el punto en la primera línea del método que expone el servicio, no en el código que hace la petición: ese se ejecuta en otra sesión.
  • breakOnError: "ExcludeTry" evita que el depurador se detenga en los errores capturados por funciones [TryFunction], que si no te frenan a cada paso. En versiones antiguas puede que no lo respete; en ese caso, dale a continuar.

Un detalle más: en la barra del depurador, en una sesión attach, el botón de parar es en realidad desconectar. La sesión del usuario sigue viva.

5. Lo que cambia en on-premises

Además del depurador, en on-premises hay errores que en SaaS no existen, porque el servidor lo administras tú:

  • Los servicios tienen que estar activos. OData viene activado por defecto, pero las APIs no. Si una llamada a /api/ devuelve 405, empieza por aquí:

    Set-NAVServerConfiguration BC -KeyName ApiServicesEnabled -KeyValue true
    Restart-NAVServerInstance BC

    El cambio no se aplica en caliente: hay que reiniciar la instancia.

  • La URL es la de tu servidor: https://bc-server:7048/BC/api/v2.0/companies(...), con el puerto de OData (7048 por defecto) abierto en el firewall. Si hay un proxy o un nombre público delante, revisa PublicODataBaseUrl: es la URL base que el servidor publica para sus servicios OData.

  • La autenticación depende de la instalación: con NavUserPassword sigue existiendo la clave de acceso a servicios web como contraseña; con Microsoft Entra ID, el token funciona igual que en SaaS.

  • El Visor de eventos de Windows es tu primera fuente de errores: Registros de aplicaciones y servicios → Microsoft → DynamicsNAV → Server → Admin. Desde PowerShell:

    Get-WinEvent -LogName 'Microsoft-DynamicsNAV-Server/Admin' -MaxEvents 50 -FilterXPath "*[System[(Level=2)]]" |
      Format-List TimeCreated, Message

    Si necesitas más detalle, el registro Debug de ese mismo canal recoge llamadas a servicios, SQL y trazas AL. Viene desactivado y oculto por defecto: actívalo solo mientras investigas, porque crece muy rápido.

  • Los límites son tuyos: el tiempo máximo de una petición, el tamaño de página y el resto de límites se configuran en la instancia, así que un 408 en on-premises puede ser una configuración demasiado estricta.

6. Y cuando la llamada la hace Business Central

A veces el problema va en el otro sentido: es tu extensión la que llama a un servicio externo con HttpClient y falla.

  • En un sandbox, las llamadas salientes están bloqueadas por defecto. Hay que permitirlas extensión a extensión: Administración de extensiones → Configurar → Permitir solicitudes de HttpClient (Allow HttpClient Requests). Incluye las extensiones de librería y, ojo, al copiar un entorno el permiso vuelve a desactivarse. Durante la ejecución de tests en un sandbox, las llamadas HTTP están bloqueadas para todas las extensiones.

  • La telemetría registra cada llamada saliente con el evento RT0019: endpoint (solo la URL base), método, código HTTP, tiempo, la pila AL que hizo la llamada y, desde la versión 29, httpRequestError, que distingue un fallo de DNS, de conexión o de certificado.

  • Comprueba siempre la respuesta en el código. Que Get o Send devuelvan true solo significa que hubo respuesta, no que fuera buena:

    procedure CallExternalService(Url: Text)
    var
        Client: HttpClient;
        Response: HttpResponseMessage;
        Body: Text;
    begin
        if not Client.Get(Url, Response) then
            Error('No se pudo conectar con %1: %2', Url, GetLastErrorText());
        Response.Content.ReadAs(Body);
        if not Response.IsSuccessStatusCode then
            Error('El servicio respondió %1: %2', Response.HttpStatusCode, CopyStr(Body, 1, 250));
    end;

Con el código de estado y el principio del cuerpo en el mensaje, el error se entiende desde la pantalla, desde la telemetría y desde el depurador.

Funciona en sandbox pero no en producción

Es el caso más habitual, y casi siempre es uno de estos:

  • La aplicación de Entra no está dada de alta en la página Aplicaciones de Microsoft Entra de producción, o tiene otros conjuntos de permisos. Cada entorno tiene la suya.
  • La URL apunta a otro entorno o usa un id de empresa que en producción es distinto.
  • La extensión que publica la API propia no está instalada en producción, o está en otra versión (de ahí los 404).
  • En el sentido contrario (funciona en producción y no en el sandbox), casi siempre es el permiso de HttpClient del apartado anterior.

Resumen

  1. Código HTTP y código de error: te dicen en qué capa mirar.
  2. Reproduce la llamada aislada y revisa el token y el $metadata.
  3. Telemetría: RT0008 para lo que entra y RT0019 para lo que sale. Envía siempre client-request-id.
  4. Depurador: adjuntar en sandbox y on-premises, snapshot en producción.

Si quieres repasar cómo autenticarte y construir las llamadas, lo tienes en OData y API en Business Central. Y si tienes una integración que falla y no hay manera de ver por qué, cuéntamelo.

¿Te encaja en tu proyecto?

Si quieres aplicar algo de esto en tu web o en tu Business Central, hablemos.