---
title: Recipe — teach an environment from your actuals
description: Close the calibration loop over the API. POST your realised costs, recalibrate an environment, and read the tightened prediction interval.
---

# Recipe: teach an environment from your actuals

[Adaptive Calibration](../concepts/calibration-environments.md) is what
turns a generic benchmark into a cortex for *your* shop. The dashboard
drives it interactively; this recipe drives the same thing **over the
API**, so you can wire it into your ERP close or a nightly job.

The loop:

1. Collect realised costs — work-order actuals from your ERP / MES.
2. POST them to a costing environment.
3. ARCNM re-quotes each part, fits the delta, and tightens the interval.
4. Every subsequent quote on that environment uses the calibrated fit.

Calibration writes need `parts:write`; the status read needs
`parts:read`.

---

## 1. The one-call path — actual unit costs

If you already have per-part actual unit costs, POST them to
`auto-calibrate`. You don't supply a predicted value — the service pairs
each part with its most recent quote in this environment and fits the
difference:

```python
import os, requests

BASE = "https://api.arcnm.io/api/v1"
H = {"X-API-Key": os.environ["ARCNM_API_KEY"]}   # scope: parts:write

r = requests.post(
    f"{BASE}/parts/calibration/environments/{env_id}/auto-calibrate",
    headers=H,
    json={"actuals": [
        {"part_revision_id": "1f…", "actual_unit_cost": 12.40, "lot_size": 50},
        {"part_revision_id": "2a…", "actual_unit_cost": 8.10,  "lot_size": 100},
    ]},
)
r.raise_for_status()
report = r.json()
# { matched, unmatched_part_ids, n_train, n_holdout, holdout_mape, holdout_coverage, … }
```

Pass `lot_size` whenever you have it (the work-order quantity) — without
it the fit can't separate one-time setup from per-unit cost. Parts with
no prior quote come back in `unmatched_part_ids`; quote them once, then
retry.

---

## 2. The ERP-native path — raw work orders

Most ERPs export work orders, not unit costs. `auto-calibrate-from-erp`
takes the raw columns (SAP `AUFK`/`AFKO`, Oracle `WIP_DISCRETE_JOBS`) and
does part-number resolution + unit-cost derivation for you:

```python
r = requests.post(
    f"{BASE}/parts/calibration/environments/{env_id}/auto-calibrate-from-erp",
    headers=H,
    json={"work_orders": [
        {"part_number": "BRACKET-001", "work_order_id": "WO-5512",
         "quantity": 50, "total_cost": 620.00},
    ]},
)
```

`work_order_id` is the idempotency key — re-POSTing the same work order
is a no-op, so you can stream your last 12 months once and replay safely.
Unknown part numbers come back in `unknown_part_numbers`.

---

## 3. Check calibration health

`GET .../environments/{env_id}/status` is the safe, read-only endpoint to
call first (and after) — it reports whether the env is calibrated, when
it last ran, how many observations are queued, and any drift alerts:

```json
{
  "env_id": "env_2a…",
  "active_posterior_count": 36,
  "last_fit_at": "2026-05-28T14:00:00Z",
  "observations": [{ "oracle": "erp", "target_metric": "unit_cost", "count": 142 }],
  "conformal": [{ "target_metric": "unit_cost", "observed_coverage": 0.91 }],
  "drift_alerts": []
}
```

`active_posterior_count: 0` means the env is still on platform
benchmarks — submit a batch to calibrate it.

---

## 4. Read the tightened interval

Once calibrated, every quote on that environment carries a narrower
quote-level interval (see
[Audit & provenance](../concepts/audit-provenance.md)). Use it to set a
margin buffer, or to trigger a re-quote when it's too wide for the
customer:

```json
{ "unit_cost": 12.84, "currency": "EUR", "interval": [12.20, 13.50] }
```

---

## Pick the right entry point

| Endpoint | Use it when |
|---|---|
| `auto-calibrate` | You have per-part actual **unit** costs. |
| `auto-calibrate-from-erp` | You have raw **work orders** (quantity + total cost). |
| `teach` | You have explicit **(predicted, actual)** pairs. |
| `learning-curve-fit` | You have **lot-progression** actuals for one part. |
| `conformal-recalibrate` | You only want to refresh the **prediction interval**. |
| `status` | Always — read calibration health before and after a run. |

---

## See also

- [Concepts → Adaptive Calibration](../concepts/calibration-environments.md) — the model behind this.
- [API → Calibration](../api/calibration.md) — every endpoint and payload.
- [Recipe → ERP integration](./erp-integration.md) — the sync-*out* side of the same loop.
