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-tenant is 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 change TENANT.


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 currency field — not on the offer, not at the envelope. trade_price is a bare string. Currency is a procurement-level concept, not a basket-level one: it first appears later, as currency_code on 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 (id 10000000-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:

Machine-readable indexes: /llms.txt (page map), /llms-full.txt (full corpus), and /openapi.json (the 2026-01 spec).