Skip to main content
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 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.
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: 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:
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.
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.
  • 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.
  • 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.
  • SAT sync — simulated in test mode: nothing is registered with the PAC, and sample documents appear in GET /v1/sat_documents. See SAT sync.
  • 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.

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: 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. Because the event log is the source of truth deliveries replay from, a missed webhook can always be reconciled by polling GET /v1/events.