Recipes
Recipe — ERP integration
Sync finished calculations into SAP, Autodesk Vault, Oracle or Odoo — field mapping, idempotent upserts and the quote-validity timestamp your ERP needs.
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/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}/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}/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.
All three are in currency and on one basis: setup_cost is the cost
sheet's setup line times lot_size, and it is already part of
unit_cost and total_cost. Map it to a separate setup field for
reference; don't add it to the lot price. Stored values were restated on
2026-10-07 (see the changelog): if you
synced setup_cost before then, sync it again.
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}/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_unresolved (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 /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.