> ## Documentation Index
> Fetch the complete documentation index at: https://invoiceapi.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Massive download (SAT descarga masiva)

> Bulk-retrieve the CFDIs (or their metadata) that an RFC issued or received in a date range,

Bulk-retrieve the CFDIs (or their metadata) that an RFC issued or received in a date range,
straight from the SAT's descarga masiva service. It is asynchronous and hemmed in by hard
SAT limits — read the limits section before you design around it.

## Prerequisites

* A **FIEL (e.firma)** registered on the fiscal profile — descarga masiva authorizes with the FIEL,
  not the CSD. Without one, `POST /v1/downloads` returns **422 `download.fiel_required`**.

  ```bash theme={null}
  # add a FIEL to an existing profile via PATCH (or include `fiel` at creation)
  # fiel = { cer_base64, key_base64, password } — same shape as csd
  ```
* **No sandbox exists** for descarga masiva. Test mode uses the simulated driver; real downloads
  only work in production against a live FIEL.

## Request a download

```bash theme={null}
curl -s -X POST localhost:3000/v1/downloads \
  -H "authorization: Bearer $API_KEY" -H 'content-type: application/json' \
  -d '{
    "fiscal_profile_id": "fp_...",
    "side": "issued",          // issued = you are the emisor; received = you are the receptor
    "type": "cfdi",            // cfdi = ZIP of XMLs; metadata = ZIP of metadata rows
    "date_from": "2026-01-01",
    "date_to":   "2026-01-31"
  }' | jq
```

`date_from`/`date_to` accept `YYYY-MM-DD` or a full ISO datetime; a bare date widens to cover the
whole day. Response is a `download` object with `status: "requested"` and a `metadata.warnings`
array carrying any SAT-limit advisories. `201` for a new request, `200` if it deduped to an
identical prior request (no new SAT slot consumed).

## Lifecycle

```
requested ─▶ processing ─▶ ready ──▶ (packages downloaded & stored)
                     └────▶ no_data     (terminal, empty — the range simply had no CFDIs)
                     └────▶ failed
```

The API fires the SAT request, then a poll job checks status and, the moment SAT marks it ready,
**downloads the ZIP packages immediately** (their URLs expire in \~5 minutes) and stores them.

```bash theme={null}
curl -s localhost:3000/v1/downloads/$DL_ID -H "authorization: Bearer $API_KEY" | jq '.status,.package_count'
curl -s localhost:3000/v1/downloads/$DL_ID/files -H "authorization: Bearer $API_KEY" | jq
curl -s localhost:3000/v1/downloads/$DL_ID/files/0 -H "authorization: Bearer $API_KEY" -o package-0.zip
```

Subscribe to `download.ready` / `download.failed` webhooks instead of polling.

## SAT limits — design around these

The SAT (not this API) enforces the following. They are the reason descarga masiva must be planned,
not fired ad hoc. Codes below map to `sat.*` in [`../errors.md`](/concepts/errors).

| Limit                                     | Code                                    | What it means                                                 | What to do                                                                                  |
| ----------------------------------------- | --------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| **2 identical lifetime requests**         | `5002` `sat.download_request_limit`     | You may request the *exact same* params at most twice — ever. | **Never retry identical params.** Vary/fragment the date range so each request is distinct. |
| **200,000 XML per request** (1M metadata) | `5003` `sat.download_too_large`         | The range produced too many documents.                        | Fragment the date range (month → weeks → days) and issue separate requests.                 |
| **Package URLs expire \~5 min**           | `5007` `sat.download_package_not_found` | The signed S3 URL went stale before download.                 | Re-poll status to re-mint URLs and download at once (the poll job already does this).       |
| **No data**                               | `5004` `sat.download_no_data`           | Terminal success, empty result — the range has no CFDIs.      | Do not retry; treat as done. Surfaces as `status: "no_data"`.                               |
| Third party not authorized                | `5001` `sat.download_unauthorized`      | The FIEL does not match the requested RFC/side.               | Ensure the FIEL belongs to the RFC and `side` matches its role.                             |

### Fragmentation strategy

Because of both the 200k cap **and** the 2-identical-lifetime rule, prefer **narrow, non-repeating
windows**:

1. Start monthly. If a month returns `5003` (too large), split it into weeks, then days.
2. Make every request's `date_from`/`date_to` unique — do not resubmit a window you already
   requested twice. If you must reprocess, reuse the stored packages via
   `GET /v1/downloads/:id/files` rather than issuing a new SAT request.
3. For ongoing sync, advance the window forward (yesterday, the day before…) so params never repeat.
4. Metadata requests are cheaper (1M cap) — pull `type: "metadata"` first to size a range before
   pulling full `type: "cfdi"`.

## Notes

* `status_filter` (`active`/`cancelled`) is recorded for request differentiation but is advisory —
  it is not currently forwarded to the SAT request.
* The FIEL PFX is the only key material sent to the PAC — to sign the carta manifiesto and for
  descarga masiva. CSD private keys never leave this server.
