💻 Ejemplos de integración REST
Esta página asume que conoces el protocolo de comunicación xPOS-Core. Aquí encontrarás el mismo flujo implementado en los lenguajes más comunes, listo para adaptar a tu POS.
Estos ejemplos son material de referencia para entender el flujo de integración con xPOS Core: no son SDKs oficiales ni código listo para producción. Ilustran el contrato del protocolo (endpoints, parámetros, manejo de errores), pero antes de usarlos en tu proyecto debes adaptarlos a tu realidad: país y tipo de documento, estructura real del document, reintentos, timeouts, logging y manejo de excepciones de tu plataforma. Valida siempre tu implementación contra el ambiente Sandbox (env=sbx) antes de pasar a Producción.
Todos los ejemplos implementan el flujo completo recomendado:
- Healthcheck —
GET /api/v1/health: verificar que el xPOS está sano antes de emitir. - Validación previa —
POST /api/v1/process-documentsconoperation=test: valida el documento sin asignar folio, firmar ni enviar a la entidad tributaria. - Emisión real — el mismo endpoint con
operation=consolidate. - Manejo de errores — si
messageesOperation Error, los camposstageyerrorDescriptionindican la causa; la acción a tomar está en el catálogo de códigos de errores. - Recuperación de estado —
GET /api/v1/status: si el POS perdió el response, no reenvíes el documento; recupera la respuesta original.
Los ejemplos usan Chile (country=cl, typeDoc=39 boleta electrónica) en Sandbox (env=sbx). Ajusta country, typeDoc e inputType según la ficha de tu país. El body document es un placeholder: la estructura real del documento depende del país y del tipo de documento — consulta la ficha correspondiente y el Swagger local (http://localhost:3200/api/v1/doc/).
Cliente de ejemplo por lenguaje
- 🔧 cURL
- 🐍 Python
- 🟨 JavaScript
- 🔷 TypeScript
- ☕ Java
- 🟪 C# / .NET
- 🐹 Go
- 🐘 PHP
# Healthcheck: 500 o status != "ok" → el xPOS no está listo para emitir
curl "http://localhost:3200/api/v1/health"
# Validar el armado del documento sin emitir (operation=test)
curl -X POST "http://localhost:3200/api/v1/process-documents?env=sbx&operation=test&typeDoc=39&country=cl&inputType=json" \
-H "Content-Type: application/json" \
-d @documento.json
# Emitir de verdad (operation=consolidate)
curl -X POST "http://localhost:3200/api/v1/process-documents?env=sbx&operation=consolidate&typeDoc=39&country=cl&inputType=json" \
-H "Content-Type: application/json" \
-d @documento.json
# Recuperar el response de un documento ya procesado (no reenviar el documento)
curl "http://localhost:3200/api/v1/status?transactionId=1ebf7ddc-94e2-4255-a741-de7e6cd0e439"
"""Cliente xPOS Core — Python 3.10+. Requiere: pip install requests"""
import requests
BASE_URL = "http://localhost:3200/api/v1"
ENV = "sbx" # sbx | prd — lo fija el onboarding
COUNTRY = "cl" # cl | co | cr | sv | gt | pa | py | do
class XposError(Exception):
def __init__(self, stage, description, transaction_id):
self.stage, self.description, self.transaction_id = stage, description, transaction_id
super().__init__(f"[{stage}] {description}")
def emitir_documento(document: dict, type_doc: int, operation: str = "test") -> dict:
"""operation: 'test' valida sin emitir; 'consolidate' emite de verdad."""
params = {
"env": ENV,
"operation": operation,
"typeDoc": type_doc, # tabla propia de cada país
"country": COUNTRY,
"inputType": "json", # json | xml | xdoc | txt (subconjunto por país)
}
resp = requests.post(f"{BASE_URL}/process-documents", params=params, json=document)
resp.raise_for_status()
body = resp.json()
if body.get("message") == "Operation Error":
raise XposError(body.get("stage"), body.get("errorDescription"), body.get("transactionId"))
return body
def consultar_estado(transaction_id=None, doc_number=None, type_doc=None) -> dict:
"""Recupera el response original: NO reenviar el documento."""
params = {k: v for k, v in {
"transactionId": transaction_id, "docNumber": doc_number, "typeDoc": type_doc,
}.items() if v is not None}
resp = requests.get(f"{BASE_URL}/status", params=params)
resp.raise_for_status()
return resp.json()
def healthcheck() -> dict:
"""500 o status != 'ok' → no está listo para emitir."""
resp = requests.get(f"{BASE_URL}/health")
resp.raise_for_status()
return resp.json()
# Uso: reemplaza el placeholder por el documento real según la ficha de tu país
boleta = {"DTE": {"Documento": {"...": "estructura según ficha del país"}}}
try:
emitir_documento(boleta, type_doc=39, operation="test") # 1) validar
r = emitir_documento(boleta, type_doc=39, operation="consolidate") # 2) emitir
print(f"OK — transactionId={r['transactionId']} docNumber={r.get('docNumber')}")
except XposError as e:
print(f"Rechazado en etapa {e.stage}: {e.description}")
/** Cliente xPOS Core — Node.js 18+ (fetch nativo). */
const BASE_URL = 'http://localhost:3200/api/v1';
const ENV = 'sbx'; // sbx | prd — lo fija el onboarding
const COUNTRY = 'cl'; // cl | co | cr | sv | gt | pa | py | do
class XposError extends Error {
constructor(stage, description, transactionId) {
super(`[${stage}] ${description}`);
Object.assign(this, { stage, description, transactionId });
}
}
/** operation: 'test' valida sin emitir; 'consolidate' emite de verdad. */
async function emitirDocumento(document, typeDoc, operation = 'test') {
const params = new URLSearchParams({
env: ENV,
operation,
typeDoc: String(typeDoc), // tabla propia de cada país
country: COUNTRY,
inputType: 'json', // json | xml | xdoc | txt (subconjunto por país)
});
const resp = await fetch(`${BASE_URL}/process-documents?${params}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(document),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const body = await resp.json();
if (body.message === 'Operation Error') {
throw new XposError(body.stage, body.errorDescription, body.transactionId);
}
return body;
}
/** Recupera el response original: NO reenviar el documento. */
async function consultarEstado({ transactionId, docNumber, typeDoc } = {}) {
const params = new URLSearchParams();
if (transactionId) params.set('transactionId', transactionId);
if (docNumber) params.set('docNumber', docNumber);
if (typeDoc != null) params.set('typeDoc', String(typeDoc));
const resp = await fetch(`${BASE_URL}/status?${params}`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return resp.json();
}
/** 500 o status != 'ok' → no está listo para emitir. */
async function healthcheck() {
const resp = await fetch(`${BASE_URL}/health`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return resp.json();
}
// Uso: reemplaza el placeholder por el documento real según la ficha de tu país
const boleta = { DTE: { Documento: { '...': 'estructura según ficha del país' } } };
try {
await emitirDocumento(boleta, 39, 'test'); // 1) validar
const r = await emitirDocumento(boleta, 39, 'consolidate'); // 2) emitir
console.log(`OK — transactionId=${r.transactionId} docNumber=${r.docNumber}`);
} catch (e) {
if (e instanceof XposError) console.error(`Rechazado en etapa ${e.stage}: ${e.description}`);
else throw e;
}
/** Cliente xPOS Core — TypeScript (Node 18+ / Deno / Bun). */
const BASE_URL = 'http://localhost:3200/api/v1';
const ENV = 'sbx'; // sbx | prd — lo fija el onboarding
const COUNTRY = 'cl'; // cl | co | cr | sv | gt | pa | py | do
type Operation = 'test' | 'consolidate';
/** Núcleo común del response; los campos opcionales dependen del país. */
interface XposResponse {
transactionId: string | null;
message: 'Operation Successful' | 'Operation Error' | null;
errorDescription: string | null;
stage: string | null;
number: string | null;
docNumber: string | null;
output: string | null; // base64 — documento tributario firmado
signedXml: string | null; // base64 — DTE fiscal
input: string | null; // base64 — documento de origen
barcodeText: string | null;
barcodeBase64: string | null; // Chile: PDF417; resto: QR
countryIdentificationCode: string | null;
statusCode: string | null; // eco de la entidad — solo países síncronos
statusDescription: string | null;
statusMessage: string | null;
applicationResponse: string | null;
timeGeneration: string | null;
timeValidation: string | null;
contingencyCode: string | null; // no aplica en Chile ni Paraguay
}
class XposError extends Error {
constructor(
readonly stage: string | null,
readonly description: string | null,
readonly transactionId: string | null,
) {
super(`[${stage}] ${description}`);
}
}
/** operation: 'test' valida sin emitir; 'consolidate' emite de verdad. */
async function emitirDocumento(
document: unknown,
typeDoc: number,
operation: Operation = 'test',
): Promise<XposResponse> {
const params = new URLSearchParams({
env: ENV,
operation,
typeDoc: String(typeDoc), // tabla propia de cada país
country: COUNTRY,
inputType: 'json', // json | xml | xdoc | txt (subconjunto por país)
});
const resp = await fetch(`${BASE_URL}/process-documents?${params}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(document),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const body = (await resp.json()) as XposResponse;
if (body.message === 'Operation Error') {
throw new XposError(body.stage, body.errorDescription, body.transactionId);
}
return body;
}
/** Recupera el response original: NO reenviar el documento. */
async function consultarEstado(opts: {
transactionId?: string; docNumber?: string; typeDoc?: number;
}): Promise<unknown> {
const params = new URLSearchParams();
if (opts.transactionId) params.set('transactionId', opts.transactionId);
if (opts.docNumber) params.set('docNumber', opts.docNumber);
if (opts.typeDoc != null) params.set('typeDoc', String(opts.typeDoc));
const resp = await fetch(`${BASE_URL}/status?${params}`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return resp.json();
}
/** 500 o status != 'ok' → no está listo para emitir. */
async function healthcheck(): Promise<{ status: string; xposVersion: string }> {
const resp = await fetch(`${BASE_URL}/health`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return resp.json();
}
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Cliente xPOS Core — Java 11+ (java.net.http, sin dependencias).
* En producción conviene parsear el JSON con Jackson o Gson.
*/
public class XposClient {
private static final String BASE_URL = "http://localhost:3200/api/v1";
private static final String ENV = "sbx"; // sbx | prd — lo fija el onboarding
private static final String COUNTRY = "cl"; // cl | co | cr | sv | gt | pa | py | do
private final HttpClient http = HttpClient.newHttpClient();
/** operation: "test" valida sin emitir; "consolidate" emite de verdad. */
public String emitirDocumento(String documentJson, int typeDoc, String operation) throws Exception {
Map<String, String> params = new LinkedHashMap<>();
params.put("env", ENV);
params.put("operation", operation);
params.put("typeDoc", String.valueOf(typeDoc)); // tabla propia de cada país
params.put("country", COUNTRY);
params.put("inputType", "json"); // json | xml | xdoc | txt (subconjunto por país)
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/process-documents?" + queryString(params)))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(documentJson))
.build();
String body = http.send(request, HttpResponse.BodyHandlers.ofString()).body();
// Con un parser JSON real: si message == "Operation Error", leer
// stage + errorDescription para decidir la acción.
if (body.contains("\"Operation Error\"")) {
throw new RuntimeException("Operation Error: " + body);
}
return body;
}
/** Recupera el response original: NO reenviar el documento. */
public String consultarEstado(String transactionId, String docNumber, Integer typeDoc) throws Exception {
Map<String, String> params = new LinkedHashMap<>();
if (transactionId != null) params.put("transactionId", transactionId);
if (docNumber != null) params.put("docNumber", docNumber);
if (typeDoc != null) params.put("typeDoc", String.valueOf(typeDoc));
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/status?" + queryString(params)))
.GET().build();
return http.send(request, HttpResponse.BodyHandlers.ofString()).body();
}
/** 500 o status != "ok" → no está listo para emitir. */
public String healthcheck() throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/health")).GET().build();
return http.send(request, HttpResponse.BodyHandlers.ofString()).body();
}
private static String queryString(Map<String, String> params) {
return params.entrySet().stream()
.map(e -> URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8)
+ "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
}
}
using System.Net.Http.Json;
using System.Text.Json.Serialization;
/// <summary>Cliente xPOS Core — C# / .NET 6+.</summary>
public class XposClient
{
private const string BaseUrl = "http://localhost:3200/api/v1";
private const string Env = "sbx"; // sbx | prd — lo fija el onboarding
private const string Country = "cl"; // cl | co | cr | sv | gt | pa | py | do
private static readonly HttpClient Http = new();
/// <summary>operation: "test" valida sin emitir; "consolidate" emite de verdad.</summary>
public async Task<XposResponse> EmitirDocumentoAsync(object document, int typeDoc, string operation = "test")
{
// typeDoc: tabla propia de cada país; inputType: subconjunto por país
var query = $"env={Env}&operation={operation}&typeDoc={typeDoc}&country={Country}&inputType=json";
var resp = await Http.PostAsJsonAsync($"{BaseUrl}/process-documents?{query}", document);
resp.EnsureSuccessStatusCode();
var body = await resp.Content.ReadFromJsonAsync<XposResponse>()
?? throw new InvalidOperationException("Response vacío");
if (body.Message == "Operation Error")
throw new XposException(body.Stage, body.ErrorDescription, body.TransactionId);
return body;
}
/// <summary>Recupera el response original: NO reenviar el documento.</summary>
public async Task<string> ConsultarEstadoAsync(string? transactionId = null, string? docNumber = null)
{
var query = string.Join("&", new[]
{
transactionId is null ? null : $"transactionId={Uri.EscapeDataString(transactionId)}",
docNumber is null ? null : $"docNumber={Uri.EscapeDataString(docNumber)}",
}.Where(p => p is not null));
return await Http.GetStringAsync($"{BaseUrl}/status?{query}");
}
/// <summary>500 o status != "ok" → no está listo para emitir.</summary>
public async Task<string> HealthcheckAsync()
=> await Http.GetStringAsync($"{BaseUrl}/health");
}
/// <summary>Núcleo común del response; los campos opcionales dependen del país.</summary>
public record XposResponse
{
[JsonPropertyName("transactionId")] public string? TransactionId { get; init; }
[JsonPropertyName("message")] public string? Message { get; init; }
[JsonPropertyName("errorDescription")] public string? ErrorDescription { get; init; }
[JsonPropertyName("stage")] public string? Stage { get; init; }
[JsonPropertyName("number")] public string? Number { get; init; }
[JsonPropertyName("docNumber")] public string? DocNumber { get; init; }
[JsonPropertyName("output")] public string? Output { get; init; } // base64 — doc. firmado
[JsonPropertyName("signedXml")] public string? SignedXml { get; init; } // base64 — DTE fiscal
[JsonPropertyName("input")] public string? Input { get; init; } // base64 — doc. de origen
[JsonPropertyName("barcodeText")] public string? BarcodeText { get; init; }
[JsonPropertyName("barcodeBase64")] public string? BarcodeBase64 { get; init; } // CL: PDF417; resto: QR
[JsonPropertyName("countryIdentificationCode")] public string? CountryIdentificationCode { get; init; }
[JsonPropertyName("statusCode")] public string? StatusCode { get; init; }
[JsonPropertyName("statusDescription")] public string? StatusDescription { get; init; }
[JsonPropertyName("statusMessage")] public string? StatusMessage { get; init; }
[JsonPropertyName("applicationResponse")] public string? ApplicationResponse { get; init; }
[JsonPropertyName("timeGeneration")] public string? TimeGeneration { get; init; }
[JsonPropertyName("timeValidation")] public string? TimeValidation { get; init; }
[JsonPropertyName("contingencyCode")] public string? ContingencyCode { get; init; }
}
public class XposException : Exception
{
public string? Stage { get; }
public string? Description { get; }
public string? TransactionId { get; }
public XposException(string? stage, string? description, string? transactionId)
: base($"[{stage}] {description}")
=> (Stage, Description, TransactionId) = (stage, description, transactionId);
}
// Cliente xPOS Core — Go 1.21+ (stdlib, sin dependencias).
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
)
const baseURL = "http://localhost:3200/api/v1"
const (
env = "sbx" // sbx | prd — lo fija el onboarding
country = "cl" // cl | co | cr | sv | gt | pa | py | do
)
// XposResponse es el núcleo común del response; los campos opcionales dependen del país.
type XposResponse struct {
TransactionID string `json:"transactionId"`
Message string `json:"message"`
ErrorDescription string `json:"errorDescription"`
Stage string `json:"stage"`
Number string `json:"number"`
DocNumber string `json:"docNumber"`
Output string `json:"output"` // base64 — documento tributario firmado
SignedXML string `json:"signedXml"` // base64 — DTE fiscal
Input string `json:"input"` // base64 — documento de origen
BarcodeBase64 string `json:"barcodeBase64"` // Chile: PDF417; resto: QR
}
// XposError representa un "Operation Error" devuelto por xPOS Core.
type XposError struct {
Stage, Description, TransactionID string
}
func (e *XposError) Error() string { return fmt.Sprintf("[%s] %s", e.Stage, e.Description) }
// EmitirDocumento: operation "test" valida sin emitir; "consolidate" emite de verdad.
func EmitirDocumento(document any, typeDoc int, operation string) (*XposResponse, error) {
params := url.Values{
"env": {env},
"operation": {operation},
"typeDoc": {strconv.Itoa(typeDoc)}, // tabla propia de cada país
"country": {country},
"inputType": {"json"}, // json | xml | xdoc | txt (subconjunto por país)
}
body, err := json.Marshal(document)
if err != nil {
return nil, err
}
resp, err := http.Post(baseURL+"/process-documents?"+params.Encode(),
"application/json", bytes.NewReader(body))
if err != nil {
return nil, err
}
defer resp.Body.Close()
var out XposResponse
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, err
}
if out.Message == "Operation Error" {
return nil, &XposError{out.Stage, out.ErrorDescription, out.TransactionID}
}
return &out, nil
}
// ConsultarEstado recupera el response original: NO reenviar el documento.
func ConsultarEstado(transactionID, docNumber string) (string, error) {
params := url.Values{}
if transactionID != "" {
params.Set("transactionId", transactionID)
}
if docNumber != "" {
params.Set("docNumber", docNumber)
}
resp, err := http.Get(baseURL + "/status?" + params.Encode())
if err != nil {
return "", err
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
return string(b), err
}
// Healthcheck: 500 o status != "ok" → no está listo para emitir.
func Healthcheck() (string, error) {
resp, err := http.Get(baseURL + "/health")
if err != nil {
return "", err
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
return string(b), err
}
<?php
/** Cliente xPOS Core — PHP 8+ (cURL, sin dependencias). */
const BASE_URL = 'http://localhost:3200/api/v1';
const ENV_XPOS = 'sbx'; // sbx | prd — lo fija el onboarding
const COUNTRY = 'cl'; // cl | co | cr | sv | gt | pa | py | do
class XposException extends Exception
{
public function __construct(
public readonly ?string $stage,
public readonly ?string $description,
public readonly ?string $transactionId,
) {
parent::__construct("[$stage] $description");
}
}
/** $operation: 'test' valida sin emitir; 'consolidate' emite de verdad. */
function emitirDocumento(array $document, int $typeDoc, string $operation = 'test'): array
{
$query = http_build_query([
'env' => ENV_XPOS,
'operation' => $operation,
'typeDoc' => $typeDoc, // tabla propia de cada país
'country' => COUNTRY,
'inputType' => 'json', // json | xml | xdoc | txt (subconjunto por país)
]);
$body = httpRequest('POST', BASE_URL . "/process-documents?$query", json_encode($document));
if (($body['message'] ?? null) === 'Operation Error') {
throw new XposException($body['stage'] ?? null, $body['errorDescription'] ?? null, $body['transactionId'] ?? null);
}
return $body;
}
/** Recupera el response original: NO reenviar el documento. */
function consultarEstado(?string $transactionId = null, ?string $docNumber = null): array
{
$query = http_build_query(array_filter([
'transactionId' => $transactionId,
'docNumber' => $docNumber,
], fn ($v) => $v !== null));
return httpRequest('GET', BASE_URL . "/status?$query");
}
/** 500 o status != 'ok' → no está listo para emitir. */
function healthcheck(): array
{
return httpRequest('GET', BASE_URL . '/health');
}
function httpRequest(string $method, string $url, ?string $jsonBody = null): array
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
if ($jsonBody !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonBody);
}
$raw = curl_exec($ch);
if ($raw === false) {
$err = curl_error($ch);
curl_close($ch);
throw new RuntimeException("Error de conexión con xPOS Core: $err");
}
curl_close($ch);
return json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
}
Recordatorios del protocolo
- Base URL:
http://localhost:3200/api/v1— xPOS Core se consume por loopback; no se publica a la red y no requiere autenticación. - Los 5 query params de
/process-documentsson obligatorios:env,operation,typeDoc,country,inputType. - El ambiente (
sbx|prd) lo fija el onboarding: no se alterna en caliente con el query param. - Campos base64 del response:
output,signedXml,input,applicationResponseybarcodeBase64(PDF417 en Chile, QR en el resto). - Países asíncronos (Chile, Paraguay): el eco de la entidad tributaria no viene en el response inicial — ver la ficha del país para saber cómo obtener el estado fiscal final.