Skip to main content
Build and rehearse your whole integration in test mode before you touch live SAT data. A 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.
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.

The manifiesto gate applies in test too

A fiscal profile is created on hold in both modes: it comes back status: "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 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. Transient PAC errors surface as retryable 502s — safe to retry with the same Idempotency-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 stays cancel_pending until 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

Send Idempotency-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.