Webhooks

Los webhooks avisan a tu sistema cuando una factura avanza en su cancelación o cuando cambia una descarga masiva, sin que consultes la API cada pocos minutos. Registras una URL https, eliges los tipos de evento y Fiscalapi le envía un POST firmado cada vez que ocurre uno.

Cómo funciona

  1. Registras un webhook: la URL que recibe los eventos, los tipos que suscribes y, si quieres, las personas cuyos eventos recibe. Guardas su secreto de firma.
  2. Un recurso cambia de estado, por ejemplo una factura pasa de Cancelando a Cancelado, y Fiscalapi registra un evento.
  3. Fiscalapi envía el evento por POST a cada webhook habilitado que lo suscribe, firmado con el secreto de ese webhook.
  4. Tu receptor verifica la firma, responde 2xx y procesa el evento después. Si no responde 2xx, Fiscalapi reintenta durante unas 75 horas.

El evento solo dice qué recurso cambió y a qué estado llegó; no lleva datos fiscales. Para el detalle, consulta el recurso con la URL que viaja en el evento.

Los webhooks están incluidos en todas las cuentas. Administrarlos requiere el permiso Webhooks.Manage, que tienen el owner y los miembros de la cuenta. La API los acepta con tu API key o con la sesión del dashboard.


Tipos de evento

Cada tipo nombra el estado al que llegó el recurso, con la forma recurso.estado. Fiscalapi emite un evento por transición.

TipoRecursoCuándo ocurre
invoice.cancellation_requestedFacturaEl PAC aceptó la solicitud de cancelación: la factura pasa de Vigente a Cancelando.
invoice.canceledFacturaEl SAT confirmó la cancelación: la factura pasa de Cancelando a Cancelado.
invoice.cancellation_rejectedFacturaLa cancelación se rechazó o no procede: la factura vuelve de Cancelando a Vigente.
download_request.completedSolicitud de descargaLa solicitud terminó y sus paquetes ya se pueden descargar.
download_request.abandonedSolicitud de descargaFiscalapi abandonó la solicitud.
download_rule.suspendedRegla de descargaFiscalapi suspendió la regla, por ejemplo por una e.firma vencida o no cargada.
webhook.testWebhookProbaste el webhook. Solo le llega al webhook que probaste y no se suscribe.

download_request.abandoned no implica un error: el motivo está en el GET de la solicitud. Una causa común es que el SAT no encuentre documentos que cumplan los criterios. Consulta el detalle con Obtener solicitud por ID y el ciclo de vida de las solicitudes.

Ignora los tipos de evento que no conozcas: el catálogo crecerá sin cambiar el sobre. Un tipo nuevo llega con la misma forma; responde 2xx y descártalo.

Códigos de estado

Los campos status y previousStatus llevan los códigos públicos de la API v4, los mismos que devuelve el GET del recurso:

RecursoCódigoEstado
Factura"V"Vigente
Factura"C"Cancelando: el SAT todavía no confirma la cancelación.
Factura"X"Cancelado
Solicitud de descarga"1"Esperando SAT
Solicitud de descarga"2"Esperando API
Solicitud de descarga"3"Completada
Solicitud de descarga"-1"Abandonada
Regla de descarga"1"Pendiente
Regla de descarga"2"Aprobada
Regla de descarga"3"Rechazada
Regla de descarga"4"Suspendida
Regla de descarga"-1"Abandonada

El estado anterior depende del camino que siguió el recurso. Por ejemplo, una solicitud se abandona desde Esperando SAT o desde Esperando API, y la solicitud simulada del ambiente de pruebas llega a Completada sin estado anterior. Trata ambos campos como texto que puede ser nulo.


El evento

Todos los eventos tienen la misma forma, el sobre. No hay campos propios de un tipo: entre tipos solo cambian los valores.

El cuerpo del POST es el sobre en JSON compacto y UTF-8. Los campos que no aplican viajan como null; nunca se omiten.

Propiedades

  • Name
    id
    Type
    string?
    Description

    Id del evento. Es el mismo valor del header webhook-id y no cambia entre reintentos ni reenvíos.

  • Name
    type
    Type
    string?
    Description

    Tipo de evento. Ver Tipos de evento. Trátalo como texto libre: pueden aparecer tipos nuevos.

  • Name
    createdAt
    Type
    string?
    Description

    Momento en que ocurrió el cambio, en ISO 8601 y UTC, al segundo y con Z final. No depende del header X-TIME-ZONE.

  • Name
    personId
    Type
    string?
    Description

    Persona dueña del recurso: el emisor de la factura o la persona de la regla de descarga. Puede ser null.

  • Name
    data
    Type
    object?

    El recurso que cambió y sus estados.

  • Name
    relatedObject
    Type
    object?

    El recurso al que se refiere el evento.

  • Name
    id
    Type
    string?
    Description

    Id del recurso, el mismo que usas en la API.

  • Name
    type
    Type
    string?
    expandible
    Description

    Tipo de recurso. Trátalo como texto libre.

    Type
    enum:
    Values
    "invoice""download_request""download_rule"
  • Name
    url
    Type
    string?
    Description

    Ruta relativa del GET v4 del recurso, por ejemplo /api/v4/invoices/ seguida del id. Antepón la URL base de tu ambiente para consultarlo.

  • Name
    status
    Type
    string?
    Description

    Código del estado al que llegó el recurso, o null. Ver Códigos de estado.

  • Name
    previousStatus
    Type
    string?
    Description

    Código del estado anterior, o null si no aplica.

Eventos

{
  "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "type": "invoice.canceled",
  "createdAt": "2026-10-12T18:47:53Z",
  "personId": "4f6a2c1e-8b3d-4e5f-9a7b-1c2d3e4f5a6b",
  "data": {
    "relatedObject": {
      "id": "0b6c1f2e-7d8a-4b9c-8e1f-2a3b4c5d6e7f",
      "type": "invoice",
      "url": "/api/v4/invoices/0b6c1f2e-7d8a-4b9c-8e1f-2a3b4c5d6e7f"
    },
    "status": "X",
    "previousStatus": "C"
  }
}

Los ejemplos están formateados para leerse; el cuerpo real llega en una sola línea, sin espacios. Como la URL es relativa, el mismo evento es idéntico en pruebas y en producción. La URL base es https://test.fiscalapi.com en pruebas y https://live.fiscalapi.com en producción.

personId puede ser nulo (factura emitida por valor) y entonces no llega a webhooks con filtro de personas. Una factura por valores no tiene una persona emisora registrada en tu cuenta, así que su evento solo lo reciben los webhooks sin filtro de personas. Si emites por valores y quieres esos eventos, registra un webhook sin filtro.


Registrar un webhook

Un webhook pertenece a tu cuenta y recibe los eventos de todas sus personas, salvo que lo limites a algunas. Puedes tener hasta 16 webhooks de URL por cuenta.

DatoRegla
URLhttps, absoluta, sin usuario ni contraseña y de hasta 2048 caracteres. No puede apuntar a una dirección privada, local o reservada, ni a localhost.
DescripciónOpcional, de hasta 256 caracteres. Te ayuda a reconocer el webhook en el dashboard y en los avisos.
Tipos de eventoAl menos uno, o solo el comodín "*" para recibir todos, también los que se agreguen después. El comodín no se combina con otros tipos.
PersonasOpcional, hasta 100. Sin personas, el webhook recibe los eventos de todas. Una persona que no es de tu cuenta se rechaza.

Para recibir eventos en tu computadora, usa el CLI: ver Probar tu receptor.

Desde el dashboard

Ve a Developers > Webhooks y presiona Agregar webhook. Captura la URL, la descripción, los eventos (Todos o los tipos que elijas) y, si quieres, las personas. Al guardar, el dashboard muestra el secreto de firma con el aviso "Guarda este secreto de firma. Lo usarás para verificar que los eventos vienen de FiscalAPI." Presiona Copiar y guárdalo en la configuración segura de tu receptor.

La página Webhooks tiene estas acciones:

AcciónDóndeQué hace
Editar, Deshabilitar, Habilitar y EliminarMenú Acciones del webhookCambian el webhook. Ver Reemplazar webhook.
Revelar secretoMenú Acciones del webhookMuestra el secreto vigente sin cambiarlo. Cada revelación queda registrada.
Rotar secretoMenú Acciones del webhookPide confirmación y muestra el secreto nuevo. Ver Rotar el secreto.
EntregasTabla Entregas, al seleccionar un webhookEl log de sus entregas: Evento, Tipo, Estado, Intentos, Último código, Motivo, Latencia, Extracto de la respuesta y Próximo intento. Recargar lo actualiza.
ReenviarMenú Acciones de una entregaReenvía su evento al webhook seleccionado, en un solo intento.
Enviar evento de pruebaBarra de la tabla EntregasManda un evento de prueba al webhook seleccionado. Ver Probar tu receptor.
EliminarTabla Escuchas de CLILas escuchas de fiscalapi listen se ven aquí y son de solo lectura: solo se eliminan.

Reenviar y Enviar evento de prueba solo aparecen con el webhook habilitado. Revelar secreto y Rotar secreto no aparecen mientras alguien de soporte accede a tu cuenta en tu nombre.

Con la API

Da de alta el webhook con Crear webhook. La respuesta trae el secreto en secret; con la API key solo lo vuelves a obtener al rotarlo, así que guárdalo al recibirla. Después, pruébalo con Probar webhook y revisa el resultado en Listar entregas.

El secreto es el prefijo whsec_ seguido de 44 caracteres en base64: 32 bytes aleatorios, con un signo igual al final. Guárdalo completo, con su prefijo.


Verificar la firma

Cada entrega sigue la especificación abierta Standard Webhooks y lleva tres headers:

HeaderContenido
webhook-idId del evento, igual al id del sobre y el mismo en cada reintento.
webhook-timestampMomento del envío de ese intento, en segundos Unix.
webhook-signatureUna o más firmas separadas por un espacio, cada una con el formato v1, coma y la firma en base64.

La firma es la única prueba de autenticidad de un evento. Fiscalapi no publica las direcciones IP de sus servidores ni recomienda filtrar las entregas por IP: esas direcciones pueden cambiar sin aviso. Rechaza toda entrega cuya firma no coincida con la que calculas.

La firma es un HMAC-SHA256 en base64 sobre el texto {id}.{timestamp}.{body}: el id del evento, un punto, el timestamp, otro punto y el cuerpo crudo, tal como llegan. La llave del HMAC es el secreto sin el prefijo whsec_, decodificado de base64.

Para verificar una entrega:

  1. Lee el cuerpo crudo, exactamente los bytes que llegaron, antes de que tu framework lo convierta en un objeto.
  2. Rechaza la entrega si el timestamp se aleja más de 5 minutos de tu reloj. Así nadie repite después una entrega capturada.
  3. Calcula la firma con cada secreto vigente. Durante una rotación tienes dos.
  4. Acepta la entrega si alguna firma v1 del header coincide con la tuya. Compara en tiempo constante, nunca con una comparación de texto normal.
  5. Solo entonces convierte el cuerpo en JSON y procésalo.

Verifica sobre el cuerpo crudo. Si tu framework convierte el JSON en un objeto y lo vuelves a serializar, el texto cambia (espacios, orden o escapes) y la firma no coincide aunque el evento sea legítimo.

Con el SDK

Los SDK oficiales hacen estos pasos por ti. Su función de verificación recibe los headers de la entrega, el cuerpo crudo y tu secreto, o una lista de secretos durante una rotación, y devuelve el evento ya verificado:

Recibir un evento

using System.Text;
using System.Threading.Channels;
using Fiscalapi.Models;
using Fiscalapi.Webhooks;

// ASP.NET Core. El secreto vigente y, durante una rotación, también el anterior.
var secrets = builder.Configuration.GetSection("FiscalApi:WebhookSecrets").Get<string[]>()!;
var events = Channel.CreateUnbounded<WebhookEvent>(); // ilustrativa: en producción, una cola durable

app.MapPost("/webhooks/fiscalapi", async (HttpRequest request) =>
{
    // El cuerpo crudo, tal como llegó.
    using var reader = new StreamReader(request.Body, Encoding.UTF8);
    var body = await reader.ReadToEndAsync();
    var headers = request.Headers.ToDictionary(h => h.Key, h => h.Value.ToString());

    WebhookEvent webhookEvent;
    try
    {
        webhookEvent = Webhook.Verify(headers, body, secrets);
    }
    catch (WebhookVerificationException)
    {
        return Results.BadRequest(); // firma inválida, header faltante o timestamp fuera de tolerancia
    }

    await events.Writer.WriteAsync(webhookEvent); // procésalo después, fuera de la petición
    return Results.Ok();
});

La verificación compara en tiempo constante, acepta cualquier firma v1 válida del header y rechaza un timestamp con más de 5 minutos de diferencia. Si la entrega no verifica, lanza la excepción de verificación: responde 400 y no la proceses. Un secreto mal copiado o una lista vacía lanzan un error de configuración, que nunca incluye el valor del secreto.

SDKFunciónEntrega que no verificaSecreto mal configurado
.NETWebhook.VerifyWebhookVerificationExceptionArgumentException
Node.jsWebhook.verifyWebhookVerificationErrorTypeError
Pythonfiscalapi.webhooks.verifyWebhookVerificationExceptionValueError
JavaWebhook.verifyWebhookVerificationExceptionIllegalArgumentException
PHPWebhook::verifyWebhookVerificationExceptionInvalidArgumentException

Las clases viven en Fiscalapi.Webhooks en .NET, en com.fiscalapi.webhooks en Java y en Fiscalapi\Webhooks en PHP. En Node.js las exporta el paquete fiscalapi.

A mano

Sin el SDK, cualquier librería de Standard Webhooks verifica las entregas de Fiscalapi con tu secreto tal cual, con su prefijo. También puedes implementarlo con la librería estándar de tu lenguaje:

Verificar la firma

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

public static class WebhookVerifier
{
    private const long ToleranceSeconds = 5 * 60;

    // id, timestamp y signature son los headers webhook-id, webhook-timestamp y webhook-signature.
    // rawBody es el cuerpo crudo, tal como llegó, sin deserializar.
    // secrets es una lista de secretos whsec_: el vigente y, durante una rotación, también el anterior.
    public static JsonDocument Verify(string id, string timestamp, string signature, byte[] rawBody,
        IEnumerable<string> secrets, DateTimeOffset? now = null)
    {
        var nowSeconds = (now ?? DateTimeOffset.UtcNow).ToUnixTimeSeconds();
        if (string.IsNullOrEmpty(id) || !long.TryParse(timestamp, out var seconds) ||
            seconds < nowSeconds - ToleranceSeconds || seconds > nowSeconds + ToleranceSeconds)
            throw new CryptographicException("Timestamp fuera de la tolerancia");

        var content = Encoding.UTF8.GetBytes($"{id}.{timestamp}.").Concat(rawBody).ToArray();
        var expected = secrets
            .Select(secret => Convert.FromBase64String(secret.StartsWith("whsec_") ? secret[6..] : secret))
            .Select(key => HMACSHA256.HashData(key, content))
            .ToList();

        foreach (var entry in (signature ?? "").Split(' '))
        {
            var parts = entry.Split(',', 2);
            var received = new byte[HMACSHA256.HashSizeInBytes];
            if (parts.Length == 2 && parts[0] == "v1" &&
                Convert.TryFromBase64String(parts[1], received, out var written) && written == received.Length &&
                expected.Any(e => CryptographicOperations.FixedTimeEquals(e, received)))
                return JsonDocument.Parse(rawBody);
        }

        throw new CryptographicException("Firma inválida");
    }
}

Prueba tu implementación con el vector oficial de Standard Webhooks. Con la hora actual igual al timestamp del vector, la firma verifica; si cambias un carácter del cuerpo, de la firma o del secreto, falla:

DatoValor
Secretowhsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw
webhook-idmsg_p5jXN8AQM9LWM0D4loKWxJek
webhook-timestamp1614265330
Cuerpo{"test": 2432232314}
webhook-signaturev1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=

Responder a una entrega

Cada entrega es un POST con Content-Type: application/json; charset=utf-8 y el User-Agent FiscalAPI-Webhooks/1. Fiscalapi espera la respuesta hasta 15 segundos en total, incluidos 5 para conectar, y guarda en el log el código y el primer KB de la respuesta.

Responde 2xx rápido y procesa después. Un 3xx cuenta como falla y un 410 deshabilita el webhook. Fiscalapi no sigue redirecciones: si cambias la URL, actualiza el webhook.

RespuestaQué hace Fiscalapi
2xxDa la entrega por recibida. El cuerpo de tu respuesta no importa.
3xxFalla: reintenta según el calendario.
410Falla y deshabilita el webhook de inmediato: no recibe más eventos hasta que lo habilites.
429 o 503 con Retry-AfterFalla: reintenta cuando toque en el calendario o cuando pediste, lo que sea más tarde, con un máximo de 24 horas.
Otro código, timeout o error de conexión o de TLSFalla: reintenta según el calendario.

Guarda el evento en una cola o una tabla y responde. Haz el trabajo pesado, como consultar el recurso o actualizar tu sistema, fuera de la petición: un receptor que tarda más de 15 segundos acumula reintentos de eventos que sí recibió.

Reintentos

Si una entrega falla, Fiscalapi la reintenta con este calendario, contado desde la falla anterior. Cada espera varía hasta 10 % para que las entregas pendientes no lleguen todas juntas.

ReintentoEspera
15 segundos; llega dentro del minuto siguiente.
25 minutos
330 minutos
42 horas
55 horas
610 horas
714 horas
820 horas
924 horas

Son 10 intentos en unas 75 horas: un receptor caído un fin de semana todavía recibe sus eventos. Cada intento lleva un webhook-timestamp nuevo y su propia firma, con el mismo webhook-id y el mismo cuerpo. Los reenvíos y las entregas de prueba son de un solo intento y no siguen este calendario.

La entrega es al menos una vez: tu receptor puede recibir el mismo evento más de una vez, por ejemplo si respondió 2xx pero la respuesta no llegó a Fiscalapi. Deduplica por webhook-id: guarda los ids que procesaste y, si llega uno conocido, responde 2xx sin repetir el trabajo.

El orden no está garantizado: un reintento puede llegar después de un evento más nuevo del mismo recurso. Para el estado autoritativo, haz GET de relatedObject.url con tu URL base y tus credenciales; la API siempre responde el estado actual.

Deshabilitación y avisos

Fiscalapi avisa por correo al owner de la cuenta cuando algo falla de forma persistente, y deshabilita los webhooks que no responden:

SituaciónQué ocurre
Una entrega agota sus reintentos.El owner recibe el correo "Tu webhook no pudo entregar un evento", como máximo uno cada 24 horas por webhook. Un reenvío o una prueba que fallan no lo generan.
Falla un reintento de una entrega y el webhook lleva 5 días o más sin responder ningún 2xx.Fiscalapi lo deshabilita en esa falla y el owner recibe el correo "Deshabilitamos tu webhook". Sin entregas pendientes, el webhook no se deshabilita solo.
El receptor responde 410.Fiscalapi lo deshabilita de inmediato y el owner recibe el correo "Deshabilitamos tu webhook".
Tú lo deshabilitas.Deja de recibir eventos. No hay correo.

El dashboard muestra el motivo de un webhook deshabilitado: Por ti, El webhook respondió 410 o 5 días sin respuestas exitosas. En la API, el motivo viaja en disabledReason.

Los eventos generados mientras un webhook está deshabilitado no se le entregan, ni al habilitarlo de nuevo. Al habilitarlo, recupera lo que te faltó con Conciliar eventos.


Probar tu receptor

Envía un evento de prueba con Enviar evento de prueba en el dashboard o con Probar webhook. Llega un evento webhook.test, firmado como cualquier otro, solo al webhook que probaste. Sigue el resultado en el log de entregas.

Las entregas de prueba (las de este botón, las de Probar webhook y las de fiscalapi trigger) se comportan distinto de las demás:

ReglaDetalle
Un solo intentoSi fallan, no se reintentan ni generan el correo de aviso.
Su 2xx no cuenta para la salud del webhookUn webhook que solo responde a pruebas sigue acumulando sus 5 días sin respuestas exitosas. Tampoco cuenta el 2xx de un evento de prueba reenviado.
Su falla sí cuentaUna falla marca el webhook como fallando si no lo estaba, y un 410 lo deshabilita.

Probar en local

Fiscalapi no entrega a localhost. Para recibir eventos en tu computadora sin exponerla a internet, usa el CLI fiscalapi: lleva los eventos de tu cuenta a tu servidor local, dispara eventos de prueba y reenvía los que te faltaron. Cómo instalarlo y usarlo está en Probar webhooks en local.


Conciliar eventos

Fiscalapi conserva los eventos y sus entregas 30 días. Después de una caída de tu receptor, o al habilitar de nuevo un webhook, Listar eventos te devuelve cada sobre tal como se envió, del más nuevo al más viejo, con filtros por tipo, recurso y rango de fechas.

Compara los id con los que ya procesaste. Procesa los que falten desde el listado o pide que se entreguen de nuevo con Reenviar evento. Un reenvío es un solo intento y llega con el mismo webhook-id, así que tu deduplicación lo reconoce. Un evento de más de 30 días ya no se puede consultar ni reenviar.

Rotar el secreto

Rota el secreto si se expuso o como rutina de seguridad. En el dashboard, Rotar secreto pide esta confirmación: "Se generará un secreto nuevo. El actual seguirá firmando durante 24 horas para que actualices tu receptor sin perder eventos. ¿Continuar?". Con la API, llama a Rotar secreto.

Durante las 24 horas siguientes, cada entrega lleva dos firmas en webhook-signature, una con el secreto nuevo y otra con el anterior. Para cambiar de secreto sin perder eventos:

  1. Rota el secreto y guarda el nuevo.
  2. Configura tu receptor con los dos secretos; la verificación acepta la entrega si cualquiera de las firmas coincide.
  3. Cuando el secreto nuevo ya esté en tu receptor, quita el anterior. Pasadas 24 horas, el anterior deja de firmar.

Si rotas otra vez antes de 24 horas, el secreto anterior conserva su vencimiento original y solo se reemplaza el nuevo. Así un doble clic no deja sin secreto a un receptor que todavía no cambió el suyo.

Para ver el secreto vigente sin rotarlo, usa Revelar secreto en el dashboard. Solo existe en el dashboard, no con la API key, y cada revelación queda registrada. Mientras alguien de soporte accede a tu cuenta en tu nombre, ninguna respuesta incluye el secreto.

Buenas prácticas del receptor

PrácticaPor qué
Verifica la firma de cada entrega y rechaza las que no verifican.La firma es la única prueba de que el evento viene de Fiscalapi.
Verifica sobre el cuerpo crudo.Volver a serializar el JSON cambia el texto firmado.
Responde 2xx en cuanto guardes el evento.Un receptor lento provoca reintentos y, si pasa 5 días o más sin un 2xx, la deshabilitación del webhook en la siguiente falla de un reintento.
Deduplica por webhook-id.La entrega es al menos una vez.
Consulta relatedObject.url antes de actuar.El orden no está garantizado y la API tiene el estado actual.
Ignora los tipos que no conoces y responde 2xx.El catálogo crecerá sin cambiar el sobre.
Guarda el secreto como guardas tu API key, fuera del código.Quien tiene el secreto puede firmar eventos falsos.
Concilia con la lista de eventos después de una caída.Lo que se perdió está disponible 30 días.

Límite de solicitudes

Probar webhook, Reenviar evento y Disparar evento de prueba, que usa fiscalapi trigger, tienen un límite de solicitudes por API key, compartido con la creación de solicitudes de descarga masiva. El dashboard no tiene este límite. Cada respuesta trae tu consumo en los headers RateLimit-Limit, RateLimit-Remaining y RateLimit-Reset.

Si lo excedes, la API responde 429 con el header Retry-After, en segundos, y no ejecuta la operación. Espera ese tiempo y repite la solicitud. El cuerpo de esa respuesta está en Fechas y errores.

Recursos relacionados

¿Le resultó útil esta página?