🎨 Respuesta personalizada
Este mecanismo se incorporó en xPOS Core 2.6.0. Las integraciones existentes no se ven afectadas: si no se activa, xPOS responde exactamente el mismo JSON de siempre.
Por defecto, xPOS Core responde al POS con un JSON de estructura fija, documentado campo a campo en Response y manejo de errores. La respuesta personalizada permite que xPOS devuelva esa misma información con la estructura que tu sistema necesita —un XML propio, un JSON distinto, o cualquier otro formato de texto—, definida por una plantilla XSLT que se configura desde el portal.
Lo relevante para el negocio: habilitar un formato nuevo no requiere una versión nueva del producto. La plantilla viaja por el mismo canal de configuración que ya usa el onboarding.
custom_responsecustom_response es un campo más dentro de la respuesta estándar de xPOS, que se llena con el resultado de custom_response_<país>.xslt. La respuesta personalizada que describe esta página es otra cosa: reemplaza la respuesta completa. Los dos mecanismos son independientes y pueden coexistir.
🔀 Cómo se activa
El modo de salida es un parámetro de la configuración del emisor. Lo define Gosocket en el portal y llega al equipo en los datos de onboarding, junto con la plantilla.
| Modo de salida | Qué devuelve xPOS |
|---|---|
Estandar xPOS (por omisión) | La respuesta JSON estándar, byte por byte igual que siempre. |
Personalizada | El resultado de aplicar tu plantilla a la respuesta maestra. |
No es un parámetro que viaje en cada llamada: no se alterna desde el POS. Para cambiarlo hay que ajustar la configuración del emisor en el portal y recargar la configuración en el equipo.
Dónde aplica: en la emisión (POST /api/v1/process-documents) y en la consulta de estado (GET /api/v1/status).
Dónde no aplica: la continuación de proceso de documento y el evento de cancelación siguen respondiendo siempre en formato estándar.
🧱 La respuesta maestra
La respuesta estándar de xPOS no tiene una forma única: cambia entre éxito y error, entre escenarios (pendiente, contingencia, prueba) y entre países, con campos que aparecen y desaparecen según el caso. Escribir una plantilla contra esa variabilidad sería inviable.
Por eso xPOS no aplica tu plantilla directamente sobre el response. Primero construye una respuesta maestra: un XML canónico y normalizado con tres garantías:
Tu plantilla se escribe contra esa entrada estable. Toda la variación por país queda confinada dentro de la plantilla, que es donde ya vivía.
Además: los literales 'null' / 'NULL' que aparecen en el JSON estándar llegan al maestro como elemento vacío, la codificación es UTF-8 y el maestro no declara namespace propio (el DTE embebido conserva los suyos).
Estructura del maestro
<xposMasterResponse version="1">
<meta>
<country/> <!-- cl | co | cr | do | gt | pa | py | sv -->
<source/> <!-- process-documents | get-status -->
<resultType/> <!-- success | pending | contingency | error -->
<sendMethod/> <!-- sdta | direct | vacío (solo aplica a cl y py) -->
<stage/> <!-- etapa del error; vacío si no hubo error -->
<httpStatus/> <!-- 200 | 400 | 503 | 510 -->
<param/> <!-- TEST | CONSOLIDATE -->
<transactionId/>
<timestamp/> <!-- ISO-8601, momento de generación del maestro -->
</meta>
<result>
<message/> <!-- Operation Successful | Operation Error -->
<errorDescription/> <!-- solo en resultType=error -->
<docNumber/>
<number/>
<series/>
<typeDoc/>
<countryIdentificationCode/> <!-- CUFE/CUDE, CDC, e-NCF, Clave, código de generación… -->
<statusCode/>
<statusDescription/>
<statusMessage/>
<errorsDescription>
<error/> <!-- 0..n -->
</errorsDescription>
<contingency>
<code/>
<name/>
</contingency>
<barcode>
<text/> <!-- contenido del QR -->
<base64/> <!-- imagen PNG en base64 -->
</barcode>
<partnerNumber/>
<internalNumber/>
<timeGeneration/>
<timeValidation/>
<customResponse/> <!-- el campo custom_response, cuando el país lo genera -->
</result>
<dte> <!-- el documento fiscal firmado -->
<decoded>…nodos XML…</decoded>
<base64/>
</dte>
<authorityResponse> <!-- respuesta de la entidad tributaria -->
<decoded/> <!-- nodos si es XML; texto si es JSON -->
<base64/>
</authorityResponse>
<input> <!-- el documento original recibido del POS -->
<decoded/>
<base64/>
</input>
<context>
<company>
<taxId/>
<name/>
<storeCode/>
</company>
<environment/>
<xposVersion/>
</context>
</xposMasterResponse>
Cada bloque binario (dte, authorityResponse, input) trae <decoded> y <base64>. Usa decoded para navegar el contenido con XPath —por ejemplo dte/decoded//*:PayableAmount— y base64 solo si tu salida debe reenviar el original byte a byte.
El motor XSLT que usa xPOS no puede decodificar base64 por su cuenta: por eso el motor lo entrega ya decodificado.
El discriminador resultType
Es el campo con el que conviene despachar las plantillas (<xsl:template match>), en lugar de inferir el caso por la presencia o ausencia de otros campos.
| Valor | Significado |
|---|---|
success | Documento procesado. Si el país entrega veredicto de la autoridad en línea, está en authorityResponse. |
pending | Documento emitido y encolado; el veredicto de la autoridad llega después. Se resuelve reconsultando con GET /api/v1/status. |
contingency | La autoridad no estaba disponible: el documento se emitió en contingencia y xPOS lo reconciliará después. |
error | Rechazo de la autoridad o fallo en una etapa del proceso; meta/stage indica dónde. |
Tres casos particulares que conviene contemplar en la plantilla:
- Éxito con reparos (Guatemala): llega como
resultType=successconmessage='Operation Error',httpStatus=400yerrorsDescriptionpoblado. - Modo prueba (
param=TEST):successcon los campos de autoridad vacíos y el DTE sin firma real. - Consulta sin resultado:
resultType=errorconerrorDescriptionindicando que el documento no existe, y el resto deresultvacío.
Ejemplo de maestro
Éxito de Colombia, recortado: los bloques dte, authorityResponse e input se abrevian y los valores son ilustrativos. La estructura es la real.
<?xml version='1.0' encoding='UTF-8'?>
<xposMasterResponse version="1">
<meta>
<country>co</country>
<source>process-documents</source>
<resultType>success</resultType>
<sendMethod></sendMethod>
<stage></stage>
<httpStatus>200</httpStatus>
<param></param>
<transactionId>0cc3ca03-86df-468c-b276-24e76b60efd5</transactionId>
<timestamp>2026-07-22T12:20:50-05:00</timestamp>
</meta>
<result>
<message>Operation Successful</message>
<errorDescription></errorDescription>
<docNumber></docNumber>
<number>TAEP289</number>
<series></series>
<typeDoc></typeDoc>
<countryIdentificationCode>b3563bf35be47ea240e63376…</countryIdentificationCode>
<statusCode>00</statusCode>
<statusDescription>Procesado Correctamente.</statusDescription>
<statusMessage>El documento TAEP289 ha sido autorizado.</statusMessage>
<errorsDescription>
<error>Regla: DEAJ30, Notificación: Este código no corresponde a un valor correcto de la lista</error>
</errorsDescription>
<contingency>
<code></code>
<name></name>
</contingency>
<barcode>
<text>https://catalogo-vpfe-hab.dian.gov.co/document/searchqr?documentkey=b3563bf35be47ea240e63376…</text>
<base64>iVBORw0KGgoAAAANSUhEUgAAAPQAAAD0CAYAAA…</base64>
</barcode>
<partnerNumber></partnerNumber>
<internalNumber></internalNumber>
<timeGeneration>12:20:50-04:00</timeGeneration>
<timeValidation>11:20:51-05:00</timeValidation>
<customResponse>
<AttachedDocument xmlns="urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2">
<!-- … contenido de custom_response_co … -->
</AttachedDocument>
</customResponse>
</result>
<dte>
<decoded>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<!-- … el DTE firmado completo, navegable con XPath … -->
</Invoice>
</decoded>
<base64>PEludm9pY2UgeG1sbnM9…</base64>
</dte>
<authorityResponse>
<decoded>
<ApplicationResponse xmlns="urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2">
<!-- … respuesta de la DIAN … -->
</ApplicationResponse>
</decoded>
<base64>PD94bWwgdmVyc2lvbj0iMS4wIi…</base64>
</authorityResponse>
<input>
<decoded>{"DTE":{"Documento":{ … }}}</decoded>
<base64>eyJEVEUiOnsiRG9jdW1lbnRvIjp7…</base64>
</input>
<context>
<company>
<taxId>900123456</taxId>
<name>EMPRESA PILOTO SAS</name>
<storeCode>001</storeCode>
</company>
<environment>cer</environment>
<xposVersion>2.6.0</xposVersion>
</context>
</xposMasterResponse>
Fíjate en el ejemplo: errorsDescription viene poblado aunque el resultado sea success —la DIAN aceptó el documento con una notificación—, y varios campos que no aplican a Colombia llegan vacíos, no ausentes. Ese es exactamente el comportamiento que hace innecesario preguntar si un nodo existe.
✍️ Cómo se escribe la plantilla
El maestro se entrega como el documento XML fuente de la transformación: la plantilla se escribe con plantillas normales (xsl:template) sobre sus nodos. No se inyecta JSON ni parámetros.
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="UTF-8"/>
<!-- una plantilla por escenario, discriminando por meta/resultType -->
<xsl:template match="/xposMasterResponse[meta/resultType='success']">
<!-- campos del resultado: result/countryIdentificationCode, result/barcode/text, …
navegar DENTRO del DTE: dte/decoded//*:LegalMonetaryTotal/*:PayableAmount, … -->
</xsl:template>
<xsl:template match="/xposMasterResponse[meta/resultType='error']">
<!-- forma de error, con meta/stage y result/errorDescription -->
</xsl:template>
<!-- ídem para 'pending' y 'contingency' -->
</xsl:stylesheet>
Restricciones del entorno de ejecución:
- XSLT 3.0 / XPath 3.1.
- Sin decodificación base64 — por eso el maestro entrega los binarios ya decodificados.
- Sin acceso a red, disco ni base de datos: la plantilla solo ve el maestro.
- La plantilla se compila con la cadena de herramientas estándar antes de subirla al portal.
La plantilla puede —y debería— comprobar xposMasterResponse/@version y fallar de forma explícita ante una versión que no conoce. Los cambios aditivos dentro de v1 (elementos nuevos, siempre presentes y vacíos) se anuncian; los cambios de estructura o de semántica suben la versión.
Al instalarse por onboarding, la plantilla pasa una prueba de humo contra un maestro de ejemplo. Una plantilla rota se detecta en la instalación, no en la primera venta.
🔎 Iterar contra la entrada real: format=master
Para escribir la plantilla no hay que adivinar qué llega. GET /api/v1/status acepta un parámetro que devuelve la respuesta maestra sin transformar:
curl "http://localhost:3200/api/v1/status?transactionId=1ebf7ddc-94e2-4255-a741-de7e6cd0e439&format=master"Lo que devuelve esa llamada es exactamente lo que va a recibir tu plantilla. Es la forma recomendada de desarrollarla: emites un documento de prueba, pides el maestro y trabajas sobre el dato real.
📄 El formato de salida lo decide la plantilla
xPOS no impone el Content-Type de la respuesta: lo determina la propia plantilla. La precedencia es esta:
| # | Qué mira xPOS | Resultado |
|---|---|---|
| 1 | El tipo MIME declarado en la plantilla | Se usa tal cual. Es la declaración explícita del autor y tiene prioridad sobre todo lo demás. |
| 2 | El método de serialización declarado, si es XML, JSON o HTML | Se mapea al tipo correspondiente. |
| 3 | Sin declaración usable | Se deduce del resultado: empieza con { o [ → JSON; con < → XML; otro contenido → texto plano. |
La forma soportada de fijar el tipo es declararlo en la plantilla. La deducción por contenido existe para que las plantillas que no declaran nada sigan funcionando, no como práctica recomendada.
Ojo con el método de serialización «texto»: no dice qué son esos caracteres, así que xPOS lo trata como «sin declaración usable» y cae al paso 3. Si tu plantilla serializa como texto pero emite JSON, declara el MIME application/json explícitamente.
Consecuencia práctica: pasar de XML a JSON —o al formato que tu sistema requiera— es un cambio en tu plantilla, sin tocar el motor y sin esperar un release.
🛟 Qué pasa si la transformación falla
Ante cualquier fallo de la transformación, xPOS responde el JSON estándar y registra el error.
Esto es deliberado y es la aplicación directa del principio de que la venta no se bloquea: una plantilla mal formada, un error en tiempo de ejecución o una plantilla ausente nunca dejan la venta sin respuesta. El POS recibe siempre algo procesable.
Si activaste el modo Personalizada y estás recibiendo el JSON estándar, es señal de que la transformación está fallando. Revisa los logs operativos con GET /api/v1/db-data/logs/search?uuid=<transactionId> — ver Registro de logs.
✅ Cómo se habilita un cliente nuevo
- Escribir la plantilla contra el contrato de la respuesta maestra.
- Iterar contra el dato real con
GET /api/v1/status?...&format=master, que devuelve exactamente la entrada que la plantilla va a recibir. - Declarar el tipo de salida en la plantilla (el MIME, o al menos el método de serialización).
- Subir la plantilla y parametrizar el modo en el portal, para el emisor correspondiente.
- El xPOS de la tienda la recibe en el siguiente refresco de configuración y empieza a responder en el formato nuevo. Para forzarlo en el momento:
PUT /api/v1/reobtain-config.
Los pasos 4 y 5 los ejecuta Gosocket junto con el cliente; los pasos 1 a 3 son trabajo del integrador.