
Claude Structured Outputs in Production: Schema-Guaranteed JSON for CFDI Extraction and Webhook Routing
As of February 4, 2026, Claude Structured Outputs is GA on the Developer Platform and Bedrock. You pass output_config.format with type "json_schema"; Claude compiles your schema into a grammar and constrains token generation, so the output cannot violate the schema. It guarantees SHAPE, not TRUTH — you still validate values like RFC checksums and totals.
What actually shipped on February 4, 2026?
Claude Structured Outputs went generally available on the Claude Developer Platform and Amazon Bedrock on February 4, 2026. It compiles your JSON Schema into a grammar that constrains token generation during inference, so every response is guaranteed to match your schema’s SHAPE. You pass it via output_config.format with type json_schema. The catch worth internalizing up front: it guarantees structure, not the truth of the values — you still validate those yourself.
If you skimmed the announcement and filed it under “nice JSON helper,” you missed the part that matters for anyone shipping payments code. The mechanism is the feature: the model literally cannot emit a token that would violate the schema. This is not “we tuned the prompt so it usually returns valid JSON.” It is a hard guarantee enforced at the sampling layer — the same family of technique as grammar-constrained decoding. The compiled grammar is cached for 24 hours from last use, a rolling window — so a busy webhook router hitting the same schema all day keeps the cache warm indefinitely and never re-pays the compilation cost. The cache is invalidated only if you change the schema structure or the set of tools in the request (changing just a field name or description does not invalidate it).
It works across recent models — claude-opus-4-8, claude-sonnet-4-6, the 4.5 family, and claude-haiku-4-5 among them. Haiku is the natural cost-sensitive pick for high-volume webhook routing. The anthropic-version header is still 2023-06-01. Check the platform docs for the current model list, since that moves faster than any blog post.
My opinion, flagged as opinion: for payments and fiscal builders this is the most boring-but-load-bearing API change of the quarter. It deletes an entire class of retry-and-parse code from production. I have written that brittle code at least a dozen times. I would like to never write it again.
The exact API: output_config.format (not output_format)
The field name is the load-bearing detail, so let me pin it precisely. You pass an output_config object containing a format of type json_schema:
{
"output_config": {
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": { "...": "..." },
"required": ["..."],
"additionalProperties": false
}
}
}
}
The two settings that make a schema actually tight are additionalProperties: false and an explicit required array. Without them the model has room to add fields you did not ask for or quietly drop ones you depend on. With them, the shape is locked.
Two disambiguations, because both trip people up:
output_config.formatis the canonical GA parameter. The bareoutput_formatyou may still see was the BETA top-level parameter (gated by the headerstructured-outputs-2025-11-13); per the docs it has moved tooutput_config.format, and the old parameter and header keep working only for a transition period before deprecation. Migrate tooutput_config.format. Separately, the Python SDK’smessages.parse()helper accepts anoutput_formatconvenience argument that maps to the same place — that is an SDK ergonomic, not the raw API.- The separate
strict: trueflag is not for JSON outputs. It lives on a tool definition (tools[].strict) and guarantees tool-INPUT schemas. Different feature, different field. Do not wirestrict: trueexpecting it to govern your response JSON.
And the honest limit, stated up front so it frames everything below: this guarantees the SHAPE of the JSON, not the correctness of the VALUES inside it.
Before vs after: killing the JSON.parse-and-pray step
Here is the workflow Structured Outputs replaces. Before: you prompt the model to “return JSON,” then you JSON.parse() inside a try/catch, regex-repair trailing commas and stray markdown fences, retry on failure, and you still get paged at 2 a.m. because a field came back null or arrived as the string "160.00" where your code expected a number.
After: the response is a schema-guaranteed object. The parse cannot throw on shape. Missing-required-field bugs and wrong-type bugs are gone before your code even runs.
What does not change: a guaranteed-shape object can still carry a wrong RFC, an IVA that does not reconcile against the line items, or an enum value the model picked badly. Shape is solved. Truth is still your job. That is the two-step mental model for the rest of this post — trust the shape from Claude, validate the values yourself.
Before — prompt & pray
- Prompt: return JSON and hope
- JSON.parse() in try/catch
- Regex-repair fences & commas
- Retry loop on parse failure
- Still paged on null / wrong-type fields
After — output_config.format
- Schema compiled to a grammar
- Shape guaranteed at token level
- No parse-throw, no repair loop
- Missing/required & type bugs gone
- But: VALUES still need validation
Job A — extracting CFDI fields from a messy receipt
First payments recipe. The job: pull rfc_emisor, rfc_receptor, subtotal, iva, total, and a conceptos[] array (descripcion, cantidad, valor_unitario, importe) out of a messy invoice or receipt so you can build a CFDI. This is the exact extraction step before auto-building the CFDI from those fields.
The request, illustrative — verify field names against the docs for your stack:
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-opus-4-8",
"max_tokens": 1024,
"messages": [
{ "role": "user", "content": "Extrae los campos fiscales de este ticket:\n\nFERRETERIA EL TORNILLO SA DE CV\nRFC: FTO120512ABC\nCliente RFC: XAXX010101000\n2 x Martillo $150.00 = 300.00\n1 x Caja tornillos $80.00\nSubtotal 380.00 IVA 60.80 TOTAL 440.80" }
],
"output_config": {
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"rfc_emisor": { "type": "string" },
"rfc_receptor": { "type": "string" },
"subtotal": { "type": "number" },
"iva": { "type": "number" },
"total": { "type": "number" },
"conceptos": {
"type": "array",
"items": {
"type": "object",
"properties": {
"descripcion": { "type": "string" },
"cantidad": { "type": "number" },
"valor_unitario": { "type": "number" },
"importe": { "type": "number" }
},
"required": ["descripcion", "cantidad", "valor_unitario", "importe"],
"additionalProperties": false
}
}
},
"required": ["rfc_emisor", "rfc_receptor", "subtotal", "iva", "total", "conceptos"],
"additionalProperties": false
}
}
}
}'
Because generation is grammar-constrained, you will never get iva back as the string "160.00 pesos", and you will never get a response missing total. The type and the presence are guaranteed.
The caveat, repeated in context because it bites here specifically: a shape-guaranteed conceptos array can still have an importe that does not equal cantidad * valor_unitario. That reconciliation is value validation, covered below, not something the schema can enforce.
And the disclaimer I always include: I am an engineer, not a contador. The schema does not know SAT rules. clave_prod_serv, uso CFDI, and régimen fiscal are governed by the SAT, not by your JSON Schema. Structured Outputs gets you clean, typed fields fast; fiscal validity is a separate problem.
Job B — routing a Stripe / Mercado Pago webhook into a typed action
Second recipe, the other half of the wedge. The job: turn a noisy webhook payload — or a support message about a payment — into a typed routing decision, instead of maintaining a brittle switch over raw event-type strings across two providers that name things differently. This is a high-volume, low-stakes-per-call classification, so claude-haiku-4-5 is often the right cost/latency tradeoff here.
The schema uses an enum for the action plus a typed amount and a provider enum:
// Illustrative — verify field names against the docs.
const body = {
model: "claude-haiku-4-5",
max_tokens: 512,
messages: [
{ role: "user", content: `Classify this payment event:\n${eventText}` }
],
output_config: {
format: {
type: "json_schema",
schema: {
type: "object",
properties: {
provider: { type: "string", enum: ["stripe", "mercado_pago"] },
action: {
type: "string",
enum: ["capture_payment", "issue_refund", "retry_charge", "flag_review", "ignore"]
},
amount_mxn: { type: "number" },
reason: { type: "string" }
},
required: ["provider", "action", "amount_mxn", "reason"],
additionalProperties: false
}
}
}
};
Because action is a schema enum, the model cannot return a value outside your allowed set. No surprise action string ever reaches your router — "capture_payment_now_pls" is simply not a token the grammar permits.
Two non-negotiables here. First: verify the real webhook with the provider’s signature — Stripe’s Stripe-Signature, Mercado Pago’s x-signature — BEFORE you ever feed it to a model. Structured Outputs is for classification, not authentication; if you are seeing signature failures, fix the verification step first. Second: an enum guarantees the value is IN your set, not that it is the RIGHT one for this event. Always keep a safe default like flag_review, and never auto-move money on the model’s say-so alone. More patterns like this live in the AI agents guides.
Shape is guaranteed. Truth is not. Here’s the value-validation layer.
This section is the credibility core, so let me state it plainly: Structured Outputs guarantees the response is valid JSON matching your schema. The VALUES inside can still be wrong or hallucinated. The model that confidently returns a perfectly-typed total can be confidently returning the wrong number. (This is the same shape-versus-truth split behind building an AI chatbot that does not hallucinate — the structure is never the hard part; the truth is.)
Here is the deterministic layer that has to sit after every extraction near money, illustrated:
const EPS = 0.01; // one-cent tolerance for float drift
const sumaConceptos = conceptos.reduce((s, c) => s + c.importe, 0);
const totalCuadra = Math.abs(subtotal + iva - total) < EPS;
const conceptosCuadran = Math.abs(sumaConceptos - subtotal) < EPS;
if (!isValidRfc(rfc_emisor) || !totalCuadra || !conceptosCuadran) {
// do NOT build the CFDI — route to human review
flagForReview({ rfc_emisor, totalCuadra, conceptosCuadran });
}
The three checks that have to pass, in plain terms:
- RFC: validate format and the check digit. An RFC of the right length and pattern can still be a fabricated taxpayer ID. The Mexican RFC carries a homoclave with a checksum — run your own format and checksum validation, do not trust the string just because it parsed.
- Totals: reconcile, do not assume. Assert
subtotal + iva == total, and assertsum(conceptos.importe) == subtotal, within a one-cent tolerance for float drift, before you trust the extraction. - Enums and routing: treat the model’s chosen action as a suggestion. Keep a safe default and a human-review path for high-value or low-confidence cases.
My opinion, flagged: the correct architecture is Claude for SHAPE plus your own deterministic code for TRUTH. Skipping the second half is exactly how you ship a CFDI whose total does not add up — and with the SAT, that is not a bug ticket, that is a rejected invoice.
The disclaimer again because it is load-bearing: for CFDI, SAT rules govern. This post is the engineering harness, not fiscal advice. Confirm fiscal correctness with your contador.
Ship it: the five-step recipe
- Define the JSON Schema with
additionalProperties: falseand an explicitrequiredlist for every field you depend on. - Send it via
output_config.formatwith typejson_schemaandanthropic-version: 2023-06-01. Reuse the same schema so the rolling 24h-from-last-use grammar cache stays warm. - Trust the SHAPE. Delete the regex repair and the parse-retry loop — the object’s structure is guaranteed, so that code is now dead weight.
- Validate the VALUES. RFC checksum, totals reconcile, enums fall back to a safe default. This step is non-negotiable for anything touching money or fiscal data.
- Act. Build the CFDI, route the webhook, move the workflow forward — with human review gating anything irreversible.
- Define the schemaadditionalProperties:false + required[] on every dependency
- Send output_config.formattype json_schema · reuse it to keep the rolling cache warm
- Trust the SHAPEDrop regex repair & parse-retry — guaranteed structure
- Validate the VALUESRFC checksum · totals reconcile · enum default
- ActBuild CFDI / route webhook · human review on irreversible steps
FAQ
Is the parameter output_config or output_format? The canonical GA parameter is output_config.format with type json_schema. The bare output_format was the earlier beta top-level parameter (header structured-outputs-2025-11-13); it still works during a transition window but is deprecated — migrate. The Python SDK’s messages.parse() also exposes an output_format convenience argument that maps to output_config.format.
Does strict:true apply to JSON outputs? No. strict: true lives on a tool definition (tools[].strict) and validates tool INPUTS. It is a different feature from response Structured Outputs.
Does it stop hallucinations? No. It guarantees the JSON shape, not the truth of the values. You still validate RFCs, totals, and enums yourself.
Which models support it? Recent Claude models including claude-opus-4-8, claude-sonnet-4-6, the 4.5 family, and claude-haiku-4-5. Check the docs for the current list.
Where is it available? Generally available on the Claude Developer Platform and Amazon Bedrock as of February 4, 2026.
Can I use it for CFDI directly? It extracts the fields reliably (shape), but SAT rules still govern fiscal validity. Engineer, not a contador — pair it with value validation and your contador’s sign-off.
The one-line takeaway
Let Claude guarantee the shape with output_config.format; you guarantee the truth with RFC checksums and reconciled totals. That split is what makes Structured Outputs safe to put near money.