factadesarrolladores API 0.1.0-esqueleto

Preparar un documento

POST /v1/dte/prepare

Alcance: issue. Aquí NO se abre el vault de firma, así que esta ruta no pide X-Facta-Sign-Key: reservar y construir es todo lo que un token robado puede hacer, y firmar no está entre esas dos cosas.

Alcance issueGasta correlativoIdempotency-Key obligatoria

Parámetros

NombreDóndeQué es
X-Facta-Keyrequerido cabecera

La llave de la API. Nunca en Authorization.

Idempotency-Keyrequerido cabecera

Un identificador por operación, elegido por usted. La misma en cada reintento.

Qué se manda

CampoTipoQué es
tipoDterequerido 01 · 03 · 05 · 06 · 11 · 14

01 factura · 03 crédito fiscal · 05 nota de crédito · 06 nota de débito · 11 exportación · 14 sujeto excluido. Los demás del catálogo llegan con su tanda.

receptoropcional Receptor o ReceptorExportacion o ReceptorSujetoExcluido o null

Su forma depende del tipo: Receptor para 01/03/05/06, ReceptorExportacion para el 11 y ReceptorSujetoExcluido para el 14. Una FE (01) puede no llevarlo — es el consumidor final anónimo.

itemsrequerido lista de Item

documentosRelacionadosopcional lista de DocumentoRelacionado

Obligatorio en 05 y 06, y prohibido en los demás. Qué documento sellado ajusta esta nota. Cada entrada se escribe entera o se nombra por su codigoGeneracion, que el servidor completa desde el índice de SU empresa.

numPagoElectronicoopcional texto

Solo en la nota de débito (06). En otro tipo es 400.

exportacionopcional cualquiera

Obligatorio en el 11, y prohibido en los demás.

aplicarReteRentaopcional sí/no

Solo en el 14. Retención de renta del 10 % (art. 156 CT) sobre servicios de una persona natural. Nunca automática: se pide.

condicionOperacionopcional 1 · 2 · 3

1 contado

plazoopcional 01 · 02 · 03

CAT-018 — días, meses o años. Solo se escribe si la operación es a crédito.

periodoopcional entero

Cuántos de esos plazos. Solo con condicionOperacion 2.

formaPagoopcional texto

CAT-017. 01 = efectivo.

observacionesopcional texto

Ejemplos

Solo la primera pestaña usa una librería de Facta; las demás son peticiones HTTP normales, porque no existe un cliente oficial para esos lenguajes.

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/prepare" \
  -H "X-Facta-Key: $FACTA_API_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,
  // 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/prepare", {
  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 (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));
}

TypeScript (SDK)

typescript
import { Facta, FactaError } from "@facta/api";

const facta = new Facta({
  apiKey: process.env.FACTA_API_KEY!,
});

// Reserva el correlativo y devuelve el documento con sus totales, SIN firmar.
const preparado = await facta.preparar(
  {
  "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
    }
  ]
},
  // La misma Idempotency-Key que usaría un emitir directo: preparar YA
  // reserva el correlativo, así que también gasta uno.
  { idempotencyKey: pedidoId },
);

console.log(preparado.totales.totalPagar);   // ya calculado por el servidor
console.log(preparado.numeroControl);        // el número ya está reservado

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"],
    "Idempotency-Key": pedido_id,  # el mismo en cada reintento de esta operación
    "Content-Type": "application/json",
}

peticion = urllib.request.Request(
    base + "/v1/dte/prepare",
    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)
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"),
    "Idempotency-Key: " . $pedidoId,  // el mismo en cada reintento de esta operación
    "Content-Type: application/json",
];

$conexion = curl_init($base . "/v1/dte/prepare");
$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 (($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/prepare");
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("Idempotency-Key", pedidoId); // el mismo en cada reintento de esta operación

var respuesta = await cliente.SendAsync(peticion);
Console.WriteLine($"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 (raiz.TryGetProperty("error", out var error) && error.GetProperty("code").GetString() == "mh_rejected")
    Console.WriteLine($"rechazado: {error.GetProperty("message")} · gastó {error.GetProperty("details").GetProperty("numeroControl")}");
else
    throw new InvalidOperationException(respuesta.StatusCode.ToString());

Java

java
// JDK 17 o superior, con java.net.http. No hay librería de Facta para Java.
import java.net.URI;
import java.net.http.*;
import java.time.Duration;

public final class Preparar {
  public static void main(String[] args) throws Exception {
    var base = System.getenv("FACTA_API_BASE_URL");
    var constructor = HttpRequest.newBuilder(URI.create(base + "/v1/dte/prepare"))
        .header("X-Facta-Key", System.getenv("FACTA_API_KEY"))
        // El mismo pedidoId en cada reintento de esta operación.
        .header("Idempotency-Key", pedidoId)
        .timeout(Duration.ofSeconds(60))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString("""
        {
          "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
            }
          ]
        }
        """));
    var peticion = constructor.build();

    var respuesta = HttpClient.newHttpClient().send(peticion, HttpResponse.BodyHandlers.ofString());
    System.out.println("cupo restante: " + respuesta.headers().firstValue("RateLimit-Remaining").orElse("?"));
    // Estados: 200 correcto · 422 con "mh_rejected" si Hacienda lo negó.
    System.out.println(respuesta.statusCode());
    System.out.println(respuesta.body());
  }
}

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/prepare", bytes.NewReader(cuerpo))
	peticion.Header.Set("Content-Type", "application/json")
	peticion.Header.Set("X-Facta-Key", os.Getenv("FACTA_API_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)
	default:
		fmt.Println("error:", datos["error"])
	}
}

Qué contesta

200

Correlativo reservado; el documento todavía no está firmado.

400

Cuerpo mal formado, de más de 1 MB, o falta Idempotency-Key.

401

unauthorized (no vino llave) o invalid_api_key (no sirve). Las dos tardan lo mismo, a propósito.

En las rutas que firman se suman las dos del segundo factor: sign_key_required (falta X-Facta-Sign-Key, o vino la de apertura por error) y sign_key_invalid (no abre el vault). details.intentosRestantes dice cuántos quedan antes de que la llave se suspenda sola.

403

La llave existe pero no puede: key_revoked, key_expired, key_inactive, forbidden_scope, dte_type_not_allowed, ip_not_allowed, environment_not_allowed o sign_vault_locked (demasiados intentos fallidos de abrir el vault: esperar no lo arregla, hay que reactivar la llave desde la app).

409

idempotency_in_flight — la primera petición con esa llave todavía trabaja. details.enVueloSegundos dice desde cuándo.

Si esa primera petición ya había empezado algo que no se deshace —un correlativo reservado, o una anulación camino de Hacienda— el mensaje NOMBRA el documento (details.codigoGeneracion) para que se pueda preguntar GET /v1/dte/{codigoGeneracion} qué fue de él, en vez de esperar las 24 h del reclamo. Si no había llegado a eso, no pasó nada irreversible y el barrido de cinco minutos suelta el reclamo solo a la media hora (details.seLiberaSola).

422

validation_failed o no_storage_destination — ninguno gasta correlativo.

429

rate_limited (por hora, por día, o la ventana propia de /v1/status) o amount_limit (el documento pasa del monto máximo de la llave). El de monto se comprueba ANTES de reservar: no gasta correlativo.

details.window dice cuál de las tres fue.

503

service_unavailable o correlative_unavailable — algo nuestro no está listo. Puede salir en CUALQUIER ruta, incluidas las de consulta: el registro de peticiones se escribe antes de trabajar y falla cerrado, así que si esa tabla no contesta, nada contesta.

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

moverse Enter abrir Esc cerrar