Saltar al contenido

Factura desde tu sistema con la API de miCFDI

Una API REST en español para emitir, consultar, descargar y cancelar CFDI 4.0 desde tu punto de venta, tu tienda en línea o tu ERP. Pruebas sin timbrar, idempotencia y webhooks desde el primer día.

La API usa los mismos timbres que el panel y las pruebas con mcfdi_test_ no gastan timbres. Cuánto cuesta.

URL base
https://api.micfdi.com.mx/v1
Autenticación
Authorization: Bearer con tu llave
Formato
JSON en camelCase · fechas del centro de México
Límite
Ráfagas de 20 peticiones y 10 por segundo, por llave
Documentación
api.micfdi.com.mx/docs/
OpenAPI
openapi/v1.json · impórtala en Postman, Insomnia o Bruno

Pensada para integrarse bien

Lo que necesitas para timbrar sin sustos

Llaves de prueba y producción

mcfdi_test_ hace las mismas validaciones y cálculos que mcfdi_live_, pero no timbra ni gasta timbres.

Idempotency-Key obligatoria

Si repites la petición con la misma llave recibes la misma respuesta. Un reintento nunca timbra dos veces.

Webhooks firmados

cfdi.timbrado, cfdi.cancelacion y más, firmados con HMAC-SHA256 y con reintentos automáticos.

XML y PDF por URL

Cada respuesta trae los enlaces al XML timbrado y al PDF con el formato de la empresa.

Cancelación con motivo

Motivos 01 a 04, con folio de sustitución en el 01, y el estatus del SAT en la misma respuesta.

Errores que se entienden

application/problem+json (RFC 9457) con un código estable y los campos que fallaron, en español.

Ejemplo

Tu primera factura por API

Una factura global a público en general por $1.00 más IVA. Con una llave de pruebas no se timbra.

cURL

# Con una llave mcfdi_test_ se valida todo y no se timbra (estado «simulado»).
curl https://api.micfdi.com.mx/v1/cfdi \
  -H "Authorization: Bearer $MICFDI_LLAVE" \
  -H "Idempotency-Key: venta-mostrador-2026-10-12" \
  -H "Content-Type: application/json" \
  -d '{
    "tipo": "factura",
    "formaPago": "01",
    "metodoPago": "PUE",
    "receptor": {
      "rfc": "XAXX010101000",
      "nombre": "PUBLICO EN GENERAL",
      "regimenFiscal": "616",
      "usoCfdi": "S01"
    },
    "informacionGlobal": { "periodicidad": "01", "meses": "10", "anio": 2026 },
    "conceptos": [{
      "claveProdServ": "01010101",
      "claveUnidad": "ACT",
      "unidad": "Actividad",
      "descripcion": "Venta",
      "cantidad": 1,
      "valorUnitario": 1.00,
      "objetoImp": "02",
      "impuestos": { "traslados": [{ "impuesto": "002", "tipoFactor": "Tasa", "tasaOCuota": 0.16 }] }
    }]
  }'

C#

using System.Net.Http.Json;
using System.Text.Json;

// La llave vive en una variable de entorno o en tu bóveda, nunca en el código.
using var http = new HttpClient { BaseAddress = new Uri("https://api.micfdi.com.mx") };
http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("MICFDI_LLAVE"));

var solicitud = new HttpRequestMessage(HttpMethod.Post, "/v1/cfdi")
{
    Content = JsonContent.Create(new
    {
        tipo = "factura",
        formaPago = "01",
        metodoPago = "PUE",
        receptor = new { rfc = "XAXX010101000", nombre = "PUBLICO EN GENERAL", regimenFiscal = "616", usoCfdi = "S01" },
        informacionGlobal = new { periodicidad = "01", meses = "10", anio = 2026 },
        conceptos = new[]
        {
            new
            {
                claveProdServ = "01010101", claveUnidad = "ACT", unidad = "Actividad", descripcion = "Venta",
                cantidad = 1, valorUnitario = 1.00m, objetoImp = "02",
                impuestos = new { traslados = new[] { new { impuesto = "002", tipoFactor = "Tasa", tasaOCuota = 0.16m } } },
            },
        },
    }),
};
// Ante un timeout o un 5xx, reintenta con la MISMA llave: nunca se timbra dos veces.
solicitud.Headers.Add("Idempotency-Key", "venta-mostrador-2026-10-12");

using HttpResponseMessage respuesta = await http.SendAsync(solicitud);
JsonElement cfdi = await respuesta.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine($"{cfdi.GetProperty("estado")} {cfdi.GetProperty("uuid")}");

JavaScript

// Node.js 18 o más reciente. Usa la llave solo en tu servidor, nunca en el navegador.
const respuesta = await fetch('https://api.micfdi.com.mx/v1/cfdi', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.MICFDI_LLAVE}`,
    'Idempotency-Key': 'venta-mostrador-2026-10-12',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    tipo: 'factura',
    formaPago: '01',
    metodoPago: 'PUE',
    receptor: { rfc: 'XAXX010101000', nombre: 'PUBLICO EN GENERAL', regimenFiscal: '616', usoCfdi: 'S01' },
    informacionGlobal: { periodicidad: '01', meses: '10', anio: 2026 },
    conceptos: [{
      claveProdServ: '01010101', claveUnidad: 'ACT', unidad: 'Actividad', descripcion: 'Venta',
      cantidad: 1, valorUnitario: 1.00, objetoImp: '02',
      impuestos: { traslados: [{ impuesto: '002', tipoFactor: 'Tasa', tasaOCuota: 0.16 }] }
    }]
  })
});
const cfdi = await respuesta.json();
console.log(cfdi.estado, cfdi.uuid);
Respuesta · 200 OK con llave de pruebas
{
  "modo": "test",
  "estado": "simulado",
  "tipo": "factura",
  "serie": "A",
  "receptor": { "rfc": "XAXX010101000", "nombre": "PUBLICO EN GENERAL" },
  "moneda": "MXN",
  "subtotal": 1.00,
  "impuestosTrasladados": 0.16,
  "total": 1.16,
  "uuid": "00000000-7E57-…",
  "enlaces": { "self": "…/v1/cfdi/00000000-7E57-…", "solicitud": "…/v1/solicitudes/…" }
}

Referencia

Endpoints principales

La referencia completa, con cada campo, sus reglas y sus errores, está en la documentación interactiva.

Abrir la documentación

Descargar la especificación OpenAPI (JSON)

Con ella generas tu cliente o una colección de Postman, Insomnia o Bruno (todos importan OpenAPI).

MétodoRutaQué hace
POST /v1/cfdi Emite un CFDI: factura, nota de crédito, honorarios o complemento de pago. Pide Idempotency-Key.
GET /v1/cfdi Lista los CFDI del emisor con filtros por fecha, receptor, tipo, estado y referencia.
GET /v1/cfdi/{uuid} Detalle del CFDI.
GET /v1/cfdi/{uuid}/xml · /pdf Descarga el XML timbrado o el PDF.
GET /v1/cfdi/{uuid}/estatus-sat Estatus real en el SAT.
POST /v1/cfdi/{uuid}/cancelacion Cancela con motivo 01 a 04 (01 con folio de sustitución).
POST /v1/cfdi/{uuid}/correo Envía el CFDI por correo.
GET /v1/solicitudes/{id} Estado de una emisión que respondió 202 (verificando): pasa a timbrado o incierto.
GET /v1/emisor Datos del emisor: sucursales, CSD, series y timbres disponibles.
GET /v1/catalogos · /{nombre} Catálogos del SAT: usos del CFDI, formas de pago, regímenes y más.
POST /v1/webhooks Registra un webhook firmado (cfdi.timbrado, cfdi.cancelacion…).
GET /v1/webhooks Lista tus webhooks.
DELETE /v1/webhooks/{id} Borra un webhook.
POST /v1/webhooks/{id}/prueba Manda un evento webhook.prueba para probar tu receptor.

Precio

Cuánto cuesta timbrar por API

Lo mismo que en el panel: cada CFDI que timbras por API usa un timbre del saldo de la empresa emisora, y las pruebas con mcfdi_test_ no gastan timbres.

Calcula cuánto pagarías al año

OpciónComprasPor timbre
Básico 100 timbres por $350 *más IVA $3.50 *más IVA
Profesional 250 timbres por $820 *más IVA $3.28 *más IVA
1,000 timbres 1,000 timbres por $2,500 *más IVA $2.50 *más IVA
Prepago a la medida De 50 a 5,000 timbres en una compra De $5.00 *más IVA a $1.00 *más IVA
Ilimitado $4,800 *más IVA al año, por sucursal Sin contar timbres durante su vigencia

Webhooks

Verifica la firma de cada webhook

Cada evento llega con la cabecera MiCFDI-Firma: t=…,v1=…, donde v1 es el HMAC-SHA256 en hexadecimal de t + "." + cuerpo con tu secreto whsec_…. Usa el cuerpo tal como llegó.

C#

using System.Security.Cryptography;
using System.Text;

static bool FirmaValida(string cabecera, string secreto, string cuerpo, TimeSpan tolerancia)
{
    string? t = null;
    var firmas = new List<string>();
    foreach (string parte in cabecera.Split(','))
    {
        string[] kv = parte.Trim().Split('=', 2);
        if (kv.Length != 2) continue;
        if (kv[0] == "t") t = kv[1];
        else if (kv[0] == "v1") firmas.Add(kv[1]);
    }
    if (t is null || !long.TryParse(t, out long marca)) return false;
    // Anti-repetición: rechaza firmas viejas.
    if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - marca) > tolerancia.TotalSeconds) return false;

    byte[] hmac = HMACSHA256.HashData(Encoding.UTF8.GetBytes(secreto), Encoding.UTF8.GetBytes(t + "." + cuerpo));
    byte[] esperada = Encoding.ASCII.GetBytes(Convert.ToHexString(hmac).ToLowerInvariant());
    return firmas.Any(f => CryptographicOperations.FixedTimeEquals(esperada, Encoding.ASCII.GetBytes(f.ToLowerInvariant())));
}

// ASP.NET Core: lee el cuerpo tal como llegó (no lo vuelvas a serializar).
app.MapPost("/webhooks/micfdi", async (HttpRequest req) =>
{
    using var lector = new StreamReader(req.Body, Encoding.UTF8);
    string cuerpo = await lector.ReadToEndAsync();
    if (!FirmaValida(req.Headers["MiCFDI-Firma"].ToString(), Environment.GetEnvironmentVariable("MICFDI_WHSEC")!, cuerpo, TimeSpan.FromMinutes(5)))
        return Results.Unauthorized();
    using var evento = System.Text.Json.JsonDocument.Parse(cuerpo);
    // Nunca proceses un evento de pruebas como real.
    if (evento.RootElement.GetProperty("modo").GetString() != "live") return Results.Ok();
    // … procesa el evento (idempotente por "id") …
    return Results.Ok();
});

Node.js

// Node.js 18 o más reciente, con Express. express.raw entrega el cuerpo tal como llegó.
import crypto from 'node:crypto';
import express from 'express';

function firmaValida(cabecera, secreto, cuerpo, toleranciaSegundos = 300) {
  let t = null;
  const firmas = [];
  for (const parte of String(cabecera || '').split(',')) {
    const [clave, valor] = parte.trim().split('=');
    if (clave === 't') t = valor;
    else if (clave === 'v1' && valor) firmas.push(valor.toLowerCase());
  }
  // Anti-repetición: rechaza firmas viejas.
  if (!/^\d+$/.test(t || '') || Math.abs(Date.now() / 1000 - Number(t)) > toleranciaSegundos) return false;
  const esperada = crypto.createHmac('sha256', secreto).update(`${t}.${cuerpo}`).digest('hex');
  return firmas.some(f => f.length === esperada.length &&
    crypto.timingSafeEqual(Buffer.from(f), Buffer.from(esperada)));
}

const app = express();
app.post('/webhooks/micfdi', express.raw({ type: 'application/json' }), (req, res) => {
  const cuerpo = req.body.toString('utf8');
  if (!firmaValida(req.get('MiCFDI-Firma'), process.env.MICFDI_WHSEC, cuerpo)) return res.sendStatus(401);
  const evento = JSON.parse(cuerpo);
  // Nunca proceses un evento de pruebas como real.
  if (evento.modo !== 'live') return res.sendStatus(200);
  // … procesa el evento (idempotente por evento.id) …
  res.sendStatus(200);
});

PHP

<?php
// PHP 7.4 o más reciente. Usa el cuerpo tal como llegó (php://input): no lo vuelvas a serializar.
function firmaValida(string $cabecera, string $secreto, string $cuerpo, int $tolerancia = 300): bool
{
    $t = null;
    $firmas = [];
    foreach (explode(',', $cabecera) as $parte) {
        $kv = explode('=', trim($parte), 2);
        if (count($kv) !== 2) continue;
        if ($kv[0] === 't') $t = $kv[1];
        elseif ($kv[0] === 'v1') $firmas[] = strtolower($kv[1]);
    }
    // Anti-repetición: rechaza firmas viejas.
    if ($t === null || !ctype_digit($t) || abs(time() - (int) $t) > $tolerancia) return false;
    $esperada = hash_hmac('sha256', $t . '.' . $cuerpo, $secreto);
    foreach ($firmas as $f) {
        if (hash_equals($esperada, $f)) return true;
    }
    return false;
}

$cuerpo = file_get_contents('php://input');
if (!firmaValida($_SERVER['HTTP_MICFDI_FIRMA'] ?? '', getenv('MICFDI_WHSEC'), $cuerpo)) {
    http_response_code(401);
    exit;
}
$evento = json_decode($cuerpo, true);
// Nunca proceses un evento de pruebas como real.
if (($evento['modo'] ?? '') !== 'live') { http_response_code(200); exit; }
// … procesa el evento (idempotente por $evento['id']) …
http_response_code(200);

Errores

Los errores que más vas a ver

Todo error es application/problem+json con un codigo estable. La lista completa está en la documentación.

StatusCódigoQué hacer
401llave_invalidaRevisa la llave y la cabecera Authorization: Bearer.
402sin_timbresEl emisor debe comprar timbres. La Idempotency-Key queda libre.
409solicitud_en_procesoEspera lo que diga Retry-After y repite con la misma Idempotency-Key.
422validacionCorrige los campos que vienen en «errores». La Idempotency-Key queda libre.
422comprobante_rechazadoEl SAT o el servicio de timbrado lo rechazó (el detail trae el código, p. ej. CFDI40145). Corrige y usa otra llave.
422sin_csd_vigente · sin_foliosConfiguración del emisor: súbela o amplíala en el panel.
429demasiadas_peticionesEspera lo que diga Retry-After.
503ocupado · sat_no_disponibleReintenta más tarde; al emitir, con la misma Idempotency-Key.

Cómo empezar

De la prueba a producción en tres pasos

  1. Pide tus llaves

    Ligamos tu integración a la empresa emisora y te damos una llave de pruebas y una de producción. Te respondemos en menos de un día hábil.

  2. Prueba sin timbrar

    Con mcfdi_test_ validas tus datos, el cálculo de impuestos y tus webhooks sin gastar timbres.

  3. Pasa a producción

    Cambia a tu llave mcfdi_live_ y timbra CFDI reales. Revisa el estado del servicio cuando lo necesites.

Pide tus llaves