Documentation · Maqaas v1.0.0

Maqaas v1 — worked examples

All values below are synthetic. No real vendor, invoice, bank account, or company appears in this file. Responses are abridged: rules_evaluated contains every rule in a real response, not only the ones shown.


A. Clean invoice → PROCESS

Everything matches, every field verifies, no rule objects.

Request — POST /v1/invoice/decision

{
  "client_reference": "ap-1001",
  "message": { "from": "ar@northgate-fasteners.example", "subject": "Invoice NGF-2026-0188" },
  "document": { "filename": "invoice.pdf", "media_type": "text/plain",
                "text": "Northgate Fasteners Ltd\nInvoice No. NGF-2026-0188\nDate 2026-09-01\nPO-7741\nTOTAL DUE GBP 7,560.00" },
  "po_context": { "po_number": "PO-7741", "total": { "amount": 7560, "currency": "GBP" }, "status": "open" },
  "vendor_context": { "vendor_id": "V-200", "known_domains": ["northgate-fasteners.example"], "known_invoice_numbers": [] },
  "rules": { "allowed_variance_pct": 0.05, "require_po": true }
}

Response — 200

{
  "request_id": "req_4b91c2fe7a0d4e18b6c3",
  "client_reference": "ap-1001",
  "versions": { "api": "v1", "schema": "2026-09-06.v3", "rules": "1.3.0", "engine": "1.0.0",
                "extraction": "2.0.0", "resolution_catalog": "1.4.0" },
  "decision": "PROCESS",
  "needs_review": false,
  "recommended_route": null,
  "decision_confidence": 1,
  "confidence_basis": "weakest critical signal: invoice_number=1.00",
  "document_type": "invoice",
  "extracted_facts": {
    "document_type": "invoice",
    "classification_confidence": 1,
    "vendor_name": { "value": "Northgate Fasteners Ltd", "model_confidence": 1, "source_verified": true,
                     "confidence": 1, "verification_reason": "exact",
                     "source": { "locator": "attachment:invoice.pdf", "snippet": "Northgate Fasteners Ltd" } },
    "invoice_number": { "value": "NGF-2026-0188", "model_confidence": 1, "source_verified": true,
                        "confidence": 1, "verification_reason": "exact",
                        "source": { "locator": "attachment:invoice.pdf", "snippet": "Invoice No. NGF-2026-0188" } },
    "invoice_total": { "value": { "amount": 7560, "currency": "GBP" }, "model_confidence": 1,
                       "source_verified": true, "confidence": 1, "verification_reason": "exact",
                       "source": { "locator": "attachment:invoice.pdf", "snippet": "TOTAL DUE GBP 7,560.00" } },
    "po_number": { "value": "PO-7741", "model_confidence": 1, "source_verified": true, "confidence": 1,
                   "verification_reason": "exact", "source": { "locator": "attachment:invoice.pdf", "snippet": "PO-7741" } },
    "vendor_id": { "value": null, "confidence": 0, "source_verified": null },
    "bank_details": { "value": null, "confidence": 0, "source_verified": null },
    "missing_fields": ["vendor_id", "bank_details"],
    "conflicting_fields": []
  },
  "rules_fired": [],
  "reasons": [],
  "warnings_structured": [
    { "code": "MISSING_FIELD", "category": "data_quality", "field": "vendor_id", "rule_id": null,
      "detail": null, "message": "vendor_id was not present in the supplied input." },
    { "code": "RULE_NOT_EVALUATED", "category": "data_quality", "field": null, "rule_id": "BANK_DETAILS_CHANGED",
      "detail": "no payment details found on the invoice",
      "message": "BANK_DETAILS_CHANGED could not be evaluated: no payment details found on the invoice" }
  ],
  "resolution": {
    "catalog_version": "1.4.0",
    "blocking_conditions": [],
    "resolution_requirements": [],
    "recommended_action": "NONE",
    "resolution_plan": [],
    "decision_if_resolved": null
  },
  "extraction": { "mode": "model", "model_calls": 1, "latency_ms": 794 },
  "processed_at": "2026-09-08T10:14:02.881Z"
}

resolution is always present, including on a clean PROCESS. Nothing is blocking, so every list is empty and recommended_action is NONE. Switch on decision first; read resolution when the decision is not PROCESS.

Note that PROCESS still reports what could not be checked. BANK_DETAILS_CHANGED was skipped because the invoice states no payment details — that is uncertainty, not a pass.


B. Review case → REVIEW (unverified identifier)

The invoice number was read as NGF-2026-01?8 — one character is a placeholder. The value is not repaired; it is flagged and routed.

Response — 200 (abridged)

{
  "request_id": "req_7c02fa5d13b84a9e0f77",
  "decision": "REVIEW",
  "needs_review": true,
  "recommended_route": "accounts_payable_clerk",
  "decision_confidence": 0.5,
  "confidence_basis": "weakest critical signal: invoice_number=0.50",
  "extracted_facts": {
    "invoice_number": {
      "value": "NGF-2026-01?8",
      "model_confidence": 0.92,
      "source_verified": false,
      "confidence": 0.5,
      "verification_reason": "identifier_contains_placeholder",
      "source": { "locator": "attachment:invoice.pdf", "snippet": "Invoice No. NGF-2026-01?8" }
    }
  },
  "rules_fired": [
    { "rule_id": "LOW_EXTRACTION_CONFIDENCE", "rule_version": "1.0.0", "status": "fired", "outcome": "fired",
      "decision": "REVIEW", "severity": 40, "fields": ["invoice_number"],
      "message": "Extraction confidence below the 0.8 floor for: invoice_number=0.50.",
      "evidence": { "inputs": { "invoice_number_confidence": 0.5, "invoice_total_confidence": 1, "vendor_name_confidence": 1 },
                    "threshold": 0.8, "computed": 1, "comparison": "1 critical field(s) below 0.8", "sources": [] } },
    { "rule_id": "UNVERIFIED_IDENTIFIER", "rule_version": "1.0.0", "status": "fired", "outcome": "fired",
      "decision": "REVIEW", "severity": 40, "fields": ["invoice_number"],
      "message": "Identifier present but not supported by the document: invoice_number (identifier_contains_placeholder, confidence 0.50). The value has not been altered; a human should confirm it.",
      "evidence": { "inputs": { "fields": "invoice_number", "reasons": "invoice_number=identifier_contains_placeholder", "identifiers_checked": 5 },
                    "threshold": "source_verified must not be false", "computed": 1,
                    "comparison": "1 identifier(s) present but unverified", "sources": [] } }
  ],
  "warnings_structured": [
    { "code": "UNVERIFIED_FIELD", "category": "decision_affecting", "field": "invoice_number", "rule_id": null,
      "detail": "identifier_contains_placeholder",
      "message": "invoice_number could not be confirmed against the document (identifier_contains_placeholder)." }
  ]
}

C. Bank details changed → HOLD_FOR_REVIEW

The canonical case. The invoice states payment details that differ from the vendor record you sent. This is the highest-value fraud signal in accounts payable, so it holds rather than reviews.

Both blocks below are the real request and response, captured from POST /v1/invoice/decision. Only request_id and processed_at are substituted, and rules_evaluated is abridged to its outcomes.

Request

{
  "client_reference": "ap-8891",
  "extracted_facts": {
    "document_type": "invoice",
    "classification_confidence": 0.99,
    "vendor_name":    { "value": "Acme Supplies Ltd", "confidence": 0.95 },
    "vendor_id":      { "value": "V-100", "confidence": 0.95 },
    "vat_number":     { "value": null, "confidence": 0 },
    "company_registration_number": { "value": null, "confidence": 0 },
    "invoice_number": { "value": "INV-2026-0417", "confidence": 0.95 },
    "invoice_total":  { "value": { "amount": 41200, "currency": "GBP" }, "confidence": 0.95 },
    "tax_total":      { "value": { "amount": 0, "currency": "GBP" }, "confidence": 0.95 },
    "invoice_date":   { "value": "2026-08-04", "confidence": 0.95 },
    "po_number":      { "value": "PO-500", "confidence": 0.95 },
    "bank_details": {
      "value": { "account_number_last4": "4417", "sort_code": "60-84-12", "bank_name": "Northern Trust Commercial" },
      "confidence": 0.95,
      "source": { "locator": "attachment:invoice.pdf", "snippet": "Sort Code: 60-84-12  Account ending 4417" }
    },
    "missing_fields": [],
    "conflicting_fields": []
  },
  "po_context": { "po_number": "PO-500", "total": { "amount": 41200, "currency": "GBP" }, "vendor_id": "V-100", "status": "open" },
  "vendor_context": {
    "vendor_id": "V-100",
    "legal_name": "Acme Supplies Ltd",
    "known_domains": ["acme-supplies.com"],
    "known_invoice_numbers": ["INV-2026-0388"],
    "trusted_bank_details": { "account_number_last4": "9021", "sort_code": "20-00-00", "bank_name": "Barclays Commercial" }
  },
  "rules": {
    "allowed_variance_pct": 0.05, "require_po": true, "bank_change_requires_review": true,
    "min_field_confidence": 0.8, "require_currency_match": true
  }
}

This example sends every field for completeness, but it does not have to. Caller-supplied facts may be partial: only document_type is required. Omitted fields are materialised internally as { "value": null, "confidence": 0, "source_verified": null }, and are not added to missing_fields — that list means the extractor looked and could not find the value. Fields you do send are validated exactly as normal. See §4 of API_V1.md.

Response — 200

{
  "request_id": "req_3f9a2c10b7e44d0f8c1e5a7b",
  "client_reference": "ap-8891",
  "versions": {
    "api": "v1", "schema": "2026-09-06.v3", "rules": "1.3.0",
    "engine": "1.0.0", "extraction": "2.0.0", "resolution_catalog": "1.4.0"
  },
  "decision": "HOLD_FOR_REVIEW",
  "needs_review": true,
  "recommended_route": "accounts_payable_manager",
  "decision_confidence": 0.95,
  "confidence_basis": "weakest critical signal: invoice_number=0.95",
  "rules_fired": [
    {
      "rule_id": "BANK_DETAILS_CHANGED",
      "rule_version": "1.2.0",
      "outcome": "fired", "status": "fired",
      "decision": "HOLD_FOR_REVIEW", "severity": 70,
      "fields": ["bank_details.account_number_last4", "bank_details.sort_code"],
      "message": "Payment details differ from the trusted vendor record (account_number_last4, sort_code). Possible invoice redirection fraud.",
      "evidence": {
        "inputs": {
          "vendor_id": "V-100",
          "compared_fields": "account_number_last4, sort_code",
          "changed_fields": "account_number_last4, sort_code",
          "fields": "bank_details.account_number_last4, bank_details.sort_code",
          "expected": "account_number_last4=9021, sort_code=20-00-00",
          "observed": "account_number_last4=4417, sort_code=60-84-12",
          "bank_name_differs": true,
          "extraction_confidence": 0.95,
          "confidence_floor": 0.8,
          "source_verified": null,
          "confirmed": true
        },
        "threshold": "any normalised identifier differs",
        "computed": 2,
        "comparison": "2 of 2 compared identifier(s) differ from the trusted record (normalised); bank_name_differs=true",
        "sources": [{ "locator": "attachment:invoice.pdf", "snippet": "Sort Code: 60-84-12  Account ending 4417" }]
      },
      "skipped_reason": null
    }
  ],
  "resolution": {
    "catalog_version": "1.4.0",
    "blocking_conditions": [
      {
        "rule_id": "BANK_DETAILS_CHANGED",
        "type": "bank_details_changed",
        "severity": "HOLD_FOR_REVIEW",
        "fields": ["bank_details.account_number_last4", "bank_details.sort_code"],
        "message": "Payment details differ from the trusted vendor record (account_number_last4, sort_code). Possible invoice redirection fraud.",
        "evidence": {
          "expected": "account_number_last4=9021, sort_code=20-00-00",
          "observed": "account_number_last4=4417, sort_code=60-84-12",
          "comparison": "2 of 2 compared identifier(s) differ from the trusted record (normalised); bank_name_differs=true"
        }
      }
    ],
    "resolution_requirements": [
      {
        "rule_id": "BANK_DETAILS_CHANGED",
        "requirement_type": "independent_bank_verification",
        "acceptable_evidence": [
          "approved_vendor_master_update",
          "verification_via_previously_trusted_supplier_contact",
          "signed_bank_change_authorisation_on_vendor_letterhead"
        ],
        "instruction": "Confirm the new payment details through a channel independent of this invoice before paying. Do not use contact details taken from the invoice itself."
      }
    ],
    "recommended_action": "REQUEST_VENDOR_BANK_VERIFICATION",
    "resolution_plan": [
      {
        "step": 1, "rule_id": "BANK_DETAILS_CHANGED",
        "action": "REQUEST_VENDOR_BANK_VERIFICATION",
        "owner": "VENDOR_MANAGEMENT",
        "requirement_type": "independent_bank_verification",
        "fields": ["bank_details.account_number_last4", "bank_details.sort_code"],
        "instruction": "Confirm the new payment details through a channel independent of this invoice before paying. Do not use contact details taken from the invoice itself."
      },
      {
        "step": 2, "rule_id": null,
        "action": "RESUBMIT_FOR_DECISION", "owner": "HOST_SYSTEM",
        "requirement_type": "decision_re_evaluation", "fields": [],
        "instruction": "Call the decision endpoint again with the updated context. `decision_if_resolved` states what the decision becomes once every step above is cleared and nothing else changes; supplying new context may activate rules listed in rules_still_not_evaluated."
      }
    ],
    "decision_if_resolved": {
      "decision": "PROCESS",
      "conditional": true,
      "assumption": {
        "resolved_rule_ids": ["BANK_DETAILS_CHANGED"],
        "statement": "Assumes this condition is cleared. No facts were substituted, corrected, or invented. 2 rules could not be evaluated for lack of context and are assumed not to object; see rules_still_not_evaluated."
      },
      "rules_still_not_evaluated": [
        { "rule_id": "APPROVAL_THRESHOLD", "reason": "approval_threshold_not_configured" },
        { "rule_id": "VENDOR_IDENTITY_MISMATCH", "reason": "vendor_domain_or_sender_not_supplied" }
      ],
      "remaining_rules_fired": [],
      "caveat": "Deterministic result with the named blockers set aside and all other supplied facts unchanged. Supplying new context to clear a blocker may activate rules listed in rules_still_not_evaluated."
    }
  },
  "extraction": { "mode": "caller_supplied", "model_calls": 0, "latency_ms": null },
  "processed_at": "2026-09-11T09:14:22.411Z"
}

How to read this

  1. Stop the payment. decision is HOLD_FOR_REVIEW and needs_review is true. Route to accounts_payable_manager.
  2. Show the reviewer the evidence. expected vs observed names both values; sources gives the locator and the snippet they came from.
  3. Act on recommended_action, not on the prose. It is a closed enum: REQUEST_VENDOR_BANK_VERIFICATION, owned by VENDOR_MANAGEMENT.
  4. decision_if_resolved.conditional is true — PROCESS here means “nothing that could be evaluated objects”, not “safe to pay”. Two rules could not run. Supply an approval_threshold and message.from and they will be evaluated on the next call.
  5. Re-submit once the requirement is met. Nothing about the bank details is changed by Maqaas; you update your own vendor master.

If the payment identifiers match and only bank_name is written differently, the rule passes and reports bank_name_differs: true in evidence. If you supply no trusted_bank_details at all, see section D.


D. No trusted vendor bank record → REVIEW

The vendor is known, but your master record carries no trusted_bank_details. The invoice does state payment details.

This is the case where an API can most easily lie. There is nothing to compare against, so Maqaas must not report a comparison — and it must not stay silent either, because paying an unverified account is the exact failure this product exists to prevent. It fires the same rule at the lower REVIEW severity and says why in evidence: computed: "no_baseline".

Both blocks below are the real request and response from POST /v1/invoice/decision. Only request_id and processed_at are substituted, and rules_evaluated is omitted.

Request

{
  "client_reference": "ap-9107",
  "extracted_facts": {
    "document_type": "invoice",
    "classification_confidence": 0.99,
    "vendor_name":    { "value": "Meridian Facilities Ltd", "confidence": 0.95 },
    "vendor_id":      { "value": "V-420", "confidence": 0.95 },
    "vat_number":     { "value": null, "confidence": 0 },
    "company_registration_number": { "value": null, "confidence": 0 },
    "invoice_number": { "value": "INV-4471", "confidence": 0.95 },
    "invoice_total":  { "value": { "amount": 8750, "currency": "GBP" }, "confidence": 0.95 },
    "tax_total":      { "value": { "amount": 0, "currency": "GBP" }, "confidence": 0.95 },
    "invoice_date":   { "value": "2026-09-02", "confidence": 0.95 },
    "po_number":      { "value": "PO-771", "confidence": 0.95 },
    "bank_details": {
      "value": { "account_number_last4": "3318", "sort_code": "04-00-75", "bank_name": "Monzo Business" },
      "confidence": 0.95,
      "source": { "locator": "attachment:invoice.pdf", "snippet": "Sort Code: 04-00-75  Account ending 3318" }
    },
    "missing_fields": [],
    "conflicting_fields": []
  },
  "po_context": { "po_number": "PO-771", "total": { "amount": 8750, "currency": "GBP" }, "vendor_id": "V-420", "status": "open" },
  "vendor_context": {
    "vendor_id": "V-420",
    "legal_name": "Meridian Facilities Ltd",
    "known_domains": ["meridian-fm.co.uk"],
    "known_invoice_numbers": []
  },
  "rules": {
    "allowed_variance_pct": 0.05, "require_po": true, "bank_change_requires_review": true,
    "min_field_confidence": 0.8, "require_currency_match": true
  }
}

Response — 200

{
  "request_id": "req_58df8356db464c3eb2af43da",
  "client_reference": "ap-9107",
  "versions": {
    "api": "v1", "schema": "2026-09-06.v3", "rules": "1.3.0",
    "engine": "1.0.0", "extraction": "2.0.0", "resolution_catalog": "1.4.0"
  },
  "decision": "REVIEW",
  "needs_review": true,
  "recommended_route": "accounts_payable_clerk",
  "decision_confidence": 0.95,
  "confidence_basis": "weakest critical signal: invoice_number=0.95",
  "document_type": "invoice",
  "rules_fired": [
    {
      "rule_id": "BANK_DETAILS_CHANGED",
      "rule_version": "1.2.0",
      "outcome": "fired", "status": "fired",
      "decision": "REVIEW",
      "severity": 40,
      "fields": [],
      "message": "Invoice carries payment details but no trusted vendor bank record exists to compare against.",
      "evidence": {
        "inputs": { "has_trusted_record": false, "fields": null },
        "threshold": "bank_change_requires_review=true",
        "computed": "no_baseline",
        "comparison": "trusted_bank_details is absent",
        "sources": [
          { "locator": "attachment:invoice.pdf", "snippet": "Sort Code: 04-00-75  Account ending 3318" }
        ]
      },
      "skipped_reason": null
    }
  ],
  "reasons": [
    "Invoice carries payment details but no trusted vendor bank record exists to compare against."
  ],
  "warnings_structured": [
    { "code": "RULE_NOT_EVALUATED", "category": "data_quality", "field": null,
      "rule_id": "DUPLICATE_INVOICE_NUMBER", "detail": "no invoice history supplied for this vendor",
      "message": "DUPLICATE_INVOICE_NUMBER could not be evaluated: no invoice history supplied for this vendor" },
    { "code": "RULE_NOT_EVALUATED", "category": "data_quality", "field": null,
      "rule_id": "APPROVAL_THRESHOLD", "detail": "no approval_threshold configured",
      "message": "APPROVAL_THRESHOLD could not be evaluated: no approval_threshold configured" },
    { "code": "RULE_NOT_EVALUATED", "category": "data_quality", "field": null,
      "rule_id": "VENDOR_IDENTITY_MISMATCH", "detail": "no sender address supplied",
      "message": "VENDOR_IDENTITY_MISMATCH could not be evaluated: no sender address supplied" }
  ],
  "missing_fields": [],
  "resolution": {
    "catalog_version": "1.4.0",
    "blocking_conditions": [
      {
        "rule_id": "BANK_DETAILS_CHANGED",
        "type": "bank_details_changed",
        "severity": "REVIEW",
        "fields": [],
        "message": "Invoice carries payment details but no trusted vendor bank record exists to compare against.",
        "evidence": {
          "expected": "bank_change_requires_review=true",
          "observed": "no_baseline",
          "comparison": "trusted_bank_details is absent"
        }
      }
    ],
    "resolution_requirements": [
      {
        "rule_id": "BANK_DETAILS_CHANGED",
        "requirement_type": "independent_bank_verification",
        "acceptable_evidence": [
          "approved_vendor_master_update",
          "verification_via_previously_trusted_supplier_contact",
          "signed_bank_change_authorisation_on_vendor_letterhead"
        ],
        "instruction": "Confirm the new payment details through a channel independent of this invoice before paying. Do not use contact details taken from the invoice itself."
      }
    ],
    "recommended_action": "REQUEST_VENDOR_BANK_VERIFICATION",
    "resolution_plan": [
      {
        "step": 1, "rule_id": "BANK_DETAILS_CHANGED",
        "action": "REQUEST_VENDOR_BANK_VERIFICATION",
        "owner": "VENDOR_MANAGEMENT",
        "requirement_type": "independent_bank_verification",
        "fields": [],
        "instruction": "Confirm the new payment details through a channel independent of this invoice before paying. Do not use contact details taken from the invoice itself."
      },
      {
        "step": 2, "rule_id": null,
        "action": "RESUBMIT_FOR_DECISION", "owner": "HOST_SYSTEM",
        "requirement_type": "decision_re_evaluation", "fields": [],
        "instruction": "Call the decision endpoint again with the updated context. `decision_if_resolved` states what the decision becomes once every step above is cleared and nothing else changes; supplying new context may activate rules listed in rules_still_not_evaluated."
      }
    ],
    "decision_if_resolved": {
      "decision": "PROCESS",
      "conditional": true,
      "assumption": {
        "resolved_rule_ids": ["BANK_DETAILS_CHANGED"],
        "statement": "Assumes this condition is cleared. No facts were substituted, corrected, or invented. 3 rules could not be evaluated for lack of context and are assumed not to object; see rules_still_not_evaluated."
      },
      "rules_still_not_evaluated": [
        { "rule_id": "DUPLICATE_INVOICE_NUMBER", "reason": "vendor_invoice_history_not_supplied" },
        { "rule_id": "APPROVAL_THRESHOLD", "reason": "approval_threshold_not_configured" },
        { "rule_id": "VENDOR_IDENTITY_MISMATCH", "reason": "vendor_domain_or_sender_not_supplied" }
      ],
      "remaining_rules_fired": [],
      "caveat": "Deterministic result with the named blockers set aside and all other supplied facts unchanged. Supplying new context to clear a blocker may activate rules listed in rules_still_not_evaluated."
    }
  },
  "extraction": { "mode": "caller_supplied", "model_calls": 0, "latency_ms": null },
  "processed_at": "2026-09-11T13:49:15.826Z"
}

How to read this

  1. fields is empty, and that is the point. No identifier was compared, so the rule names none. Contrast section C, where fields lists the two identifiers that actually differ.
  2. computed is no_baseline, not a value. comparison says trusted_bank_details is absent in plain terms. Nothing in the response claims the invoice bank details were checked against anything.
  3. Severity is REVIEW, not HOLD_FOR_REVIEW. A missing baseline is missing information, not evidence of a change. The invoice still stops — needs_review is true either way.
  4. The remedy is the same. REQUEST_VENDOR_BANK_VERIFICATION owned by VENDOR_MANAGEMENT: verify the account out-of-band, then populate your vendor master so the next invoice has a baseline.
  5. Three rules could not run, each named in warnings_structured with a reason. An unevaluated safety rule is reported as uncertainty, never silently treated as a pass.

E. Missing PO → REQUEST_MORE_INFORMATION

Your policy requires a PO and neither the invoice nor the request supplies one.

Request fragment

{ "rules": { "require_po": true } }

Response — 200 (abridged)

{
  "decision": "REQUEST_MORE_INFORMATION",
  "needs_review": true,
  "recommended_route": "accounts_payable_clerk",
  "rules_fired": [
    { "rule_id": "MISSING_PO", "rule_version": "1.0.0", "status": "fired", "outcome": "fired",
      "decision": "REQUEST_MORE_INFORMATION", "severity": 50, "fields": [],
      "message": "Customer policy requires a purchase order, but no PO number was found on the invoice or supplied as context.",
      "evidence": { "inputs": { "po_number_on_invoice": null, "po_number_in_context": null },
                    "threshold": "require_po=true", "computed": null,
                    "comparison": "po_number is null", "sources": [] } }
  ]
}

Maqaas does not contact the vendor. REQUEST_MORE_INFORMATION tells your system that input is missing; chasing it is your workflow.


F. Error — scanned PDF

Response — 422

{
  "error": {
    "code": "OCR_REQUIRED_NOT_AVAILABLE",
    "message": "This document appears to be a scan and requires OCR, which is not enabled.",
    "request_id": "req_1a4d90c7e5b2483fa6d0",
    "retryable": false,
    "details": [{ "path": "document", "message": "OCR_REQUIRED_NOT_AVAILABLE" }]
  }
}

retryable: false — resubmitting the same scan will fail identically. Route it to manual entry instead.


G. Uploading a document — the complete call

Sections A–F post already-extracted facts or inline text. This is the other transport: you hand Maqaas the file itself and it does the extraction.

The endpoint is POST /v1/invoice/decision/document. The response contract is identical to the JSON route — same decision, same extracted_facts, same resolution — plus one extra ingestion block describing how the file was read.

Prepare the document. content_base64 is the raw file, base64-encoded, with no data-URI prefix and no line wrapping:

# Linux
base64 -w0 invoice.pdf > invoice.b64
# macOS
base64 -i invoice.pdf -o invoice.b64

-w0 is a GNU option and is not available on macOS, which is why the two commands differ. Both write the same thing: one unwrapped line in invoice.b64.

Send it. The request reads that file, so this call is identical on either platform:

curl -sS https://api.maqaas.com/v1/invoice/decision/document \
  -H "Authorization: Bearer $MAQAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ap-2031-attempt-1" \
  -d @- <<JSON
{
  "client_reference": "ap-2031",
  "document": {
    "filename": "invoice.pdf",
    "media_type": "application/pdf",
    "content_base64": "$(cat invoice.b64)"
  },
  "po_context": { "po_number": "PO-7741", "total": { "amount": 7560, "currency": "GBP" }, "status": "open" },
  "rules": { "allowed_variance_pct": 0.05, "require_po": true }
}
JSON

Never put a real key in a file you commit — read it from the environment, as above.

The request shape, if you are building it in code rather than curl. This one is complete and runnable as written — the document is a short plain-text invoice rather than a PDF, so the base64 fits on the page:

{
  "client_reference": "ap-2031",
  "document": {
    "filename": "invoice.txt",
    "media_type": "text/plain",
    "content_base64": "Tm9ydGhnYXRlIEZhc3RlbmVycyBMdGQKSW52b2ljZSBOby4gTkdGLTIwMjYtMDE4OApQTy03NzQxClRPVEFMIERVRSBHQlAgNyw1NjAuMDA="
  },
  "po_context": { "po_number": "PO-7741", "total": { "amount": 7560, "currency": "GBP" }, "status": "open" },
  "rules": { "allowed_variance_pct": 0.05, "require_po": true }
}

For a PDF, swap filename, set media_type to application/pdf, and put the output of base64 -w0 invoice.pdf in content_base64. Nothing else changes.

document is the only required member. media_type may be omitted — it is inferred from the file’s magic bytes, and a mismatch between the declared type and the actual bytes is rejected rather than guessed.

What comes back that the JSON route does not send:

{
  "ingestion": {
    "filename": "invoice.pdf",
    "media_type": "application/pdf",
    "extraction_method": "native_pdf",
    "page_count": 2,
    "extraction_ms": 118,
    "warnings": []
  }
}

extraction_method is one of plain_text, native_pdf or ocr. A scan Maqaas cannot read never returns a decision — it returns 422 OCR_REQUIRED_NOT_AVAILABLE instead, as in section F. Accepted types are application/pdf and text/plain; limits are 10 MB decoded and 30 pages, in PILOT_SECURITY.md.