<!-- title: Quickstart | order: 2 | summary: Zero to a confirmed parts order in under five minutes, copy-paste curl. -->

# 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:

```text
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`](https://jqlang.github.io/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.

```bash
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.

```bash
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:

```json
{
  "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):
> ```bash
> 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:

```text
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](./authentication.md) 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`):

```bash
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:

```json
{
  "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`).

```bash
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):

```json
{
  "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](./for-ai-agents.md) 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:

```bash
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):

```json
{
  "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:

```bash
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):

```json
{
  "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`):

```bash
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):

```json
{
  "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](./lifecycle.md)** — the full ordered flow, every status transition, and exactly where the faithful `2026-01` contract ends and the proposed extensions begin.
- **[Webhooks](./webhooks.md)** — the HMAC-SHA256 signed event envelope, the 5-minute replay window, and copy-paste verification snippets (TypeScript + Python).
- **[API Reference](./api-reference.md)** — all 14 wire methods plus the proposed extensions, with request/response schemas and coded errors.
- **[For AI Agents](./for-ai-agents.md)** — 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`](/llms.txt) (page map), [`/llms-full.txt`](/llms-full.txt) (full corpus), and [`/openapi.json`](/openapi.json) (the `2026-01` spec).
