sk_test_ key stamps against the PAC’s test environment: it issues well-formed CFDIs, tolerates
expired/test CSDs, and never affects your live records. Test and live data are fully isolated — a
sk_test_ key only ever sees test resources.
Test vs live
sk_test_...→ the PAC test environment. Expired and SAT test CSDs are accepted, but the carta manifiesto still has to be signed before a profile can stamp (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.
The manifiesto gate applies in test too
A fiscal profile is created on hold in both modes: it comes backstatus: "requires_action"
with a sign_manifest action, and any stamp attempt against it returns 409
fiscal_profile.manifest_required until you sign the carta manifiesto. Because the gate is
identical in test and live, the requires_action → active flow you exercise in test is exactly what
live does — so onboarding is fully rehearsable. See
Stamping your first invoice for the full sequence.
Exercising each lifecycle branch
Test mode runs the real lifecycle, so you drive the 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. Transient PAC errors surface as retryable502s — safe to retry with the sameIdempotency-Key. - Cancellation — the con-aceptación vs sin-aceptación paths both run in test: a receptor of
XAXX010101000(público en general) cancels immediately (cancelled), while a normal receptor stayscancel_pendinguntil it accepts, rejects, or the 72h window lapses. See Cancellation. - Massive download — note that descarga masiva has no test environment; it runs only in live mode against a real FIEL. See Massive download.
Idempotency and retries
SendIdempotency-Key: <unique> on any POST. The first response is stored for 24h and replayed
byte-for-byte for identical retries (replays carry idempotent-replayed: true); a different body
under the same key is a 409 idempotency.key_reuse. This is what makes retrying a 502 safe — the
API guards against a duplicate stamp, and 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.