Skip to main content

Combine Multiple M3 API Calls into a Single REST Call

The problem​

Many M3 customers integrate with other systems, but M3's REST APIs are not stateless — getting a complete picture of a single record often means calling several transactions in sequence. For example, to retrieve a full sales order you need:

  • OIS100MI/GetOrderHead
  • OIS100MI/GetOrderValue
  • OIS100MI/LstHeadText
  • OIS100MI/LstOrderCharge
  • OIS100MI/GetAddress for the delivery address
  • OIS100MI/GetAddress for the customer address
  • OIS100MI/GetAddress for the invoice address
  • OIS100MI/LstLine for the order lines

That's eight API calls just to read one order. For a team evaluating whether to integrate with M3, that complexity can be a deal-breaker — and even once they commit, cryptic M3 field names like NTAM, CRD0, and CUCD add further friction.

The solution​

A Vince Platform workflow can make all of these calls in a single request and return the combined result as one clean, human-readable JSON document.

Workflows can be triggered and called synchronously, returning the output of their last step as the response (see Step 3 below). Since a workflow can also transform the M3 output along the way, callers get back a readable structure instead of raw M3 field codes — effectively turning the workflow into your own purpose-built API.

Prerequisites​

Step 1 — Read all the order data in one step​

Since all the required transactions belong to the same M3 program (OIS100MI), they can be read in a single REST API step that calls M3's bulk REST API endpoint, using the M3 Connection from the prerequisites.

The request needs an order number as input. Rather than hard-coding it, pass it in as a variable so the workflow can be reused for any order: reference {{ body.orderNumber }} wherever the order number is needed, and Vince Platform fills it in from the request body at execution time — see Using data from earlier steps for how templating works in the REST API component.

Configure the step's settings as follows:

SettingValue
Use connectionOn
ConnectionYour M3 connection from the prerequisites
Endpoint URLhttps://mingle-ionapi.eu1.inforcloudsuite.com/VINCE_DEM/M3/m3api-rest/v2/execute
HTTP Methodpost

Under Headers, select Add header and enter:

KeyValue
content-typeapplication/json

Under Request body, switch to the Body tab and enter:

{
"program": "OIS100MI",
"cono": 790,
"transactions": [
{ "transaction": "GetOrderHead", "record": { "ORNO": "{{ body.orderNumber }}" }, "selectedColumns": ["CUNO", "ORNO", "ORTP", "ORST", "ORSL", "WHLO", "FACI", "ORDT", "RLDT", "TEPY", "TEDL", "MODL", "TEPA", "OREF", "CUOR", "YREF"] },
{ "transaction": "GetOrderValue", "record": { "ORNO": "{{ body.orderNumber }}" } },
{ "transaction": "LstOrderCharge", "record": { "ORNO": "{{ body.orderNumber }}" } },
{ "transaction": "GetAddress", "record": { "ORNO": "{{ body.orderNumber }}", "ADRT": "01" } },
{ "transaction": "GetAddress", "record": { "ORNO": "{{ body.orderNumber }}", "ADRT": "02" } },
{ "transaction": "GetAddress", "record": { "ORNO": "{{ body.orderNumber }}", "ADRT": "03" } },
{ "transaction": "LstLine", "record": { "ORNO": "{{ body.orderNumber }}" } }
]
}

The response is classic M3: structured, but not pretty. Every transaction comes back in the same result set — Get transactions return a single record, Lst transactions return multiple.

Step 2 — Reshape the response with Transform​

Add a Transform step to turn the raw M3 output into a clean, predictable JSON structure using a JSONata expression.

(
$root := $context.data.all.rest_api_1.body.results;
$orderHead := $root[transaction="GetOrderHead"].records[0];
$orderValue := $root[transaction="GetOrderValue"].records[0];
$orderCharges := $root[transaction="LstOrderCharge"].records;
$orderLines := $root[transaction="LstLine"].records;
$orderAddr := $root[transaction="GetAddress"].records;

/* utility function to generate an address */
$formatAddress := function($addr, $type) {
"type": $type,
"name": $addr.CUNM,
"addressLine1": $addr.CUA1,
"addressLine2": $addr.CUA2,
"addressLine3": $addr.CUA3,
"addressLine4": $addr.CUA4,
"city": $addr.TOWN,
"zip": $addr.PONO,
"countryCode": $addr.CSCD,
"phone": $addr.PHNO,
"contact": $addr.YREF
};

/* build the new output */
{
"orderNumber": $orderHead.ORNO,
"orderDate": $substring($orderHead.ORDT,0,4) & "-" & $substring($orderHead.ORDT,4,2) & "-" & $substring($orderHead.ORDT,6,2),
"deliveryDate": $substring($orderHead.RLDT,0,4) & "-" & $substring($orderHead.RLDT,4,2) & "-" & $substring($orderHead.RLDT,6,2),
"paymentTerms": $orderHead.TEPY,
"deliveryTerms": $orderHead.TEDL,
"deliveryMethod": $orderHead.MODL,
"ourReference": $orderHead.OREF,
"customerOrderNumber": $orderHead.CUOR,
"customerNumber": $orderHead.CUNO,
"customerName": $orderAddr[1].CUNM,
"statusLow": $number($orderHead.ORSL),
"statusHigh": $number($orderHead.ORST),

/* financials */
"currencyCode": $orderValue.CUCD,
"toPayAmount": $number($orderValue.TOAM ? $orderValue.TOAM : 0),
"chargesAmount": $number($orderValue.EFAM ? $orderValue.EFAM : 0),
"orderAmount": $number($orderValue.BRAM ? $orderValue.BRAM : 0),
"taxAmount": $number($orderValue.VTAM ? $orderValue.VTAM : 0),
"netAmount": $number($orderValue.NTAM ? $orderValue.NTAM : 0),

/* charges */
"charges": $orderCharges.{
"deliveryNumber": DLIX,
"chargeType": CRID,
"description": CRD0,
"amount": $number(PBAM ? PBAM : 0),
"currencyCode": CUCD
}[],

/* addresses */
"addresses": [
$formatAddress($orderAddr[0], "delivery"),
$formatAddress($orderAddr[1], "customer"),
$formatAddress($orderAddr[2], "invoice")
],

/* order lines */
"lines": $orderLines.{
/* combine line number and suffix */
"lineNumber": $number(PONR) * 1000 + $number(POSX),
"item": ITNO,
"ean": ALWQ = 'EA13' ? POPN : null,
"gtin": ALWQ = 'GTIN' ? POPN : null,
"shortDescription": ITDS,
"fullDescription": FUDS,
"technicalDescription": TEDS,
"requestedDate": $substring(DWDT,0,4) & "-" & $substring(DWDT,4,2) & "-" & $substring(DWDT,6,2),
"confirmedDate": $substring(CODT,0,4) & "-" & $substring(CODT,4,2) & "-" & $substring(CODT,6,2),
"fromWarehouse": WHLO,
"status": $number(ORST),
"orderedQuantity": $number(ORQA),
"outstandingQuantity": $number(ORQA) - $number(DLQT),
"quantityUom": ALUN,

/* financials */
"discountAmount": $number(DIA1) + $number(DIA2) + $number(DIA3) + $number(DIA4) + $number(DIA5) + $number(DIA6),
"netLineAmount": $number(NLAM),
"price": $number(SAPR),
"netPrice": $number(NEPR)
}[]
}
)

This expression relies only on standard JSONata features: variable assignments, type casting, simple arithmetic, and a custom function. The result for order 0010000386 looks like this:

{
"orderNumber": "0010000386",
"orderDate": "2020-08-17",
"deliveryDate": "2020-08-30",
"paymentTerms": "N30",
"deliveryTerms": "FOB",
"deliveryMethod": "003",
"ourReference": "Our reference",
"customerOrderNumber": "2000000416",
"customerNumber": "INTCUSTBBB",
"customerName": "Internal Customer for DivBBB (Sales)",
"statusLow": 66,
"statusHigh": 66,
"currencyCode": "SEK",
"toPayAmount": 10062.5,
"chargesAmount": 62.5,
"orderAmount": 10000,
"taxAmount": 0,
"netAmount": 10000,
"charges": [
{ "deliveryNumber": "228", "chargeType": "FC001", "description": "Freight charge example", "amount": 62.5, "currencyCode": "SEK" }
],
"addresses": [
{ "type": "delivery", "name": "Our Company Number", "addressLine1": "Warehouse 901", "addressLine2": "Warehouse 901 Address 2", "addressLine3": "City ZIP", "addressLine4": "IE", "city": "City", "zip": "ZIP", "countryCode": "IE", "phone": "TELEPHONE 1", "contact": "Contact 1" },
{ "type": "customer", "name": "Internal Customer for DivBBB (Sales)", "addressLine1": "Internal Customer line 1", "addressLine2": "Internal Customer line 2", "addressLine3": "POSTCODE Göteborg", "addressLine4": "Sweden", "city": "Göteborg", "zip": "POSTCODE", "countryCode": "SE", "phone": "TELEPHONE 1", "contact": "Contact 1" },
{ "type": "invoice", "name": "Internal Customer for DivBBB (Sales)", "addressLine1": "Internal Customer line 1", "addressLine2": "Internal Customer line 2", "addressLine3": "POSTCODE Göteborg", "addressLine4": "Sweden", "city": "Göteborg", "zip": "POSTCODE", "countryCode": "SE", "phone": "TELEPHONE 1", "contact": "Contact 1" }
],
"lines": [
{ "lineNumber": 1000, "item": "IS001", "ean": null, "gtin": null, "shortDescription": "Internal Sales Item 001", "technicalDescription": "Internal Sales Item 001", "requestedDate": "2020-08-30", "confirmedDate": "2020-08-31", "fromWarehouse": "001", "status": 66, "orderedQuantity": 100, "outstandingQuantity": 0, "quantityUom": "EA", "discountAmount": 0, "netLineAmount": 10000, "price": 100, "netPrice": 100 }
]
}

Step 3 — Call the workflow synchronously​

To test the workflow as an API, call the synchronous workflow execution endpoint:

POST /live/workflows/{workflowId}/sync

The /sync suffix tells Vince Platform to wait for the workflow to finish and return its result, instead of just acknowledging that it started.

caution

AWS API Gateway enforces a 29-second timeout, so the workflow must complete and return a response before that limit.

To get the transformed data back — rather than the full execution metadata — add this header to the request:

vl-wf-control-return-data: true

Send the order number as JSON in the request body:

{ "orderNumber": "0010000386" }

Testing with Postman​

Here's how the header configuration looks in Postman:

Postman request configured with the vl-wf-control-return-data header set to true

And the full request and response:

Postman POST request to the synchronous workflow endpoint returning the transformed order JSON

Response times can be slower the first time a workflow runs, since there may be a cold start behind the scenes.

Step 4 — Give it a human-readable URL with a Custom API endpoint​

Calling https://api.vince.live/live/workflows/WORKFLOW-a73a456a34554663928f51f5ce2af767/sync works, but it's not something you'd want to hand to another team. If you build several of these workflow-backed APIs, tracking which workflow ID belongs to which workflow gets tricky fast.

Custom API endpoints let you wrap a workflow behind a short, human-readable URL. Endpoints are grouped under a basePath — for example m3 — with one endpoint per workflow underneath it.

Create the endpoint​

POST /v1/custom-apis/control
{
"endpoint": "sales-order",
"tenantId": "TENANT-196886ffcb894a688405f0586872e9fa",
"basePath": "m3",
"config": {
"target": {
"resource": "WORKFLOW",
"identifier": "WORKFLOW-a73a456a34554663928f51f5ce2af767",
"parameters": { "vl-wf-control-return-data": true }
},
"source": {
"contentTypes": ["application/json"],
"httpMethod": "GET"
}
}
}

This results in a Custom API endpoint at:

GET /v1/custom-apis/live/m3/sales-order

Within the same m3 group you could add further endpoints, such as GET /v1/custom-apis/live/m3/purchase-order to fetch a full purchase order, or POST /v1/custom-apis/live/m3/sales-order to create a customer order.

Set up permissions​

Custom API endpoints aren't yet exposed in the Access Management UI, so permissions are granted by updating the target role directly through the API.

First, fetch the role's current permissions:

GET /roles/{roleId}
{
"roleName": "External API role",
"permissions": [
{ "action": "*", "vrn": "vrn:TENANT-196886ffcb894a688405f0586872e9fa:fnd:workflow:Sales Order Workflow" },
{ "action": "*", "vrn": "vrn:TENANT-196886ffcb894a688405f0586872e9fa:fnd:workflow:Some other workflow" }
],
"tenantId": "TENANT-196886ffcb894a688405f0586872e9fa",
"roleId": "ROLE-0369a162d6ed47c298fe67d01fe6fac5"
}

Then send back the full permissions array — including the existing entries — with the new Custom API permission appended. PATCH /roles replaces the whole array, so omitting existing entries removes them.

Custom API permission names follow the pattern {basePath}_{endpoint} — for example m3_sales-order. Wildcards are supported, e.g. m3_*order.

PATCH /roles
{
"permissions": [
{ "action": "*", "vrn": "vrn:TENANT-196886ffcb894a688405f0586872e9fa:fnd:workflow:Sales Order Workflow" },
{ "action": "*", "vrn": "vrn:TENANT-196886ffcb894a688405f0586872e9fa:fnd:workflow:Some other workflow" },
{ "action": "*", "vrn": "vrn:TENANT-196886ffcb894a688405f0586872e9fa:fnd:custom-api:m3_sales-order" }
],
"tenantId": "TENANT-196886ffcb894a688405f0586872e9fa",
"roleId": "ROLE-0369a162d6ed47c298fe67d01fe6fac5"
}

Call the Custom API endpoint​

No extra header is needed this time — vl-wf-control-return-data is already baked into the endpoint configuration:

GET /v1/custom-apis/live/m3/sales-order
{ "orderNumber": "0010000386" }

The response is the same transformed order JSON as before:

GET request to the Custom API sales-order endpoint returning the transformed order JSON

Further reading​

  • Connections — manage the M3 connection used by the REST API step
  • REST API component — full reference for the component used in Step 1
  • Transform component — full reference for reshaping data with JSONata
  • Roles — grant access to workflows and Custom API endpoints
  • API Clients — authenticate external systems calling these endpoints