Documentation · Maqaas v1.0.0

Maqaas in three requests

Every response on this page is real output, generated by calling the running API and pasted unedited. Nothing is illustrative. All vendors, invoices, bank details and companies are synthetic.

Zero model calls were made: each request supplies extracted_facts directly, so what you are looking at is the deterministic decision layer on its own.

Demo 1 was regenerated against the current build (resolution catalogue 1.4.0) by replaying the request below locally. Zero model calls, as the request supplies extracted_facts directly — so it is still real output, not an illustration.

It needed regenerating rather than a version-number edit: since the original capture, BANK_DETAILS_CHANGED stopped counting bank_name as a compared identifier, so the message, evidence and comparison below all changed. Editing only the version would have labelled stale content as current.

Demos 2 and 3 are still the earlier capture and are marked abridged. Their decisions and rule ids are unchanged; treat their wording as indicative.

The claim these three demos are meant to support:

Most APIs tell you what is wrong. Maqaas tells your software what is wrong, proves it against the document, tells it what must happen next, and shows — deterministically — what the decision becomes once that blocker is cleared.


Demo 1 — the flagship: a perfect invoice with changed bank details

This is the case the product exists for. Everything about this invoice is correct. The PO matches, the totals agree to the penny, the currency agrees, the vendor is known, the sending domain is recognised, the invoice number has never been seen before, the total is under the approval threshold, and every field verifies against the document.

One thing has changed: the bank account.

A conventional AP tool raises “exception: bank details changed” and drops it in a queue. Here is what Maqaas returns instead.

Request — POST /v1/invoice/decision

{
  "client_reference": "ap-2026-04417",
  "message": { "from": "ar@acme-supplies.example", "subject": "Invoice INV-2026-0412" },
  "extracted_facts": {
    "document_type": "invoice",
    "invoice_number":  { "value": "INV-2026-0412", "confidence": 0.99, "source_verified": true },
    "invoice_total":   { "value": { "amount": 41200, "currency": "GBP" }, "confidence": 0.99, "source_verified": true },
    "vendor_name":     { "value": "Acme Supplies Ltd", "confidence": 0.99, "source_verified": true },
    "po_number":       { "value": "PO-500", "confidence": 0.99, "source_verified": true },
    "bank_details":    { "value": { "bank_name": "Monzo", "sort_code": "30-96-14", "account_number_last4": "4471" },
                         "confidence": 0.99, "source_verified": true }
  },
  "po_context":     { "po_number": "PO-500", "total": { "amount": 41200, "currency": "GBP" }, "status": "open" },
  "vendor_context": {
    "vendor_id": "V-100",
    "legal_name": "Acme Supplies Ltd",
    "known_domains": ["acme-supplies.example"],
    "known_invoice_numbers": ["INV-2026-0388"],
    "trusted_bank_details": { "bank_name": "Barclays", "sort_code": "20-00-00", "account_number_last4": "9021" }
  },
  "rules": {
    "allowed_variance_pct": 0.05, "require_po": true, "min_field_confidence": 0.8,
    "approval_threshold": { "amount": 100000, "currency": "GBP" }
  }
}

Response — 200

{
  "decision": "HOLD_FOR_REVIEW",
  "needs_review": true,
  "recommended_route": "accounts_payable_manager",
  "decision_confidence": 0.99,
  "confidence_basis": "weakest critical signal: invoice_number=0.99",
  "reasons": [
    "Payment details differ from the trusted vendor record (account_number_last4, sort_code). Possible invoice redirection fraud."
  ],
  "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=4471, sort_code=30-96-14",
          "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": false,
      "assumption": {
        "resolved_rule_ids": [
          "BANK_DETAILS_CHANGED"
        ],
        "statement": "Assumes this condition is cleared. No facts were substituted, corrected, or invented. Every rule was evaluated."
      },
      "rules_still_not_evaluated": [],
      "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."
    }
  }
}

What your software can now do without a human opening the PDF

  1. Route it correctly — accounts_payable_manager, not a generic exception bin.
  2. Show the reviewer exactly what changed — 9021 / 20-00-00 / Barclays became 4471 / 30-96-14 / Monzo. No document re-reading.
  3. Act programmatically — REQUEST_VENDOR_BANK_VERIFICATION is an enum your workflow switches on, with the acceptable evidence enumerated.
  4. Assign it — step 1 is owned by VENDOR_MANAGEMENT; step 2 is yours.
  5. Tell the reviewer what happens next — decision_if_resolved: PROCESS.

Why that last line is trustworthy

It was produced by removing BANK_DETAILS_CHANGED from the rule registry and re-running the real engine on the identical context. No PO was invented, no bank details corrected, no total adjusted.

The two empty arrays are what make it safe to act on:

  • rules_still_not_evaluated: [] — every rule could be evaluated on the facts supplied. Nothing is waiting to fire once new context arrives.
  • remaining_rules_fired: [] — no other finding survives.

Clearing this one condition genuinely settles the invoice. Contrast with Demo 2, where the same field is deliberately not empty.


Demo 2 — missing PO: why the honesty matters

Same vendor, no purchase order. The interesting part is what Maqaas refuses to promise.

Request (abridged)

{
  "client_reference": "ap-2026-04418",
  "extracted_facts": { "…": "…", "po_number": { "value": null, "confidence": 0 } },
  "vendor_context": { "vendor_id": "V-100", "legal_name": "Acme Supplies Ltd" },
  "rules": { "require_po": true }
}

Response — 200

{
  "decision": "REQUEST_MORE_INFORMATION",
  "recommended_route": "accounts_payable_clerk",
  "reasons": [
    "Customer policy requires a purchase order, but no PO number was found on the invoice or supplied as context."
  ],
  "resolution": {
    "recommended_action": "REQUEST_PO_REFERENCE",

    "resolution_plan": [
      {
        "step": 1,
        "rule_id": "MISSING_PO",
        "action": "REQUEST_PO_REFERENCE",
        "owner": "PROCUREMENT",
        "requirement_type": "purchase_order_reference",
        "fields": [],
        "instruction": "Supply the purchase order this invoice relates to, either as po_context or by obtaining a PO reference from the vendor."
      },
      { "step": 2, "rule_id": null, "action": "RESUBMIT_FOR_DECISION", "owner": "HOST_SYSTEM", "…": "…" }
    ],

    "decision_if_resolved": {
      "decision": "PROCESS",
      "assumption": {
        "resolved_rule_ids": ["MISSING_PO"],
        "statement": "Assumes this condition is cleared. No facts were substituted, corrected, or invented."
      },
      "rules_still_not_evaluated": [
        { "rule_id": "INVOICE_PO_VARIANCE", "reason": "purchase_order_not_supplied" },
        { "rule_id": "CURRENCY_MISMATCH",   "reason": "purchase_order_not_supplied" },
        { "rule_id": "BANK_DETAILS_CHANGED", "reason": "vendor_bank_record_not_supplied" },
        { "rule_id": "APPROVAL_THRESHOLD",   "reason": "approval_threshold_not_configured" }
      ],
      "remaining_rules_fired": []
    }
  }
}

Read rules_still_not_evaluated before you trust PROCESS

decision_if_resolved says PROCESS — and that is literally true: with MISSING_PO set aside and every other supplied fact unchanged, the engine returns PROCESS.

But attaching a real PO does not just clear MISSING_PO. It supplies data that two other rules have been waiting for. INVOICE_PO_VARIANCE and CURRENCY_MISMATCH were skipped for lack of a PO — skipped, not passed — and they will run for the first time on the resubmission. The same is true of BANK_DETAILS_CHANGED if a trusted bank record arrives.

A system that quietly answered “PROCESS” here, then returned a variance exception thirty seconds later, would have lied to the reviewer. Maqaas names the four rules it could not evaluate instead.

This is the single most important behaviour to understand about the counterfactual, which is why the demo exists.


Demo 3 — an identifier we could read but cannot confirm

A real failure mode from live documents: the PDF’s embedded font maps a glyph inside the invoice number to nothing, so the extracted text contains a character that is genuinely rendered but unreadable.

Request (abridged)

{
  "extracted_facts": {
    "invoice_number": {
      "value": "QK27WMBD 0009",
      "confidence": 0.91,
      "source": { "snippet": "Invoice number QK27WMBD 0009" },
      "source_verified": false,
      "verification_reason": "source_encoding_degraded"
    }
  }
}

Response — 200

{
  "decision": "REVIEW",
  "decision_confidence": 0.91,
  "confidence_basis": "weakest critical signal: invoice_number=0.91",
  "reasons": [
    "Identifier present but not supported by the document: invoice_number (source_encoding_degraded, confidence 0.91). The value has not been altered; a human should confirm it."
  ],
  "resolution": {
    "recommended_action": "VERIFY_EXTRACTED_FIELDS",
    "blocking_conditions": [
      {
        "rule_id": "UNVERIFIED_IDENTIFIER",
        "severity": "REVIEW",
        "fields": ["invoice_number"],
        "evidence": {
          "expected": "source_verified must not be false",
          "observed": 1,
          "comparison": "1 identifier(s) present but unverified"
        }
      }
    ],
    "resolution_plan": [
      {
        "step": 1,
        "rule_id": "UNVERIFIED_IDENTIFIER",
        "action": "VERIFY_EXTRACTED_FIELDS",
        "owner": "AP_REVIEWER",
        "requirement_type": "identifier_verification",
        "fields": ["invoice_number"],
        "instruction": "An identifier was read but could not be confirmed against the document. Confirm it before relying on it; Maqaas has not altered the value."
      },
      { "step": 2, "rule_id": null, "action": "RESUBMIT_FOR_DECISION", "owner": "HOST_SYSTEM", "…": "…" }
    ]
  }
}

What is deliberately absent

No guessed replacement. Maqaas does not silently repair QK27WMBD 0009 into QK27WMBD-0009, does not pick the likelier separator, and does not drop the unreadable character to make the value look clean. It says the value was read, says it could not be confirmed, names the field, and says explicitly that the value has not been altered.

That matters because an invoice number is a duplicate-payment key. A silently “corrected” identifier is worse than an unverified one: it looks trustworthy and is not. This is also why source_verified is tri-state — true, false, and null for “could not check” are three different things, and collapsing them would hide exactly this case.


What these three have in common

Demo 1Demo 2Demo 3
DecisionHOLD_FOR_REVIEWREQUEST_MORE_INFORMATIONREVIEW
Blocker named✓✓✓
Evidence from the document✓✓✓
Machine-actionable next step✓✓✓
Ordered owner-assigned plan✓✓✓
CounterfactualPROCESS, nothing pendingPROCESS, 4 rules pendingPROCESS, 2 rules pending
Anything inventednonenonenone

Zero model calls. Identical inputs produce byte-identical output, every time.