The Spanish version is the authoritative reference. View in Spanish
💻 REST integration examples
This page assumes you know the xPOS-Core communication protocol. Here you will find the same flow implemented in the most common languages, ready to adapt to your POS.
These examples are reference material to understand the integration flow with xPOS Core: they are not official SDKs nor production-ready code. They illustrate the protocol contract (endpoints, parameters, error handling), but before using them in your project you must adapt them to your context: country and document type, actual document structure, retries, timeouts, logging, and your platform's exception handling. Always validate your implementation against the Sandbox environment (env=sbx) before moving to Production.
All examples implement the full recommended flow:
- Healthcheck —
GET /api/v1/health: verify the xPOS is healthy before issuing. - Pre-validation —
POST /api/v1/process-documentswithoperation=test: validates the document without assigning a folio, signing, or sending it to the tax authority. - Real issuance — the same endpoint with
operation=consolidate. - Error handling — if
messageisOperation Error, thestageanderrorDescriptionfields indicate the cause; the action to take is in the error codes catalog. - State recovery —
GET /api/v1/status: if the POS lost the response, do not resend the document; recover the original response.
The examples use Chile (country=cl, typeDoc=39 electronic receipt) in Sandbox (env=sbx). Adjust country, typeDoc, and inputType according to your country's sheet. The document body is a placeholder: the actual document structure depends on the country and document type — check the corresponding sheet and the local Swagger (http://localhost:3200/api/v1/doc/).
Sample client per language
- 🔧 cURL
- 🐍 Python
- 🟨 JavaScript
- 🔷 TypeScript
- ☕ Java
- 🟪 C# / .NET
- 🐹 Go
- 🐘 PHP
# Healthcheck: 500 or status != "ok" → the xPOS is not ready to issue
curl "http://localhost:3200/api/v1/health"
# Validate the document structure without issuing (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 @document.json
# Issue for real (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 @document.json
# Recover the response of an already processed document (do not resend the document)
curl "http://localhost:3200/api/v1/status?transactionId=1ebf7ddc-94e2-4255-a741-de7e6cd0e439"
"""xPOS Core client — Python 3.10+. Requires: pip install requests"""
import requests
BASE_URL = "http://localhost:3200/api/v1"
ENV = "sbx" # sbx | prd — set by 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 issue_document(document: dict, type_doc: int, operation: str = "test") -> dict:
"""operation: 'test' validates without issuing; 'consolidate' issues for real."""
params = {
"env": ENV,
"operation": operation,
"typeDoc": type_doc, # each country has its own table
"country": COUNTRY,
"inputType": "json", # json | xml | xdoc | txt (subset per country)
}
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 query_status(transaction_id=None, doc_number=None, type_doc=None) -> dict:
"""Recovers the original response: do NOT resend the document."""
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 or status != 'ok' → not ready to issue."""
resp = requests.get(f"{BASE_URL}/health")
resp.raise_for_status()
return resp.json()
# Usage: replace the placeholder with the real document per your country's sheet
receipt = {"DTE": {"Documento": {"...": "structure per country sheet"}}}
try:
issue_document(receipt, type_doc=39, operation="test") # 1) validate
r = issue_document(receipt, type_doc=39, operation="consolidate") # 2) issue
print(f"OK — transactionId={r['transactionId']} docNumber={r.get('docNumber')}")
except XposError as e:
print(f"Rejected at stage {e.stage}: {e.description}")
/** xPOS Core client — Node.js 18+ (native fetch). */
const BASE_URL = 'http://localhost:3200/api/v1';
const ENV = 'sbx'; // sbx | prd — set by 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' validates without issuing; 'consolidate' issues for real. */
async function issueDocument(document, typeDoc, operation = 'test') {
const params = new URLSearchParams({
env: ENV,
operation,
typeDoc: String(typeDoc), // each country has its own table
country: COUNTRY,
inputType: 'json', // json | xml | xdoc | txt (subset per country)
});
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;
}
/** Recovers the original response: do NOT resend the document. */
async function queryStatus({ 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 or status != 'ok' → not ready to issue. */
async function healthcheck() {
const resp = await fetch(`${BASE_URL}/health`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return resp.json();
}
// Usage: replace the placeholder with the real document per your country's sheet
const receipt = { DTE: { Documento: { '...': 'structure per country sheet' } } };
try {
await issueDocument(receipt, 39, 'test'); // 1) validate
const r = await issueDocument(receipt, 39, 'consolidate'); // 2) issue
console.log(`OK — transactionId=${r.transactionId} docNumber=${r.docNumber}`);
} catch (e) {
if (e instanceof XposError) console.error(`Rejected at stage ${e.stage}: ${e.description}`);
else throw e;
}
/** xPOS Core client — TypeScript (Node 18+ / Deno / Bun). */
const BASE_URL = 'http://localhost:3200/api/v1';
const ENV = 'sbx'; // sbx | prd — set by onboarding
const COUNTRY = 'cl'; // cl | co | cr | sv | gt | pa | py | do
type Operation = 'test' | 'consolidate';
/** Common response core; optional fields depend on the country. */
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 — signed tax document
signedXml: string | null; // base64 — fiscal DTE
input: string | null; // base64 — source document
barcodeText: string | null;
barcodeBase64: string | null; // Chile: PDF417; others: QR
countryIdentificationCode: string | null;
statusCode: string | null; // tax authority echo — synchronous countries only
statusDescription: string | null;
statusMessage: string | null;
applicationResponse: string | null;
timeGeneration: string | null;
timeValidation: string | null;
contingencyCode: string | null; // not applicable in Chile or Paraguay
}
class XposError extends Error {
constructor(
readonly stage: string | null,
readonly description: string | null,
readonly transactionId: string | null,
) {
super(`[${stage}] ${description}`);
}
}
/** operation: 'test' validates without issuing; 'consolidate' issues for real. */
async function issueDocument(
document: unknown,
typeDoc: number,
operation: Operation = 'test',
): Promise<XposResponse> {
const params = new URLSearchParams({
env: ENV,
operation,
typeDoc: String(typeDoc), // each country has its own table
country: COUNTRY,
inputType: 'json', // json | xml | xdoc | txt (subset per country)
});
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;
}
/** Recovers the original response: do NOT resend the document. */
async function queryStatus(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 or status != 'ok' → not ready to issue. */
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;
/**
* xPOS Core client — Java 11+ (java.net.http, no dependencies).
* For production, parse the JSON with Jackson or Gson.
*/
public class XposClient {
private static final String BASE_URL = "http://localhost:3200/api/v1";
private static final String ENV = "sbx"; // sbx | prd — set by onboarding
private static final String COUNTRY = "cl"; // cl | co | cr | sv | gt | pa | py | do
private final HttpClient http = HttpClient.newHttpClient();
/** operation: "test" validates without issuing; "consolidate" issues for real. */
public String issueDocument(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)); // each country has its own table
params.put("country", COUNTRY);
params.put("inputType", "json"); // json | xml | xdoc | txt (subset per country)
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();
// With a real JSON parser: if message == "Operation Error", read
// stage + errorDescription to decide the action.
if (body.contains("\"Operation Error\"")) {
throw new RuntimeException("Operation Error: " + body);
}
return body;
}
/** Recovers the original response: do NOT resend the document. */
public String queryStatus(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 or status != "ok" → not ready to issue. */
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>xPOS Core client — C# / .NET 6+.</summary>
public class XposClient
{
private const string BaseUrl = "http://localhost:3200/api/v1";
private const string Env = "sbx"; // sbx | prd — set by onboarding
private const string Country = "cl"; // cl | co | cr | sv | gt | pa | py | do
private static readonly HttpClient Http = new();
/// <summary>operation: "test" validates without issuing; "consolidate" issues for real.</summary>
public async Task<XposResponse> IssueDocumentAsync(object document, int typeDoc, string operation = "test")
{
// typeDoc: each country has its own table; inputType: subset per country
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("Empty response");
if (body.Message == "Operation Error")
throw new XposException(body.Stage, body.ErrorDescription, body.TransactionId);
return body;
}
/// <summary>Recovers the original response: do NOT resend the document.</summary>
public async Task<string> QueryStatusAsync(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 or status != "ok" → not ready to issue.</summary>
public async Task<string> HealthcheckAsync()
=> await Http.GetStringAsync($"{BaseUrl}/health");
}
/// <summary>Common response core; optional fields depend on the country.</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 — signed document
[JsonPropertyName("signedXml")] public string? SignedXml { get; init; } // base64 — fiscal DTE
[JsonPropertyName("input")] public string? Input { get; init; } // base64 — source document
[JsonPropertyName("barcodeText")] public string? BarcodeText { get; init; }
[JsonPropertyName("barcodeBase64")] public string? BarcodeBase64 { get; init; } // CL: PDF417; others: 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);
}
// xPOS Core client — Go 1.21+ (stdlib, no dependencies).
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 — set by onboarding
country = "cl" // cl | co | cr | sv | gt | pa | py | do
)
// XposResponse is the common response core; optional fields depend on the country.
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 — signed tax document
SignedXML string `json:"signedXml"` // base64 — fiscal DTE
Input string `json:"input"` // base64 — source document
BarcodeBase64 string `json:"barcodeBase64"` // Chile: PDF417; others: QR
}
// XposError represents an "Operation Error" returned by xPOS Core.
type XposError struct {
Stage, Description, TransactionID string
}
func (e *XposError) Error() string { return fmt.Sprintf("[%s] %s", e.Stage, e.Description) }
// IssueDocument: operation "test" validates without issuing; "consolidate" issues for real.
func IssueDocument(document any, typeDoc int, operation string) (*XposResponse, error) {
params := url.Values{
"env": {env},
"operation": {operation},
"typeDoc": {strconv.Itoa(typeDoc)}, // each country has its own table
"country": {country},
"inputType": {"json"}, // json | xml | xdoc | txt (subset per country)
}
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
}
// QueryStatus recovers the original response: do NOT resend the document.
func QueryStatus(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 or status != "ok" → not ready to issue.
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
/** xPOS Core client — PHP 8+ (cURL, no dependencies). */
const BASE_URL = 'http://localhost:3200/api/v1';
const ENV_XPOS = 'sbx'; // sbx | prd — set by 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' validates without issuing; 'consolidate' issues for real. */
function issueDocument(array $document, int $typeDoc, string $operation = 'test'): array
{
$query = http_build_query([
'env' => ENV_XPOS,
'operation' => $operation,
'typeDoc' => $typeDoc, // each country has its own table
'country' => COUNTRY,
'inputType' => 'json', // json | xml | xdoc | txt (subset per country)
]);
$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;
}
/** Recovers the original response: do NOT resend the document. */
function queryStatus(?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 or status != 'ok' → not ready to issue. */
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("Connection error with xPOS Core: $err");
}
curl_close($ch);
return json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
}
Protocol reminders
- Base URL:
http://localhost:3200/api/v1— xPOS Core is consumed via loopback; it is not published to the network and requires no authentication. - The 5 query params of
/process-documentsare mandatory:env,operation,typeDoc,country,inputType. - The environment (
sbx|prd) is set by onboarding: it cannot be switched on the fly via the query param. - Base64 response fields:
output,signedXml,input,applicationResponse, andbarcodeBase64(PDF417 in Chile, QR elsewhere). - Asynchronous countries (Chile, Paraguay): the tax authority echo does not come in the initial response — see the country sheet to learn how to obtain the final fiscal status.