ARCNM

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:

  1. A poller that detects when a calculation finishes.
  2. Field mapping for SAP TC (S/4HANA), Autodesk Vault, Oracle Fusion, and Odoo.
  3. 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