Primitive spec

Devers

The procurement and accounts-payable primitive. Devers is the buy side of the platform: vendors, purchase orders, receiving, vendor bills, three-way match, and AP aging — the mirror image of Hardin/Hober selling and Mallow recording AR. Devers buys; Mallow records AP.

Note: In-platform ACH awaits a rail partner — today money-out is record-only (check and manual payments are recorded against the ledger, not moved). The procurement and AP workflow itself — vendors through three-way match and aging — is live end to end.

What it owns

Devers owns the purchasing loop. A Vendor is a supplier with payment terms; a PurchaseOrder is what you asked them for; a GoodsReceipt is what actually arrived at the dock; a VendorBill is what they charged you; a BillPayment is how the bill gets settled. Three-way match reconciles ordered vs received vs billed at approval time, and every approval and payment posts double-entry legs into the Mallow ledger.

Devers does not own stock levels (that is Trantor — receiving calls Trantor replenishment in the same transaction), the ledger itself (that is Mallow — Devers posts journals with sourceType: devers_bill | devers_payment), or money movement rails (a driver seam, mirroring the Payments processor registry). It composes them into one buy-side flow.

Concepts

Vendor & VendorItem
A supplier (vend_) with contact details, termsNetDays (default 30), a default expense account code, and an optional tokenized rail counterparty. A VendorItem (vitm_) is that vendor's catalog entry for something you stock: vendorSku, lastCostCents, minOrderQty, packSize (order in cases of 12, receive and count in units), and a preferred flag — one preferred vendor per resource type.
PurchaseOrder
The procurement document (dpo_): per-tenant human-readable poNumber (PO-000123), status draft → submitted → partially_received → received → closed (or cancelled). Each PurchaseOrderLine is either a stock line (a Trantor resource type) or a non-stock line (an expense account code — services, freight), carrying qtyOrdered, unitCostCents, and receipt/bill counters maintained by the receiving and billing flows.
GoodsReceipt (receiving)
What arrived (dgr_), line by line with a condition of ok | damaged | rejected. Posting a receipt is one transaction: PO line counters bump, PO status transitions, and each ok stock line calls Trantor replenishment — stock quantity increments, lastReplenishedAt stamps, the low-stock latch clears. Damaged and rejected lines never replenish; over-receipt beyond qtyOrdered warns and requires a reason. Stock truth commits or rolls back together — no soft-fail at the loading dock.
VendorBill & three-way match
The vendor's invoice (dbil_), PO-linked or standalone — rent and utilities are first-class non-PO bills. At approval, PO-linked lines run ThreeWayMatchService: ordered qty/cost vs received qty vs billed qty/cost, within ApPolicy tolerances ⇒ matched; outside ⇒ variance, which demands an approval reason recorded in the AuditEvent. Duplicate bills are rejected outright — resubmitting a vendor's bill number returns the existing bill instead of paying it twice. Bills are USD-only in v1.
AP ledger postings
Expense-on-purchase (periodic inventory) in v1: bill approval posts DR expense per line / CR 2100 Accounts Payable via DeversPostingService, with stock lines defaulting to 5000 Cost of Goods Sold. Rail disbursement walks a dedicated in-transit liability: submit DR 2100 / CR 2110 AP Payments In Transit, paid DR 2110 / CR 1000 Cash, returns reverse. Reversals are idempotent — a replay can never double-post.
Payment rail seam
A BillPayment (dpay_) allocates across bills (partial payments and one-payment-many-bills both work) and executes through ApPaymentRailDriverInterface: counterparty tokenization (raw bank details discarded after tokenization — encrypted-at-rest fallback, masked reads), payment initiation, cancellation, and webhook handling. Webhook ingest is per-event transactional and deduplicated; unmatched or closed-fiscal-period events park visibly and can be replayed once resolved. The MockRailDriver runs the full lifecycle today, including R01 returns.
Reorder automation
A scheduled sweep (ReorderSweepService, a Daneel step) finds Trantor inventory at or below its reorder level, groups by preferred VendorItem vendor, and builds draft POs respecting minOrderQty and packSize, notifying the purchasing role via Speaker. Draft-only by design — humans submit POs.
ApPolicy & AP aging
The audited per-tenant policy row (dapl_): match tolerances (qtyTolerance, costTolerancePermille) and requireSecondApproverAboveCents for a second approver on large payments. ApAgingReportService buckets unpaid approved bills current/30/60/90+, mirroring the AR aging shape — console tile plus Pelorat stream.

API surface

Endpoints are versioned under /devers/v1/. Operator surfaces are permission-gated; the money-out transitions (bill approve/void, payment execute/cancel) require owner-level finance.approve. The rail webhook route is machine-facing and signature-verified per driver.

Devers endpoints in the Foundation API reference OpenAPI 3.1 schema for Devers with request/response shapes, parameters, and a try-it client.

Quick reference

MethodPathPurpose
POST / GET/devers/v1/vendorsCreate or list Vendors.
POST/devers/v1/vendors/{id}/counterpartyTokenize vendor bank details into a rail counterparty ref.
POST / GET/devers/v1/vendor-itemsVendor catalog entries — SKU, pack size, min order qty, preferred flag.
POST / GET/devers/v1/purchase-ordersDraft or list PurchaseOrders.
POST/devers/v1/purchase-orders/{id}/submitSubmit a draft PO (also /cancel).
POST/devers/v1/receiptsPost a GoodsReceipt — PO counters + Trantor replenishment in one transaction.
POST / GET/devers/v1/billsEnter or list VendorBills, PO-linked or standalone.
POST/devers/v1/bills/{id}/approveRun three-way match and post the AP journal (also /void).
POST/devers/v1/paymentsBuild a payment run: bill ids → allocations; then /{id}/execute or /{id}/cancel.
GET/devers/v1/reports/ap-agingAP aging buckets: current / 30 / 60 / 90+.
GET/devers/v1/reorder-suggestionsDraft-PO suggestions from low stock, grouped by preferred vendor.
GET / PUT/devers/v1/ap-policyMatch tolerances and second-approver threshold.
POST/devers/v1/webhooks/{railKey}Signature-verified rail webhook ingest, deduped per event.

Example: PO → receive → bill → match

A salon restocks retail shampoo: draft and submit a PO, post the receipt when the cases land (stock updates in the same transaction), enter the vendor's bill, and approve — three-way match runs and the AP journal posts.

// 1. Draft the PO, then POST /devers/v1/purchase-orders/{id}/submit
POST /devers/v1/purchase-orders
{
  "vendorId": "vend_01JZM4…",
  "expectedAt": "2026-07-21",
  "lines": [
    { "trantorResourceTypeId": "rt_01JX2K…",
      "vendorItemId": "vitm_01JZM7…",          // packSize 12 — ordered as 4 cases
      "description": "Argan repair shampoo, 300ml",
      "qtyOrdered": 48, "unitCostCents": 950 }
  ]
}
→ 201  { "id": "dpo_01JZN1…", "poNumber": "PO-000123",
         "status": "draft", "totalCents": 45600 }

// 2. Cases arrive — one transaction: PO counters + Trantor replenishment
POST /devers/v1/receipts
{
  "purchaseOrderId": "dpo_01JZN1…",
  "lines": [ { "poLineId": "dpol_01JZN1…", "qtyReceived": 48, "condition": "ok" } ]
}
→ 201  { "id": "dgr_01JZP8…" }            // PO status: received;
                                            // stockQuantity +48, low-stock latch cleared

// 3. Enter the vendor's bill against the PO
POST /devers/v1/bills
{
  "vendorId": "vend_01JZM4…",
  "purchaseOrderId": "dpo_01JZN1…",
  "billNumber": "INV-8841",                  // UNIQUE per vendor — duplicates 409
  "billDate": "2026-07-21",
  "lines": [ { "poLineId": "dpol_01JZN1…", "qty": 48, "unitCostCents": 950 } ]
}
→ 201  { "id": "dbil_01JZQ3…", "status": "draft", "totalCents": 45600 }

// 4. Approve — three-way match (ordered vs received vs billed), then post
POST /devers/v1/bills/dbil_01JZQ3…/approve   // requires finance.approve
→ 200  { "id": "dbil_01JZQ3…", "status": "approved",
         "matchStatus": "matched",           // within ApPolicy tolerances
         "dueAt": "2026-08-20",              // termsNetDays: 30
         "mallowJournalId": "jrnl_01JZQ4…" }
// Journal: DR 5000 Cost of Goods Sold 45600 / CR 2100 Accounts Payable 45600

How it fits with the rest

flowchart TB
  Con[Demerzel purchasing console] --> Dv(Devers)
  Dv -- receiving replenishes --> Tr[Trantor]
  Tr -. low-stock signal .-> Dv
  Dv -- AP journal legs --> Ml[Mallow ledger]
  Dv -- payment rail seam --> Rail[ACH rail driver]
  Rail -. signed webhooks .-> Dv
            

Trantor is on both ends of the loop: its low-stock signal feeds the reorder sweep, and posting a receipt replenishes its inventory in the same transaction. Mallow records every approval, payment, and reversal as double-entry journals that flow onward through the daily accounting summary. The rail driver seam mirrors the Payments processor registry — when a rail partner lands, the real driver slots in behind the same interface, webhooks, and replay machinery the Mock driver exercises today. The purchasing console lives in Demerzel at /dashboard/purchasing, and the money-out transitions — bill approve and void, payment execute and cancel — are deny-by-default behind owner-level finance.approve.