Quickstart
Go from nothing to a confirmed parts order in under five minutes — no signup, no dashboard, no waiting on a sales call. Credentials are obtained programmatically in the first call, and a fully-seeded collision-repair job is already loaded, so every step below is copy-paste runnable against the deployed mock.
This is Rails Sandbox — a Rails-compatible API: a faithful mock of the public Integrations API (
2026-01), plus two clearly-labelled proposed extensions (steps 5 and 7). It is not a real vendor's own documentation and not a real production endpoint.
Calling convention. The API is RPC-over-POST. Every call is:
POST https://partifact-mock-rails.thanhvuttv.workers.dev/api/2026-01/<dotted.method>
with a JSON body. The dotted method name is the final path segment (e.g. .../api/2026-01/repairer.jobs.get). All examples below assume bash + jq.
Steps 1–4 and 6 are faithful to the public 2026-01 contract; steps 5 and 7 are proposed extensions (each marked below).
0. Pick your sandbox
This sandbox gives every distinct x-partifact-tenant value its own fresh, isolated copy of the world — seeded identically (the same Corolla job, the same demo credentials), self-healing after an hour of inactivity. Pick any name and send it as a header on every call below, and your Quickstart runs in private — the install in step 1 is guaranteed to work for you even if a hundred other people ran it first.
BASE=https://partifact-mock-rails.thanhvuttv.workers.dev
TENANT="quickstart-$(whoami)" # any [a-z0-9_-] name; your own private sandbox
x-partifact-tenantis a sandbox convenience for isolation, not part of the contract — a real the tenant boundary is the organization. Omit the header and you share one base world with everyone else (where the single-use access code in step 1 may already be spent). To start over from a clean seed, just changeTENANT.
1. Install — get credentials programmatically
integrations.insert is the only unauthenticated method: it completes the OAuth install handshake and mints your api_key + integration_id. The demo OAuth client and its access code are pre-provisioned in every fresh sandbox, so you can run this verbatim.
INSTALL=$(curl -sS -X POST "$BASE/api/2026-01/integrations.insert" \
-H "x-partifact-tenant: $TENANT" \
-H "Content-Type: application/json" \
-d '{"access_code":"ac_demo_valid_15m","client_id":"partly_client_demo","client_secret":"secret_demo_8f3a"}')
API_KEY=$(echo "$INSTALL" | jq -r .api_key)
INTEGRATION_ID=$(echo "$INSTALL" | jq -r .integration_id)
echo "api_key=$API_KEY"
echo "integration_id=$INTEGRATION_ID"
Exchanges {access_code, client_id, client_secret} for long-lived, org-scoped credentials. Expected response shape:
{
"api_key": "partly_6bf45534812d4a0ea5a0dd700585caf3",
"integration_id": "b07982d2-b41d-428a-81cc-596f99d2604f"
}
The api_key is freshly minted (prefix partly_ followed by a UUID with dashes stripped); the integration_id is a fresh UUID. The access code is single-use per sandbox — after a successful install, ac_demo_valid_15m is consumed in this TENANT, and re-running install here returns {"type":"invalid_access_code"} (HTTP 401). That is expected, not a bug — change TENANT for a fresh code.
Prefer to skip the install? Every sandbox also pre-loads a ready-to-use repairer credential you can drop straight into the headers below (step 6 uses the matching supplier credential):
API_KEY=partly_demo_repairer_3f8a1c0d9e2b4a67b1c2 INTEGRATION_ID=0c000000-0000-4000-8000-000000000001
2. The two headers on every authenticated call
Every method except integrations.insert requires both of these headers:
Authorization: Bearer <api_key>
Partly-Integration-ID: <integration_id>
The Bearer scheme prefix is required. If either header is missing or the (api_key, integration_id) pair is unknown, the call returns {"type":"unauthorized"} (HTTP 401). If the credential is valid but holds the wrong role for the method, you get {"type":"forbidden"} (HTTP 403) — see Authentication for the scope model. From here on, every call carries these two headers and your x-partifact-tenant.
3. Read the pre-seeded job
A complete collision-repair job is already loaded — a 2019 Toyota Corolla (front-end collision) with external_id CCC-2026-04817. No setup required. Fetch it by external reference (the identity wrapper is oneOf {external} or {id} — send exactly one; note the key is external, not external_id):
curl -sS -X POST "$BASE/api/2026-01/repairer.jobs.get" \
-H "x-partifact-tenant: $TENANT" \
-H "Authorization: Bearer $API_KEY" \
-H "Partly-Integration-ID: $INTEGRATION_ID" \
-H "Content-Type: application/json" \
-d '{"identity":{"external":"CCC-2026-04817"}}'
Returns the job header — vehicle, site, insurer, claim. Expected response shape:
{
"id": "0d000000-0000-4000-8000-000000000001",
"external_id": "CCC-2026-04817",
"job_number": "J-4817",
"claim_number": "AMI-CLM-771204",
"repairer_site_id": "0b000000-0000-4000-8000-000000000001",
"work_provider_id": "0a000000-0000-4000-8000-000000000020",
"vehicle": { "chassis_number": "JTDBR32E730012345", "plate_number": "MJL472", "plate_state": "NZ" },
"images": []
}
The wire repairer.jobs.get returns the header only — it does not embed parts, procurements, or invoices. The job's parts are priced in the basket (next step), where each offer references the job_part_ids it fulfils.
4. Get the recommended basket
repairer.jobs.baskets.latest.get returns the latest priced basket for the job: the recommended offers and the suppliers they reference. Note the request key here is job_identity (not identity).
curl -sS -X POST "$BASE/api/2026-01/repairer.jobs.baskets.latest.get" \
-H "x-partifact-tenant: $TENANT" \
-H "Authorization: Bearer $API_KEY" \
-H "Partly-Integration-ID: $INTEGRATION_ID" \
-H "Content-Type: application/json" \
-d '{"job_identity":{"external":"CCC-2026-04817"}}'
Read-only; a job with no priced parts returns empty offers/suppliers (not an error). Expected response shape (two of the twelve seeded offers shown):
{
"offers": [
{
"name": "Toyota Genuine Headlamp RH",
"sellable_id": "0f000000-0000-4000-8000-000000000007",
"supplier_business_id": "0b000000-0000-4000-8000-000000000010",
"job_part_ids": ["0e000000-0000-4000-8000-000000000004"],
"quantity": 1,
"trade_price": "631.00",
"condition": { "type": "new", "payload": {} }
},
{
"name": "Toyota Genuine Radiator Support",
"sellable_id": "0f000000-0000-4000-8000-00000000000a",
"supplier_business_id": "0b000000-0000-4000-8000-000000000010",
"job_part_ids": ["0e000000-0000-4000-8000-000000000006"],
"quantity": 1,
"trade_price": "468.00",
"condition": { "type": "new", "payload": {} }
}
],
"suppliers": [
{ "business_id": "0b000000-0000-4000-8000-000000000010", "name": "Christchurch Toyota — Parts" },
{ "business_id": "0b000000-0000-4000-8000-000000000011", "name": "Repco Trade — Christchurch" },
{ "business_id": "0b000000-0000-4000-8000-000000000012", "name": "PartsMaster Aftermarket NZ" }
]
}
Wire-truth note (D52). The basket response carries no
currencyfield — not on the offer, not at the envelope.trade_priceis a bare string. Currency is a procurement-level concept, not a basket-level one: it first appears later, ascurrency_codeon the procurement (and on the invoice). Any tool that shows a currency on a basket offer is deriving it for presentation, not reading it off the wire. The seed prices are NZD ex-GST.
The two offers above both come from Christchurch Toyota — Parts (supplier_business_id 0b000000-0000-4000-8000-000000000010). That matters for the next step.
5. Place the order
(Proposed extension beyond the public 2026-01 API — the public contract stops at
order_confirmed; this completes the buyer loop.)
repairer.procurements.insert creates a procurement from selected basket offers in status: "order_requested" and moves the selected parts estimated → ordered. This method is not part of the public 2026-01 contract — its response carries "x_extension": true to mark it as a proposed extension.
Pick offers from one confirmable supplier. The sandbox holds exactly one supplier credential — for Christchurch Toyota — Parts (business_id 0b000000-0000-4000-8000-000000000010). The raw wire will let you place an order with any supplier, but only an order placed entirely with Christchurch Toyota can be confirmed in step 6 — no other supplier has a confirming credential, so a mismatch surfaces as not_found at confirm time and the order is left stranded in order_requested. (The companion Rails MCP adds a guard that refuses a non-confirmable placement up front; the raw wire does not.) So the two Christchurch Toyota offers from step 4 are selected here:
PLACE=$(curl -sS -X POST "$BASE/api/2026-01/repairer.procurements.insert" \
-H "x-partifact-tenant: $TENANT" \
-H "Authorization: Bearer $API_KEY" \
-H "Partly-Integration-ID: $INTEGRATION_ID" \
-H "Content-Type: application/json" \
-d '{
"job_identity": {"external": "CCC-2026-04817"},
"offer_selections": [
{
"sellable_id": "0f000000-0000-4000-8000-000000000007",
"supplier_business_id": "0b000000-0000-4000-8000-000000000010",
"job_part_ids": ["0e000000-0000-4000-8000-000000000004"],
"quantity": 1
},
{
"sellable_id": "0f000000-0000-4000-8000-00000000000a",
"supplier_business_id": "0b000000-0000-4000-8000-000000000010",
"job_part_ids": ["0e000000-0000-4000-8000-000000000006"],
"quantity": 1
}
]
}')
PROCUREMENT_ID=$(echo "$PLACE" | jq -r .procurement.id)
echo "procurement_id=$PROCUREMENT_ID"
echo "$PLACE" | jq '{x_extension, id: .procurement.id, status: .procurement.status, currency_code: .procurement.currency_code, supplier: .procurement.supplier_business.name}'
Creates the order and captures its id. Expected response shape (abridged):
{
"x_extension": true,
"procurement": {
"id": "3263c65c-d458-410b-a5f0-df2b51c16a17",
"job_id": "0d000000-0000-4000-8000-000000000001",
"status": "order_requested",
"currency_code": "NZD",
"supplier_business": { "id": "0b000000-0000-4000-8000-000000000010", "name": "Christchurch Toyota — Parts" }
}
}
The procurement is now in order_requested — the RH headlamp (631.00) plus the radiator support (468.00) total 1099.00 NZD. The created procurement is immediately retrievable via the real repairer.procurements.get and confirmable via the real supplier.procurements.confirm — the extension only adds the missing "place" front door; it never alters the real state machine.
6. Confirm the order → order_confirmed
supplier.procurements.confirm is the faithful terminal step of the public 2026-01 lifecycle: it transitions the procurement order_requested → order_confirmed, locks the order, and creates a reconciled invoice. Confirming is a supplier-scope action, so it needs the pre-loaded supplier credential (not the repairer one used above) — same sandbox, different headers:
SUP_API_KEY=partly_demo_supplier_8b4e2f1a6c0d3e9f7a25
SUP_INTEGRATION_ID=0c000000-0000-4000-8000-000000000002
curl -sS -X POST "$BASE/api/2026-01/supplier.procurements.confirm" \
-H "x-partifact-tenant: $TENANT" \
-H "Authorization: Bearer $SUP_API_KEY" \
-H "Partly-Integration-ID: $SUP_INTEGRATION_ID" \
-H "Content-Type: application/json" \
-d "{\"identity\":{\"id\":\"$PROCUREMENT_ID\"}}"
Confirms by procurement id, as the supplier. Expected response shape (abridged):
{
"id": "3263c65c-d458-410b-a5f0-df2b51c16a17",
"status": "order_confirmed"
}
order_confirmed is the terminal state of the public 2026-01 contract — the faithful lifecycle ends here. A supplier can only confirm its own orders; confirming an order placed with a different supplier returns {"type":"not_found"} (which is exactly why step 5 selected Christchurch Toyota — Parts). You have now driven a parts order from zero to confirmed.
Want to confirm without placing first? Every sandbox also ships one procurement already in
order_requested(id10000000-0000-4000-8000-000000000001) — substitute that id above to exercise confirm on its own.
7. (Optional) Reconcile the invoice
(Proposed extension beyond the public 2026-01 API — the public contract stops at
order_confirmed; this completes the buyer loop.)
repairer.procurements.invoices.list reads the reconciled invoices and credit notes produced for a procurement (or for all of a job's procurements). Invoices appear only after order_confirmed. Like step 5, this is not part of the public 2026-01 contract — its response carries "x_extension": true. Use the repairer credentials again, with exactly one selector (procurement_identity XOR job_identity):
curl -sS -X POST "$BASE/api/2026-01/repairer.procurements.invoices.list" \
-H "x-partifact-tenant: $TENANT" \
-H "Authorization: Bearer $API_KEY" \
-H "Partly-Integration-ID: $INTEGRATION_ID" \
-H "Content-Type: application/json" \
-d "{\"procurement_identity\":{\"id\":\"$PROCUREMENT_ID\"}}"
Lists invoices for the procurement you just confirmed. Expected response shape (abridged):
{
"x_extension": true,
"items": [
{
"document_type": "invoice",
"status": "reconciled",
"currency_code": "NZD",
"total": "1099.00",
"line_items": [ { "reconciliation": "matched" }, { "reconciliation": "matched" } ]
}
]
}
That closes the buyer loop: a reconciled invoice for 1099.00 NZD, every line matched. Providing both selectors or neither returns {"type":"bad_request"}.
What next
You now have working credentials and a full place → confirm → reconcile transcript. Go deeper:
- Lifecycle — the full ordered flow, every status transition, and exactly where the faithful
2026-01contract ends and the proposed extensions begin. - Webhooks — the HMAC-SHA256 signed event envelope, the 5-minute replay window, and copy-paste verification snippets (TypeScript + Python).
- API Reference — all 14 wire methods plus the proposed extensions, with request/response schemas and coded errors.
- For AI Agents — the same lifecycle as an MCP server: 13 tools with enriched, self-correcting errors, for agents that build integrations from the docs alone.
Machine-readable indexes: /llms.txt (page map), /llms-full.txt (full corpus), and /openapi.json (the 2026-01 spec).