factadesarrolladores API 0.1.0-esqueleto

Anular un documento sellado

POST /v1/dte/{codigoGeneracion}/invalidate

Alcance: issue. Y X-Facta-Sign-Key: la anulación se firma con el certificado del emisor, igual que el documento que anula.

Alcance issueGasta correlativoFirma · X-Facta-Sign-KeyIdempotency-Key obligatoria

Parámetros

NombreDóndeQué es
X-Facta-Keyrequerido cabecera

La llave de la API. Nunca en Authorization.

X-Facta-Sign-Keyrequerido cabecera

La contraseña que abre el vault de firma, durante esta petición.

Idempotency-Keyrequerido cabecera

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

codigoGeneracionrequerido ruta

Qué se manda

CampoTipoQué es
tipoAnulacionrequerido 1 · 2 · 3

1 error en el documento, 2 rescisión, 3 otro.

motivoopcional texto

Obligatorio en los tipos 1 y 3; el campo 111 describe el error.

codigoGeneracionReemplazoopcional texto (uuid)

El documento que reemplaza al anulado. Obligatorio en los tipos 1 y 3, y prohibido en el 2.

responsablerequerido cualquiera

Quien responde por la anulación ante Hacienda.

solicitarequerido cualquiera

Quien la pidió. Puede ser la misma persona.

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/7875BC7A-9580-441D-94E4-FA455E9D8BD0/invalidate" \
  -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 '{
  "tipoAnulacion": 1,
  "motivo": "El precio unitario iba sin el descuento pactado",
  "codigoGeneracionReemplazo": "7875BC7A-9580-441D-94E4-FA455E9D8BD0",
  "responsable": {
    "nombre": "Ana Rivas",
    "tipoDocumento": "36",
    "numDocumento": "06142803901121"
  },
  "solicita": {
    "nombre": "Beto Cruz",
    "tipoDocumento": "13",
    "numDocumento": "012345678"
  }
}'

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/7875BC7A-9580-441D-94E4-FA455E9D8BD0/invalidate", {
  method: "POST",
  headers,
  body: JSON.stringify({
    "tipoAnulacion": 1,
    "motivo": "El precio unitario iba sin el descuento pactado",
    "codigoGeneracionReemplazo": "7875BC7A-9580-441D-94E4-FA455E9D8BD0",
    "responsable": {
      "nombre": "Ana Rivas",
      "tipoDocumento": "36",
      "numDocumento": "06142803901121"
    },
    "solicita": {
      "nombre": "Beto Cruz",
      "tipoDocumento": "13",
      "numDocumento": "012345678"
    }
  }),
  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!,
  signKey: process.env.FACTA_SIGN_KEY!,
});

const anulado = await facta.invalidate(
  "7875BC7A-9580-441D-94E4-FA455E9D8BD0",
  {
    "tipoAnulacion": 1,
    "motivo": "El precio unitario iba sin el descuento pactado",
    "codigoGeneracionReemplazo": "7875BC7A-9580-441D-94E4-FA455E9D8BD0",
    "responsable": {
      "nombre": "Ana Rivas",
      "tipoDocumento": "36",
      "numDocumento": "06142803901121"
    },
    "solicita": {
      "nombre": "Beto Cruz",
      "tipoDocumento": "13",
      "numDocumento": "012345678"
    }
  },
  { idempotencyKey: pedidoId },
);

if (anulado.yaEstabaInvalidado) console.log("ya estaba anulado; no se mandó nada");

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/7875BC7A-9580-441D-94E4-FA455E9D8BD0/invalidate",
    data=json.dumps({
      "tipoAnulacion": 1,
      "motivo": "El precio unitario iba sin el descuento pactado",
      "codigoGeneracionReemplazo": "7875BC7A-9580-441D-94E4-FA455E9D8BD0",
      "responsable": {
        "nombre": "Ana Rivas",
        "tipoDocumento": "36",
        "numDocumento": "06142803901121"
      },
      "solicita": {
        "nombre": "Beto Cruz",
        "tipoDocumento": "13",
        "numDocumento": "012345678"
      }
    }).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"),
    "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/7875BC7A-9580-441D-94E4-FA455E9D8BD0/invalidate");
$cuerpo = json_encode([
    "tipoAnulacion" => 1,
    "motivo" => "El precio unitario iba sin el descuento pactado",
    "codigoGeneracionReemplazo" => "7875BC7A-9580-441D-94E4-FA455E9D8BD0",
    "responsable" => [
        "nombre" => "Ana Rivas",
        "tipoDocumento" => "36",
        "numDocumento" => "06142803901121",
    ],
    "solicita" => [
        "nombre" => "Beto Cruz",
        "tipoDocumento" => "13",
        "numDocumento" => "012345678",
    ],
], 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/7875BC7A-9580-441D-94E4-FA455E9D8BD0/invalidate");
peticion.Content = JsonContent.Create(new { tipoAnulacion = 1, motivo = "El precio unitario iba sin el descuento pactado", codigoGeneracionReemplazo = "7875BC7A-9580-441D-94E4-FA455E9D8BD0", responsable = new { nombre = "Ana Rivas", tipoDocumento = "36", numDocumento = "06142803901121" }, solicita = new { nombre = "Beto Cruz", tipoDocumento = "13", numDocumento = "012345678" } });
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($"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 Anular {
  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/7875BC7A-9580-441D-94E4-FA455E9D8BD0/invalidate"))
        .header("X-Facta-Key", System.getenv("FACTA_API_KEY"))
        .header("X-Facta-Sign-Key", System.getenv("FACTA_SIGN_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("""
        {
          "tipoAnulacion": 1,
          "motivo": "El precio unitario iba sin el descuento pactado",
          "codigoGeneracionReemplazo": "7875BC7A-9580-441D-94E4-FA455E9D8BD0",
          "responsable": {
            "nombre": "Ana Rivas",
            "tipoDocumento": "36",
            "numDocumento": "06142803901121"
          },
          "solicita": {
            "nombre": "Beto Cruz",
            "tipoDocumento": "13",
            "numDocumento": "012345678"
          }
        }
        """));
    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{"tipoAnulacion": 1, "motivo": "El precio unitario iba sin el descuento pactado", "codigoGeneracionReemplazo": "7875BC7A-9580-441D-94E4-FA455E9D8BD0", "responsable": map[string]any{"nombre": "Ana Rivas", "tipoDocumento": "36", "numDocumento": "06142803901121"}, "solicita": map[string]any{"nombre": "Beto Cruz", "tipoDocumento": "13", "numDocumento": "012345678"}})
	peticion, _ := http.NewRequest("POST", base+"/v1/dte/7875BC7A-9580-441D-94E4-FA455E9D8BD0/invalidate", 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)
	default:
		fmt.Println("error:", datos["error"])
	}
}

Qué contesta

200

Anulado, con el sello del evento.

400

Cuerpo mal formado, 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).

404

No existe ese documento en la empresa de la llave.

409

idempotency_in_flight (la primera petición con esa llave todavía trabaja — reintenta) o sign_vault_missing (la llave no está provisionada para firmar — reintentar no la arregla, hay que acuñarla de nuevo desde la app). El code los distingue.

422

validation_failed (el documento no tiene sello, o el evento no cumple el esquema) o mh_rejected (Hacienda leyó la anulación y la negó — el plazo vencido es la razón más común).

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.

502

mh_unreachable — no llegamos a Hacienda, así que no se anuló nada y el documento sigue vivo. Reintente con la MISMA Idempotency-Key.

Aquí sí ocurre, a diferencia de la emisión: un DTE firmado existe y se le debe a Hacienda, pero un evento que no llegó no anuló nada.

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