
Validate an RFC Without the Constancia de Situación Fiscal
No. Per SAT Comunicado 04/2026, demanding the Constancia de Situación Fiscal to invoice is an infracción under CFF art. 83-IX, finable roughly $21,420–$122,440 MXN. A CFDI 4.0 only needs RFC, exact name, CP, régimen and uso. Validate the RFC format and check digit locally, collect the rest via dropdowns, then let the stamp catch mismatches.
Validate an RFC without the Constancia de Situación Fiscal: SAT 04/2026 makes demanding it a finable infracción (CFF 83-IX)
No. You do not need the Constancia de Situación Fiscal to validate an RFC or issue a CFDI 4.0. As of SAT Comunicado 04/2026, demanding the Constancia de Situación Fiscal (CSF) — also called the Cédula de Identificación Fiscal (CIF) — as a condition to invoice is an infracción under CFF artículo 83, fracción IX, and it is sanctionable, with a reported fine bracket of roughly $21,420 to $122,440 MXN. To validate an RFC and stamp a CFDI 4.0 you only need five structured fields — RFC, exact nombre/razón social, código postal, régimen fiscal, and uso CFDI — none of which require a PDF. The engineering move is to validate the RFC format and check digit locally, collect the rest through dropdowns backed by SAT catalogs, and let the timbrado catch any mismatch.
The Constancia de Situación Fiscal (CSF), also rendered as the Cédula de Identificación Fiscal (CIF), is just a human-readable document that contains some of these fields — it is not itself a field of the comprobante.
This is an engineer’s onboarding-flow guide, not tax advice. Confirm the current fine amount and the exact CFF fracción against the live CFF/RMF before you ship copy that quotes a peso figure to a customer.
The 4 (really 5) data points a CFDI 4.0 stamp actually needs
Forget the constancia. Here is the complete receptor payload a CFDI 4.0 stamp requires, and what each maps to in the XML:
<cfdi:Receptor
Rfc="XAXX010101000"
Nombre="EMPRESA EJEMPLO SA DE CV"
DomicilioFiscalReceptor="06600"
RegimenFiscalReceptor="601"
UsoCFDI="G03" />
That is the whole identity surface:
Rfc— 12 chars for persona moral, 13 for persona física.Nombre— the razón social or full name exactly as registered with SAT. CFDI 4.0 is strict here; a stray accent or a missingSA DE CVwill bounce at timbrado.DomicilioFiscalReceptor— the código postal of the domicilio fiscal, not a full address.RegimenFiscalReceptor— a key from thec_RegimenFiscalcatalog (e.g.601,605,626).UsoCFDI— a key fromc_UsoCFDI(e.g.G03,S01), and it must be compatible with the régimen.
None of these is a document. Every one is a short string you can collect in a form. The CSF is just a human-readable rendering of data you are about to ask for field-by-field anyway.
Validate the RFC locally: format by persona moral/física + the mod-11 dígito verificador
You can reject most garbage RFCs before any network call. The structure is fixed: persona moral = 3 letters + 6 date digits + 3 homoclave, persona física = 4 letters + 6 date + 3 homoclave. The final homoclave character is a dígito verificador computed mod 11, so a typo in the body usually breaks the check digit.
import re
DICT = "0123456789ABCDEFGHIJKLMN&OPQRSTUVWXYZ Ñ" # SAT positional dictionary
# SAT generic RFCs whose check digit is decreed, not computed.
GENERIC_RFCS = {"XAXX010101000", "XEXX010101000"}
def rfc_shape_ok(rfc: str) -> bool:
rfc = rfc.strip().upper()
moral = r"^[A-ZÑ&]{3}[0-9]{6}[A-Z0-9]{3}$" # 12 chars
fisica = r"^[A-ZÑ&]{4}[0-9]{6}[A-Z0-9]{3}$" # 13 chars
return bool(re.match(moral, rfc) or re.match(fisica, rfc))
def check_digit(rfc: str) -> str:
rfc = rfc.strip().upper()
body = rfc[:-1]
# SAT pads persona moral (12) to 13 with a leading space
body = body.rjust(12, " ") if len(rfc) == 12 else body
if any(ch not in DICT for ch in body):
raise ValueError("RFC contains characters outside the SAT dictionary")
n = len(body)
total = sum(DICT.index(ch) * (n + 1 - i) for i, ch in enumerate(body))
r = total % 11
return "0" if r == 0 else "A" if r == 10 else str(11 - r)
def rfc_valid_local(rfc: str) -> bool:
rfc = rfc.strip().upper()
if rfc in GENERIC_RFCS: # whitelist; their DV is decreed, not computed
return True
return rfc_shape_ok(rfc) and check_digit(rfc) == rfc[-1]
Two caveats. First, the check digit only proves the string is internally consistent — it does not prove the RFC exists or belongs to this person; only SAT records do that. Second, the generic RFCs XAXX010101000 (público en general) and XEXX010101000 (extranjeros) are special-cased — XAXX010101000 does not even satisfy the computed mod-11 check, so you whitelist them rather than running the algorithm. The same dígito-verificador logic shows up when you build a Mexican-PII redactor for RFC/CURP/CLABE checksums.
- Normalizetrim + upper; strip pasted whitespace
- Shape12 chars persona moral / 13 persona física via regex
- Datethe 6 middle digits must be a valid AAMMDD date
- Check digitrecompute mod 11 and compare with the last char (whitelist the generics)
The honest reality: SAT has no real-time validation API — only the batch .txt tool
Here is the part most tutorials get wrong. SAT’s official free validator, “Validación de la clave en el RFC”, checks RFC, nombre, régimen fiscal and código postal together. It runs one-by-one in the browser, or masivo up to 5,000 registros via a file upload. But it is a web tool, not a REST endpoint — there is currently no official SAT real-time API you can call per signup.
The batch file is a .txt, pipe-delimited, UTF-8, no header, with the columns consecutivo|RFC|nombre|CP:
# validacion_masiva.txt (UTF-8, no header row, '|' delimited)
1|XAXX010101000|PUBLICO EN GENERAL|06600
2|GODE561231GR8|JUAN PEREZ LOPEZ|44100
3|EKU9003173C9|ESCUELA KEMPER URGATE SA DE CV|03100
def build_sat_batch(rows) -> str:
# rows: list of (rfc, nombre, cp); consecutivo is the 1-based index
lines = [f"{i}|{rfc}|{nombre}|{cp}"
for i, (rfc, nombre, cp) in enumerate(rows, start=1)]
return "\n".join(lines) # write with encoding='utf-8', max 5000 rows
This is great for a nightly reconciliation job over your existing customer base — dump 5,000 rows, upload, diff the result file against your DB. It is useless for blocking a “Sign up” button in real time, because there is nothing to call synchronously.
SAT validador (.txt, hasta 5,000)
- Free, official, authoritative
- Web upload only — no REST API
- Pipe-delimited UTF-8, no header
- Great for nightly reconciliation
- Cannot block a signup synchronously
PAC web service
- Real-time per-request validation
- Wraps the SAT data behind an API
- Paid, rate-limited, has SLAs
- Fits an onboarding form
- Same answer the stamp will give you
Real-time validation means a PAC web service (SW, Facturapi, Facturoporti)
If you need a synchronous yes/no while the user is still on the form, you call a PAC (Proveedor Autorizado de Certificación) that wraps the SAT data behind an HTTP API. SW (Smarter Web), Facturapi, and Facturoporti all expose an RFC-validation or list-validation endpoint.
// Conceptual PAC validation call — check the specific PAC's docs for the real shape.
async function validateAtPac(input: {
rfc: string; nombre: string; cp: string;
}): Promise<{ valid: boolean; reasons: string[] }> {
const res = await fetch("https://api.pac.example/v1/rfc/validate", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.PAC_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ rfc: input.rfc, nombre: input.nombre, cp: input.cp }),
});
if (!res.ok) throw new Error(`PAC ${res.status}`);
return res.json();
}
Treat this as advisory at onboarding, not a hard gate — PACs have rate limits and downtime, and you do not want a PAC outage to block signups. The authoritative check happens anyway at timbrado. If you are already wiring a PAC for issuance, the same credentials and outbox machinery apply; see CFDI timbrado: outbox + idempotency for PAC failures.
Régimen and Uso compatibility via c_RegimenFiscal and c_UsoCFDI (rejected at timbrado)
The UsoCFDI is not free-choice: it must be compatible with the receptor’s RegimenFiscalReceptor per the SAT catalogs c_RegimenFiscal and c_UsoCFDI. An incompatible pair — say a régimen 605 (Sueldos y Salarios) receptor with a deduction-only uso meant for personas morales — is rejected at timbrado. Encode the matrix once and filter the uso dropdown by the chosen régimen:
# Trimmed example — load the full matrix from the SAT c_UsoCFDI catalog.
USO_BY_REGIMEN = {
"601": {"G01", "G03", "I01", "I04", "P01", "S01", "CP01"}, # General Ley PM
"605": {"D01", "D02", "D04", "D10", "S01", "CP01"}, # Sueldos y Salarios
"626": {"G01", "G03", "I01", "P01", "S01", "CP01"}, # RESICO
}
def usos_for(regimen: str) -> set[str]:
return USO_BY_REGIMEN.get(regimen, set())
Filtering client-side means the user can never submit an incompatible pair, which kills a whole class of timbrado rejections before they happen. This is the same catalog discipline that keeps automated flows green, like auto-invoicing CFDI from Stripe / Mercado Pago or generating the factura global within 24h.
The onboarding decision tree: dropdowns, optional verify, then let the stamp catch mismatches
Put it together as a flow. Note what is a hard gate versus advisory:
- Validate RFC format + check digit locally. Hard gate — a malformed RFC can never be valid.
- Collect
CP,Régimen(c_RegimenFiscal),Uso(c_UsoCFDI) via dropdowns, never a PDF upload. FilterUsobyRégimen. - Optionally verify RFC + nombre + CP against the SAT validador (batch, later) or a PAC web service (real-time, advisory). Never block signup on a PAC outage.
- Stamp. The CFDI 4.0 timbrado fails if
Rfc+Nombre+CP+Régimendo not match SAT records — so a mismatch is caught at timbrado regardless of what you did upstream. The stamp is your real validation gate.
async function onboardReceptor(form: Receptor) {
if (!rfcValidLocal(form.rfc)) return reject("RFC inválido (formato/dígito)");
if (!usosFor(form.regimen).has(form.uso)) return reject("Uso incompatible con régimen");
// optional, advisory, non-blocking:
try { await validateAtPac(form); } catch { /* log, do not block */ }
return save(form); // truth is enforced at timbrado
}
This design leans on the one check you cannot fake — the stamp — instead of a document upload that proves nothing about whether the data matches SAT. The same “let the authoritative system reject” pattern applies to the complemento de pago (REP), CFDI de Nómina, and CFDI de Egreso for refunds.
What NOT to do: storing the Constancia de Situación Fiscal PDF and conditioning invoicing on it
Two anti-patterns to delete from your signup flow:
- Do not require a CSF/CIF upload to invoice. Per Comunicado 04/2026 this is the CFF art. 83-IX infracción itself, in the ~$21,420–$122,440 MXN range. SAT explicitly says this — including that employers must not demand the Constancia de Situación Fiscal from employees for payroll, and that they may instead request the data through a caso de aclaración.
- Do not store the CSF PDF as your source of truth. It is a PII-heavy document (full address, RFC, often a QR with more) that you now have to secure and retain for no fiscal benefit. The five structured fields are everything you need; the PDF is liability you opted into.
If a customer volunteers their Constancia de Situación Fiscal, fine — but parse it into the five fields, discard the PDF, and never gate issuance on its presence.
Engineer, not contador: confirm the fine and fracción against the current CFF/RMF
I am an engineer who ships invoicing flows against SAT and PACs, not your accountant. Everything above is about the signup and validation flow and the law as of Comunicado 04/2026 — not tax advice. The fine bracket (~$21,420–$122,440 MXN) sits in a CFF artículo 84 range and the prohibition is CFF art. 83 fracción IX; confirm both the current amount and the exact fracción against the live CFF and RMF before you put a peso figure in customer-facing copy. For the broader pattern, see the sibling guide validate RFC without the Constancia de Situación Fiscal and the Mexico CFDI invoicing hub.
Sources: