Recipes
Recipe — ERP integration
Sync calculations to SAP / Autodesk Vault / Oracle / Odoo. Field mapping, idempotent upserts.
Sync every finished ARCNM calculation into your ERP — unit price, part number, customer RFQ id, and quote-validity timestamp — with idempotent upserts. A price on a dashboard isn't enough; it has to land where your team works. This recipe shows the canonical pattern.
By the end you'll have:
- A poller that detects when a calculation finishes.
- Field mapping for SAP TC (S/4HANA), Autodesk Vault, Oracle Fusion, and Odoo.
- Idempotent upserts so retries don't double-create.
1. Detect completion by polling
ARCNM delivers webhooks for wallet events (wallet.credited,
wallet.debited, wallet.low_balance) and an endpoint.test
ping — there is no calculation.* webhook. To learn when a
calculation finishes, poll
GET /api/v1/parts/calculations/{id} until status reaches a
terminal value (succeeded, failed, cancelled, timed_out).
import os, time, requests
BASE = "https://api.arcnm.io/api/v1"
HEADERS = {"X-API-Key": os.environ["ARCNM_API_KEY"]} # scope: parts:read
def poll(calc_id: str, timeout_s: int = 180) -> dict:
deadline = time.time() + timeout_s
while time.time() < deadline:
r = requests.get(f"{BASE}/parts/calculations/{calc_id}", headers=HEADERS)
r.raise_for_status()
calc = r.json()
if calc["status"] in ("succeeded", "failed", "cancelled", "timed_out"):
return calc
time.sleep(3)
raise TimeoutError(calc_id)
Run the poller from the same worker that enqueued the calc (it already has the id), or sweep your open quotes on a schedule:
# Periodically reconcile every quote your ERP is still waiting on.
for row in erp.open_arcanum_quotes():
calc = requests.get(f"{BASE}/parts/calculations/{row.calc_id}", headers=HEADERS).json()
if calc["status"] == "succeeded":
sync_to_erp(calc)
elif calc["status"] in ("failed", "cancelled", "timed_out"):
flag_for_review(row, calc.get("error"))
2. The fields you get back
A succeeded calculation carries:
{
"id": "9d8f…",
"part_id": "…",
"part_revision_id": "…",
"costing_environment_id": "…",
"status": "succeeded",
"engine": "arcanum",
"lot_size": 50,
"annual_volume": 500,
"unit_cost": 12.84,
"total_cost": 642.00,
"setup_cost": 52.50,
"currency": "EUR",
"material_grade_id": "…",
"material_ref": "1.4301",
"finished_at": "2026-05-28T14:23:32Z"
}
Map unit_cost / total_cost / setup_cost + the sibling currency
into your ERP price fields, and keep id as the cross-reference back
to ARCNM.
3. Field mapping per ERP
SAP TC (S/4HANA) — MARA + MBEW
def sync_to_sap(calc: dict):
sap.upsert_material({
"MATNR": erp_part_number(calc), # 18-char alphanumeric
})
sap.upsert_pricing({
"MATNR": erp_part_number(calc),
"VPRSV": "S", # standard price
"WAERS": calc["currency"], # ISO-4217
"STPRS": round(calc["unit_cost"] * 100), # SAP wants cents
"AWERT": round(calc["total_cost"] * 100), # extended value in cents
"EXTERNAL_REF": calc["id"], # back to ARCNM
})
Autodesk Vault
Vault stores quotes as iProperties on the file:
def sync_to_vault(calc: dict):
vault.set_iprops(
file_id=lookup_vault_id(calc["part_revision_id"]),
iprops={
"ArcanumCalcId": calc["id"],
"UnitCost": calc["unit_cost"],
"TotalCost": calc["total_cost"],
"SetupCost": calc["setup_cost"],
"Currency": calc["currency"],
"LotSize": calc["lot_size"],
"PricedAt": calc["finished_at"],
},
)
Oracle Fusion (ITEM_PRICE)
def sync_to_oracle(calc: dict):
oracle.upsert_item_price({
"item_number": erp_part_number(calc),
"price_list_name": "ARCNM",
"unit_price": calc["unit_cost"],
"currency_code": calc["currency"],
"effective_from": calc["finished_at"],
"comments": f"ARCNM {calc['id']}, lot={calc['lot_size']}, env={calc['costing_environment_id']}",
})
Odoo (product.template)
def sync_to_odoo(calc: dict):
odoo.env["product.template"].search(
[("default_code", "=", erp_part_number(calc))]
).write({
"standard_price": calc["unit_cost"],
"x_arcanum_calc_id": calc["id"],
"x_arcanum_currency": calc["currency"],
})
erp_part_number(calc) is your own lookup from calc["part_id"] /
calc["part_revision_id"] to the part number you stored when you
created the calc — ARCNM doesn't echo your ERP part number back on
the calc projection.
4. Idempotent upserts
Your reconcile sweep can revisit the same succeeded calc more than once. Dedupe on the calculation id so a re-sync is a no-op:
def sync_to_erp(calc: dict):
if erp.already_synced(calc["id"]):
return # idempotent skip
sync_to_sap(calc)
erp.mark_synced(calc["id"], synced_at=datetime.utcnow())
Build an arcanum_synced table with (calc_id PRIMARY KEY, synced_at)
so a re-visit is an O(1) lookup. A succeeded calc is immutable — its
costs never change after finished_at, so syncing once is enough.
5. Material references from your ERP
When you create a calculation, you can hand ARCNM your own material
reference so the cost resolves against the grade you actually buy.
Pass either a free-form material_ref (Werkstoffnummer, AISI/SAE
code, trade name, or an org SKU) or the typed material_grade_id:
requests.post(
f"{BASE}/parts/calculations/quote",
headers={"X-API-Key": os.environ["ARCNM_API_KEY"]}, # scope: parts:write
json={
"part_revision_id": part_revision_id,
"costing_environment_id": env_id,
"material_ref": erp_material_code, # e.g. "1.4301"
},
)
If both are supplied, material_grade_id wins. A reference that can't
be matched comes back as material_not_resolved (422). To discover the
grade ids and codes ARCNM recognises — and to map your own internal
codes — see Materials & overrides.
Common gotchas
| Problem | Cause / fix |
|---|---|
| Duplicate ERP rows | You're not deduping on the calc id. A reconcile sweep can see the same succeeded calc twice — gate on calc["id"]. |
| Waiting forever for a webhook | There is no calculation.* webhook. Poll GET /parts/calculations/{id} instead (see step 1). |
| Wrong currency on the ERP row | The ERP defaults to org currency; ARCNM stamps currency per calc. Always pull it from the calc. |
402 insufficient_funds when enqueuing |
The wallet hold for the run exceeds your balance. Top up, then retry. Subscribe to wallet.low_balance to get ahead of it. |
See also
- Webhooks — wallet events + signature verification.
- Calculations — the full calc projection.
- Pricing & billing — wallet holds + usage.
- Recipes → Bulk RFQ triage — for the inbound side.
- Recipe → Teach from your actuals — push realised costs back to calibrate.