Ir al contenido

FISCALISTO / API

SDKs: TypeScript, Python, PHP, .NET, Go y Java

Timbre desde Node, Bun, Deno, Workers, Python, PHP, .NET, Go o Java con tipos para cada campo, reintentos seguros y webhooks verificados en una línea.

Los seis SDKs son la misma API con los nombres de su lenguaje: se generan del mismo documento OpenAPI, así que tienen cada ruta y cada campo, con su descripción y el nombre que lleva en el XML del SAT. Ninguno tiene dependencias, salvo Jackson en Java.

SDKFunciona enInstalar
TypeScriptNode 20 o más reciente, Bun, Deno, Cloudflare Workersnpm install fiscalisto
PythonPython 3.10 o más recientepip install fiscalisto
PHPPHP 8.1 o más reciente, con curl y jsoncomposer require fiscalisto/fiscalisto
.NET (C#).NET 8 o más recientedotnet add package Fiscalisto
GoGo 1.23 o más recientego get fiscalisto.com/go
JavaJava 17 o más reciente (también Kotlin)Maven com.fiscalisto:fiscalisto
import { Fiscalisto } from "fiscalisto";

const fiscalisto = new Fiscalisto(); // la llave viene de FISCALISTO_API_KEY

const invoice = await fiscalisto.invoices.create(
  {
    customer: { tax_id: "ICV060329BY0", legal_name: "INMOBILIARIA CVA", tax_system: "601", zip: "33826" },
    payment_form: "03",
    items: [{ description: "Mensualidad", product_key: "86121700", quantity: 2, unit_price: 2500 }],
  },
  { idempotencyKey: "pedido-A-1042" },
);

console.log(invoice.uuid, invoice.total);

También puede pasar la llave al crear el cliente: new Fiscalisto("fk_test_…"), Fiscalisto("fk_test_…"), new Fiscalisto('fk_test_…'), new FiscalistoClient("fk_test_…") (en C# y en Java) o fiscalisto.NewClient("fk_test_…"). Con una llave fk_test_ todo se timbra en el ambiente de pruebas del SAT; con una fk_live_, en producción (vea Pruebas y producción).

Cada ruta, un método

Los métodos llevan el nombre del recurso y lo que hacen. Los campos son los mismos de la API, en inglés y con guion bajo, así que la referencia vale igual: cada página trae el ejemplo en los seis lenguajes.

RutaTypeScriptPythonPHP
POST /v1/invoicesinvoices.create({…})invoices.create({…})invoices->create([…])
GET /v1/invoices/{id}invoices.get("inv_…")invoices.get("inv_…")invoices->get('inv_…')
GET /v1/invoices/{id}/pdfinvoices.pdf(id): Uint8Arrayinvoices.pdf(id): bytesinvoices->pdf($id): string
GET /v1/invoices/{id}/xmlinvoices.xml(id)invoices.xml(id)invoices->xml($id)
POST /v1/invoices/{id}/cancelinvoices.cancel(id, { motive: "02" })invoices.cancel(id, {"motive": "02"})invoices->cancel($id, ['motive' => '02'])
POST /v1/paymentspayments.create({…})payments.create({…})payments->create([…])
POST /v1/credit_notescreditNotes.create({…})credit_notes.create({…})creditNotes->create([…])
POST /v1/invoice_batchesinvoiceBatches.create({…})invoice_batches.create({…})invoiceBatches->create([…])
GET /v1/sat_catalogs/{name}satCatalogs.get("product_keys", { q })sat_catalogs.get("product_keys", {"q": q})satCatalogs->get('product_keys', ['q' => $q])
POST /v1/webhook_endpoints/{id}/roll_secretwebhookEndpoints.rollSecret(id)webhook_endpoints.roll_secret(id)webhookEndpoints->rollSecret($id)

En Python los recursos y métodos van con guion bajo (credit_notes, roll_secret); en TypeScript y PHP, como creditNotes y rollSecret; en C#, con mayúscula y Async (fiscalisto.CreditNotes.CreateAsync(…), fiscalisto.WebhookEndpoints.RollSecretAsync(id)), y los campos como propiedades (TaxId, UnitPrice). En TypeScript cada respuesta tiene su tipo (Invoice, Customer…); en Python es un TypedDict que su editor y mypy revisan; en PHP es un arreglo con su forma documentada para PHPStan y Psalm; en C# es una clase (Invoice, Customer…) con la descripción de cada campo en su editor, y lo que manda también (InvoiceCreate, CancelInvoiceParams…), con required en lo obligatorio para que el compilador le avise.

En Go, cada recurso es un campo del cliente (client.Invoices.Create(ctx, …), client.WebhookEndpoints.RollSecret(ctx, id, nil)), lo que manda es un struct (InvoiceCreate, CancelInvoiceParams…) y lo opcional es un apuntador: fiscalisto.String("PUE"), fiscalisto.Float64(2500), fiscalisto.Int64(10). Un cliente es un id o sus datos: fiscalisto.IDOf[fiscalisto.InvoiceCreateCustomer]("cus_…") o fiscalisto.ObjectOf(fiscalisto.InvoiceCreateCustomer{…}). En Java, cada recurso es un método (fiscalisto.invoices().create(…), fiscalisto.webhookEndpoints().rollSecret(id)) y cada clase tiene setters encadenados (new InvoiceCreate().setPaymentForm("03")); los importes son BigDecimal y los setters aceptan también un número.

import { writeFile } from "node:fs/promises";

await writeFile("factura.pdf", await fiscalisto.invoices.pdf(invoice.id));

Idempotency-Key sin pensarlo

Lo que timbra lleva siempre su Idempotency-Key. Si no pasa una, el SDK crea una por llamada y la repite en sus reintentos, así que un corte de red nunca timbra dos veces. Pase la suya (idempotencyKey, idempotency_key=, 'idempotency_key' =>, new RequestOptions { IdempotencyKey = … }, fiscalisto.WithIdempotencyKey(…) o RequestOptions.idempotencyKey(…)) cuando quiera que repetir la llamada desde su sistema, aunque se haya reiniciado, conteste la misma factura. Vea Idempotencia.

Reintentos seguros

Cada SDK manda una Idempotency-Key nueva en cada POST (la misma en sus reintentos) y reintenta dos veces, con una espera que crece (medio segundo, uno, dos… hasta ocho), solo lo que no puede hacer nada dos veces:

  • Los 429 (respeta Retry-After hasta 60 segundos) y las solicitudes con la misma Idempotency-Key aún en proceso (409 request_in_progress).
  • Los errores de red y los 5xx: las lecturas y los borrados no cambian nada al repetirse, y un POST con la misma llave contesta lo de la primera vez.

Si usted pasa su propia llave (el id de su pedido), sus reintentos también quedan protegidos aunque su proceso se reinicie. Una solicitud propia sin llave, con el método genérico de solicitudes, no se repite si pudo haber llegado. Cambie cuántos reintentos y cuánto esperar cada respuesta, para todo el cliente o en una llamada:

const fiscalisto = new Fiscalisto(undefined, { maxRetries: 4, timeout: 30_000 }); // milisegundos

await fiscalisto.invoices.get("inv_…", { maxRetries: 0 });

En .NET puede pasar su propio HttpClient (por ejemplo de IHttpClientFactory) en FiscalistoClientOptions.HttpClient; use un solo FiscalistoClient en toda la aplicación (regístrelo como singleton). En Go pase su propio *http.Client con fiscalisto.WithHTTPClient(…) y en Java su HttpClient con ClientOptions.builder().httpClient(…); los clientes de Go y de Java también se comparten entre hilos y goroutines.

Por omisión cada respuesta se espera 80 segundos: timbrar puede tardar lo que tarde el PAC.

Listas

Recorrer la lista le da todos los objetos, página por página, sin que usted maneje starting_after. Para una sola página, lea data y has_more:

for await (const invoice of fiscalisto.invoices.list({ status: "valid", created_gte: "2026-10-01", limit: 100 })) {
  console.log(invoice.uuid, invoice.total);
}

const page = await fiscalisto.customers.list({ limit: 10 });
console.log(page.data.length, page.has_more);

Errores

Un error de la API lanza FiscalistoError (en PHP, C# y Java, FiscalistoException; en Go la llamada devuelve un *fiscalisto.Error) con los mismos datos del cuerpo de error. Decida con el código; muestre el mensaje. Cada código está en ErrorCode (ErrorCode.InvoiceRejected, ErrorCode.INVOICE_REJECTED en Python, ErrorCode::INVOICE_REJECTED en PHP, ErrorCode.INVOICE_REJECTED en Java, fiscalisto.CodeInvoiceRejected en Go), con su descripción en su editor: un código mal escrito no compila.

DatoTypeScriptPythonPHPC#GoJava
Tipo (invalid_request_error, invoice_error…)error.typee.type$e->typee.Typee.Typee.getType()
Código (invoice_rejected…)error.codee.code$e->errorCodee.Codee.Codee.getCode()
Qué corregirerror.messagee.message$e->getMessage()e.Messagee.Messagee.getMessage()
Campo con el problemaerror.parame.param$e->parame.Parame.Parame.getParam()
Código del SAT (CFDI40145…)error.satCodee.sat_code$e->satCodee.SatCodee.SatCodee.getSatCode()
El comprobante que quedóerror.invoicee.invoice$e->invoicee.Invoicee.Invoicee.getInvoice()
Estado HTTPerror.statuse.status$e->statuse.StatusCodee.StatusCodee.getStatusCode()
Id de la solicitud, para soporteerror.requestIde.request_id$e->requestIde.RequestIde.RequestIDe.getRequestId()
import { ErrorCode, Fiscalisto, FiscalistoError } from "fiscalisto";

try {
  await fiscalisto.invoices.create(datos, { idempotencyKey: pedido.id });
} catch (error) {
  if (error instanceof FiscalistoError && error.code === ErrorCode.InvoiceRejected) {
    // El SAT no la aceptó: error.message dice qué corregir, error.param en qué campo.
    await avisarAlCliente(error.message);
  } else {
    throw error;
  }
}

Si la API no contesta, el tipo es connection_error (código timeout o connection_failed). Con lo que timbra, repita la llamada con la misma Idempotency-Key para saber si se timbró. Si lo que contestó no es la API (la página de un proxy o del portal de una red), el tipo es api_error con el código invalid_response.

Webhooks

Verificar un webhook no necesita la llave de la API, solo el secreto del endpoint (whsec_…): verifyWebhook (TypeScript), verify_webhook (Python), Webhook::verify (PHP), FiscalistoClient.Webhooks.Verify (C#), fiscalisto.VerifyWebhook (Go) y Webhooks.verify (Java) revisan la firma y la hora y le devuelven el evento: según type, data.object es una factura, una constancia de retenciones o un lote. Pase el cuerpo tal como llegó, sin interpretarlo. Si la firma no es válida o el evento tiene más de 5 minutos, lanza FiscalistoWebhookError (en PHP, WebhookException; en C#, FiscalistoWebhookException; en Java, Webhooks.VerificationException; en Go devuelve un error con errors.Is(err, fiscalisto.ErrWebhookSignature)): conteste 400.

// Next.js (app/webhooks/fiscalisto/route.ts); en Bun, Deno, Hono o Workers es igual.
import { FiscalistoWebhookError, verifyWebhook } from "fiscalisto";

export async function POST(request: Request) {
  try {
    const event = await verifyWebhook(
      await request.text(),
      request.headers,
      process.env.FISCALISTO_WEBHOOK_SECRET as string,
    );
    if (event.type === "invoice.stamped") await guardar(event.data.object.uuid);
    return new Response(null, { status: 200 });
  } catch (error) {
    if (error instanceof FiscalistoWebhookError) return new Response(null, { status: 400 });
    throw error;
  }
}

Con un cliente ya creado, fiscalisto.webhooks.verify(…) ($fiscalisto->webhooks->verify(…) en PHP) hace lo mismo. Durante un cambio de secreto llegan dos firmas y basta con que una coincida. Vea Webhooks.

Proyectos de inicio

Una app pequeña que ya timbra, para copiar lo que necesite: un formulario que timbra una factura de prueba con su Idempotency-Key, la descarga del PDF y el XML, el manejo de un rechazo del SAT y los webhooks con la firma verificada. Ninguna necesita base de datos.

ProyectoDescargarCorrerlo
Next.js (App Router)nextjs.zipnpm install && npm run dev
Laravellaravel.zipcomposer install && php artisan serve
Djangodjango.zippip install -r requirements.txt && python manage.py runserver
ASP.NET Coreaspnet.zipdotnet run

Ponga su llave en FISCALISTO_API_KEY (cada uno trae su .env.example y su README) y, para los webhooks en su computadora, corra npx fiscalisto listen --forward-to http://localhost:…/webhooks/fiscalisto (vea CLI).

Postman, Bruno e Insomnia

Para probar sin escribir código, importe la colección de la API: una carpeta por recurso, cada ruta con su ejemplo y una Idempotency-Key nueva en lo que timbra. Postman la abre directo; en Bruno use Import Collection › Postman Collection, y en Insomnia Import. La llave va en la variable apiKey de la colección. Al crear una llave de pruebas en Desarrolladores › Llaves, descárguela con su llave ya puesta.

Otros lenguajes

Para Ruby, Rust o cualquier otro, llame a la API con su cliente HTTP (cada página de la referencia trae el ejemplo con curl y fetch) o genere un cliente con el documento OpenAPI.