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

# Massive download

> Download the CFDIs (Mexican electronic invoices) an RFC issued or received in a date range, as a ZIP of XML files.

Download the CFDIs (Mexican electronic invoices) an RFC issued or received in a date range, as a ZIP of XML files.

| How you authorize | What you need | When it is used |
| - | - | - |
| **SAT password (CIEC)**, recommended | The taxpayer's RFC and password, [saved once](https://docs.newinvoice.dev/guides/sat-credentials.md) | Automatically, whenever the profile has a verified, unexpired password |
| **FIEL (e.firma)** | The taxpayer's certificate and key on the profile | Otherwise, and for `type: "metadata"` |

You never choose: `POST /v1/downloads` is the same request either way.

**Live downloads with a SAT password need a subscription; test mode does not.**

## Pricing

Downloads with a SAT password are charged on top of your plan. Prices are in MXN with IVA included.

| What you pay for | Price |
| - | - |
| Each XML delivered | \$0.696 |
| Each active RFC, per calendar month | \$114.84 on Starter, cheaper by step on Growth and Scale |

An RFC is active in a month when it receives at least one delivered XML that month, on the Mexico
City calendar. You pay for it once that month, however many downloads you make.

On **Growth** and **Scale**, active RFCs get cheaper as you add them. Each RFC pays its own step's
rate, so adding an RFC never lowers the bill:

| Active RFCs in the month | Price per RFC |
| - | - |
| 1–9 | \$114.84 |
| 10–24 | \$91.64 |
| 25–49 | \$68.44 |
| 50 and more | \$45.24 |

For example, 30 active RFCs on Growth cost 9 × $114.84 + 15 × $91.64 + 6 × $68.44 = **$2,818.80
MXN\*\*. Subscriptions renew on the 1st of each month, so the steps count one calendar month.
`GET /v1/billing` returns your plan's steps in `sat_active_rfc_tiers`.

**You pay for each invoice once.** An invoice an earlier download already delivered for that RFC and
`side` is skipped and not charged again, even if the new date range is different.

**Example:** 500 XMLs for one RFC in a month cost 500 × $0.696 = $348.00, plus $114.84 for the
active RFC: **$462.84 MXN\*\* with IVA.

Failed downloads and downloads with no invoices are free, and test mode is free. Usage appears on
your plan's invoice. `GET /v1/billing` shows the XMLs and active RFCs so far this month.

> **Test mode is simulated.** With a `sk_test_` key nothing reaches the SAT. A download finishes
> about 30 seconds after you request it and contains sample XMLs, not real invoices. The year of
> `date_from` picks the outcome, and those years ignore the date limits below:
>
> | `date_from` year | Outcome |
> | - | - |
> | `1999` | `no_data` |
> | `2000`, `2001` | `failed` |
> | `2002` | `partial` (SAT password only) |
> | any other | `ready` |

## From request to XML files

This works as written with a test key and any test fiscal profile.

```bash theme={null}
export API_KEY=sk_test_... FP_ID=fp_... API=https://api.newinvoice.dev/v1
```

**1. Save the SAT password.** A test key verifies it immediately. With a live key, wait for the
`fiscal_profile.sat_credential_verified` webhook ([details](https://docs.newinvoice.dev/guides/sat-credentials.md)).

```bash theme={null}
curl -s -X POST $API/fiscal_profiles/$FP_ID/sat_credential \
  -H "authorization: Bearer $API_KEY" -H 'content-type: application/json' \
  -d '{ "password": "any-password-in-test-mode" }' | jq .status
# "verified"
```

**2. Request the download.** `side` is `issued` (the RFC is the issuer) or `received`.

```bash theme={null}
DL_ID=$(curl -s -X POST $API/downloads \
  -H "authorization: Bearer $API_KEY" -H 'content-type: application/json' \
  -d "{ \"fiscal_profile_id\": \"$FP_ID\", \"side\": \"issued\", \"type\": \"cfdi\",
        \"date_from\": \"2026-01-01\", \"date_to\": \"2026-01-31\" }" | jq -r .id)
```

```json theme={null}
{ "object": "download", "id": "dl_01J...", "status": "processing", "package_count": 0,
  "date_from": "2026-01-01T00:00:00", "date_to": "2026-01-31T23:59:59", "result": null,
  "continued_from": null, "error": null, "metadata": { "warnings": [] } }
```

That is a `201`. Sending the same request again returns the same download with `200`, with no new
charge. See [Asking again](#asking-again) for when a repeat fetches new invoices.

**3. Wait for it to finish.** Live downloads usually take minutes and can take 40 minutes or more.
Subscribe to `download.ready`, `download.partial`, `download.no_data` and `download.failed`
[webhooks](https://docs.newinvoice.dev/guides/webhooks.md), or poll:

```bash theme={null}
while :; do
  STATUS=$(curl -s $API/downloads/$DL_ID -H "authorization: Bearer $API_KEY" | jq -r .status)
  echo "$STATUS"
  case "$STATUS" in requested|processing) sleep 10 ;; *) break ;; esac
done
```

```json theme={null}
{ "id": "dl_01J...", "status": "ready", "package_count": 1,
  "result": { "complete": true, "delivered": 2, "already_delivered": 0, "failed": 0, "not_checked": 0,
              "reason": null, "remediation": null, "report_url": "/v1/downloads/dl_01J.../report" } }
```

**4. Fetch the files.** `GET /v1/downloads/:id/files` lists every ZIP.

```bash theme={null}
curl -s $API/downloads/$DL_ID/files/0 -H "authorization: Bearer $API_KEY" -o invoices.zip
unzip -l invoices.zip
```

## Partial results

`partial` means some invoices arrived and some did not. The ZIP holds what we got, and it is not
the whole range. `download.partial` fires instead of `download.ready`, and `result` has the counts:

```json theme={null}
{ "status": "partial",
  "result": { "complete": false, "delivered": 202, "failed": 1, "not_checked": 0,
              "report_url": "/v1/downloads/dl_01J.../report" } }
```

**See what is missing.** The report is a CSV. Rows with `status` `error` or `not_checked` were not
delivered, and the `error` column says why.

```bash theme={null}
curl -s $API/downloads/$DL_ID/report -H "authorization: Bearer $API_KEY" -o report.csv
```

**Get the rest.** Send the same `POST /v1/downloads` again. You get a new download (`201`) with
`continued_from` set to the previous id, containing only invoices you do not have yet. Keep the
files from both. An invoice the SAT will not release stays in the report as `error`.

## Asking again

It is safe to send a request more than once, and safe for date ranges to overlap, so you can pull
"month to date" every day.

| You send | You get |
| - | - |
| The same dates, and the range ended at least 4 days before the last download started | The same download (`200`). The range is final. |
| The same dates, less than 6 hours after the last download finished | The same download (`200`). |
| The same dates, 6 hours or more after the last download finished, on a range that is not final yet | A new download (`201`) with only the invoices you do not have yet. |
| Different dates that overlap earlier downloads | A new download (`201`) with only the invoices you do not have yet. |
| Dates that overlap a download still running | `409 download.overlapping_in_progress`. Wait for it to finish and send the request again. |

A range stays open for 4 days after it ends because an invoice can reach the SAT up to 72 hours
after its date.

Test mode does not simulate skipping: a new test download always contains sample XMLs.

A new download holds only what is new, so **keep the files from each one**. Skipped invoices are
counted in `result.already_delivered`, and `continued_from` points to the earlier download of the
same dates. If nothing is new, the download ends as `no_data`, with no ZIP and no charge.

## Limits with a SAT password

| Limit | Beyond it |
| - | - |
| 366 days per request | `422 download.range_too_large`. Split the range. |
| Dates from 2011-01-01, `date_to` not in the future | `422 download.range_too_old` or `download.invalid_range`. |
| 2,000 invoices per download | Ends `partial`. Send the request again for the rest. |
| 2,000 XMLs per taxpayer per day (SAT portal) | Ends `partial`. Send the request again the next day. |
| Whole days only | A datetime is widened to the full day, with a warning in `metadata.warnings`. |
| About 18,000 already-delivered invoices in the range | `422 download.range_too_large`. Request fewer days. |

Requests are never split automatically, because that would spend the daily limit without telling you.

## Statuses

| `status` | Meaning | What to do |
| - | - | - |
| `requested` | Waiting for a free slot | Wait; it starts on its own. |
| `processing` | Running | Wait. |
| `ready` | Everything in the range was delivered | Fetch the files. |
| `partial` | Some invoices were delivered | Fetch the files, read the report, request the rest. |
| `no_data` | The range has no invoices, or none you do not already have | Nothing. See [Asking again](#asking-again). |
| `failed` | Nothing was delivered | Follow `error.remediation`. Requesting again starts fresh. |

## Common errors

| Code | Meaning | What to do |
| - | - | - |
| `billing.subscription_required` | Live download on the free plan | Subscribe in the dashboard or with `POST /v1/billing/checkout_sessions`. |
| `download.fiel_required` | No verified SAT password or FIEL | [Save the SAT password](https://docs.newinvoice.dev/guides/sat-credentials.md) or add a FIEL. `type: "metadata"` needs a FIEL. |
| `sat_service.credential_rejected` (in `error`) | The SAT refused the password | Confirm it with the taxpayer and save it again. Do not retry automatically. |
| `download.overlapping_in_progress` | A download touching these dates is still running | Wait for it to finish, then send the request again. |
| `download.request_failed` | The request could not be started | Follow `remediation`, then send it again. |

Every code is in the [error reference](https://docs.newinvoice.dev/concepts/errors.md).

## FIEL details

A FIEL [saved as the SAT credential](https://docs.newinvoice.dev/guides/sat-credentials.md) behaves exactly like a saved SAT password.
Without a saved credential the profile uses its FIEL, added with `PATCH /v1/fiscal_profiles/:id`.
The SAT allows **2 identical requests, ever** (`sat.download_request_limit`), so never repeat exact
parameters, and **200,000 XMLs per request** (`sat.download_too_large`), so split wide ranges by
month, week, then day. `type: "metadata"` returns an index without XML files, useful for sizing a
range. For an ongoing feed instead of one-off pulls, see [SAT sync](https://docs.newinvoice.dev/guides/sat-sync.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.