Descargar el JSON o el PDF
Alcance: download.
Parámetros
| Nombre | Dónde | Qué es |
|---|---|---|
X-Facta-Keyrequerido |
cabecera | La llave de la API. Nunca en |
codigoGeneracionrequerido |
ruta | |
kindopcional |
consulta |
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
# -D - vuelca las cabeceras de la respuesta, con RateLimit-Remaining (y, si aplica, Retry-After).
curl --fail-with-body -X GET "$FACTA_API_BASE_URL/v1/dte/7875BC7A-9580-441D-94E4-FA455E9D8BD0/file?kind=json" \
-H "X-Facta-Key: $FACTA_API_KEY" \
-D - \
--max-time 60 \
-o "archivo.json"
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,
};
const controlador = new AbortController();
const plazo = setTimeout(() => controlador.abort(), 60_000);
const respuesta = await fetch(base + "/v1/dte/7875BC7A-9580-441D-94E4-FA455E9D8BD0/file?kind=json", {
method: "GET",
headers,
signal: controlador.signal,
}).finally(() => clearTimeout(plazo));
console.log("cupo restante:", respuesta.headers.get("RateLimit-Remaining"));
if (!respuesta.ok) throw new Error(`HTTP ${respuesta.status}`);
// Los bytes exactos: nunca se parsean ni se vuelven a serializar.
const bytes = new Uint8Array(await respuesta.arrayBuffer());
await writeFile("archivo.json", bytes);
TypeScript (SDK)
import { Facta, FactaError } from "@facta/api";
const facta = new Facta({
apiKey: process.env.FACTA_API_KEY!,
});
// Los bytes EXACTOS, sin parsear ni volver a serializar.
const archivo = await facta.downloadDocument("7875BC7A-9580-441D-94E4-FA455E9D8BD0", "json");
await writeFile(archivo.filename ?? `${archivo}.json`, archivo.bytes);
console.log(archivo.contentType); // "application/json"
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"],
}
peticion = urllib.request.Request(
base + "/v1/dte/7875BC7A-9580-441D-94E4-FA455E9D8BD0/file?kind=json",
method="GET",
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"))
# Los bytes exactos: nunca se parsean ni se vuelven a serializar.
datos = respuesta.read()
with open("archivo.json", "wb") as archivo:
archivo.write(datos)
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 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"),
];
$conexion = curl_init($base . "/v1/dte/7875BC7A-9580-441D-94E4-FA455E9D8BD0/file?kind=json");
curl_setopt_array($conexion, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => $cabeceras,
]);
$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";
// Los bytes exactos: nunca se parsean ni se vuelven a serializar.
$bytes = substr($crudo, $tamCabeceras);
file_put_contents("archivo.json", $bytes);
C#
// .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.Get, base_ + "/v1/dte/7875BC7A-9580-441D-94E4-FA455E9D8BD0/file?kind=json");
peticion.Headers.Add("X-Facta-Key", Environment.GetEnvironmentVariable("FACTA_API_KEY"));
var respuesta = await cliente.SendAsync(peticion);
Console.WriteLine($"cupo restante: {respuesta.Headers.GetValues("RateLimit-Remaining").FirstOrDefault()}");
// Los bytes exactos: nunca se parsean ni se vuelven a serializar.
var bytes = await respuesta.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync("archivo.json", bytes);
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 Archivo {
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/file?kind=json"))
.header("X-Facta-Key", System.getenv("FACTA_API_KEY"))
.timeout(Duration.ofSeconds(60))
.GET();
var peticion = constructor.build();
var respuesta = HttpClient.newHttpClient().send(peticion, HttpResponse.BodyHandlers.ofByteArray());
System.out.println("cupo restante: " + respuesta.headers().firstValue("RateLimit-Remaining").orElse("?"));
// Los bytes exactos: nunca se parsean ni se vuelven a serializar.
java.nio.file.Files.write(java.nio.file.Path.of("archivo.json"), respuesta.body());
}
}
Go
// Go 1.22 con net/http. No hay librería de Facta para Go.
package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
base := os.Getenv("FACTA_API_BASE_URL")
peticion, _ := http.NewRequest("GET", base+"/v1/dte/7875BC7A-9580-441D-94E4-FA455E9D8BD0/file?kind=json", nil)
peticion.Header.Set("X-Facta-Key", os.Getenv("FACTA_API_KEY"))
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"))
// Los bytes exactos: nunca se parsean ni se vuelven a serializar.
bytes, _ := io.ReadAll(respuesta.Body)
os.WriteFile("archivo.json", bytes, 0o600)
}
Qué contesta
Los bytes decifrados, tal como se guardaron.
El código no tiene forma de UUID.
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.
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).
not_found — igual para "ese documento no existe en el área de retención" que para "existe, pero es de otra empresa o de otro ambiente". Nunca 403: distinguir los dos casos confirmaría códigos de generación ajenos, y el código va impreso en cada factura.
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.
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.