The Spanish version is the authoritative reference. View in Spanish
🎨 Custom response
This mechanism was introduced in xPOS Core 2.6.0. Existing integrations are unaffected: if it is not enabled, xPOS returns exactly the same JSON as always.
By default, xPOS Core responds to the POS with a fixed-structure JSON, documented field by field in Response and error handling. The custom response lets xPOS return that same information with the structure your system needs — your own XML, a different JSON, or any other text format — defined by an XSLT template configured from the portal.
What matters for the business: enabling a new format does not require a new product release. The template travels over the same configuration channel that onboarding already uses.
custom_responsecustom_response is one more field inside the standard xPOS response, populated with the result of custom_response_<country>.xslt. The custom response described on this page is a different thing: it replaces the whole response. The two mechanisms are independent and can coexist.
🔀 How it is enabled
The output mode is a parameter of the issuer's configuration. Gosocket defines it in the portal and it reaches the device in the onboarding data, together with the template.
| Output mode | What xPOS returns |
|---|---|
Estandar xPOS (default) | The standard JSON response, byte for byte the same as always. |
Personalizada | The result of applying your template to the master response. |
It is not a parameter that travels on each call: it cannot be switched from the POS. To change it, the issuer's configuration must be adjusted in the portal and the configuration reloaded on the device.
Where it applies: on issuance (POST /api/v1/process-documents) and on status query (GET /api/v1/status).
Where it does not apply: document process continuation and the cancellation event always respond in standard format.
🧱 The master response
The standard xPOS response does not have a single shape: it changes between success and error, between scenarios (pending, contingency, test) and between countries, with fields appearing and disappearing depending on the case. Writing a template against that variability would be unworkable.
That is why xPOS does not apply your template directly to the response. It first builds a master response: a canonical, normalized XML with three guarantees:
Your template is written against that stable input. All per-country variation stays confined inside the template, which is where it already lived.
On top of that: the 'null' / 'NULL' literals that appear in the standard JSON reach the master as an empty element, the encoding is UTF-8, and the master declares no namespace of its own (the embedded DTE keeps its own).
Master structure
<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 | empty (applies to cl and py only) -->
<stage/> <!-- error stage; empty if there was no error -->
<httpStatus/> <!-- 200 | 400 | 503 | 510 -->
<param/> <!-- TEST | CONSOLIDATE -->
<transactionId/>
<timestamp/> <!-- ISO-8601, when the master was generated -->
</meta>
<result>
<message/> <!-- Operation Successful | Operation Error -->
<errorDescription/> <!-- only on resultType=error -->
<docNumber/>
<number/>
<series/>
<typeDoc/>
<countryIdentificationCode/> <!-- CUFE/CUDE, CDC, e-NCF, Clave, generation code… -->
<statusCode/>
<statusDescription/>
<statusMessage/>
<errorsDescription>
<error/> <!-- 0..n -->
</errorsDescription>
<contingency>
<code/>
<name/>
</contingency>
<barcode>
<text/> <!-- QR content -->
<base64/> <!-- PNG image, base64-encoded -->
</barcode>
<partnerNumber/>
<internalNumber/>
<timeGeneration/>
<timeValidation/>
<customResponse/> <!-- the custom_response field, where the country generates it -->
</result>
<dte> <!-- the signed tax document -->
<decoded>…XML nodes…</decoded>
<base64/>
</dte>
<authorityResponse> <!-- tax authority response -->
<decoded/> <!-- nodes if XML; text if JSON -->
<base64/>
</authorityResponse>
<input> <!-- the original document received from the POS -->
<decoded/>
<base64/>
</input>
<context>
<company>
<taxId/>
<name/>
<storeCode/>
</company>
<environment/>
<xposVersion/>
</context>
</xposMasterResponse>
Every binary block (dte, authorityResponse, input) carries both <decoded> and <base64>. Use decoded to navigate the content with XPath — for example dte/decoded//*:PayableAmount — and base64 only if your output must forward the original byte for byte.
The XSLT engine xPOS uses cannot decode base64 on its own: that is why the engine delivers it already decoded.
The resultType discriminator
This is the field to dispatch templates on (<xsl:template match>), rather than inferring the case from the presence or absence of other fields.
| Value | Meaning |
|---|---|
success | Document processed. If the country returns the authority's verdict inline, it is in authorityResponse. |
pending | Document issued and queued; the authority's verdict arrives later. Resolved by re-querying with GET /api/v1/status. |
contingency | The authority was unavailable: the document was issued in contingency mode and xPOS will reconcile it later. |
error | Rejection by the authority or a failure at some stage of the process; meta/stage indicates where. |
Three particular cases worth handling in the template:
- Success with observations (Guatemala): arrives as
resultType=successwithmessage='Operation Error',httpStatus=400anderrorsDescriptionpopulated. - Test mode (
param=TEST):successwith the authority fields empty and the DTE without a real signature. - Query with no result:
resultType=errorwitherrorDescriptionstating the document does not exist, and the rest ofresultempty.
Master example
A Colombian success, trimmed: the dte, authorityResponse and input blocks are abbreviated and the values are illustrative. The structure is the real one.
<?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">
<!-- … custom_response_co content … -->
</AttachedDocument>
</customResponse>
</result>
<dte>
<decoded>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<!-- … the full signed DTE, navigable with XPath … -->
</Invoice>
</decoded>
<base64>PEludm9pY2UgeG1sbnM9…</base64>
</dte>
<authorityResponse>
<decoded>
<ApplicationResponse xmlns="urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2">
<!-- … DIAN response … -->
</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>
Note in the example: errorsDescription is populated even though the result is success — DIAN accepted the document with an observation — and several fields that do not apply to Colombia arrive empty, not absent. That is exactly the behaviour that makes it unnecessary to ask whether a node exists.
✍️ How the template is written
The master is delivered as the transformation's source XML document: the template is written with ordinary templates (xsl:template) over its nodes. No JSON and no parameters are injected.
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="UTF-8"/>
<!-- one template per scenario, discriminating by meta/resultType -->
<xsl:template match="/xposMasterResponse[meta/resultType='success']">
<!-- result fields: result/countryIdentificationCode, result/barcode/text, …
navigate INSIDE the DTE: dte/decoded//*:LegalMonetaryTotal/*:PayableAmount, … -->
</xsl:template>
<xsl:template match="/xposMasterResponse[meta/resultType='error']">
<!-- error shape, with meta/stage and result/errorDescription -->
</xsl:template>
<!-- likewise for 'pending' and 'contingency' -->
</xsl:stylesheet>
Runtime constraints:
- XSLT 3.0 / XPath 3.1.
- No base64 decoding — which is why the master delivers binaries already decoded.
- No network, disk or database access: the template only sees the master.
- The template is compiled with the standard toolchain before being uploaded to the portal.
The template can — and should — check xposMasterResponse/@version and fail explicitly on a version it does not know. Additive changes within v1 (new elements, always present and empty) are announced; structural or semantic changes bump the version.
When installed through onboarding, the template passes a smoke test against a sample master. A broken template is caught at installation, not on the first sale.
🔎 Iterating against the real input: format=master
You do not have to guess what arrives when writing the template. GET /api/v1/status accepts a parameter that returns the master response untransformed:
curl "http://localhost:3200/api/v1/status?transactionId=1ebf7ddc-94e2-4255-a741-de7e6cd0e439&format=master"What that call returns is exactly what your template will receive. It is the recommended way to develop it: issue a test document, request the master and work against the real data.
📄 The template decides the output format
xPOS does not impose the response Content-Type: the template itself determines it. The precedence is:
| # | What xPOS looks at | Result |
|---|---|---|
| 1 | The MIME type declared in the template | Used as is. It is the author's explicit declaration and takes precedence over everything else. |
| 2 | The declared serialization method, if it is XML, JSON or HTML | Mapped to the matching type. |
| 3 | No usable declaration | Inferred from the result: starts with { or [ → JSON; with < → XML; other content → plain text. |
The supported way to set the type is to declare it in the template. Inference by content exists so that templates declaring nothing keep working, not as a recommended practice.
Watch out for the "text" serialization method: it does not say what those characters are, so xPOS treats it as "no usable declaration" and falls through to step 3. If your template serializes as text but emits JSON, declare the application/json MIME type explicitly.
Practical consequence: moving from XML to JSON — or to whatever format your system requires — is a change in your template, without touching the engine and without waiting for a release.
🛟 What happens if the transformation fails
On any transformation failure, xPOS returns the standard JSON and logs the error.
This is deliberate and is the direct application of the principle that the sale never blocks: a malformed template, a runtime error or a missing template never leave the sale without a response. The POS always receives something it can process.
If you enabled Personalizada mode and you are receiving the standard JSON, that is a sign the transformation is failing. Check the operational logs with GET /api/v1/db-data/logs/search?uuid=<transactionId> — see Log record.
✅ How to enable a new customer
- Write the template against the master response contract.
- Iterate against real data with
GET /api/v1/status?...&format=master, which returns exactly the input the template will receive. - Declare the output type in the template (the MIME type, or at least the serialization method).
- Upload the template and set the mode in the portal, for the corresponding issuer.
- The store's xPOS receives it on the next configuration refresh and starts responding in the new format. To force it immediately:
PUT /api/v1/reobtain-config.
Steps 4 and 5 are executed by Gosocket together with the customer; steps 1 to 3 are the integrator's work.