factadesarrolladores API 0.1.0-esqueleto

Java

HTTP directo · no hay librería de Facta para Java

La API es HTTP y JSON, así que Java la llama con java.net.http, sin nada más. Lo que sigue es un programa completo —no un fragmento— que emite una factura y trata cada final por su nombre.

Requisitos

  • JDK 17 o superior, con java.net.http (parte del JDK desde la 11).
  • Una llave de API con el alcance issue, y su llave de firma.
  • Un destino de almacenamiento conectado en la app — sin él la emisión se detiene con 422 no_storage_destination antes de reservar correlativo.
URL basehttps://hcnvknpsbadplnfcflxx.supabase.co/functions/v1/api-v1

Variables de entorno

sh
# Las tres cadenas se muestran UNA sola vez, al acuñar la llave.
FACTA_API_KEY=facta_test_k7f3a9c21.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
FACTA_SIGN_KEY=factask_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Esta tercera NO se le pasa a Facta ni al cliente: abre sus credenciales
# de almacenamiento en SU servidor.
FACTA_UNLOCK_KEY=factauk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

El ejemplo completo

javaarchivo real · sdks/java/examples/Emitir.java
// Emitir un DTE contra la API de Facta con java.net.http.
//
// Objetivo: JDK 17 o superior. No hay librería de Facta para Java — esto es
// HTTP directo, no un cliente oficial. Sin dependencias: el JSON se arma y se
// lee a mano porque añadir Jackson o Gson solo para un ejemplo sería pedirle
// al lector una decisión de empaquetado que no es suya.
//
//   FACTA_API_BASE_URL=https://hcnvknpsbadplnfcflxx.supabase.co/functions/v1/api-v1 \
//   FACTA_API_KEY=facta_test_... FACTA_SIGN_KEY=factask_... \
//   PEDIDO_ID=pedido-00417 java Emitir.java
//
// Lo que cubre: estado antes de emitir, una Idempotency-Key que su sistema
// posee (no una al azar por intento), 200 / 202 / 422 distintos, timeout de
// 60 s, lectura de RateLimit-Remaining y descarga del JSON firmado byte a
// byte. Nunca usa FACTA_UNLOCK_KEY — esa cadena abre SU almacenamiento, no
// esta API.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public final class Emitir {

  private static final HttpClient CLIENTE = HttpClient.newHttpClient();
  private static final Duration PLAZO = Duration.ofSeconds(60);

  public static void main(String[] args) throws Exception {
    String base = System.getenv("FACTA_API_BASE_URL").replaceAll("/+

{{EJEMPLO}}

quot;, ""); String apiKey = System.getenv("FACTA_API_KEY"); String signKey = System.getenv("FACTA_SIGN_KEY"); // El identificador de ESTA venta — en un sistema real viene de su propia // base de datos y se persiste antes de la primera llamada, para que un // reintento use el mismo valor en vez de uno nuevo por intento. String pedidoId = System.getenv().getOrDefault("PEDIDO_ID", "pedido-00417"); // 1) El estado primero: confirma que la llave sirve y cuánto cupo queda. // No pide X-Facta-Sign-Key y no gasta el cupo de emitir. HttpRequest estadoPeticion = HttpRequest.newBuilder(URI.create(base + "/v1/status")) .header("X-Facta-Key", apiKey) .timeout(PLAZO) .GET() .build(); HttpResponse<String> estadoRespuesta = CLIENTE.send(estadoPeticion, HttpResponse.BodyHandlers.ofString()); if (estadoRespuesta.statusCode() != 200) { System.err.println("no se pudo leer el estado de la llave: " + estadoRespuesta.body()); System.exit(1); } System.out.println("ambiente: " + campo(estadoRespuesta.body(), "ambiente") + " · cupo por hora restante: " + numeroTrasEtiqueta(estadoRespuesta.body(), "\"remaining\"")); // 2) Emitir. Una factura (01) a consumidor final: el IVA va INCLUIDO en // precioUni, como exige el esquema oficial de este tipo de documento. String cuerpo = """ { "tipoDte": "01", "items": [ { "descripcion": "Café", "cantidad": 2, "precioUni": 1.5 } ] } """; HttpRequest emitirPeticion = HttpRequest.newBuilder(URI.create(base + "/v1/dte")) .header("X-Facta-Key", apiKey) .header("X-Facta-Sign-Key", signKey) .header("Idempotency-Key", pedidoId) .header("Content-Type", "application/json") .timeout(PLAZO) .POST(HttpRequest.BodyPublishers.ofString(cuerpo)) .build(); HttpResponse<String> emitirRespuesta = CLIENTE.send(emitirPeticion, HttpResponse.BodyHandlers.ofString()); System.out.println("cupo restante: " + emitirRespuesta.headers().firstValue("RateLimit-Remaining").orElse("?")); String respuestaCuerpo = emitirRespuesta.body(); switch (emitirRespuesta.statusCode()) { case 200 -> System.out.println( "sellado: " + campo(respuestaCuerpo, "numeroControl") + " " + campo(respuestaCuerpo, "selloRecibido")); case 202 -> System.out.println( // Firmado, sin respuesta del Ministerio todavía. El documento // EXISTE y se le debe a Hacienda: NO se reintenta esta llamada. "en contingencia: " + campo(respuestaCuerpo, "numeroControl") + " — " + campo(respuestaCuerpo, "detalle")); case 422 -> { if ("mh_rejected".equals(campo(respuestaCuerpo, "code"))) { System.err.println("rechazado: " + campo(respuestaCuerpo, "message")); System.err.println("número ya gastado: " + campo(respuestaCuerpo, "numeroControl")); } else { System.err.println("no se pudo emitir: " + campo(respuestaCuerpo, "code")); } System.exit(1); } default -> { System.err.println("error inesperado: " + emitirRespuesta.statusCode() + " " + respuestaCuerpo); System.exit(1); } } // 3) Descargar los bytes EXACTOS del JSON firmado — nunca se vuelven a // serializar, porque eso no reproduciría los bytes que Hacienda validó. String codigo = campo(respuestaCuerpo, "codigoGeneracion"); HttpRequest archivoPeticion = HttpRequest.newBuilder(URI.create(base + "/v1/dte/" + codigo + "/file?kind=json")) .header("X-Facta-Key", apiKey) .timeout(PLAZO) .GET() .build(); HttpResponse<byte[]> archivoRespuesta = CLIENTE.send(archivoPeticion, HttpResponse.BodyHandlers.ofByteArray()); if (archivoRespuesta.statusCode() == 200) { String nombre = codigo + ".json"; Files.write(Path.of(nombre), archivoRespuesta.body()); System.out.println("archivado: " + nombre); } } /** Extrae un valor de cadena de un JSON plano — solo para este ejemplo sin * dependencias; un sistema real usa una librería de JSON de verdad. */ private static String campo(String json, String clave) { Matcher m = Pattern.compile("\"" + clave + "\"\\s*:\\s*\"([^\"]*)\"").matcher(json); return m.find() ? m.group(1) : null; } private static String numeroTrasEtiqueta(String json, String etiqueta) { int en = json.indexOf(etiqueta); if (en == -1) return "?"; Matcher m = Pattern.compile(":\\s*(-?\\d+)").matcher(json.substring(en)); return m.find() ? m.group(1) : "?"; } }

Lo que este ejemplo no hace

Los demás lenguajes

El mismo documento, en los otros seis. Cambia la sintaxis; no cambian las cabeceras, ni los códigos, ni las reglas.

curl

curl
# -D - vuelca las cabeceras de la respuesta, con RateLimit-Remaining (y, si aplica, Retry-After).
curl --fail-with-body -X POST "$FACTA_API_BASE_URL/v1/dte" \
  -H "X-Facta-Key: $FACTA_API_KEY" \
  -H "X-Facta-Sign-Key: $FACTA_SIGN_KEY" \
  -H "Idempotency-Key: $PEDIDO_ID" \
  -H "Content-Type: application/json" \
  -D - \
  --max-time 60 \
  -d '{
  "tipoDte": "03",
  "receptor": {
    "customerId": "374114b6-e957-4c7a-8911-dd6381b1e0ea"
  },
  "items": [
    {
      "descripcion": "Integración de la API de facturación electrónica",
      "cantidad": 1,
      "precioUni": 25
    }
  ]
}'

JavaScript

javascript
// JavaScript puro, sin ninguna librería de Facta. Node 22 o 24; fetch es global.
const base = process.env.FACTA_API_BASE_URL;

const headers = {
  "X-Facta-Key": process.env.FACTA_API_KEY,
  "X-Facta-Sign-Key": process.env.FACTA_SIGN_KEY,
  // Un identificador que USTED elige y REUTILIZA en cada reintento de
  // esta misma operación — su número de pedido, su ticket de caja.
  "Idempotency-Key": pedidoId,
  "Content-Type": "application/json",
};

const controlador = new AbortController();
const plazo = setTimeout(() => controlador.abort(), 60_000);

const respuesta = await fetch(base + "/v1/dte", {
  method: "POST",
  headers,
  body: JSON.stringify({
    "tipoDte": "03",
    "receptor": {
      "customerId": "374114b6-e957-4c7a-8911-dd6381b1e0ea"
    },
    "items": [
      {
        "descripcion": "Integración de la API de facturación electrónica",
        "cantidad": 1,
        "precioUni": 25
      }
    ]
  }),
  signal: controlador.signal,
}).finally(() => clearTimeout(plazo));

console.log("cupo restante:", respuesta.headers.get("RateLimit-Remaining"));

const cuerpo = await respuesta.json();

if (respuesta.status === 200) {
  // Resultado correcto.
  console.log(cuerpo);
}
else if (respuesta.status === 202) {
  // 202: firmado, sin respuesta del Ministerio todavía. NO lo reintente.
  console.log("en contingencia:", cuerpo.numeroControl);
}
else if (cuerpo.error?.code === "mh_rejected") {
  console.error("rechazado:", cuerpo.error.message, "· número gastado:", cuerpo.error.details.numeroControl);
}
else {
  throw new Error(cuerpo.error?.code ?? String(respuesta.status));
}

Python

python
import json
import os
import urllib.error
import urllib.request

base = os.environ["FACTA_API_BASE_URL"]

cabeceras = {
    "X-Facta-Key": os.environ["FACTA_API_KEY"],
    "X-Facta-Sign-Key": os.environ["FACTA_SIGN_KEY"],
    "Idempotency-Key": pedido_id,  # el mismo en cada reintento de esta operación
    "Content-Type": "application/json",
}

peticion = urllib.request.Request(
    base + "/v1/dte",
    data=json.dumps({
      "tipoDte": "03",
      "receptor": {
        "customerId": "374114b6-e957-4c7a-8911-dd6381b1e0ea"
      },
      "items": [
        {
          "descripcion": "Integración de la API de facturación electrónica",
          "cantidad": 1,
          "precioUni": 25
        }
      ]
    }).encode("utf-8"),
    method="POST",
    headers=cabeceras,
)

try:
    # El Ministerio puede tardar unos cuarenta segundos en contestar.
    with urllib.request.urlopen(peticion, timeout=60) as respuesta:
        print("cupo restante:", respuesta.headers.get("RateLimit-Remaining"))
        cuerpo = json.load(respuesta)
        if respuesta.status == 200:
            print(cuerpo)
        elif respuesta.status == 202:
            # Firmado, sin respuesta del Ministerio todavía. NO lo reintente.
            print("en contingencia:", cuerpo["numeroControl"])
except urllib.error.HTTPError as fallo:
    error = json.load(fallo).get("error", {})
    if error.get("code") == "mh_rejected":
        detalle = error.get("details", {})
        print("rechazado:", error.get("message"), "· número gastado:", detalle.get("numeroControl"))
    else:
        raise

PHP

php
<?php
// PHP 8 con la extensión cURL. No hay librería de Facta para PHP.
$base = getenv("FACTA_API_BASE_URL");

$cabeceras = [
    "X-Facta-Key: " . getenv("FACTA_API_KEY"),
    "X-Facta-Sign-Key: " . getenv("FACTA_SIGN_KEY"),
    "Idempotency-Key: " . $pedidoId,  // el mismo en cada reintento de esta operación
    "Content-Type: application/json",
];

$conexion = curl_init($base . "/v1/dte");
$cuerpo = json_encode([
    "tipoDte" => "03",
    "receptor" => [
        "customerId" => "374114b6-e957-4c7a-8911-dd6381b1e0ea",
    ],
    "items" => [
        [
            "descripcion" => "Integración de la API de facturación electrónica",
            "cantidad" => 1,
            "precioUni" => 25,
        ],
    ],
], JSON_UNESCAPED_UNICODE);
curl_setopt_array($conexion, [
    CURLOPT_CUSTOMREQUEST => "POST",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HEADER => true,
    CURLOPT_TIMEOUT => 60,
    CURLOPT_HTTPHEADER => $cabeceras,
    CURLOPT_POSTFIELDS => $cuerpo,
]);

$crudo = curl_exec($conexion);
$estado = curl_getinfo($conexion, CURLINFO_RESPONSE_CODE);
$tamCabeceras = curl_getinfo($conexion, CURLINFO_HEADER_SIZE);
curl_close($conexion);

$cabecerasRespuesta = substr($crudo, 0, $tamCabeceras);
echo "cupo restante: " . (preg_match("/RateLimit-Remaining: (\d+)/i", $cabecerasRespuesta, $m) ? $m[1] : "?") . "\n";

$datos = json_decode(substr($crudo, $tamCabeceras), true);

if ($estado === 200) {
    echo json_encode($datos) . "\n";
}
elseif ($estado === 202) {
    // Firmado, sin respuesta del Ministerio todavía. NO lo reintente.
    echo "en contingencia: {$datos['numeroControl']}\n";
}
elseif (($datos['error']['code'] ?? null) === 'mh_rejected') {
    echo "rechazado: {$datos['error']['message']}\n";
    echo "número gastado: {$datos['error']['details']['numeroControl']}\n";
} else {
    throw new RuntimeException($datos['error']['code'] ?? "HTTP $estado");
}

C#

csharp
// .NET 8 con HttpClient. No hay librería de Facta para C#.
using System.Net.Http.Json;
using System.Text.Json;

var base_ = Environment.GetEnvironmentVariable("FACTA_API_BASE_URL");
using var cliente = new HttpClient { Timeout = TimeSpan.FromSeconds(60) };

var peticion = new HttpRequestMessage(HttpMethod.Post, base_ + "/v1/dte");
peticion.Content = JsonContent.Create(new { tipoDte = "03", receptor = new { customerId = "374114b6-e957-4c7a-8911-dd6381b1e0ea" }, items = new[] { new { descripcion = "Integración de la API de facturación electrónica", cantidad = 1, precioUni = 25 } } });
peticion.Headers.Add("X-Facta-Key", Environment.GetEnvironmentVariable("FACTA_API_KEY"));
peticion.Headers.Add("X-Facta-Sign-Key", Environment.GetEnvironmentVariable("FACTA_SIGN_KEY"));
peticion.Headers.Add("Idempotency-Key", pedidoId); // el mismo en cada reintento de esta operación

var respuesta = await cliente.SendAsync(peticion);
Console.WriteLine(

{{OTROS}}

quot;cupo restante: {respuesta.Headers.GetValues("RateLimit-Remaining").FirstOrDefault()}"); using var datos = JsonDocument.Parse(await respuesta.Content.ReadAsStringAsync()); var raiz = datos.RootElement; if ((int)respuesta.StatusCode == 200) Console.WriteLine(raiz); else if ((int)respuesta.StatusCode == 202) // Firmado, sin respuesta del Ministerio todavía. NO lo reintente. Console.WriteLine(

{{OTROS}}

quot;en contingencia: {raiz.GetProperty("numeroControl")}"); else if (raiz.TryGetProperty("error", out var error) && error.GetProperty("code").GetString() == "mh_rejected") Console.WriteLine(

{{OTROS}}

quot;rechazado: {error.GetProperty("message")} · gastó {error.GetProperty("details").GetProperty("numeroControl")}"); else throw new InvalidOperationException(respuesta.StatusCode.ToString());

Go

go
// Go 1.22 con net/http. No hay librería de Facta para Go.
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"time"
)

func main() {
	base := os.Getenv("FACTA_API_BASE_URL")
	cuerpo, _ := json.Marshal(map[string]any{"tipoDte": "03", "receptor": map[string]any{"customerId": "374114b6-e957-4c7a-8911-dd6381b1e0ea"}, "items": []any{map[string]any{"descripcion": "Integración de la API de facturación electrónica", "cantidad": 1, "precioUni": 25}}})
	peticion, _ := http.NewRequest("POST", base+"/v1/dte", bytes.NewReader(cuerpo))
	peticion.Header.Set("Content-Type", "application/json")
	peticion.Header.Set("X-Facta-Key", os.Getenv("FACTA_API_KEY"))
	peticion.Header.Set("X-Facta-Sign-Key", os.Getenv("FACTA_SIGN_KEY"))
	// El mismo pedidoId en cada reintento de esta operación.
	peticion.Header.Set("Idempotency-Key", pedidoId)

	cliente := &http.Client{Timeout: 60 * time.Second}
	respuesta, err := cliente.Do(peticion)
	if err != nil {
		panic(err)
	}
	defer respuesta.Body.Close()

	fmt.Println("cupo restante:", respuesta.Header.Get("RateLimit-Remaining"))

	var datos map[string]any
	json.NewDecoder(respuesta.Body).Decode(&datos)

	switch respuesta.StatusCode {
	case 200:
		fmt.Println(datos)
	case 202:
		// Firmado, sin respuesta del Ministerio todavía. No reintentar.
		fmt.Println(datos)
	default:
		fmt.Println("error:", datos["error"])
	}
}

Escriba para buscar Por ejemplo: idempotencia, contingencia, 422, emitir.

↑ ↓ moverse Enter abrir Esc cerrar