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
.json file and POST it to the right web address.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 sending | Where to send it | If you send it again |
|---|---|---|
| Purchase history — rates you've paid vendors | POST /ingest/mrr | Adds new rows; exact duplicates are skipped |
| Model → BOM list — which parts make up a product | POST /ingest/model-bom-map | Replaces that model + BOM's parts |
| BOM details — full BOM lines with dates | POST /ingest/bom-items | Replaces 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
.jsonfile. Save your data as a file and upload it as multipart form-data in a field namedfile. (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 —
recordsfor purchase history,mappingsfor the model → BOM list, orbomsfor 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 example3BA24800X3M-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.
| Limit | How much | If you go over |
|---|---|---|
| File size | Up to 50 MB per .json file | 413 — split into smaller files |
| Rows in one file | Up 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 minute | Up to 120 per key | 429 — 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.
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)
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
}
]
}- Scope
- Your key needs
ingest:mrr. Without it you get403. - Rows
- Up to 5,000 items in
records— one record is one row. Over that gets400. - File
- One
.jsonfile, up to 50 MB. Bigger gets413. An inline JSON body is never accepted —400. - Rate
- 120 requests per minute per key. Over that gets
429with aRetry-Afterheader. - 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 send | Saved as | Needed? |
|---|---|---|
cpartno | cpartno | required |
irate | irate | required |
supplier | supplier | required |
dated | dated | required |
type | type | required |
mrrNo | mrr_no | optional |
itemName | item_name | optional |
unit | unit | optional |
poNo | p_o_no | optional |
icode | icode | optional |
billChl | bill_chl | optional |
advised | advised | optional |
rcvd | rcvd | optional |
accept | accept | optional |
reject | reject | optional |
lc | lc | optional |
tcNo | tc_no | optional |
excDoc | exc_doc | optional |
testCert | test_cert | optional |
finvno | finvno | optional |
wono | wono | optional |
gateEntry | gate_entry | optional |
gateDate | gate_date | optional |
entBy | ent_by | optional |
inspBy | insp_by | optional |
rwCond | rw_cond | optional |
qcDate | qcdate | optional |
store | store | optional |
mtime | mtime | optional |
invDate | invdate | optional |
cdrgno | cdrgno | optional |
revNo | rev_no | optional |
exchRate | exch_rate | optional |
awb | awb | optional |
modeTpt | mode_tpt | optional |
poDate | podate | optional |
boeNo | boe_no | optional |
Send: Model → BOM list
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
}
]
}
]
}- Scope
- Your key needs
ingest:model-bom-map. Without it you get403. - 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
.jsonfile, up to 50 MB. Bigger gets413. An inline JSON body is never accepted —400. - Rate
- 120 requests per minute per key. Over that gets
429with aRetry-Afterheader. - Repeat sends
- Replaces, not adds. Every
(modelNo, bomNo)you send has its existing components deleted and rewritten from your file.
Every field, explained
| Field you send | Where | Saved as | Needed? |
|---|---|---|---|
modelNo | header | model_no | required |
bomNo | header | bom_no | required |
productName | header | product_name | optional |
cpartno | component | cpartno | required |
childQty | component | child_qty | required |
childName | component | child_name | optional |
childCode | component | child_code | optional |
parentCode | component | parent_code | optional |
childUnit | component | chl_unit | optional |
Send: BOM details
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
}
]
}
]
}- Scope
- Your key needs
ingest:bom-items. Without it you get403. - Rows
- Up to 5,000 — counted as the total components in the file, not the number of BOMs. Over that gets
400. - File
- One
.jsonfile, up to 50 MB. Bigger gets413. An inline JSON body is never accepted —400. - Rate
- 120 requests per minute per key. Over that gets
429with aRetry-Afterheader. - Repeat sends
- Replaces, not adds. Every
(modelNo, bomNo)you send has its existing components deleted and rewritten from your file.
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 send | Where | Saved as | Needed? |
|---|---|---|---|
modelNo | header | model_no + parent_code | required |
bomNo | header | bom_no | required |
bomDate | header | bom_date | required |
bomNoRaw | header | bom_no_raw | optional |
productName | header | product_name | optional |
versionNo | header | ver_no | optional |
revisionNo | header | rev_no | optional |
enteredBy | header | ent_by | optional |
enteredDate | header | ent_dt | optional |
approvedBy | header | app_by | optional |
approvedDate | header | app_dt | optional |
vdd | header | vdd | optional |
voucherDate | header | vchdate | optional |
cpartno | component | cpartno + child_partno | required |
childQty | component | child_qty | required |
childName | component | child_name | optional |
childCode | component | child_code | optional |
ibcode | component | ibcode | optional |
parentCode | component | parent_code | optional |
parentUnit | component | par_unit | optional |
childUnit | component | chl_unit | optional |
lotSize | component | lot_size | optional |
remarks | component | rmks | optional |
srNo | component | srno | optional |
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.
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"
}- Scope
- Any active key works — no dataset scope needed.
- Paging
pagestarts at 1 (default 1).limitis 1–100 (default 50). Outside that gets400.- Filter
datasetis optional and must be one ofmrr,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
429with aRetry-Afterheader.
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"
}- 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
errorsholds at most 25 examples, even when more rows were rejected —rejectedhas the true count.- Rate
- 120 requests per minute per key. Over that gets
429with aRetry-Afterheader.
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 happened | You get |
|---|---|
| The JSON is malformed or a field is the wrong type | 400 |
No .json file attached (an inline JSON body isn't accepted) | 400 |
| Key is missing, wrong, or revoked | 401 |
| Key isn't allowed to send this kind of data | 403 |
| More than 5,000 rows in one file | 400 |
| File is bigger than 50 MB (see limits) | 413 |
| Too many requests too fast | 429 |
| Something broke on our side | 500 |
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
}
]
}