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/).
The origin in the examples (<taxId>) is the issuer's tax identifier. If your installation has a single issuer you can omit the parameter; with two or more it is required.
Sample client per languageβ
- π§ cURL
- π Python
- π¨ JavaScript
- π· TypeScript
- β Java
- πͺ C# / .NET
- πΉ Go
- π PHP
# issuer taxId. Optional with 1 onboarding; required with 2 or more
ORIGIN="<taxId>"
# 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&origin=$ORIGIN" \
-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&origin=$ORIGIN" \
-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
ORIGIN = "<taxId>" # issuer taxId. Optional with 1 onboarding; required with 2 or more
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
"origin": ORIGIN, # issuer taxId; required with 2+ onboardings
}
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:
if healthcheck().get("status") != "ok": # 1) is xPOS ready?
raise SystemExit("xPOS is not ready to issue")
issue_document(receipt, type_doc=39, operation="test") # 2) validate
r = issue_document(receipt, type_doc=39, operation="consolidate") # 3) 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
const ORIGIN = '<taxId>'; // issuer taxId. Optional with 1 onboarding; required with 2 or more
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
origin: ORIGIN, // issuer taxId; required with 2+ onboardings
});
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 {
const health = await healthcheck(); // 1) is xPOS ready?
if (health.status !== 'ok') throw new Error('xPOS is not ready to issue');
await issueDocument(receipt, 39, 'test'); // 2) validate
const r = await issueDocument(receipt, 39, 'consolidate'); // 3) 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
const ORIGIN = '<taxId>'; // issuer taxId. Optional with 1 onboarding; required with 2 or more
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
origin: ORIGIN, // issuer taxId; required with 2+ onboardings
});
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();
}
// Usage: replace the placeholder with the real document per your country's sheet
const receipt = { DTE: { Documento: { '...': 'structure per country sheet' } } };
try {
const health = await healthcheck(); // 1) is xPOS ready?
if (health.status !== 'ok') throw new Error('xPOS is not ready to issue');
await issueDocument(receipt, 39, 'test'); // 2) validate
const r = await issueDocument(receipt, 39, 'consolidate'); // 3) 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;
}
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;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* xPOS Core client β Java 11+ (java.net.http).
* Requires: com.fasterxml.jackson.core:jackson-databind (response parsing).
*/
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 static final String ORIGIN = "<taxId>"; // issuer taxId. Optional with 1 onboarding; required with 2 or more
private final HttpClient http = HttpClient.newHttpClient();
private final ObjectMapper mapper = new ObjectMapper();
/** Common response core; optional fields depend on the country. */
@JsonIgnoreProperties(ignoreUnknown = true)
public static class XposResponse {
public String transactionId;
public String message;
public String errorDescription;
public String stage;
public String number;
public String docNumber;
public String output; // base64 β signed tax document
public String signedXml; // base64 β fiscal DTE
public String input; // base64 β source document
public String barcodeBase64; // Chile: PDF417; others: QR
}
/** "Operation Error" returned by xPOS Core. */
public static class XposException extends RuntimeException {
public final String stage;
public final String description;
public final String transactionId;
public XposException(String stage, String description, String transactionId) {
super("[" + stage + "] " + description);
this.stage = stage;
this.description = description;
this.transactionId = transactionId;
}
}
/** operation: "test" validates without issuing; "consolidate" issues for real. */
public XposResponse 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
params.put("origin", ORIGIN); // required with 2+ onboardings
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();
XposResponse resp = mapper.readValue(body, XposResponse.class);
if ("Operation Error".equals(resp.message)) {
throw new XposException(resp.stage, resp.errorDescription, resp.transactionId);
}
return resp;
}
/** Recovers the original response: do NOT resend the document. */
public XposResponse 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();
String body = http.send(request, HttpResponse.BodyHandlers.ofString()).body();
return mapper.readValue(body, XposResponse.class);
}
/** 500 or status != "ok" β not ready to issue. */
public boolean healthcheck() throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/health")).GET().build();
HttpResponse<String> resp = http.send(request, HttpResponse.BodyHandlers.ofString());
return resp.statusCode() == 200
&& "ok".equals(mapper.readTree(resp.body()).path("status").asText());
}
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("&"));
}
// Usage: replace the placeholder with the real document per your country's sheet
public static void main(String[] args) throws Exception {
XposClient client = new XposClient();
String receipt = "{\"DTE\":{\"Documento\":{}}}"; // structure per country sheet
if (!client.healthcheck()) { // 1) is xPOS ready?
throw new IllegalStateException("xPOS is not ready to issue");
}
try {
client.issueDocument(receipt, 39, "test"); // 2) validate
XposResponse r = client.issueDocument(receipt, 39, "consolidate"); // 3) issue
System.out.printf("OK β transactionId=%s docNumber=%s%n", r.transactionId, r.docNumber);
} catch (XposException e) {
System.err.printf("Rejected at stage %s: %s%n", e.stage, e.description);
}
}
}
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 const string Origin = "<taxId>"; // issuer taxId. Optional with 1 onboarding; required with 2 or more
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: see the country sheet
var query = $"env={Env}&operation={operation}&typeDoc={typeDoc}&country={Country}&inputType=json&origin={Origin}";
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, int? typeDoc = null)
{
var query = string.Join("&", new[]
{
transactionId is null ? null : $"transactionId={Uri.EscapeDataString(transactionId)}",
docNumber is null ? null : $"docNumber={Uri.EscapeDataString(docNumber)}",
typeDoc is null ? null : $"typeDoc={typeDoc}",
}.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<HealthResponse?> HealthcheckAsync()
=> await Http.GetFromJsonAsync<HealthResponse>($"{BaseUrl}/health");
}
public record HealthResponse(
[property: JsonPropertyName("status")] string? Status,
[property: JsonPropertyName("xposVersion")] string? XposVersion);
/// <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);
}
/// <summary>Usage: replace the placeholder with the real document per your country's sheet.</summary>
public static class Program
{
public static async Task Main()
{
var client = new XposClient();
var receipt = new { DTE = new { Documento = new { } } }; // structure per country sheet
var health = await client.HealthcheckAsync(); // 1) is xPOS ready?
if (health?.Status != "ok")
throw new InvalidOperationException("xPOS is not ready to issue");
try
{
await client.IssueDocumentAsync(receipt, 39, "test"); // 2) validate
var r = await client.IssueDocumentAsync(receipt, 39, "consolidate"); // 3) issue
Console.WriteLine($"OK β transactionId={r.TransactionId} docNumber={r.DocNumber}");
}
catch (XposException e)
{
Console.WriteLine($"Rejected at stage {e.Stage}: {e.Description}");
}
}
}
// xPOS Core client β Go 1.21+ (stdlib, no dependencies).
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
"strings"
)
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
origin = "<taxId>" // issuer taxId. Optional with 1 onboarding; required with 2 or more
)
// 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
"origin": {origin}, // required with 2+ onboardings
}
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.
// typeDoc <= 0 omits it from the query.
func QueryStatus(transactionID, docNumber string, typeDoc int) (string, error) {
params := url.Values{}
if transactionID != "" {
params.Set("transactionId", transactionID)
}
if docNumber != "" {
params.Set("docNumber", docNumber)
}
if typeDoc > 0 {
params.Set("typeDoc", strconv.Itoa(typeDoc))
}
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
}
// Usage: replace the placeholder with the real document per your country's sheet.
func main() {
receipt := map[string]any{"DTE": map[string]any{"Documento": map[string]any{"...": "structure per country sheet"}}}
// 1) is xPOS ready?
if h, err := Healthcheck(); err != nil || !strings.Contains(h, `"ok"`) {
log.Fatal("xPOS is not ready to issue")
}
if _, err := IssueDocument(receipt, 39, "test"); err != nil { // 2) validate
log.Fatal(err)
}
r, err := IssueDocument(receipt, 39, "consolidate") // 3) issue
if err != nil {
var xposErr *XposError
if errors.As(err, &xposErr) {
log.Fatalf("Rejected at stage %s: %s", xposErr.Stage, xposErr.Description)
}
log.Fatal(err)
}
fmt.Printf("OK β transactionId=%s docNumber=%s\n", r.TransactionID, r.DocNumber)
}
<?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
const ORIGIN = '<taxId>'; // issuer taxId. Optional with 1 onboarding; required with 2 or more
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
'origin' => ORIGIN, // required with 2+ onboardings
]);
$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, ?int $typeDoc = null): array
{
$query = http_build_query(array_filter([
'transactionId' => $transactionId,
'docNumber' => $docNumber,
'typeDoc' => $typeDoc,
], 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);
}
// Usage: replace the placeholder with the real document per your country's sheet
$receipt = ['DTE' => ['Documento' => ['...' => 'structure per country sheet']]];
try {
if ((healthcheck()['status'] ?? null) !== 'ok') { // 1) is xPOS ready?
exit('xPOS is not ready to issue');
}
issueDocument($receipt, 39, 'test'); // 2) validate
$r = issueDocument($receipt, 39, 'consolidate'); // 3) issue
echo "OK β transactionId={$r['transactionId']} docNumber={$r['docNumber']}";
} catch (XposException $e) {
echo "Rejected at stage {$e->stage}: {$e->description}";
}
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 sixth,origin, is conditional: optional with a single issuer installed, required with two or more (without it,HTTP 400). See Multiroot. inputType=txtis the body exception: it travels withContent-Type: text/plainand the body is the plain text as is, with no JSON and no base64. The examples on this page useinputType=json.- 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.