For integrators
Your AI never parses our sentences.
Every answer is a code it can act on. Factum speaks to a client in closed, typed vocabularies, never in prose. Every verdict, refusal and gap is a registered code, and every refusal carries a remedy class. The prose is for humans. A client never needs it.
4 remedy classes
A client holds the whole decision table. The vocabularies are disjoint and enumerable: 4 remedy classes over a closed list of codes.
| Remedy class | What the client does | Intake codes | Certify codes |
|---|---|---|---|
| retry | A fault, not the caller's doing. Send it again. | 0 | 1 |
| declare | Re-declare the named member and resubmit. | 15 | 6 |
| different_input | The request or the document must change. | 24 | 45 |
| not_curable | Nothing the caller can send will work. | 1 | 4 |
| Every code maps to exactly one class. | 40 | 56 |
Closed vocabularies
Every one of the 40 document-intake codes and every one of the 56 certify-path codes carries a remedy class. Each table's totality is held when the engine is built, in both directions: a missing row and a stranger row both stop the build, and there is no default branch.
The claim covers these two registered vocabularies. It is not a claim about every refusal the service can return.
The 56 certify-path codes, across 7 stages
| Code | Status | Stage | Remedy | Declaration needed | Cure fields |
|---|---|---|---|---|---|
| unknown_report_type | 404 | catalogue | different_input | availableReportTypes | |
| unknown_member_report_type | 404 | catalogue | different_input | availableReportTypes | |
| report_type_declares_no_questions | 422 | report_type | different_input | ||
| cross_references_need_a_segmented_document | 422 | report_type | declare | ReportType.statementPoints[].inSegment | |
| certificate_surface_unhonorable | 422 | report_type | declare | ReportType.surface | |
| member_report_type_kind_ambiguous | 422 | report_type | different_input | ||
| source_kind_mismatch | 422 | document | different_input | ||
| document_unreadable | 422 | document | different_input | ||
| pairing_required | 422 | parameters | declare | pair | |
| no_declared_pairing | 422 | parameters | different_input | ||
| revision_pair_required | 422 | parameters | declare | revision | |
| report_type_compares_no_revision | 422 | report_type | different_input | ||
| revision_baseline_unreadable | 422 | document | different_input | ||
| revision_declaration_malformed | 422 | parameters | different_input | ||
| revision_roles_name_one_document | 422 | parameters | different_input | ||
| revision_documents_do_not_match_the_declaration | 422 | parameters | different_input | ||
| as_of_malformed | 422 | parameters | different_input | ||
| as_of_without_history | 422 | parameters | different_input | ||
| as_of_entity_resolution_declared | 422 | parameters | not_curable | ||
| as_of_after_issue | 422 | parameters | different_input | ||
| portfolio_declaration_malformed | 422 | parameters | different_input | ||
| member_document_empty | 422 | parameters | different_input | ||
| portfolio_declares_no_documents | 422 | parameters | different_input | ||
| no_document_submitted | 422 | parameters | different_input | ||
| document_not_in_the_declared_portfolio | 422 | parameters | different_input | ||
| citation_corpus_not_available | 422 | corpus | declare | registerCitationCorpus | |
| citation_corpus_mismatch | 422 | corpus | declare | registerCitationCorpus | |
| citation_corpus_loading | 422 | corpus | retry | ||
| citation_corpus_load_refused | 422 | corpus | not_curable | ||
| member_certificate_refused | 422 | member | not_curable | ||
| reconciliation_claim_refused | 422 | accounting | not_curable | ||
| fill_malformed | 422 | parameters | different_input | ||
| fill_schema_unknown | 422 | parameters | different_input | ||
| fill_key_not_scope | 422 | parameters | different_input | ||
| fill_mode_required | 422 | parameters | different_input | ||
| fill_column_invalid | 422 | parameters | different_input | ||
| fill_source_unknown | 422 | parameters | different_input | ||
| fill_source_duplicated | 422 | parameters | different_input | ||
| fill_workbook_unreadable | 422 | document | different_input | ||
| fill_sheet_unknown | 422 | document | different_input | ||
| fill_source_unreadable | 422 | document | different_input | ||
| fill_nothing_to_fill | 422 | document | different_input | ||
| composition_malformed | 422 | parameters | different_input | ||
| composition_schema_unknown | 422 | parameters | different_input | ||
| composition_key_not_scope | 422 | parameters | different_input | ||
| composition_id_invalid | 422 | parameters | different_input | ||
| composition_with_report_type | 422 | parameters | different_input | ||
| composition_base_unknown | 404 | catalogue | different_input | availableReportTypes | |
| composition_item_unknown | 422 | parameters | different_input | ||
| composition_item_duplicated | 422 | parameters | different_input | ||
| composition_input_not_taken | 422 | parameters | different_input | ||
| composition_mechanism_not_composable | 422 | parameters | different_input | ||
| composition_settings_conflict | 422 | parameters | different_input | ||
| composition_boundary_conflict | 422 | parameters | different_input | ||
| composition_takes_nothing | 422 | parameters | different_input | ||
| composition_not_registrable | 422 | report_type | different_input |
The 40 document-intake codes
| Code | Remedy | Declaration needed |
|---|---|---|
| format_not_in_union | declare | format |
| both_bytes_and_html | different_input | |
| no_payload | different_input | |
| content_type_unreadable | declare | contentType |
| content_type_disagrees | declare | contentType |
| format_disagrees_with_html | declare | format |
| filename_disagrees_with_html | declare | filename |
| not_html_content | different_input | |
| format_disagrees_with_xlsx | declare | format |
| filename_disagrees_with_xlsx | declare | filename |
| format_disagrees_with_docx | declare | format |
| filename_disagrees_with_docx | declare | filename |
| docx_unreadable | different_input | |
| format_disagrees_with_json | declare | format |
| filename_disagrees_with_json | declare | filename |
| json_unreadable | different_input | |
| json_not_an_array | different_input | |
| json_empty_array | different_input | |
| json_element_not_object | different_input | |
| json_nested_value | different_input | |
| json_inconsistent_keys | different_input | |
| json_duplicate_key | different_input | |
| json_number_not_representable | different_input | |
| container_not_inspectable | different_input | |
| container_not_a_spreadsheet | different_input | |
| bytes_unrecognised | different_input | |
| filename_disagrees_with_pdf | declare | filename |
| pdf_format_undeclared | declare | format |
| format_disagrees_with_pdf | declare | format |
| pdf_is_a_scan | not_curable | |
| pdf_has_no_pages | different_input | |
| workbook_unreadable | different_input | |
| workbook_multi_sheet | different_input | |
| sheet_no_header_block | different_input | |
| sheet_header_cell_empty | different_input | |
| csv_encoding_undetermined | different_input | |
| csv_dialects_too_close | different_input | |
| csv_no_dialect_above_floor | different_input | |
| csv_empty | different_input | |
| format_disagrees_with_csv | declare | format |
Verdict and code
The same code at refused says fix the input. At fault it says retry. The verdict separates them, so a client never resubmits a fault forever.
| Verdict | What it says |
|---|---|
| refused | The engine will not do this as asked. The remedy class says what to change. |
| gap | A declared item has no answer. It is named, with its code. |
| fault | Something went wrong on the service side. Retry. |
| mixed | More than one of the above in a single answer, each counted. |
A refusal is a formal statement
Every refusal is RFC 9457 application/problem+json, so a generic client works too. Below, a PDF is sent to intake with no declared format. Two PDF formats cannot be told apart from the bytes, and the wrong reader returns confident nonsense. Factum refuses, names the member to declare, and lists the values it accepts.
{
"requestId": "94835c8a-4ff5-4e1d-8305-1ce09bbc9f1e",
"type": "/problems/intake/pdf_format_undeclared",
"title": "Pdf format undeclared",
"status": 422,
"detail": "a PDF was supplied with no declared `format` - `pdf_table` and `pdf_form` are the same file type and cannot be told apart from the bytes. Running the …",
"verdict": "refused",
"echo": {
"contentType": "application/pdf",
"bytes": 903
},
"stage": "intake",
"decisions": [
{
"code": "pdf_format_undeclared",
"reason": "a PDF was supplied with no declared `format` - `pdf_table` and `pdf_form` are the same file type and cannot be told apart from the bytes. Running the …",
"remedy": "declare",
"cure": {
"acceptedFormats": [
"pdf_table",
"pdf_form"
]
},
"declarationNeeded": "format"
}
],
"ok": false,
"code": "pdf_format_undeclared",
"reason": "a PDF was supplied with no declared `format` - `pdf_table` and `pdf_form` are the same file type and cannot be told apart from the bytes. Running the …",
… 2 more fields: two that carry wire identifiers, printed in the API reference
}
The proof is a test, not a claim. Factum's own suite hands a client a refusal body with ok, code, reason, detail and title deleted, and requires it to still decide correctly. A client that fell back to the prose would not pass.
| What was sent | Status | Code | Stage | Remedy | What the client does |
|---|---|---|---|---|---|
| The corpus invoice, under a report type id that is not registered. | 404 | refusedunknown_report_type | catalogue | different_input | Change the input. The body lists 16 availableReportTypes. |
| 64 random bytes to the certify door, typed application/octet-stream. | 415 | refusedpayload | transport | different_input | Change the input. The body lists 6 acceptedContentTypes. |
| A one-page PDF to document intake, with no declared format. | 422 | refusedpdf_format_undeclared | intake | declare | Declare format: one of pdf_table, pdf_form, and resubmit. |
| 64 random bytes to document intake. | 422 | refusedbytes_unrecognised | intake | different_input | Change the input. These bytes will not be read. |
A certificate is data
One REST call returns the certificate as JSON. The blocks below are whole objects from the recorded certificate on the demonstration page, at the paths named. Receipts and rule references are deterministic ids: the same input, run again, yields the same ones.
certificate.answers[0], a located answer
{
"dataPointId": "dp-bol-number",
"label": "the bill of lading number",
"status": "resolved",
"slotId": "S1",
"factId": "fact-81627aff4e85c7ca",
"value": "PBLU2026034771",
"subject": "Bill of Lading Number",
"field": "Value",
"at": {
"kind": "grid",
"table": 1,
"row": 2,
"col": 2
}
}
certificate.document
{
"name": "syn-inv-01-complete-import.html",
"kind": "html",
"bytes": 3442,
"sha256": "409c2b8b6b3a9e9731ee590dd552e4b4179c284d5aecfbdcee144a78b2720104"
}
certificate.signature
{
"alg": "ed25519",
"canonical": "VD-CANON-1",
"keyId": "vd-418b7ced1cea5f8e",
"signature": "rQUj3NgAFK1qzsTCT2RgTpYuUi+iIENMiqya5Yw22/6IbSb3HJWpic6/nx7vbPsSDflW2U3mknKQAmrv/I6NBw=="
}
certificate.answers[10], an absent answer
{
"dataPointId": "dp-earliest-return",
"label": "the earliest return date",
"status": "gap",
"code": "no_fact_answers_this_point",
"derivation": {
"entity": "drv:071ed9039ffbfd065d19f8a9e46c640d",
"wasGeneratedBy": "…/certify/data-point-resolution@1",
"wasDerivedFrom": [
"rule:data-point/fmc-demurrage-detention-invoice-completeness%2Fdp-earliest-return",
"input:document/syn-inv-01-complete-import.html"
]
},
"decision": {
"code": "no_fact_answers_this_point",
"reason": "the earliest return date: no_fact_answers_this_point",
"remedy": "not_curable",
"evidence": {
"family": "answers",
"itemId": "dp-earliest-return",
"label": "the earliest return date"
}
}
}
certificate.arithmetic[1], a computed check
{
"arithmeticPointId": "ap-arith-charge-total",
"label": "Does the number of dates charged multiplied by the daily rate equal the total amount due?",
"status": "satisfied",
"statement": "5 day(s) charged at 275.00 is 1375.00; the invoice states 1375.00.",
"rounding": null,
"cite": "§ 541.6(c)(1)–(3)",
"derivation": {
"entity": "drv:12250656e4fd1dc4dc741516fc95e746",
"wasGeneratedBy": "…/arithmetic/count-times-rate-equals-total@1",
"wasDerivedFrom": [
"rule:arithmetic-point/fmc-demurrage-detention-invoice-completeness%2Fap-arith-charge-total",
"node:fact-1f2875fc61c25ccc#value",
"node:fact-d6ee3650e96769fd#value",
"node:fact-34fe5c129f430c4f#value",
"node:fact-f6fe79882ec59943#value"
]
}
}
The full signed bytes: run-1.json. Check them with the Factum Verifier.
Canonical form and signature
A certificate is signed with Ed25519 over a published canonical form. The rule is short enough to implement from its statement, and the verifier carries a frozen known-answer test of it.
Retired public keys stay published, and the certificate's own key id selects among them. A certificate issued before a rotation still verifies.
The canonical rule, as the engine states it
VD-CANON-1: UTF-8 JSON, no insignificant whitespace, object keys sorted ascending by UTF-16 code unit, undefined object values dropped and undefined array elements emitted as null (both matching JSON.stringify, so an in-memory certificate and a parsed one canonicalize identically), strings and numbers emitted by JSON.stringify (ECMAScript QuoteJSONString and Number::toString, as RFC 8785 specifies), -0 normalised to 0, and a REFUSAL on a non-finite number, on any value that is not JSON data, and on a key named "signature" at any depth.
The same doors as the Console
The Console is a reference client. It calls the doors a program calls, and every screen prints the call it rendered. Nothing exists only in the interface.
A composed checklist rides on the certify call as one header that carries the declaration. A two-source report names its declared pairing on the same call. A workbook fill, a revision comparison and document intake are doors of the same shape, with the same refusal envelope.
An AI engine is registered per tenant: a model, a key and a transport. Two vendors' own interfaces are spoken, and so is the common chat-completions interface, at an address the tenant gives.
Registration is not admission. An engine is admitted by measurement on the duties it will perform, and an engine that has not been measured writes nothing. How admission is measured
Each door is shown on a recorded run, request line included: worked examples.
Arguments, as a graph
The comparison report type serializes its verdicts as an argument graph in the Argument Interchange Format. The graph attests structure. An argument that is "in" has not thereby "won".
The API reference, with every wire identifier printed exactly, is provided with access.