Procurement lifecycle
This page walks the parts-procurement lifecycle of Rails Sandbox — a Rails-compatible API (a faithful mock of the public Integrations API, plus clearly-labelled proposed extensions). It names every dotted wire method in order, the status each produces, and the exact point where the public 2026-01 contract ends.
Every example below is an RPC-over-POST call:
POST https://partifact-mock-rails.thanhvuttv.workers.dev/api/2026-01/<dotted.method>
Authorization: Bearer <api_key>
Partly-Integration-ID: <integration_id>
Content-Type: application/json
See Quickstart for credentials and a first call, the API Reference for full request/response shapes, and Errors for the coded failure model.
The two state machines
Two things move through states: parts and procurements. The enum values below are quoted verbatim from the contract — do not invent others.
Part status — PartStatusV1:
"estimated" | "ordered"
A newly inserted part starts "estimated". It transitions to "ordered" once a procurement has been placed for it.
Procurement status — procurements.v1.buyer.Status:
"order_requested" | "order_confirmed" | "cancelled"
A procurement is created in "order_requested" and is transitioned to "order_confirmed" by the supplier. "order_confirmed" is the terminal public state — see the boundary marker below. ("cancelled" is enumerated in the contract; the sandbox lifecycle does not drive a cancellation path.)
ASCII view of the happy path:
── PUBLIC 2026-01 CONTRACT ──────────────────────────┐
part: estimated ──(place)──► ordered │
│
procurement: (create) ──► order_requested ──(supplier confirms)──► │
order_confirmed │ ◄── terminal public state
────────────────────────────────────────────────────────────────── ┘
│
── PROPOSED EXTENSIONS (beyond 2026-01) ┴── (see below)
invoices appear after order_confirmed
Ordered walkthrough — tier1.prepare → order_confirmed
The faithful path uses these dotted wire methods, in workflow order. Read-only resolution helpers (repairer.work-providers.list, plus the job-read methods) are listed where they fit.
1. tier1.prepare — warm the catalogue (no state change)
Queues a vehicle VIN/VRM for future use in other the API (bulk of up to 1000). Fire-and-forget: returns { invalid_identifiers } and makes no state change to any job, part, or procurement.
curl -X POST https://partifact-mock-rails.thanhvuttv.workers.dev/api/2026-01/tier1.prepare \
-H "Authorization: Bearer partly_demo_repairer_3f8a1c0d9e2b4a67b1c2" \
-H "Partly-Integration-ID: 0c000000-0000-4000-8000-000000000001" \
-H "Content-Type: application/json" \
-d '{ "identifiers": [{ "vin": "JTDBR32E730012345" }] }'
2. repairer.work-providers.list — resolve the insurer (read-only)
Fetches the list of work providers (insurers) for a repairer so you can attach the right work_provider_id to a job. Empty request body. Omit the work provider entirely for cash / self-pay jobs.
3. repairer.jobs.insert — open or update a repair job
Upserts a repair job (insert-or-update keyed by identity). On success it emits a repairer.jobs webhook whose payload carries change_action of "inserted" or "updated" (see Webhooks). No procurement exists yet.
curl -X POST https://partifact-mock-rails.thanhvuttv.workers.dev/api/2026-01/repairer.jobs.insert \
-H "Authorization: Bearer partly_demo_repairer_3f8a1c0d9e2b4a67b1c2" \
-H "Partly-Integration-ID: 0c000000-0000-4000-8000-000000000001" \
-H "Content-Type: application/json" \
-d '{
"identity": { "external": "CCC-2026-04817" },
"repairer_site_id": "0b000000-0000-4000-8000-000000000001",
"work_provider_id": "0a000000-0000-4000-8000-000000000020"
}'
4. repairer.jobs.parts.insert — identify parts
A full-list upsert of the job's parts, keyed by part identity. Each newly inserted part starts at status: "estimated".
Wire note:
repairer.jobs.parts.insertis the one method whose error bodies are bare JSON strings ("not_found","unprocessable", …), not{ "type": "..." }objects. This asymmetry is reproduced faithfully from the contract; see Errors.
5. repairer.jobs.baskets.latest.get — recommend a basket (read-only)
Fetches the latest priced basket for the job. Returns:
{ "offers": [], "suppliers": [] }
A job that exists but has no priced parts returns an empty basket, not an error.
Currency: the basket response carries no
currencyfield.OfferV1exposestrade_price(a bare string, NZD ex-GST) with no accompanying currency, and the basket envelope has onlyoffersandsuppliers. Currency is a procurement-level concept — it first appears onprocurements.v1.buyer.Procurement.currency_code("NZD"). This is decision D52: any currency shown alongside a basket offer is invented. The Rails MCP's projected basket may attach a derived presentation-layercurrencylabel, but that is never a wire claim.
6. repairer.procurements.get — track a procurement (read-only)
Buyer/repairer view of one procurement by UUID: its status and lines. A procurement belonging to a different org resolves as not_found (scoped read).
7. supplier.procurements.confirm — confirm the order
Transitions a procurement order_requested → order_confirmed. This is a supplier-scope action: it requires a credential whose scope is "supplier". On confirm the order is locked, an invoice is created, and buyer + supplier …procurements webhooks fire with status order_confirmed.
The seed ships one procurement already placed in order_requested — PR-1, id 10000000-0000-4000-8000-000000000001 — so you can exercise confirm immediately, using the supplier credential:
curl -X POST https://partifact-mock-rails.thanhvuttv.workers.dev/api/2026-01/supplier.procurements.confirm \
-H "Authorization: Bearer partly_demo_supplier_8b4e2f1a6c0d3e9f7a25" \
-H "Partly-Integration-ID: 0c000000-0000-4000-8000-000000000002" \
-H "Content-Type: application/json" \
-d '{ "identity": { "id": "10000000-0000-4000-8000-000000000001" } }'
The supplier and repairer credentials are both preloaded by the seed, so a single caller can drive both sides by swapping the two header pairs. See Quickstart.
Lifecycle summary table
| # | Dotted wire method | Faithful? | Resulting status / effect |
|---|---|---|---|
| 1 | tier1.prepare |
faithful | Catalogue warmup; returns { invalid_identifiers }; no state change |
| 2 | repairer.work-providers.list |
faithful | Read-only; lists insurers for the repairer |
| 3 | repairer.jobs.insert |
faithful | Upserts job; emits repairer.jobs webhook (change_action) |
| 4 | repairer.jobs.parts.insert |
faithful | Full-list upsert of parts; new parts start estimated |
| 5 | repairer.jobs.baskets.latest.get |
faithful | Read-only; { offers, suppliers }; empty (not an error) if no basket. No currency field |
| 6 | repairer.procurements.get |
faithful | Read-only buyer view by UUID; cross-org → not_found |
| 7 | supplier.procurements.confirm |
faithful | order_requested → order_confirmed; locks order; creates invoice; fires webhooks |
── End of the public 2026-01 contract:
order_confirmedis the terminal state. ──
Everything above is a faithful mirror of the public 2026-01 surface. The public lifecycle terminates at order_confirmed. The methods below are not part of that contract.
Proposed extensions (beyond 2026-01)
These two steps complete the buyer's procurement loop — placing the order and reconciling the resulting invoice — but the public 2026-01 API exposes no public method for either. the upstream platform performs procurement-placement and invoice-reconciliation internally; here they are reconstructed as a clearly-labelled proposed extension so the end-to-end loop is demonstrable, never passed off as the real contract.
Every extension method announces itself: its HTTP response carries x_extension: true, and the Rails MCP result carries is_extension: true. Both extension surfaces open with this verbatim banner:
(Proposed extension beyond the public 2026-01 API — the public contract stops at
order_confirmed; this completes the buyer loop.)
repairer.procurements.insert — place a procurement (proposed extension)
(Proposed extension beyond the public 2026-01 API — the public contract stops at
order_confirmed; this completes the buyer loop.)
Creates a procurement in status: "order_requested" from a selection of offers, and moves the selected parts estimated → ordered. It emits buyer + supplier …procurements webhooks (status order_requested). This is the step that, in the faithful path, the seed has already performed for PR-1; for a brand-new job you place first, then confirm.
The full proposed-extension workflow order is therefore:
prepare_vehicle → open_repair_job → identify_parts → recommend_basket
→ place_procurement (ext) → confirm_procurement → reconcile_invoice (ext)
Single-confirmable-supplier constraint
The seed has three supplier orgs but one supplier credential — S1, "Christchurch Toyota — Parts", business_id 0b000000-0000-4000-8000-000000000010. A supplier can only confirm its own orders. Because this demo holds only S1's supplier credential, only a procurement placed from S1's offers can ever reach order_confirmed.
Where that constraint bites depends on which surface you use:
- On the raw wire,
repairer.procurements.insertis permissive — it will happily create a procurement from S2 / S3 offers and returnorder_requested. The mismatch only surfaces later, at confirm time:supplier.procurements.confirmreturnsnot_foundbecause no seeded credential can act as that supplier, and the order is left stranded inorder_requested. - Through the Rails MCP, the
place_procurementtool adds a client-side guard (decision D53): if any selected offer'ssupplier_business_idis not S1's, it fails fast before the wire with anunprocessableextension error that names the only confirmable supplier and points you back to the basket — so an agent never creates the dead-end order in the first place. This guard lives in the MCP tool, not in the wire contract.
See Errors for the exact recovery text.
repairer.procurements.invoices.list — reconcile invoices (proposed extension)
(Proposed extension beyond the public 2026-01 API — the public contract stops at
order_confirmed; this completes the buyer loop.)
Reads reconciled invoices and credit notes for a procurement (or job). An invoice is created on confirm with status: "reconciled", document_type: "invoice", and each line reconciliation: "matched". Invoices therefore appear only after order_confirmed — calling this before the order is confirmed returns nothing to reconcile.
Re-reading state — never assume, re-fetch
The lifecycle is event-driven and webhooks are identifier-only (payloads carry ids, not records), so after any transition you re-fetch the current truth by id:
repairer.jobs.get— the job header (vehicle,repairer_site_id,work_provider_id,claim_number, images) byexternalreference (e.g.CCC-2026-04817) orid. The wirejobs.getdoes not embed parts, procurements, or invoices — read those separately viarepairer.jobs.parts.list,repairer.procurements.get, andrepairer.procurements.invoices.list. (The Rails MCP'sget_jobaggregates all of them into one composite view; the wire method does not.) The natural first call to orient yourself on the seeded job.repairer.procurements.get— buyer-view read of one procurement's status and lines by UUID. Call it after a place or confirm to read the new status rather than inferring it.
After you confirm, call repairer.procurements.get for the new truth (order_confirmed), and repairer.jobs.parts.list to see the ordered parts flipped to ordered.
See also
- Quickstart — credentials, headers, and your first call.
- API Reference — full request/response schemas for every method.
- Errors — the coded error model, the bare-string asymmetry, and retryability.
- Webhooks — the events emitted at each transition.
- Machine-readable: /openapi.json, and the LLM digests /llms.txt and /llms-full.txt.