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

# Testing

> Build and rehearse your whole integration in test mode before you touch live SAT data.

Build and rehearse your whole integration in **test mode** before you touch live SAT data. Test
mode isolates your **data**: a `sk_test_` key only ever sees test resources and never affects your
live records. It is **not** a separate PAC environment. A `sk_test_` key talks to the PAC's
production hosts, and only some operations are sandboxed, so read
[what is and isn't sandboxed](#what-is-and-isnt-sandboxed) before you send real fiscal material
with a test key.

## Test vs live

* `sk_test_...` → **test mode**. Stamps go to the PAC's production host flagged `prueba=true`, which
  issues a well-formed but non-fiscal test CFDI that is not reported to the SAT. Expired and SAT test
  CSDs are accepted by this API, and **no carta manifiesto is needed**. You can stamp with no fiscal
  profile at all (see below).
* `sk_live_...` → **live** stamping at the SAT. A live CSD must be non-expired and its manifiesto
  signed. See [Going live](https://docs.newinvoice.dev/guides/going-live.md).

The mode is decided entirely by the key; nothing in the request body switches it. Every resource
carries `livemode` so you always know which world you are in.

## What is and isn't sandboxed

Here is what a `sk_test_` key actually does on the hosted API, operation by operation:

| Operation | Endpoint | What a `sk_test_` key does | Sandboxed? |
| - | - | - | - |
| Stamping | `POST /v1/invoices`, `POST /v1/invoices/:id/stamp` | Sent to the PAC's production host with `prueba=true`. Returns a non-fiscal test CFDI that is not reported to the SAT. | **Yes** |
| Cancellation | `POST /v1/invoices/:id/cancel` | Never reaches the PAC or the SAT. Returns a simulated result. | **Yes** (simulated) |
| Carta manifiesto | `POST /v1/fiscal_profiles/:id/manifest` | Always signed on the PAC's live signing service, with no test flag. | **No: creates a real authorization** (see the warning below) |
| Massive download | `POST /v1/downloads` | Never reaches the PAC or the SAT, with a saved SAT password or a FIEL. Runs the normal job lifecycle and serves simulated packages. | **Yes** (simulated) |
| SAT sync | `POST` / `DELETE /v1/fiscal_profiles/:id/sync` | Never reaches the PAC or the SAT. Nothing is registered, and simulated documents are mirrored into `GET /v1/sat_documents`. | **Yes** (simulated) |
| Constancia | `POST /v1/fiscal_documents` | Never reaches the SAT. Runs the normal lifecycle and serves a PDF marked as not being a SAT document. | **Yes** (simulated) |
| SAT status check | `POST /v1/validations` with a stamped XML | Queries the real status of that UUID on the PAC's production host. | **No** (read-only) |
| Balance | `GET /v1/balance` | Reads the real balance of the PAC account. | **No** (read-only) |

Test stamps share the production host with live ones. Whether `prueba=true` also isolates the PAC's
Serie+Folio duplicate check from your live stamps is unconfirmed, so don't reuse a live Serie+Folio
in test.

Self-hosting with `PAC_DRIVER=simulated` (the local default) is the one fully offline option: every
operation above is simulated and nothing leaves your machine.

## Stamp with no setup

With only a `sk_test_` key, omit `fiscal_profile_id` and the invoice is issued from a **default
sandbox issuer**, the SAT's public test taxpayer `EKU9003173C9`:

```bash theme={null}
curl -s -X POST https://api.newinvoice.dev/v1/invoices \
  -H "authorization: Bearer $API_KEY" -H 'content-type: application/json' \
  -d '{ "type": "I", "payment_form": "03", "payment_method": "PUE", "use": "G03",
        "customer": { "rfc": "URE180429TM6", "name": "UNIVERSIDAD ROBOTICA ESPANOLA",
                      "zip": "65000", "tax_regime": "601" },
        "items": [ { "product_key": "01010101", "unit_key": "H87", "description": "Test",
                     "quantity": 1, "unit_price": "100.00" } ] }'
```

How the sandbox issuer behaves:

* **It applies until you register your own test profile.** After `POST /v1/fiscal_profiles`,
  `fiscal_profile_id` is required again, and naming the sandbox profile returns 422
  `fiscal_profile.sandbox_retired`. A draft already created from it can still be stamped. If you
  delete your last test profile, the default applies again.
* **Its folio is generated.** Every organization shares this RFC, so the API assigns a unique folio
  per invoice and ignores any `folio` you send. Your `serie` is kept.
* **It is not one of your profiles.** It is left out of `GET /v1/fiscal_profiles`. You can retrieve
  it by the `fiscal_profile_id` on your invoice, where it shows `sandbox: true`.
* **It mostly stamps.** Manifiesto signing, SAT sync, ticket self-invoicing, updates and deletion
  return 422 `fiscal_profile.sandbox_not_supported`. Register your own test profile for those.
  Massive download works once you save a SAT password on it (the password is simulated and sent
  nowhere); without one it returns the same 422, because the sandbox issuer has no FIEL.
* **It is test mode only.** A `sk_live_` key cannot use or see it, and `fiscal_profile_id` is always
  required in live mode.

## The manifiesto is only required in live mode

A test-mode profile can stamp without a signed carta manifiesto, so it reports `status: "active"`
with no `sign_manifest` action, even while its `manifest_status` is `pending_signature`. A live
profile is created **on hold**: it comes back `status: "requires_action"` and any stamp returns
**409 `fiscal_profile.manifest_required`** until the manifiesto is signed. See
[Going live](https://docs.newinvoice.dev/guides/going-live.md).

> **Warning: signing with a real FIEL creates a real authorization, even with a `sk_test_` key.**
> The carta manifiesto has no test mode. `POST /v1/fiscal_profiles/:id/manifest` always goes to the
> PAC's live signing service, because the manifiesto is a real authorization for the RFC and the PAC
> has no sandbox for it. If you sign with a taxpayer's real FIEL on a test profile, the PAC receives
> a real signed carta manifiesto authorizing it to certify CFDIs for that RFC, and the signed
> document is emailed to the address on the request. The `sk_test_` key does not limit it, and this
> API has no call that revokes it. Only sign with a FIEL whose owner intends to give that
> authorization.
>
> The FIEL you send inline is also stored, encrypted, on the test profile, where massive download
> and SAT sync can use it. In test mode those two never send it to the PAC or the SAT.

## Exercising each lifecycle branch

Test mode runs the API's real invoice lifecycle, so you drive most branches with real inputs rather
than mock flags:

* **Stamping** — a well-formed request returns a `stamped` invoice with a valid UUID and sellos.
  Coherence violations come back as `422 cfdi.invalid_input` with a `path`; malformed CFDI as
  `422 cfdi.malformed_xml`. A PAC maintenance window surfaces as a retryable `502`. A stamp whose
  outcome is unknown (timeout, network failure, PAC internal error) is a `422
  pac.stamp_outcome_unknown`, because the CFDI may already exist: see
  [Idempotency and retries](#idempotency-and-retries).
* **Cancellation** — simulated in test mode: a test-mode cancel never calls the PAC or the SAT and
  returns a simulated result. Use it to exercise your own handling, not as evidence of how the SAT
  would respond. See [Cancellation](https://docs.newinvoice.dev/guides/cancellation.md).
* **Massive download** — simulated in test mode: the request never reaches the PAC or the SAT and
  uses none of the SAT's limits. The first status poll, about 30 seconds later, finishes the job,
  and a `ready` download serves a real ZIP of sample XMLs. Force the other outcomes with the year of
  `date_from`: `1999` gives `no_data`, `2000` and `2001` give `failed`, and with a saved SAT
  password `2002` gives `partial`. See [Massive download](https://docs.newinvoice.dev/guides/massive-download.md).
* **SAT sync** — simulated in test mode: nothing is registered with the PAC, and sample documents
  appear in `GET /v1/sat_documents`. See [SAT sync](https://docs.newinvoice.dev/guides/sat-sync.md).
* **Constancia** — simulated in test mode: the first status poll, about 15 seconds later, finishes
  the job with a PDF marked as not being a SAT document. Force another outcome with the test-only
  `simulate` field on `POST /v1/fiscal_documents`: `failed`, `needs_action`, `cancelled`,
  `credential_rejected`, or `capacity_reached` (stays `pending` until its deadline). A live key that
  sends `simulate` gets 422. See [Constancia](https://docs.newinvoice.dev/guides/constancia.md).

## Idempotency and retries

Send `Idempotency-Key: <unique>` on any POST. A successful (`2xx`) response is stored for 24h and
replayed byte-for-byte for identical retries (replays carry `idempotent-replayed: true`); a
different body under a key with a stored response is a `409 idempotency.key_reuse`, and a duplicate
of a request still running is a `409 idempotency.key_processing`.

Any non-`2xx` releases the key, so you can fix the cause (attach a CSD, sign the manifiesto, retry a
`409 invoice.folio_contention`) and send it again with the **same** key, even with a corrected body.
A `POST /v1/invoices` rejected by the PAC leaves its row as `stamp_failed` and uses up its folio; the
retry creates a new invoice with the next folio.

Three errors stay bound to their key and replay like a success, because repeating the request could
do real damage. To go ahead anyway, use a new key:

| Code | Why it is held |
| - | - |
| `pac.stamp_outcome_unknown` | The stamp timed out or the PAC returned no verdict, so a CFDI may exist. Recover with `POST /v1/invoices/:id/stamp` within 3 months, which returns an existing stamp instead of minting a second one. |
| `pac.auth_failed` | Repeated failures lock the PAC account. |
| `download.request_failed` | The SAT allows only 2 identical requests per lifetime. |

The PAC's own duplicate code `307` is resolved as a success-replay rather than an error.

## Rate limits

Rate limiting is enforced with **two independent token buckets per key** — one for READ
(`GET`/`HEAD`), one for WRITE (`POST`/`PATCH`/`DELETE`) — so a read burst can't starve writes. When
you exceed a bucket you get `429` with a `Retry-After` header and a `rate_limit.exceeded` problem
body. Both SDKs honor `Retry-After` automatically on `429`/`5xx`, so building on an SDK gives you
correct backoff for free.

## Receiving webhooks while you build

To test webhook delivery against a local handler, expose it with any tunnel (`cloudflared`,
`ngrok`) and register the tunnel URL as a test-mode webhook endpoint. Your handler should verify
every delivery's signature against the endpoint's `whsec_` secret before acting on it — both SDKs
ship this as `verifyWebhook` (Node) / `verify_webhook` (Python). Trigger a delivery by stamping an
invoice, and replay any past event with `POST /v1/events/:id/resend`. Full signing scheme and
verification code: [Webhooks](https://docs.newinvoice.dev/guides/webhooks.md).

Because the event log is the source of truth deliveries replay from, a missed webhook can always be
reconciled by polling `GET /v1/events`.


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