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 flaggedprueba=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.
livemode so you always know which world you are in.
What is and isn’t sandboxed
Here is what ask_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 ask_test_ key, omit fiscal_profile_id and the invoice is issued from a default
sandbox issuer, the SAT’s public test taxpayer EKU9003173C9:
- It applies until you register your own test profile. After
POST /v1/fiscal_profiles,fiscal_profile_idis required again, and naming the sandbox profile returns 422fiscal_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
folioyou send. Yourserieis kept. - It is not one of your profiles. It is left out of
GET /v1/fiscal_profiles. You can retrieve it by thefiscal_profile_idon your invoice, where it showssandbox: 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, andfiscal_profile_idis 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 reportsstatus: "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 ask_test_key. The carta manifiesto has no test mode.POST /v1/fiscal_profiles/:id/manifestalways 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. Thesk_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
stampedinvoice with a valid UUID and sellos. Coherence violations come back as422 cfdi.invalid_inputwith apath; malformed CFDI as422 cfdi.malformed_xml. A PAC maintenance window surfaces as a retryable502. A stamp whose outcome is unknown (timeout, network failure, PAC internal error) is a422 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
readydownload serves a real ZIP of sample XMLs. Force the other outcomes with the year ofdate_from:1999givesno_data,2000and2001givefailed, and with a saved SAT password2002givespartial. 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
simulatefield onPOST /v1/fiscal_documents:failed,needs_action,cancelled,credential_rejected, orcapacity_reached(stayspendinguntil its deadline). A live key that sendssimulategets 422. See Constancia.
Idempotency and retries
SendIdempotency-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.