For Suppliers
Developer documentation

VCE Ingestion API

Send your purchase and BOM data straight from your own system (SAP, any ERP, or a script) into Workwise — no Excel files, no manual work. This guide walks you through it, step by step.

What this is

To estimate what a part should cost, Workwise needs three kinds of data from you. Instead of uploading Excel sheets, your system can send them here as JSON. We accept the data, save it, and reply right away with a summary of what happened.

How it works

1
Get a key
Create an API key in Workwise. Think of it as a password your system uses to prove who it is.
2
Upload your file
Save your data as a .json file and POST it to the right web address.
3
Get a reply
We tell you how many rows were added, updated, or skipped — and flag any that had problems.

The three kinds of data

Each one has its own web address. They all start with:

https://api.letsworkwise.com/api/v1/vcost-estimation
What you're sendingWhere to send itIf you send it again
Purchase history — rates you've paid vendorsPOST /ingest/mrrAdds new rows; exact duplicates are skipped
Model → BOM list — which parts make up a productPOST /ingest/model-bom-mapReplaces that model + BOM's parts
BOM details — full BOM lines with datesPOST /ingest/bom-itemsReplaces that model + BOM's lines

Getting a key

Every request needs a key so we know it's really you. Create one on the Integrations → API Keys page (any signed-in Workwise user can do this). You'll see the key only once, so copy it somewhere safe — if you lose it, just make a new one.

Add the key to every request, like this:

Authorization: Bearer wvce_live_xxxxxxxx_………
# or
X-API-Key: wvce_live_xxxxxxxx_………

What a key is allowed to send

When you create a key, you tick which kinds of data it may send. If a key tries to send something it isn't allowed to, the request is refused (403). The three permissions are:

ingest:mrringest:model-bom-mapingest:bom-items

Keys never expire — to rotate, make a new one and revoke the old one. A key already knows which company it belongs to, so you never send a company id yourself.

Good to know

A few simple things before you start:

  • You always send a .json file. Save your data as a file and upload it as multipart form-data in a field named file. (Sending JSON straight in the request body is not accepted — it must be a file.) We keep a copy of every file you send, so you can look it up later.
  • The file holds one JSON object. That object has a single list inside it — records for purchase history, mappings for the model → BOM list, or boms for BOM details. See the exact shape in each section below.
  • Dates look like "2025-10-27" (year-month-day).
  • The part number matters most. We read the part's type and its matching keys straight from cpartno, so it has to be written in the standard Bliss format (for example 3BA24800X3M-001R1T7B). If it's written wrong, the part can't be matched to a price.
  • You don't send everything. We fill in the behind-the-scenes fields for you — your company, the record id, the matching keys, and so on. Just send the fields shown in the examples below.

How much at once

There are three limits. Each one has its own reply, so you always know which you hit. If you have more data than this, split it across several files and send them one after another.

Every endpoint below also lists its own limits next to it, so you don't have to come back here while you build.

LimitHow muchIf you go over
File sizeUp to 50 MB per .json file413 — split into smaller files
Rows in one fileUp to 5,000. For MRR that's the number of records. For mapping and BOM it's the total components across the whole file — not the number of models.400 — split into smaller files
Requests per minuteUp to 120 per key429 — wait and retry (we tell you how long in the Retry-After header)

Why size and rows are both capped

They protect different things, so one doesn't replace the other. The row cap keeps a request quick to process. The size cap is separate because you're welcome to send extra fields of your own — which means 5,000 rows tells us nothing about how big the file actually is. Both are checked, and you'll hear about whichever you cross first.

Counting components, not models. A mapping or BOM file with 100 models that have 60 components each is 6,000 rows, not 100 — so it will be turned away. Keep whole models together in a file (never split one model across two files) and keep each file under 5,000 components.

What going over looks like

A file bigger than 50 MB:

HTTP 413 Payload Too Large

{
  "status": 0,
  "message": "JSON file is too large. The limit is 50 MB per file — split
              the payload across smaller files and send them one after
              another."
}

More than 5,000 rows in one file:

HTTP 400 Bad Request

// An MRR file holding 6,400 records:
{
  "status": 2,
  "errors": {
    "records": "Too many records in one file. The limit is 5,000 rows per
                file — split the payload across smaller files and send them
                one after another."
  }
}

// A BOM file whose components add up to 6,400:
{
  "status": 0,
  "message": "Too many rows in one file (6,400). The limit is 5,000 rows per
              file — split the payload across smaller files and send them one
              after another."
}

Nothing is saved when you go over a limit — we check before writing anything, so you'll never end up with half a file loaded. Fix the file and send it again.

Send: Purchase history (MRR)

POSThttps://api.letsworkwise.com/api/v1/vcost-estimation/ingest/mrr

These are the rates you've actually paid vendors in the past — the numbers Workwise uses to price parts. Send them as a simple list under records. Five fields are required on each record: cpartno, irate, supplier, dated, and type. Everything else is optional — send what you have.

Your .json file should contain a records list, like this:

{
  "source": "SAP-ECC",
  "records": [
    {
      "cpartno":   "3BA24800X3M-001R1T7B",  // required  -> cpartno   (drives match keys)
      "irate":     231.00,                   // required  -> irate     (the vendor rate)
      "supplier":  "INTRICAST PVT. LTD.",    // required  -> supplier
      "dated":     "2025-10-27",             // required  -> dated     (ISO date, recency)
      "type":      "02",                      // required  -> type      (02/07 = complete buy)
      "mrrNo":     "001864",                  //           -> mrr_no
      "itemName":  "BODY",                    //           -> item_name
      "unit":      "NOS",                     //           -> unit
      "poNo":      "PO-2025-1180",            //           -> p_o_no
      "icode":     70060257,                  //           -> icode
      "billChl":   "BILL",                    //           -> bill_chl
      "advised":   10,                        //           -> advised
      "rcvd":      10,                        //           -> rcvd
      "accept":    10,                        //           -> accept
      "reject":    0,                         //           -> reject
      "lc":        0,                         //           -> lc
      "tcNo":      "TC-1180",                 //           -> tc_no
      "excDoc":    "",                        //           -> exc_doc
      "testCert":  "TC/2025/114",             //           -> test_cert
      "finvno":    "INV-8842",                //           -> finvno
      "wono":      "WO-551",                  //           -> wono
      "gateEntry": "GE-9931",                 //           -> gate_entry
      "gateDate":  "2025-10-25",              //           -> gate_date
      "entBy":     "RAVI",                    //           -> ent_by
      "inspBy":    "SATISH",                  //           -> insp_by
      "rwCond":    "OK",                      //           -> rw_cond
      "qcDate":    "2025-10-26",              //           -> qcdate
      "store":     "MAIN",                    //           -> store
      "mtime":     0,                         //           -> mtime
      "invDate":   "2025-10-24",              //           -> invdate
      "cdrgno":    "24800X3",                 //           -> cdrgno
      "revNo":     "R1",                      //           -> rev_no
      "exchRate":  1,                         //           -> exch_rate
      "awb":       "",                        //           -> awb
      "modeTpt":   "ROAD",                    //           -> mode_tpt
      "poDate":    "2025-10-01",              //           -> podate
      "boeNo":     ""                         //           -> boe_no
    }
  ]
}
Limits for this endpoint
Scope
Your key needs ingest:mrr. Without it you get 403.
Rows
Up to 5,000 items in records — one record is one row. Over that gets 400.
File
One .json file, up to 50 MB. Bigger gets 413. An inline JSON body is never accepted — 400.
Rate
120 requests per minute per key. Over that gets 429 with a Retry-After header.
Repeat sends
Safe. Nothing is overwritten — identical records come back counted as duplicates, and you can send the same file twice without harm.

Every field, explained

Field you sendSaved asNeeded?
cpartnocpartnorequired
irateiraterequired
suppliersupplierrequired
dateddatedrequired
typetyperequired
mrrNomrr_nooptional
itemNameitem_nameoptional
unitunitoptional
poNop_o_nooptional
icodeicodeoptional
billChlbill_chloptional
advisedadvisedoptional
rcvdrcvdoptional
acceptacceptoptional
rejectrejectoptional
lclcoptional
tcNotc_nooptional
excDocexc_docoptional
testCerttest_certoptional
finvnofinvnooptional
wonowonooptional
gateEntrygate_entryoptional
gateDategate_dateoptional
entByent_byoptional
inspByinsp_byoptional
rwCondrw_condoptional
qcDateqcdateoptional
storestoreoptional
mtimemtimeoptional
invDateinvdateoptional
cdrgnocdrgnooptional
revNorev_nooptional
exchRateexch_rateoptional
awbawboptional
modeTptmode_tptoptional
poDatepodateoptional
boeNoboe_nooptional

Send: Model → BOM list

POSThttps://api.letsworkwise.com/api/v1/vcost-estimation/ingest/model-bom-map

This says which child parts make up each model. Each entry has a header (the model + BOM) and a list of components (the parts). If you send the same model and BOM again, we replace that list with the new one — so you never get duplicates. Required: modelNo and bomNo in the header, plus cpartno and childQty on each component.

Your .json file should contain a mappings list, like this:

{
  "source": "SAP-ECC",
  "externalRef": "MBM-90010001",
  "mappings": [
    {
      "modelNo":     "26DA10-120",            // required -> model_no  (upsert key with bomNo)
      "bomNo":       "90010001",              // required -> bom_no     (upsert key with modelNo)
      "productName": "GLOBE VALVE 2\" 300#",  //          -> product_name
      "components": [
        {
          "cpartno":    "3BA24800X3M-001R1T7B", // required -> cpartno   (drives match keys)
          "childQty":   1,                      // required -> child_qty
          "childName":  "BODY",                 //          -> child_name
          "childCode":  "70060257",             //          -> child_code
          "parentCode": "26DA10-120",           //          -> parent_code
          "childUnit":  "NOS"                   //          -> chl_unit
        }
      ]
    }
  ]
}
Limits for this endpoint
Scope
Your key needs ingest:model-bom-map. Without it you get 403.
Rows
Up to 5,000 — counted as the total components in the file, not the number of mappings. 100 models with 60 components each is 6,000 and gets 400.
File
One .json file, up to 50 MB. Bigger gets 413. An inline JSON body is never accepted — 400.
Rate
120 requests per minute per key. Over that gets 429 with a Retry-After header.
Repeat sends
Replaces, not adds. Every (modelNo, bomNo) you send has its existing components deleted and rewritten from your file.
Send each model complete, in one file. Because a send replaces the whole list for that model, a file containing only one component will leave that model with only that component — the rest are deleted. And if you split one model across two files, the second file wipes what the first one wrote. When you chunk a big load, always keep a model's components together.

Every field, explained

Field you sendWhereSaved asNeeded?
modelNoheadermodel_norequired
bomNoheaderbom_norequired
productNameheaderproduct_nameoptional
cpartnocomponentcpartnorequired
childQtycomponentchild_qtyrequired
childNamecomponentchild_nameoptional
childCodecomponentchild_codeoptional
parentCodecomponentparent_codeoptional
childUnitcomponentchl_unitoptional

Send: BOM details

POSThttps://api.letsworkwise.com/api/v1/vcost-estimation/ingest/bom-items

This is the full BOM — the same as the "SRV BOM Item" export — including the BOM date, which Workwise uses to know which BOM is the latest. Like the mapping above, it has a header and a list of components, and sending the same model + BOM again replaces it. Required: modelNo, bomNo, and bomDate in the header, plus cpartno and childQty on each component.

Your .json file should contain a boms list, like this:

{
  "source": "SAP-ECC",
  "externalRef": "BOM-90010001-r0",
  "boms": [
    {
      "modelNo":      "26DA10-120",              // required -> model_no + parent_code
      "bomNo":        "90010001",                // required -> bom_no
      "bomDate":      "2020-07-31",              // required -> bom_date (ISO date, recency)
      "bomNoRaw":     "90010001/2020",           //          -> bom_no_raw
      "productName":  "GLOBE VALVE 2\" 300# CF8M", //       -> product_name
      "versionNo":    "1",                       //          -> ver_no
      "revisionNo":   0,                          //          -> rev_no
      "enteredBy":    "RAVI",                     //          -> ent_by
      "enteredDate":  "2020-07-30",               //          -> ent_dt
      "approvedBy":   "SATISH",                   //          -> app_by
      "approvedDate": "2020-07-31",               //          -> app_dt
      "vdd":          "VDD-2020-114",             //          -> vdd
      "voucherDate":  "2020-07-31",               //          -> vchdate
      "components": [
        {
          "cpartno":    "3BA24800X3M-001R1T7B",   // required -> cpartno + child_partno
          "childQty":   1,                        // required -> child_qty
          "childName":  "BODY",                   //          -> child_name
          "childCode":  "70060257",               //          -> child_code
          "ibcode":     70060257,                 //          -> ibcode (child_code falls back to it)
          "parentCode": "26DA10-120",             //          -> parent_code
          "parentUnit": "NOS",                    //          -> par_unit
          "childUnit":  "NOS",                    //          -> chl_unit
          "lotSize":    1,                        //          -> lot_size
          "remarks":    "",                       //          -> rmks
          "srNo":       1                         //          -> srno
        }
      ]
    }
  ]
}
Limits for this endpoint
Scope
Your key needs ingest:bom-items. Without it you get 403.
Rows
Up to 5,000 — counted as the total components in the file, not the number of BOMs. Over that gets 400.
File
One .json file, up to 50 MB. Bigger gets 413. An inline JSON body is never accepted — 400.
Rate
120 requests per minute per key. Over that gets 429 with a Retry-After header.
Repeat sends
Replaces, not adds. Every (modelNo, bomNo) you send has its existing components deleted and rewritten from your file.
Send each BOM complete, in one file. Same rule as the mapping above: a send replaces the whole component list for that model and BOM, so never split one BOM across two files — the second would wipe what the first wrote.
Good to know: the header modelNo is the product; each component's cpartno is a child part inside it — that's the one we match to a purchase rate.

Every field, explained

Field you sendWhereSaved asNeeded?
modelNoheadermodel_no + parent_coderequired
bomNoheaderbom_norequired
bomDateheaderbom_daterequired
bomNoRawheaderbom_no_rawoptional
productNameheaderproduct_nameoptional
versionNoheaderver_nooptional
revisionNoheaderrev_nooptional
enteredByheaderent_byoptional
enteredDateheaderent_dtoptional
approvedByheaderapp_byoptional
approvedDateheaderapp_dtoptional
vddheadervddoptional
voucherDateheadervchdateoptional
cpartnocomponentcpartno + child_partnorequired
childQtycomponentchild_qtyrequired
childNamecomponentchild_nameoptional
childCodecomponentchild_codeoptional
ibcodecomponentibcodeoptional
parentCodecomponentparent_codeoptional
parentUnitcomponentpar_unitoptional
childUnitcomponentchl_unitoptional
lotSizecomponentlot_sizeoptional
remarkscomponentrmksoptional
srNocomponentsrnooptional

Check past sends

Want to see what you sent earlier? These two let you look it up. Both only ever show your own company's data.

GEThttps://api.letsworkwise.com/api/v1/vcost-estimation/ingest/history

Your recent sends, newest first. You can filter with dataset, page, and limit.

// GET /ingest/history
{
  "status": 1,
  "data": [
    {
      "id": 5012,
      "dataset_type": "bom_items",
      "status": "completed",          // completed | partial | failed
      "total_rows": 120,
      "inserted_rows": 116,
      "duplicate_rows": 1,
      "error_rows": 3,
      "source": "api",
      "started_at": "2026-07-16T09:51:57.125Z",
      "finished_at": "2026-07-16T09:51:57.125Z",
      "created_at": "2026-07-16T09:51:56.944Z"
    }
    // …most recent first
  ],
  "message": "Success"
}
Limits for this endpoint
Scope
Any active key works — no dataset scope needed.
Paging
page starts at 1 (default 1). limit is 1–100 (default 50). Outside that gets 400.
Filter
dataset is optional and must be one of mrr, model_bom_map, bom_items.
Shows
Only sends made through this API, for your own company. Excel uploads made in the dashboard don't appear here.
Rate
120 requests per minute per key. Over that gets 429 with a Retry-After header.
GEThttps://api.letsworkwise.com/api/v1/vcost-estimation/ingest/:ingestId

The details of one send — how many rows went in and which ones had problems. Use the ingestId that came back when you sent it.

// GET /ingest/5012
{
  "status": 1,
  "data": {
    "id": 5012,
    "dataset_type": "bom_items",
    "status": "partial",
    "total_rows": 120,
    "inserted_rows": 116,
    "duplicate_rows": 1,
    "error_rows": 3,
    "last_error": "3 row(s) rejected during ingest.",
    "error_samples": [
      { "index": 44, "ref": "26DA10-120/90010001#2", "error": "…" }
    ],
    "source": "api",
    "created_at": "2026-07-16T09:51:56.944Z"
  },
  "message": "Success"
}
Limits for this endpoint
Scope
Any active key works — no dataset scope needed.
ingestId
A positive whole number. Anything else gets 400.
Shows
Only sends made through this API, for your own company. An id belonging to an Excel upload returns 404, even though it's your own.
Error list
errors holds at most 25 examples, even when more rows were rejected — rejected has the true count.
Rate
120 requests per minute per key. Over that gets 429 with a Retry-After header.

What we send back

If your request is well-formed, you always get a 200 OK with a summary. Here's the nice part: if a few rows have problems, we still save the good ones and simply list the bad ones — we don't throw the whole batch away. The outcome tells you how it went:

  • completed — every row went in.
  • partial — most rows went in; a few are listed under errors.
  • failed — nothing went in (check errors).
{
  "status": 1,
  "data": {
    "ingestId": 5012,
    "correlationId": "b3f1a9e2-…",
    "dataset": "bom_items",
    "outcome": "partial",        // completed | partial | failed
    "received": 120,
    "inserted": 116,
    "replaced": 3,               // BOM/mapping only (rows overwritten)
    "duplicates": 1,
    "rejected": 3,
    "errors": [                  // capped at 25 samples
      { "index": 44, "ref": "26DA10-120/90010001#2", "error": "…" }
    ]
  },
  "message": "116 inserted, 3 replaced, 3 rejected"
}

When something is wrong with the request itself

These are about the request, not individual rows:

What happenedYou get
The JSON is malformed or a field is the wrong type400
No .json file attached (an inline JSON body isn't accepted)400
Key is missing, wrong, or revoked401
Key isn't allowed to send this kind of data403
More than 5,000 rows in one file400
File is bigger than 50 MB (see limits)413
Too many requests too fast429
Something broke on our side500

Every reply includes an X-Request-Id — share it with us if you ever need help tracing a request.

Try it (cURL)

Make a key with the ingest:mrr permission, save your data as mrr.json, then upload the file:

curl -X POST "https://api.letsworkwise.com/api/v1/vcost-estimation/ingest/mrr" \
  -H "Authorization: Bearer wvce_live_xxxxxxxx_………" \
  -F "file=@mrr.json;type=application/json"

Where mrr.json is a file containing:

{
  "source": "SAP-ECC",
  "records": [
    {
      "cpartno":   "3BA24800X3M-001R1T7B",  // required  -> cpartno   (drives match keys)
      "irate":     231.00,                   // required  -> irate     (the vendor rate)
      "supplier":  "INTRICAST PVT. LTD.",    // required  -> supplier
      "dated":     "2025-10-27",             // required  -> dated     (ISO date, recency)
      "type":      "02",                      // required  -> type      (02/07 = complete buy)
      "mrrNo":     "001864",                  //           -> mrr_no
      "itemName":  "BODY",                    //           -> item_name
      "unit":      "NOS",                     //           -> unit
      "poNo":      "PO-2025-1180",            //           -> p_o_no
      "icode":     70060257,                  //           -> icode
      "billChl":   "BILL",                    //           -> bill_chl
      "advised":   10,                        //           -> advised
      "rcvd":      10,                        //           -> rcvd
      "accept":    10,                        //           -> accept
      "reject":    0,                         //           -> reject
      "lc":        0,                         //           -> lc
      "tcNo":      "TC-1180",                 //           -> tc_no
      "excDoc":    "",                        //           -> exc_doc
      "testCert":  "TC/2025/114",             //           -> test_cert
      "finvno":    "INV-8842",                //           -> finvno
      "wono":      "WO-551",                  //           -> wono
      "gateEntry": "GE-9931",                 //           -> gate_entry
      "gateDate":  "2025-10-25",              //           -> gate_date
      "entBy":     "RAVI",                    //           -> ent_by
      "inspBy":    "SATISH",                  //           -> insp_by
      "rwCond":    "OK",                      //           -> rw_cond
      "qcDate":    "2025-10-26",              //           -> qcdate
      "store":     "MAIN",                    //           -> store
      "mtime":     0,                         //           -> mtime
      "invDate":   "2025-10-24",              //           -> invdate
      "cdrgno":    "24800X3",                 //           -> cdrgno
      "revNo":     "R1",                      //           -> rev_no
      "exchRate":  1,                         //           -> exch_rate
      "awb":       "",                        //           -> awb
      "modeTpt":   "ROAD",                    //           -> mode_tpt
      "poDate":    "2025-10-01",              //           -> podate
      "boeNo":     ""                         //           -> boe_no
    }
  ]
}
Keep your key private — anyone who has it can send data as your company. If it ever leaks, revoke it and make a new one.