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
| Method | Path | Purpose |
|---|---|---|
POST | /v1/invoice/decision | Text or pre-extracted facts |
POST | /v1/invoice/decision/document | A PDF, base64 in JSON |
GET | /health | Liveness. Unauthenticated. |
GET | /ready | Readiness (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 key | Default | Omitting it means |
|---|---|---|
require_po | true | A PO is required |
bank_change_requires_review | true | Bank changes are reviewed |
require_currency_match | true | Currency must match the PO |
allowed_variance_pct | 0.05 | 5% variance allowed |
min_field_confidence | 0.8 | Fields below 0.8 are not trusted |
approval_threshold | unset | No 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
| Decision | Meaning | Your move |
|---|---|---|
PROCESS | No evaluated rule blocks processing | Continue — after checking what was evaluated |
REVIEW | Something needs a human eye | Queue for a clerk |
REQUEST_MORE_INFORMATION | Missing context | Gather it, call again |
HOLD_FOR_REVIEW | Do not pay until cleared | Queue for a manager |
ESCALATE | Needs authority, not correction | Route to an approver |
BLOCK | Reserved; not currently emitted | Handle 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, withexpected/observed/comparisontaken 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 bycatalog_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 mirrordecision: anESCALATEinvoice can carryREQUEST_VENDOR_BANK_VERIFICATIONbecause verifying a bank change matters more than obtaining an approval. Switch onrecommended_actionfor what to do anddecisionfor how hard to stop. Full precedence in API_V1.md §10a.resolution_plan— ordered steps, each with anowner, ending in aRESUBMIT_FOR_DECISIONstep. 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": "…" } ] } }
| Code | Status | Retryable |
|---|---|---|
INVALID_INPUT | 400 / 404 | no |
UNAUTHORIZED | 401 | no |
TENANT_SUSPENDED | 402 | no |
USAGE_LIMIT_EXCEEDED | 402 | no |
RATE_LIMITED | 429 | yes (see Retry-After) |
IDEMPOTENCY_IN_PROGRESS | 409 | yes, once the original finishes |
IDEMPOTENCY_ALREADY_COMPLETED | 409 | no |
IDEMPOTENCY_KEY_REUSED | 409 | no |
IDEMPOTENCY_NOT_CONFIRMED | 503 | yes, with the same key |
FILE_TOO_LARGE / TOO_MANY_PAGES | 413 | no |
MALFORMED_DOCUMENT / EMPTY_DOCUMENT | 422 | no |
OCR_REQUIRED_NOT_AVAILABLE | 422 | no |
EXTRACTION_UNRELIABLE | 422 | no |
EXTRACTION_FAILED | 502 | yes |
INTERNAL_ERROR | 500 | yes |
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
| Limit | Value |
|---|---|
| JSON body | 15 MB |
| Decoded document | 10 MB |
| Pages | 30 |
| Rate | 60 requests/minute/tenant (configurable for the pilot) |
| Provider timeout | 60 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_CONFIRMEDis safe to retry with the sameIdempotency-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_factspath. - 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:
- Read
resolution_plan. Each step has anownerand a machine-readableaction— create the task in your system. - Show the reviewer
blocking_conditions[].evidence. Expected versus observed, already extracted. - Show them
decision_if_resolvedso they know what clearing it achieves — andrules_still_not_evaluatedso they know what it does not. - 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.