# Descargas del SAT

> Las facturas que un emisor recibió o emitió, directo del SAT con su XML: la descarga masiva con la e.firma del emisor, con un evento al terminar.

Con la descarga masiva del SAT usted trae todas las facturas que un emisor **recibió** (sus gastos, de cualquier proveedor) o **emitió** (aunque no las haya hecho con Fiscalisto), con su XML. Sirve para la contabilidad, para conciliar pagos o para revisar a los proveedores contra las [listas del SAT](https://fiscalisto.com/docs/api/listas-del-sat). En producción usan timbres según cuántas facturas traen; en pruebas no cuestan.

El SAT tarda de unos minutos a unas horas en preparar cada descarga. Usted la pide, sigue con lo suyo y le llega `sat_download.completed` cuando las facturas ya están.

## En pruebas y en producción

- **En producción** se le pregunta al SAT con la e.firma del emisor, que usted guarda una vez (abajo).
- **En pruebas** el SAT no tiene un ambiente de pruebas para esto, así que no se le pregunta: la descarga termina al instante con facturas de ejemplo entre el emisor y los RFC de prueba del SAT, con su XML y su evento. Pedir lo mismo otra vez trae las mismas facturas, sin duplicarlas. No necesita e.firma.

## Guardar la e.firma del emisor

En producción, mande la e.firma (el `.cer` y el `.key` en base64 y su contraseña) con `keep_for_downloads: true`. Se revisa que sea una e.firma vigente de ese RFC y se guarda cifrada, como en la app. Si el emisor aún no firmó el contrato con el PAC y ya tiene su CSD, la misma llamada lo firma.

```bash tab="cURL"
curl https://api.fiscalisto.com/v1/issuers/iss_OufpKqnrvx9p/efirma \
  -H "Authorization: Bearer $FISCALISTO_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"cer\": \"$(base64 < efirma.cer | tr -d '\n')\",
    \"key\": \"$(base64 < efirma.key | tr -d '\n')\",
    \"password\": \"$EFIRMA_PASSWORD\",
    \"keep_for_downloads\": true
  }"
```

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

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

const issuer = await fiscalisto.issuers.signEfirma("iss_OufpKqnrvx9p", {
  cer: (await readFile("efirma.cer")).toString("base64"),
  key: (await readFile("efirma.key")).toString("base64"),
  password: process.env.EFIRMA_PASSWORD!,
  keep_for_downloads: true,
});
```

```python tab="Python"
import base64
import os

from fiscalisto import Fiscalisto

# La llave viene de FISCALISTO_API_KEY.
fiscalisto = Fiscalisto()

issuer = fiscalisto.issuers.sign_efirma("iss_OufpKqnrvx9p", {
    "cer": base64.b64encode(open("efirma.cer", "rb").read()).decode(),
    "key": base64.b64encode(open("efirma.key", "rb").read()).decode(),
    "password": os.environ["EFIRMA_PASSWORD"],
    "keep_for_downloads": True,
})
```

```php tab="PHP"
<?php

use Fiscalisto\Fiscalisto;

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

$issuer = $fiscalisto->issuers->signEfirma('iss_OufpKqnrvx9p', [
    'cer' => base64_encode(file_get_contents('efirma.cer')),
    'key' => base64_encode(file_get_contents('efirma.key')),
    'password' => getenv('EFIRMA_PASSWORD'),
    'keep_for_downloads' => true,
]);
```

```go tab="Go"
package main

import (
	"encoding/base64"
	"context"
	"fmt"
	"log"
	"os"

	fiscalisto "fiscalisto.com/go"
)

func main() {
	// La llave viene de FISCALISTO_API_KEY.
	client, err := fiscalisto.NewClient("")
	if err != nil {
		log.Fatal(err)
	}
	ctx := context.Background()

	issuer, err := client.Issuers.SignEfirma(ctx, "iss_OufpKqnrvx9p", &fiscalisto.SignEfirmaParams{
		Cer:              readBase64("efirma.cer"),
		Key:              readBase64("efirma.key"),
		Password:         os.Getenv("EFIRMA_PASSWORD"),
		KeepForDownloads: fiscalisto.Bool(true),
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(issuer.ID)
}

func readBase64(path string) string {
	b, err := os.ReadFile(path)
	if err != nil {
		log.Fatal(err)
	}
	return base64.StdEncoding.EncodeToString(b)
}
```

```java tab="Java"
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
import com.fiscalisto.FiscalistoClient;
import com.fiscalisto.model.SignEfirmaParams;

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

var issuer = fiscalisto.issuers().signEfirma("iss_OufpKqnrvx9p", new SignEfirmaParams()
        .setCer(Base64.getEncoder().encodeToString(Files.readAllBytes(Path.of("efirma.cer"))))
        .setKey(Base64.getEncoder().encodeToString(Files.readAllBytes(Path.of("efirma.key"))))
        .setPassword(System.getenv("EFIRMA_PASSWORD"))
        .setKeepForDownloads(true));
System.out.println(issuer.getId());
```

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

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

var issuer = await fiscalisto.Issuers.SignEfirmaAsync("iss_OufpKqnrvx9p", new SignEfirmaParams
{
    Cer = Convert.ToBase64String(File.ReadAllBytes("efirma.cer")),
    Key = Convert.ToBase64String(File.ReadAllBytes("efirma.key")),
    Password = Environment.GetEnvironmentVariable("EFIRMA_PASSWORD")!,
    KeepForDownloads = true,
});
```

El emisor trae `sat_downloads.available: true` y cuándo vence su e.firma en `sat_downloads.efirma_expires_at`. Para borrarla: `DELETE /v1/issuers/{id}/efirma`. El emisor sigue timbrando; solo deja de poder pedir descargas.

> **Con permiso del dueño.** Pida la e.firma solo con el permiso del dueño del RFC y dígale para qué es. Con ella se consultan todas sus facturas en el SAT.

## Pedir una descarga

```bash tab="cURL"
curl https://api.fiscalisto.com/v1/sat_downloads \
  -H "Authorization: Bearer $FISCALISTO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "direction": "received", "from": "2026-09-01", "to": "2026-09-30" }'
```

```ts tab="TypeScript"
import { Fiscalisto } from "fiscalisto";

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

const satDownload = await fiscalisto.satDownloads.create({
  direction: "received",
  from: "2026-09-01",
  to: "2026-09-30",
});
```

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

# La llave viene de FISCALISTO_API_KEY.
fiscalisto = Fiscalisto()

sat_download = fiscalisto.sat_downloads.create({
    "direction": "received",
    "from": "2026-09-01",
    "to": "2026-09-30",
})
```

```php tab="PHP"
<?php

use Fiscalisto\Fiscalisto;

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

$satDownload = $fiscalisto->satDownloads->create([
    'direction' => 'received',
    'from' => '2026-09-01',
    'to' => '2026-09-30',
]);
```

```go tab="Go"
package main

import (
	"context"
	"fmt"
	"log"

	fiscalisto "fiscalisto.com/go"
)

func main() {
	// La llave viene de FISCALISTO_API_KEY.
	client, err := fiscalisto.NewClient("")
	if err != nil {
		log.Fatal(err)
	}
	ctx := context.Background()

	satDownload, err := client.SatDownloads.Create(ctx, &fiscalisto.CreateSatDownloadParams{
		Direction: fiscalisto.String("received"),
		From:      "2026-09-01",
		To:        "2026-09-30",
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(satDownload.ID)
}
```

```java tab="Java"
import com.fiscalisto.FiscalistoClient;
import com.fiscalisto.model.CreateSatDownloadParams;

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

var satDownload = fiscalisto.satDownloads().create(new CreateSatDownloadParams()
        .setDirection("received")
        .setFrom("2026-09-01")
        .setTo("2026-09-30"));
System.out.println(satDownload.getId());
```

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

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

var satDownload = await fiscalisto.SatDownloads.CreateAsync(new CreateSatDownloadParams
{
    Direction = "received",
    From = "2026-09-01",
    To = "2026-09-30",
});
```

Contesta `202` con la descarga (`sdl_…`) en `status: "processing"` (en pruebas ya viene `completed`):

```json
{
  "id": "sdl_OufpKqnrvx9p3n8vQd2LkW7mZr1XyT5uBc",
  "object": "sat_download",
  "livemode": true,
  "issuer": "iss_OufpKqnrvx9p",
  "direction": "received",
  "from": "2026-09-01",
  "to": "2026-09-30",
  "type": null,
  "counterparty_tax_id": null,
  "uuid": null,
  "status": "processing",
  "new_invoices": null,
  "error": null,
  "created_at": "2026-10-24T10:20:00-06:00",
  "completed_at": null
}
```

| Campo | Qué pide |
| --- | --- |
| `direction` | `received` (por omisión) o `issued`. |
| `from`, `to` | Los días, incluidos. Hasta un año por descarga, desde el 1 de enero de hace 5 años (lo que da el SAT) y hasta hoy. |
| `type` | Solo un tipo de comprobante: `I`, `E`, `P`, `T` o `N`. |
| `counterparty_tax_id` | Solo las de un proveedor (recibidas) o un cliente (emitidas). |
| `uuid` | Una sola factura, por su folio fiscal. |

Las recibidas llegan **vigentes**: el SAT solo entrega el XML de una factura recibida mientras no esté cancelada.

## Cuando termina

Llega el evento `sat_download.completed` con la descarga y `new_invoices`, cuántas facturas nuevas trajo (las que ya tenía no se cuentan dos veces). Si el SAT no pudo, llega `sat_download.failed` con el motivo en `error`, por ejemplo una e.firma vencida. También puede consultarla con `GET /v1/sat_downloads/{id}` o listarlas con `GET /v1/sat_downloads`.

El SAT limita cuántas veces se puede pedir exactamente lo mismo (el mismo periodo con los mismos filtros). Si se agotan, la respuesta es `sat_request_rejected`: pida un periodo un poco distinto.

## Las facturas

```bash tab="cURL"
curl "https://api.fiscalisto.com/v1/sat_invoices?direction=received&date_gte=2026-09-01&limit=20" \
  -H "Authorization: Bearer $FISCALISTO_API_KEY"
```

```ts tab="TypeScript"
import { Fiscalisto } from "fiscalisto";

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

for await (const item of fiscalisto.satInvoices.list({
  direction: "received",
  date_gte: "2026-09-01",
  limit: 20,
})) {
  console.log(item.id);
}
```

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

# La llave viene de FISCALISTO_API_KEY.
fiscalisto = Fiscalisto()

for item in fiscalisto.sat_invoices.list({
    "direction": "received",
    "date_gte": "2026-09-01",
    "limit": 20,
}):
    print(item["id"])
```

```php tab="PHP"
<?php

use Fiscalisto\Fiscalisto;

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

foreach ($fiscalisto->satInvoices->list([
    'direction' => 'received',
    'date_gte' => '2026-09-01',
    'limit' => 20,
]) as $item) {
    echo $item['id'], "\n";
}
```

```go tab="Go"
package main

import (
	"context"
	"fmt"
	"log"

	fiscalisto "fiscalisto.com/go"
)

func main() {
	// La llave viene de FISCALISTO_API_KEY.
	client, err := fiscalisto.NewClient("")
	if err != nil {
		log.Fatal(err)
	}
	ctx := context.Background()

	for item, err := range client.SatInvoices.ListAutoPaging(ctx, &fiscalisto.ListSatInvoicesParams{
		Direction: fiscalisto.String("received"),
		DateGte:   fiscalisto.String("2026-09-01"),
		Limit:     fiscalisto.Int64(20),
	}) {
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(item.ID)
	}
}
```

```java tab="Java"
import com.fiscalisto.FiscalistoClient;
import com.fiscalisto.model.ListSatInvoicesParams;

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

for (var item : fiscalisto.satInvoices().listAutoPaging(new ListSatInvoicesParams()
        .setDirection("received")
        .setDateGte("2026-09-01")
        .setLimit(20))) {
    System.out.println(item.getId());
}
```

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

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

await foreach (var item in fiscalisto.SatInvoices.ListAutoPagingAsync(new ListSatInvoicesParams
{
    Direction = "received",
    DateGte = "2026-09-01",
    Limit = 20,
}))
    Console.WriteLine(item.Id);
```

Cada factura (`sat_…`) trae su folio fiscal, quién la emitió (`issued_by`) y a quién (`issued_to`), los importes, los impuestos trasladados y retenidos, y su estado en el SAT (`sat_status`). Se filtran por `direction`, `type`, `counterparty_tax_id`, `date_gte` y `date_lte`, de la más reciente a la más antigua, con la [paginación](https://fiscalisto.com/docs/api/paginacion) de siempre. El XML, tal como lo entregó el SAT, está en `GET /v1/sat_invoices/{id}/xml`.

## Límites

Hasta 300 descargas al día por cuenta en producción y 50 en pruebas. En producción, pedir una usa un timbre, que alcanza para los primeros 100 CFDI (si el SAT la rechaza, el timbre regresa). Cuando el SAT dice cuántos tiene, cada 100 más, o los que sobren, usan otro timbre antes de descargarlos: 1,250 CFDI son 13 timbres. Si a su cuenta no le alcanzan, la descarga termina en `failed` (evento `sat_download.failed`), sin descargar nada, y el timbre de la solicitud no regresa. En pruebas no cuestan. Vea [Límites](https://fiscalisto.com/docs/api/limites#lo-que-usa-timbres-sin-timbrar).

Pedir otra vez una descarga igual (mismo emisor, fechas y filtros) mientras el SAT todavía trabaja en ella contesta esa misma descarga, sin otro timbre: así un reintento después de un timeout no gasta las pocas solicitudes idénticas que acepta el SAT. Cuando ya terminó, una igual es una descarga nueva.
