Documentation · Maqaas v1.0.0

Pilot integration guide

Audience: an engineer at a procurement or AP SaaS company wiring Maqaas in behind their own product.

Reference docs: API_V1.md (full contract), RISK_PATTERN_LIBRARY.md (what it detects), PILOT_DEMO.md (real responses), v1-examples.md (worked request/response examples), PILOT_SECURITY.md (security posture), maqaas-v1.yaml (machine-readable spec).


1. What Maqaas does

You send an invoice — as text, as a PDF, or as facts you already extracted — plus the business context you hold: the purchase order, the vendor record, your policy thresholds.

You get back a decision, the evidence behind it, the conditions blocking it, what would clear each one, and what the decision deterministically becomes once they are cleared.

You keep your workflow, your UI, and your data. Maqaas is the decision layer underneath.

2. Authentication

A static API key, issued manually for the pilot, mapped to exactly one tenant.

Authorization: Bearer mqk_live_...

X-Api-Key: mqk_live_... also works. Keys are compared in constant time, never logged, and never echoed. Rotation is supported: we add your new key, both work, then the old one is withdrawn.

3. Endpoints

Base URL: https://api.maqaas.com

MethodPathPurpose
POST/v1/invoice/decisionText or pre-extracted facts
POST/v1/invoice/decision/documentA PDF, base64 in JSON
GET/healthLiveness. Unauthenticated.
GET/readyReadiness (database reachable). Unauthenticated.

4. Request shape

{
  "client_reference": "your-own-id",        // echoed back; your correlation key

  // ONE of these three:
  "document":        { "text": "…" },        // raw invoice text
  "extracted_facts": { … },                  // you already extracted — no model call
  // …or use /document with { filename, media_type, content_base64 }

  "message": { "from": "ar@vendor.example", "subject": "…", "body_text": "…" },

  "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": { "require_po": true, "allowed_variance_pct": 0.05,
             "min_field_confidence": 0.8,
             "approval_threshold": { "amount": 100000, "currency": "GBP" },
             "bank_change_requires_review": true }
}

Context is what makes the answer good. Every context field you omit takes a rule’s input away, so the rule cannot run. No trusted_bank_details means no bank-change detection; no known_invoice_numbers means no duplicate detection. Maqaas tells you which rules it could not evaluate rather than pretending they passed — check warnings_structured for RULE_NOT_EVALUATED.

rules is the opposite: with one exception, omitting it enables policy rather than disabling it. Context and rule configuration behave differently, and conflating them is the easiest way to get a decision you did not expect. Every rules key has a default, and the defaults are strict:

rules keyDefaultOmitting it means
require_potrueA PO is required
bank_change_requires_reviewtrueBank changes are reviewed
require_currency_matchtrueCurrency must match the PO
allowed_variance_pct0.055% variance allowed
min_field_confidence0.8Fields below 0.8 are not trusted
approval_thresholdunsetNo threshold; ESCALATE cannot occur

Send rules in full when you want a policy you control. A request with no rules object is not permissive — it enforces every default above.

Bank details are partial only. account_number_last4, iban_last4, sort_code, swift_bic, bank_name. There is no field that accepts a full account number, by design.

5. Response shape

{
  "request_id": "req_…",              // also in the X-Request-Id header
  "client_reference": "your-own-id",
  "versions": { "api": "v1", "schema": "…", "rules": "…",
                "engine": "…", "extraction": "…", "resolution_catalog": "…" },

  "decision": "HOLD_FOR_REVIEW",
  "needs_review": true,
  "recommended_route": "accounts_payable_manager",
  "decision_confidence": 0.98,
  "confidence_basis": "weakest critical signal: …",

  "extracted_facts": { … },           // every field with confidence + provenance
  "rules_fired": [ … ],               // with structured evidence
  "rules_evaluated": [ … ],           // includes passed AND skipped, with reasons
  "reasons": [ "…" ],
  "warnings_structured": [ … ],
  "resolution": { … },                // see §7

  "extraction": { "mode": "caller_supplied", "model_calls": 0, "latency_ms": null },
  "processed_at": "2026-09-09T…Z"
}

6. Decisions

DecisionMeaningYour move
PROCESSNo evaluated rule blocks processingContinue — after checking what was evaluated
REVIEWSomething needs a human eyeQueue for a clerk
REQUEST_MORE_INFORMATIONMissing contextGather it, call again
HOLD_FOR_REVIEWDo not pay until clearedQueue for a manager
ESCALATENeeds authority, not correctionRoute to an approver
BLOCKReserved; not currently emittedHandle defensively — stop

ESCALATE occurs only when you supply rules.approval_threshold. BLOCK is published so it can be added without breaking you, but no rule emits it today — see API_V1.md §6.

PROCESS does not mean every relevant rule ran. It means no rule that was evaluated objected. A rule whose input you did not supply is reported as RULE_NOT_EVALUATED in warnings_structured and does not change decision, needs_review or recommended_action — so a PROCESS whose bank-change check never ran can have the same decision, needs_review and recommended_action as one where that check ran and passed.

Before you treat a PROCESS as fully checked, inspect warnings_structured for RULE_NOT_EVALUATED and decide whether the rules that did not run matter to you. if (decision === "PROCESS") pay() will pay invoices whose payment details were never compared against anything.

needs_review is the boolean shortcut. recommended_route is a default suggestion — map it to your own queues.

7. Resolution Intelligence

The part you cannot get from a rules engine you build in a week.

  • blocking_conditions — one per fired rule, with expected / observed / comparison taken from the rule’s own evidence. Render this and a reviewer never opens the PDF.
  • resolution_requirements — what evidence would clear each blocker. Authored, static, versioned by catalog_version. Default good practice, not universal policy — compare it against your own controls.
  • recommended_action — a closed enum your workflow switches on. It is chosen by remediation urgency, not decision severity, so it does not have to mirror decision: an ESCALATE invoice can carry REQUEST_VENDOR_BANK_VERIFICATION because verifying a bank change matters more than obtaining an approval. Switch on recommended_action for what to do and decision for how hard to stop. Full precedence in API_V1.md §10a.
  • resolution_plan — ordered steps, each with an owner, ending in a RESUBMIT_FOR_DECISION step. Byte-identical for identical input.
  • decision_if_resolved — a deterministic counterfactual, not a prediction: the real engine re-run with the blockers set aside and every other fact unchanged.

Read rules_still_not_evaluated before you trust decision_if_resolved. Clearing a blocker often supplies data that other rules were waiting for. Attaching a PO clears MISSING_PO and simultaneously activates INVOICE_PO_VARIANCE and CURRENCY_MISMATCH. Maqaas names them rather than promising PROCESS and returning a variance exception a moment later.

owner values — AP_REVIEWER, PROCUREMENT, VENDOR_MANAGEMENT, APPROVER, MANUAL_REVIEW, HOST_SYSTEM — are default workflow roles, not mandates. Map them to your own queues. HOST_SYSTEM means your software, not a person.

8. Pattern library

Twelve deterministic patterns, each documented with what it concludes and what it explicitly does not: RISK_PATTERN_LIBRARY.md.

9. Errors

{ "error": { "code": "INVALID_INPUT", "message": "…",
             "request_id": "req_…", "retryable": false,
             "details": [ { "path": "document.text", "message": "…" } ] } }
CodeStatusRetryable
INVALID_INPUT400 / 404no
UNAUTHORIZED401no
TENANT_SUSPENDED402no
USAGE_LIMIT_EXCEEDED402no
RATE_LIMITED429yes (see Retry-After)
IDEMPOTENCY_IN_PROGRESS409yes, once the original finishes
IDEMPOTENCY_ALREADY_COMPLETED409no
IDEMPOTENCY_KEY_REUSED409no
IDEMPOTENCY_NOT_CONFIRMED503yes, with the same key
FILE_TOO_LARGE / TOO_MANY_PAGES413no
MALFORMED_DOCUMENT / EMPTY_DOCUMENT422no
OCR_REQUIRED_NOT_AVAILABLE422no
EXTRACTION_UNRELIABLE422no
EXTRACTION_FAILED502yes
INTERNAL_ERROR500yes

Branch on retryable, not on the status code. Every error carries a request_id matching the X-Request-Id header — quote it to us and we can trace the request without you resending the invoice.

The two 402s are deliberately not 401. Your credential is valid; the account is not currently entitled. Rotating your key will not help — email support@maqaas.com. Neither is billed: both are refused before any work is done.

Retry safely with Idempotency-Key. Send the header on every decision call and a network-timeout retry cannot become a second decision or a second billable unit. Same key plus same request inside 24 hours returns 409 naming the original request id; same key with different content returns 409 IDEMPOTENCY_KEY_REUSED; a failed original releases the key. We do not store your invoice or the response, so a duplicate is told what happened rather than replayed — keep your own copy of the first response. Without the header, behaviour is unchanged. See API_V1.md §12.

You will not be suspended by a payment hiccup. A tenant whose payment is late moves to grace_period, which is served in full. Only a deliberate suspension stops traffic.

10. Limits

LimitValue
JSON body15 MB
Decoded document10 MB
Pages30
Rate60 requests/minute/tenant (configurable for the pilot)
Provider timeout60 s hard ceiling

Machine-readable PDFs only. A scan with no text layer returns OCR_REQUIRED_NOT_AVAILABLE rather than a guess.

Pilot allowance

Your pilot plan carries 5,000 decisions per month, with overage set to allow: crossing the allowance does not block you, and requests keep being served. We watch usage with our own tooling and will talk to you long before it becomes a commercial question.

This is a pilot operating policy, not final pricing. Nothing about it implies the commercial terms of a later agreement.

Timeouts and retries

The 60-second ceiling inside Maqaas is per provider attempt, not per request. Extraction may make up to two attempts within a single HTTP request — a second attempt happens when the first returns output we cannot parse — so one request can legitimately spend close to 120 seconds in the provider before ingestion and decisioning are added.

Set your client timeout to 150 seconds for both decision endpoints. That sits above the two-attempt worst case with margin for ingestion and network.

This is not an SLA and not a guaranteed maximum — it is a conservative client setting derived from current internal behaviour, which may change. Almost every request finishes in a small fraction of it.

A 30- or 90-second default will abort work that was about to succeed, and every such abort becomes a retry.

Send Idempotency-Key on every decision call. With it, a timeout retry cannot become a second decision or a second billable unit. Without it, a retry is a new request and will be processed and charged again. Retry only what the error says is retryable: the response carries retryable on every error, and §9 lists which codes set it.

Use exponential backoff starting at one second, and honour Retry-After on a 429 rather than guessing.

If Maqaas is unavailable

A 503, a connection failure or a timeout means we could not answer — it never means the invoice is fine.

  • Never treat an error as approval. Hold the invoice and retry, or route it to a human. An unanswered request is not a PROCESS.
  • 503 IDEMPOTENCY_NOT_CONFIRMED is safe to retry with the same Idempotency-Key: the decision was computed but the retry guarantee was not recorded, so you were not charged.
  • If the API is unreachable for a sustained period, fall back to your existing manual AP process. Maqaas is an advisory layer; your payment run should never depend on it being up.

And the rule that matters most: anything other than PROCESS must not be paid or processed automatically. REVIEW, HOLD_FOR_REVIEW, REQUEST_MORE_INFORMATION and ESCALATE all mean a human decides. Treating a non-PROCESS decision as a pass defeats the entire point of the product.

We publish no uptime guarantee and no SLA for the pilot.

Support

Email support@maqaas.com for anything: a key, an unexpected decision, an outage, or a question about this document.

Quote the request_id from the response or the X-Request-Id header. It identifies the request on our side without you resending the invoice, which is the point of it — we never need the document again to investigate.

11. Security and privacy assumptions

Full detail in PILOT_SECURITY.md. The short version:

  • No invoice data at rest. No documents, facts, decisions or evidence are persisted, and no temp files are written. You remain the system of record. Commercial and operational metadata — your tenant record, hashed API-key metadata, plan, usage counters and idempotency hashes — is persisted. See PILOT_SECURITY.md §4.
  • To the LLM provider goes the document text and email metadata only. Your vendor_context, po_context, trusted_bank_details, policy rules, tenant id and API keys never leave the process. Enforced by test.
  • Zero provider calls on the extracted_facts path.
  • Logs carry request_id, tenant, route, decision, rule ids and latency — never document content, field values, credentials, or bank identifiers.
  • Tenant identity comes only from your verified key; no header or body field can override it.

12. Integration sequence

1. Your customer receives an invoice.
2. You POST the invoice + your PO/vendor context + your policy rules.
3. Maqaas returns decision + evidence + resolution + plan.
4. Your application renders and acts on it — your UI, your queues, your rules
   about who sees what.
5. New evidence arrives (PO attached, bank change verified, field corrected).
   You POST again with the updated context.
6. You remain the system of record throughout. Maqaas stored none of your invoice data.

Step 5 is a fresh, independent call. There is no session, no job id, and no state to reconcile — which is also why there is nothing to clean up if a pilot ends.

13. Sample request

curl -sS https://api.maqaas.com/v1/invoice/decision \
  -H "Authorization: Bearer $MAQAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "client_reference": "ap-2026-04417",
    "message": { "from": "ar@acme-supplies.example", "subject": "Invoice INV-2026-0412" },
    "document": { "text": "ACME SUPPLIES LTD\nInvoice Number: INV-2026-0412\nCustomer PO: PO-500\nTOTAL DUE GBP 41,200.00\nRemit to: Sort Code 30-96-14 Account ending 4471" },
    "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"],
      "trusted_bank_details": { "bank_name": "Barclays", "sort_code": "20-00-00", "account_number_last4": "9021" }
    },
    "rules": { "require_po": true, "allowed_variance_pct": 0.05 }
  }'

For a PDF, POST to /v1/invoice/decision/document with { "document": { "filename": "invoice.pdf", "media_type": "application/pdf", "content_base64": "…" } }.

14. What happens after REVIEW or HOLD

Maqaas does nothing. It has no queue, no timer, no follow-up, and no memory of the request.

The intended loop:

  1. Read resolution_plan. Each step has an owner and a machine-readable action — create the task in your system.
  2. Show the reviewer blocking_conditions[].evidence. Expected versus observed, already extracted.
  3. Show them decision_if_resolved so they know what clearing it achieves — and rules_still_not_evaluated so they know what it does not.
  4. When the evidence exists, POST again with the updated context.

The new call is judged entirely on the facts supplied. Maqaas does not remember that you asked before, and does not treat a resubmission as pre-approved.

15. What Maqaas does not do

  • Does not send email or contact your suppliers. It never owns supplier communication.
  • Does not pay, schedule, or approve anything. No autonomous execution.
  • Does not persist your invoice data. No document storage, no decision history, no audit ledger. (Account metadata, usage counters and idempotency hashes are kept — see PILOT_SECURITY.md §4.)
  • Does not OCR scans.
  • Does not learn from your traffic, or apply anything from one customer to another.
  • Does not produce fraud scores or probabilities. It has no outcome data that would make a number honest.
  • Does not replace your workflow, your UI, or your ERP.
  • Does not decide who is right in a dispute. It reports discrepancies.

On the roadmap, explicitly not built

Maqaas may later generate supplier-ready correction requests and evidence-backed clarification text — a deterministic draft your product sends, under your branding, through your channel.

Even then: Maqaas will not send it. The host application owns the supplier relationship and the communication channel. Nothing in this area is implemented today, and none of it is being sold in the pilot.