# 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](https://api.fiscalisto.com/v1/openapi.json), 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.

| SDK | Funciona en | Instalar |
| --- | --- | --- |
| TypeScript | Node 20 o más reciente, Bun, Deno, Cloudflare Workers | `npm install fiscalisto` |
| Python | Python 3.10 o más reciente | `pip install fiscalisto` |
| PHP | PHP 8.1 o más reciente, con `curl` y `json` | `composer require fiscalisto/fiscalisto` |
| .NET (C#) | .NET 8 o más reciente | `dotnet add package Fiscalisto` |
| Go | Go 1.23 o más reciente | `go get fiscalisto.com/go` |
| Java | Java 17 o más reciente (también Kotlin) | Maven `com.fiscalisto:fiscalisto` |

```ts tab="TypeScript"
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);
```

```python tab="Python"
from fiscalisto import Fiscalisto

fiscalisto = Fiscalisto()  # la llave viene de FISCALISTO_API_KEY

invoice = 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}],
    },
    idempotency_key="pedido-A-1042",
)

print(invoice["uuid"], invoice["total"])
```

```php tab="PHP"
<?php
use Fiscalisto\Fiscalisto;

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

$invoice = $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]],
], ['idempotency_key' => 'pedido-A-1042']);

echo $invoice['uuid'], ' ', $invoice['total'];
```

```csharp tab="C#"
using Fiscalisto;

var fiscalisto = new FiscalistoClient(); // la llave viene de FISCALISTO_API_KEY

var invoice = await fiscalisto.Invoices.CreateAsync(new InvoiceCreate
{
    Customer = new InvoiceCreateCustomer { TaxId = "ICV060329BY0", LegalName = "INMOBILIARIA CVA", TaxSystem = "601", Zip = "33826" },
    PaymentForm = "03",
    Items = new() { new() { Description = "Mensualidad", ProductKey = "86121700", Quantity = 2, UnitPrice = 2500 } },
}, new RequestOptions { IdempotencyKey = "pedido-A-1042" });

Console.WriteLine($"{invoice.Uuid} {invoice.Total}");
```

```go tab="Go"
import fiscalisto "fiscalisto.com/go"

client, err := fiscalisto.NewClient("") // la llave viene de FISCALISTO_API_KEY

invoice, err := client.Invoices.Create(ctx, &fiscalisto.InvoiceCreate{
	Customer: fiscalisto.ObjectOf(fiscalisto.InvoiceCreateCustomer{
		TaxID: "ICV060329BY0", LegalName: "INMOBILIARIA CVA", TaxSystem: "601", Zip: "33826",
	}),
	PaymentForm: fiscalisto.String("03"),
	Items: []fiscalisto.InvoiceCreateItem{{
		Description: fiscalisto.String("Mensualidad"),
		ProductKey:  fiscalisto.String("86121700"),
		Quantity:    fiscalisto.Float64(2),
		UnitPrice:   fiscalisto.Float64(2500),
	}},
}, fiscalisto.WithIdempotencyKey("pedido-A-1042"))

fmt.Println(*invoice.UUID, *invoice.Total)
```

```java tab="Java"
import com.fiscalisto.*;
import com.fiscalisto.model.*;
import java.util.List;

var fiscalisto = new FiscalistoClient(); // la llave viene de FISCALISTO_API_KEY

var invoice = fiscalisto.invoices().create(new InvoiceCreate()
        .setCustomer(new InvoiceCreateCustomer()
                .setTaxId("ICV060329BY0").setLegalName("INMOBILIARIA CVA").setTaxSystem("601").setZip("33826"))
        .setPaymentForm("03")
        .setItems(List.of(new InvoiceCreateItem()
                .setDescription("Mensualidad").setProductKey("86121700").setQuantity(2).setUnitPrice(2500))),
        RequestOptions.idempotencyKey("pedido-A-1042"));

System.out.println(invoice.getUuid() + " " + invoice.getTotal());
```

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](https://fiscalisto.com/docs/api/pruebas-y-produccion)).

## 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](https://fiscalisto.com/docs/api/referencia/facturas/create-invoice) vale igual: cada página trae el ejemplo en los seis lenguajes.

| Ruta | TypeScript | Python | PHP |
| --- | --- | --- | --- |
| `POST /v1/invoices` | `invoices.create({…})` | `invoices.create({…})` | `invoices->create([…])` |
| `GET /v1/invoices/{id}` | `invoices.get("inv_…")` | `invoices.get("inv_…")` | `invoices->get('inv_…')` |
| `GET /v1/invoices/{id}/pdf` | `invoices.pdf(id)`: `Uint8Array` | `invoices.pdf(id)`: `bytes` | `invoices->pdf($id)`: `string` |
| `GET /v1/invoices/{id}/xml` | `invoices.xml(id)` | `invoices.xml(id)` | `invoices->xml($id)` |
| `POST /v1/invoices/{id}/cancel` | `invoices.cancel(id, { motive: "02" })` | `invoices.cancel(id, {"motive": "02"})` | `invoices->cancel($id, ['motive' => '02'])` |
| `POST /v1/payments` | `payments.create({…})` | `payments.create({…})` | `payments->create([…])` |
| `POST /v1/credit_notes` | `creditNotes.create({…})` | `credit_notes.create({…})` | `creditNotes->create([…])` |
| `POST /v1/invoice_batches` | `invoiceBatches.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_secret` | `webhookEndpoints.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.

```ts tab="TypeScript"
import { writeFile } from "node:fs/promises";

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

```python tab="Python"
from pathlib import Path

Path("factura.pdf").write_bytes(fiscalisto.invoices.pdf(invoice["id"]))
```

```php tab="PHP"
file_put_contents('factura.pdf', $fiscalisto->invoices->pdf($invoice['id']));
```

```csharp tab="C#"
await File.WriteAllBytesAsync("factura.pdf", await fiscalisto.Invoices.PdfAsync(invoice.Id));
```

```go tab="Go"
pdf, err := client.Invoices.PDF(ctx, invoice.ID, nil)
err = os.WriteFile("factura.pdf", pdf, 0o644)
```

```java tab="Java"
Files.write(Path.of("factura.pdf"), fiscalisto.invoices().pdf(invoice.getId()));
```

## 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](https://fiscalisto.com/docs/api/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:

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

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

```python tab="Python"
fiscalisto = Fiscalisto(max_retries=4, timeout=30)  # segundos

fiscalisto.invoices.get("inv_…", max_retries=0)
```

```php tab="PHP"
$fiscalisto = new Fiscalisto(null, ['max_retries' => 4, 'timeout' => 30]); // segundos

$fiscalisto->invoices->get('inv_…', ['max_retries' => 0]);
```

```csharp tab="C#"
var fiscalisto = new FiscalistoClient(options: new FiscalistoClientOptions { MaxRetries = 4, Timeout = TimeSpan.FromSeconds(30) });

await fiscalisto.Invoices.GetAsync("inv_…", options: new RequestOptions { MaxRetries = 0 });
```

```go tab="Go"
client, err := fiscalisto.NewClient("", fiscalisto.WithMaxRetries(4), fiscalisto.WithTimeout(30*time.Second))

invoice, err := client.Invoices.Get(ctx, "inv_…", nil, fiscalisto.WithRequestMaxRetries(0))
```

```java tab="Java"
var fiscalisto = new FiscalistoClient(ClientOptions.builder().maxRetries(4).timeout(Duration.ofSeconds(30)).build());

fiscalisto.invoices().get("inv_…", null, new RequestOptions().setMaxRetries(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`:

```ts tab="TypeScript"
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);
```

```python tab="Python"
for invoice in fiscalisto.invoices.list({"status": "valid", "created_gte": "2026-10-01", "limit": 100}):
    print(invoice["uuid"], invoice["total"])

page = fiscalisto.customers.list({"limit": 10})
print(len(page.data), page.has_more)
```

```php tab="PHP"
foreach ($fiscalisto->invoices->list(['status' => 'valid', 'created_gte' => '2026-10-01', 'limit' => 100]) as $invoice) {
    echo $invoice['uuid'], ' ', $invoice['total'], "\n";
}

$page = $fiscalisto->customers->list(['limit' => 10]);
echo count($page->data()), ' ', $page->hasMore() ? 'hay más' : 'es todo';
```

```csharp tab="C#"
await foreach (var invoice in fiscalisto.Invoices.ListAutoPagingAsync(new ListInvoicesParams { Status = "valid", CreatedGte = "2026-10-01", Limit = 100 }))
    Console.WriteLine($"{invoice.Uuid} {invoice.Total}");

var page = await fiscalisto.Customers.ListAsync(new ListCustomersParams { Limit = 10 });
Console.WriteLine($"{page.Data.Count} {page.HasMore}");
```

```go tab="Go"
params := &fiscalisto.ListInvoicesParams{Status: fiscalisto.String("valid"), CreatedGte: fiscalisto.String("2026-10-01"), Limit: fiscalisto.Int64(100)}
for invoice, err := range client.Invoices.ListAutoPaging(ctx, params) {
	if err != nil {
		return err
	}
	fmt.Println(*invoice.UUID, *invoice.Total)
}

page, err := client.Customers.List(ctx, &fiscalisto.ListCustomersParams{Limit: fiscalisto.Int64(10)})
fmt.Println(len(page.Data), page.HasMore) // page.Next(ctx) da la siguiente
```

```java tab="Java"
for (var invoice : fiscalisto.invoices().listAutoPaging(new ListInvoicesParams().setStatus("valid").setCreatedGte("2026-10-01").setLimit(100))) {
    System.out.println(invoice.getUuid() + " " + invoice.getTotal());
}

var page = fiscalisto.customers().list(new ListCustomersParams().setLimit(10));
System.out.println(page.getData().size() + " " + page.hasMore()); // page.next() da la siguiente
```

## 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](https://fiscalisto.com/docs/api/errores). 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.

| Dato | TypeScript | Python | PHP | C# | Go | Java |
| --- | --- | --- | --- | --- | --- | --- |
| Tipo (`invalid_request_error`, `invoice_error`…) | `error.type` | `e.type` | `$e->type` | `e.Type` | `e.Type` | `e.getType()` |
| Código (`invoice_rejected`…) | `error.code` | `e.code` | `$e->errorCode` | `e.Code` | `e.Code` | `e.getCode()` |
| Qué corregir | `error.message` | `e.message` | `$e->getMessage()` | `e.Message` | `e.Message` | `e.getMessage()` |
| Campo con el problema | `error.param` | `e.param` | `$e->param` | `e.Param` | `e.Param` | `e.getParam()` |
| Código del SAT (`CFDI40145`…) | `error.satCode` | `e.sat_code` | `$e->satCode` | `e.SatCode` | `e.SatCode` | `e.getSatCode()` |
| El comprobante que quedó | `error.invoice` | `e.invoice` | `$e->invoice` | `e.Invoice` | `e.Invoice` | `e.getInvoice()` |
| Estado HTTP | `error.status` | `e.status` | `$e->status` | `e.StatusCode` | `e.StatusCode` | `e.getStatusCode()` |
| Id de la solicitud, para soporte | `error.requestId` | `e.request_id` | `$e->requestId` | `e.RequestId` | `e.RequestID` | `e.getRequestId()` |

```ts tab="TypeScript"
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;
  }
}
```

```python tab="Python"
from fiscalisto import ErrorCode, FiscalistoError

try:
    fiscalisto.invoices.create(datos, idempotency_key=pedido.id)
except FiscalistoError as e:
    if e.code != ErrorCode.INVOICE_REJECTED:
        raise
    # El SAT no la aceptó: e.message dice qué corregir, e.param en qué campo.
    avisar_al_cliente(e.message)
```

```php tab="PHP"
use Fiscalisto\ErrorCode;
use Fiscalisto\FiscalistoException;

try {
    $fiscalisto->invoices->create($datos, ['idempotency_key' => $pedido->id]);
} catch (FiscalistoException $e) {
    if ($e->errorCode !== ErrorCode::INVOICE_REJECTED) {
        throw $e;
    }
    // El SAT no la aceptó: getMessage() dice qué corregir, $e->param en qué campo.
    avisarAlCliente($e->getMessage());
}
```

```csharp tab="C#"
try
{
    await fiscalisto.Invoices.CreateAsync(datos, new RequestOptions { IdempotencyKey = pedido.Id });
}
catch (FiscalistoException e) when (e.Code == ErrorCode.InvoiceRejected)
{
    // El SAT no la aceptó: e.Message dice qué corregir, e.Param en qué campo.
    await AvisarAlCliente(e.Message);
}
```

```go tab="Go"
_, err := client.Invoices.Create(ctx, datos, fiscalisto.WithIdempotencyKey(pedido.ID))
var e *fiscalisto.Error
if errors.As(err, &e) && e.Code == fiscalisto.CodeInvoiceRejected {
	// El SAT no la aceptó: e.Message dice qué corregir, e.Param en qué campo.
	avisarAlCliente(e.Message)
} else if err != nil {
	return err
}
```

```java tab="Java"
try {
    fiscalisto.invoices().create(datos, RequestOptions.idempotencyKey(pedido.getId()));
} catch (FiscalistoException e) {
    if (!ErrorCode.INVOICE_REJECTED.equals(e.getCode())) throw e;
    // El SAT no la aceptó: getMessage() dice qué corregir, getParam() en qué campo.
    avisarAlCliente(e.getMessage());
}
```

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`.

```ts tab="TypeScript"
// 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;
  }
}
```

```python tab="Python"
# Flask; en Django use request.body y request.headers.
import os
from flask import Flask, request
from fiscalisto import FiscalistoWebhookError, verify_webhook

app = Flask(__name__)

@app.post("/webhooks/fiscalisto")
def fiscalisto_webhook():
    try:
        event = verify_webhook(
            request.get_data(), request.headers, os.environ["FISCALISTO_WEBHOOK_SECRET"]
        )
    except FiscalistoWebhookError:
        return "", 400
    if event["type"] == "invoice.stamped":
        guardar(event["data"]["object"]["uuid"])
    return "", 200
```

```php tab="PHP"
<?php
// PHP puro; en Laravel use $request->getContent() y $request->headers->all().
use Fiscalisto\Webhook;
use Fiscalisto\WebhookException;

try {
    $event = Webhook::verify(
        file_get_contents('php://input'),
        getallheaders(),
        getenv('FISCALISTO_WEBHOOK_SECRET'),
    );
} catch (WebhookException $e) {
    http_response_code(400);
    exit;
}
if ($event['type'] === 'invoice.stamped') {
    guardar($event['data']['object']['uuid']);
}
http_response_code(200);
```

```csharp tab="C#"
// ASP.NET Core (minimal API).
using Fiscalisto;

app.MapPost("/webhooks/fiscalisto", async (HttpRequest request) =>
{
    var body = await new StreamReader(request.Body).ReadToEndAsync();
    try
    {
        var @event = FiscalistoClient.Webhooks.Verify(
            body,
            name => request.Headers[name],
            Environment.GetEnvironmentVariable("FISCALISTO_WEBHOOK_SECRET")!);
        if (@event.Type == "invoice.stamped") await Guardar(@event.DataObject<Invoice>().Uuid);
        return Results.Ok();
    }
    catch (FiscalistoWebhookException)
    {
        return Results.BadRequest();
    }
});
```

```go tab="Go"
// net/http; en Gin use c.Request, en Echo c.Request().
func fiscalistoWebhook(w http.ResponseWriter, r *http.Request) {
	payload, _ := io.ReadAll(r.Body)
	event, err := fiscalisto.VerifyWebhook(payload, r.Header, os.Getenv("FISCALISTO_WEBHOOK_SECRET"))
	if err != nil {
		http.Error(w, "firma inválida", http.StatusBadRequest)
		return
	}
	if event.Type == "invoice.stamped" {
		invoice, _ := fiscalisto.DataObject[fiscalisto.Invoice](event)
		guardar(*invoice.UUID)
	}
	w.WriteHeader(http.StatusOK)
}
```

```java tab="Java"
// Spring Boot.
@PostMapping("/webhooks/fiscalisto")
ResponseEntity<Void> fiscalistoWebhook(@RequestBody byte[] body, @RequestHeader Map<String, String> headers) {
    try {
        var event = Webhooks.verify(body, headers, System.getenv("FISCALISTO_WEBHOOK_SECRET"));
        if (event.getType().equals("invoice.stamped")) guardar(event.dataObject(Invoice.class).getUuid());
        return ResponseEntity.ok().build();
    } catch (Webhooks.VerificationException e) {
        return ResponseEntity.badRequest().build();
    }
}
```

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](https://fiscalisto.com/docs/api/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.

| Proyecto | Descargar | Correrlo |
| --- | --- | --- |
| Next.js (App Router) | [nextjs.zip](https://fiscalisto.com/docs/api/starters/nextjs.zip) | `npm install && npm run dev` |
| Laravel | [laravel.zip](https://fiscalisto.com/docs/api/starters/laravel.zip) | `composer install && php artisan serve` |
| Django | [django.zip](https://fiscalisto.com/docs/api/starters/django.zip) | `pip install -r requirements.txt && python manage.py runserver` |
| ASP.NET Core | [aspnet.zip](https://fiscalisto.com/docs/api/starters/aspnet.zip) | `dotnet 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](https://fiscalisto.com/docs/api/cli#webhooks-en-su-computadora)).

## Postman, Bruno e Insomnia

Para probar sin escribir código, importe la [colección de la API](https://fiscalisto.com/docs/api/fiscalisto.postman_collection.json): 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](https://api.fiscalisto.com/v1/openapi.json).
