rest-client

Call external REST APIs from Mendix — the three approaches (inline REST CALL, consumed REST client document, generated from OpenAPI) and how to choose. Use when integrating with any external REST API.

SKILL.md

GitHub
<!-- skill-index: generated by scripts/skill-index.py; make check-skill-index -->

Find it fast

sed -n '<from>,<to>p' SKILL.md one section rather than reading the file; ./mxcli syntax <topic> answers how do I write this for a fraction of either.

lines section
29-44 Three Approaches
45-110 Approach 0: OpenAPI Import (Fastest)
111-337 Approach 1: REST Client (Manual)
338-383 Approach 2: REST CALL (Inline HTTP)
384-416 Data Transformers (JSLT — Mendix 11.9+)
417-454 JSON Structures & Mappings
455-526 Complete Pipeline Example
527-557 Published REST: Authentication Parameters
558-567 Mendix Validation Rules
568-588 BSON Types Reference
<!-- /skill-index -->

REST Integration Skill

Use this skill when integrating with external REST APIs from Mendix.

Three Approaches

Mendix offers three ways to call REST APIs from microflows. Choose based on the use case:

Approach When to Use Artifacts
OpenAPI import API has an OpenAPI 3.0 spec — auto-generate from the spec REST client document generated in one command
REST Client (manual) No spec available, or need fine-grained control REST client document + microflow
REST CALL (inline) One-off calls, quick prototyping, dynamic URLs, low-level HTTP control Microflow only

Both REST Client approaches can be combined with Data Transformers (Mendix 11.9+) and Import/Export Mappings to map between JSON and entities.

No API to call against yet — or one you would rather not depend on while building? mock-rest-apis covers standing up an endpoint you control and pointing the app at it.


Approach 0: OpenAPI Import (Fastest)

If the API has an OpenAPI 3.x spec (JSON or YAML), generate the REST client in one command. A Swagger 2.0 spec ("swagger": "2.0", e.g. petstore.swagger.io/v2/swagger.json) is refused — convert it first (npx swagger2openapi in.json -o out.json, or https://converter.swagger.io) and import the converted file:

mdl 1;
-- From a local file (relative to the .mpr file)
create or modify consumed rest service CapitalModule.CapitalAPI (
  OpenAPI: 'specs/capital.json'
);

-- From a URL
create or modify consumed rest service PetStoreModule.PetStoreAPI (
  OpenAPI: 'https://petstore3.swagger.io/api/v3/openapi.json'
);

-- Override the base URL (replaces servers[0].url from the spec)
create or modify consumed rest service PetStoreModule.PetStoreStaging (
  OpenAPI: 'https://petstore3.swagger.io/api/v3/openapi.json',
  BaseUrl: 'https://staging.petstore.example.com/api/v3'
);

This generates:

  • All operations with correct HTTP method, path, parameters, headers, body, and response type
  • Resource groups based on OpenAPI tags
  • Basic auth if the spec declares it at the top level
  • The spec stored inside the document for Studio Pro parity

BaseUrl is optional. When omitted, servers[0].url from the spec is used — but only if it is absolute. A relative server URL (/api/v3) cannot be a BaseUrl: the import warns ("server URL … is relative and cannot be used as BaseUrl") and leaves the client without one, which fails at call time rather than at import time. Set BaseUrl explicitly in that case.

When provided, BaseUrl overrides the spec's value — useful when the spec points at production but you need to import against staging, a different version, or a local mock (see mock-rest-apis).

Preview without writing:

describe contract operation from openapi 'specs/capital.json';

After import: the REST client is ready to use with SEND REST REQUEST. No manual operation definition needed.

But every JSON response is imported as response: json — a raw string, not objects. The import does not generate response entities yet (ako/mxcli#1234). To get objects, create the non-persistent entities yourself and give the operations you call a mapping (see Response Types below):

mdl 1;
alter consumed rest service PetStoreModule.PetStoreAPI
  modify operation getPetById (
    response: mapping PetStoreModule.Pet { Id = id, Name = name }
  );

Writing the entities and an import-mapping document but leaving the operation at response: json is the common mistake: nothing uses the mapping, and nothing reports that.


Approach 1: REST Client (Manual)

Define the API once as a REST client document, then call its operations from microflows.

Step 1 — Create the REST Client

mdl 1;
create consumed rest service Module.OpenMeteoAPI (
  BaseUrl: 'https://api.open-meteo.com/v1',
  authentication: none
)
{
  operation GetForecast (
    method: get,
    path: '/forecast',
    query: ($latitude: decimal, $longitude: decimal, $current: string),
    headers: ('Accept': 'application/json'),
    timeout: 30,
    response: json as $WeatherJson
  )

  operation PostData (
    method: post,
    path: '/submit',
    headers: ('Content-Type': 'application/json'),
    body: json from $JsonPayload,
    response: none
  )
};

A header value is a template, like the path: {Name} is the operation parameter Name, which must be declared in Parameters: (CE7056 otherwise).

parameters: ($Token: string),
headers: ('Authorization': 'Bearer {Token}')

'Bearer ' + $Token is the old spelling of the same header (MDL-DEPR711); mxcli fmt --upgrade rewrites it. It used to store only Bearer .

Authentication

-- No authentication
authentication: none

-- Basic auth
authentication: basic (username: 'user', password: 'secret')

Body Types

-- JSON variable (Mendix expression stored on the operation)
body: json from $JsonPayload

-- String template with parameter placeholders
body: template '{ "name": "{name}", "value": {value} }'

-- Export mapping (entity → JSON, aligned with export mapping syntax)
body: mapping Module.RequestEntity {
  name = Name,
  email = Email,
}

There is no binary/file body here. A consumed operation's body is one of Rest$JsonBody, Rest$StringBody or Rest$ImplicitMappingBody, so a file document has nowhere to go. body: file from $Doc is refused as MDL-REST02 — it used to be written as a string body holding the literal text $Doc, which returns HTTP 200 with a 4-byte payload.

Upload binary from a microflow instead, which does have a binary body:

call rest service post 'https://api.example.com/upload' (
  Headers: ('ContentType': 'application/pdf'),
  Body: binary $Doc/Contents,
) returns response;

response: file as $Doc on an operation is unaffected — downloads work.

Response Types

-- Simple types. The variable is optional and is NOT stored — the call site
-- binds it — so `describe` prints the bare kind (`response: json`), and that
-- form runs again as written.
response: json
response: json as $Result
response: string as $text
response: file as $Document
response: status as $Code
response: none

-- Import mapping (JSON → entity, aligned with import mapping syntax)
response: mapping Module.ResponseEntity {
  "Id" = "id",
  "status" = "status",
  create Module.Items_Response/Module.Item = items {
    "Sku" = "sku",
    "Quantity" = "quantity",
  }
}

mapping takes an ENTITY plus a body — never a mapping document. The name after mapping is the target entity, and the { ... } body lists the JSON fields; Mendix stores the result inline on the operation (Rest$ImplicitMappingResponseHandling). A consumed REST operation has nowhere to put a reference to a standalone import/export mapping — the metamodel defines only the inline handler and "no response handling". So this is wrong, and is now rejected by mxcli check as MDL-REST01:

response: mapping Module.IMM_Something   -- ✗ names a mapping document, no body

If you already have an import mapping document you want to reuse, call it from the microflow instead — send rest request with response: json as $Raw, then $Obj = import from mapping Module.IMM_Something($Raw).

Reaching a nested JSON member

A member several levels down is written with /, exactly as in a mapping document — you do NOT need an entity for the levels in between:

response: mapping Module.FlatProbe {
  "ItemId" = "id",
  "Title"  = "fields/Title",     -- reaches {"fields": {"Title": …}}
}

Mendix stores that as the pipe path (Object)|fields|Title. Before mxcli 0.19.x the inline serializer appended the text verbatim, producing (Object)|fields/Title — one member with a slash in its name — which passed mxcli check, exec, mx check and describe, and returned an empty column at runtime with no diagnostic. If you are on an older build, give each level its own entity instead.

An export body's nested elements

Body: MAPPING is an EXPORT mapping, and its nested elements must not be "create". mxcli writes them correctly, but it is worth knowing what a wrong one looks like, because mxbuild does not report it as a check error — it throws while loading the project, so the app cannot be opened at all:

System.AggregateException: … (Export Object Mappings cannot have
ObjectHandling set to 'Create')

Query parameters carry no type in the Mendix model: Rest$QueryParameter stores a name only. MDL still requires $name: Type for the sake of the grammar, but the type is dropped at write time, so describe re-emits every query parameter as String.

Step 2 — Call from a Microflow

mdl 1;
create microflow Module.ACT_GetWeather ()
returns Module.WeatherInfo as $Weather
begin
  -- Call the REST client operation
  send rest request Module.OpenMeteoAPI.GetForecast;

  -- Extract response body from system variable
  declare $RawJson string = $latestHttpResponse/content;

  -- (Optional) Transform with JSLT
  $SimplifiedJson = transform $RawJson with Module.SimplifyWeather;

  -- Import into entity
  $Weather = import from mapping Module.IMM_Weather($SimplifiedJson);

  return $Weather;
end;

CRITICAL: After send rest request, the response is in $latestHttpResponse (System.HttpResponse):

  • $latestHttpResponse/content — response body (String)
  • $latestHttpResponse/StatusCode — HTTP status (Integer)

Show / Describe / Drop

list consumed rest services [in module];
describe consumed rest service Module.ClientName;
drop consumed rest service Module.ClientName;
create or modify consumed rest service Module.ClientName ...  -- idempotent; REPLACES the operation list

Change an Existing Client — alter, Not create or modify

create or modify declares the client's whole operation list: an operation it does not list is deleted (the output names it). To add, change or remove one operation — above all on a client made in Studio Pro, whose operations carry settings MDL cannot express — use alter. Every operation it does not name is written back exactly as stored.

mdl 1;

alter consumed rest service Module.BookingApi
  add operation GetBooking (
    method: get,
    path: '/bookings/{id}',
    parameters: ($id: string),
    response: json
  ),
  modify operation PostBooking (timeout: 60),     -- only the keys listed change
  drop operation LegacyBooking,
  set (BaseUrl: 'https://api.example.com/v2');   -- BaseUrl, Authentication

add refuses a name that exists (use modify); modify and drop refuse one that does not, listing the operations there are. Actions apply in order, so drop operation X, add operation X (…) replaces X whole.


Approach 2: REST CALL (Inline HTTP)

Call an HTTP endpoint directly from a microflow — no REST client document needed. Best for one-off calls, dynamic URLs, or low-level control.

-- Simple GET returning a string
$response = call rest service get 'https://api.example.com/data' (
  Headers: ('Accept': 'application/json'),
  Timeout: 30,
) returns string;

-- GET with URL template parameters
$response = call rest service get 'https://api.example.com/users/{1}' with (
  {1} = toString($UserId)
) (
  Headers: ('Accept': 'application/json'),
) returns string;

-- POST with body
$response = call rest service post 'https://api.example.com/items' (
  Headers: ('Content-Type': 'application/json'),
  Body: template '{"name": "test"}',
) returns string;

-- With basic auth
$response = call rest service get 'https://api.example.com/secure' (
  Authentication: basic (Username: 'username', Password: 'password'),
) returns string;

-- With import mapping (JSON → entity)
$item = call rest service get 'https://api.example.com/item/1' (
  Headers: ('Accept': 'application/json'),
) returns mapping Module.IMM_Item as Module.Item;

-- Fire and forget
call rest service delete 'https://api.example.com/item/1'
  returns nothing;

-- Error handling
$response = call rest service get 'https://api.example.com/data'
  returns string
  on error continue;

Data Transformers (JSLT — Mendix 11.9+)

Transform complex JSON responses into simpler structures before import mapping.

-- Define the transformer
create data transformer Module.SimplifyWeather
source json '{"latitude": 52.52, "current": {"temperature_2m": 12.8, "wind_speed_10m": 18.3}}'
{
  jslt $$
{
  "temp": .current.temperature_2m,
  "wind": .current.wind_speed_10m,
  "lat": .latitude
}
  $$;
};

-- Use in a microflow
$SimplifiedJson = transform $RawJson with Module.SimplifyWeather;

Single-line JSLT: jslt '{ "temp": .current.temperature_2m }'; Multi-line JSLT: jslt $$ { ... } $$; (dollar-quoting, same as Java actions)

list data transformers [in module];
describe data transformer Module.Name;
drop data transformer Module.Name;

JSON Structures & Mappings

See json-structures-and-mappings for full reference. Quick summary:

mdl 1;
-- JSON structure from snippet
create json structure Module.JSON_Weather
sample '{"temp": 12.8, "wind": 18.3, "lat": 52.52}';

-- Non-persistent entity
create non-persistent entity Module.WeatherInfo (
  Temperature: decimal,
  WindSpeed: decimal,
  Latitude: decimal
);

-- Import mapping (JSON → entity)
create import mapping Module.IMM_Weather
  with json structure Module.JSON_Weather
{
  create Module.WeatherInfo {
    Temperature = temp,
    WindSpeed = wind,
    Latitude = lat
  }
};

Use them in a microflow:

$Weather = import from mapping Module.IMM_Weather($JsonString);
$JsonOutput = export to mapping Module.EMM_Weather($WeatherEntity);

Complete Pipeline Example

Full example: call weather API → transform → import → show on page.

mdl 1;
-- 1. Entity
create non-persistent entity Module.CurrentWeather (
  Temperature: decimal,
  WindSpeed: decimal,
  Latitude: decimal,
  ObservationTime: datetime
);

-- 2. Data Transformer (simplify API response)
create data transformer Module.SimplifyWeather
source json '{"latitude":52.52,"current":{"time":"2024-01-15T14:00","temperature_2m":12.8,"wind_speed_10m":18.3}}'
{
  jslt $$
{
  "temperature": .current.temperature_2m,
  "windSpeed": .current.wind_speed_10m,
  "latitude": .latitude,
  "observationTime": .current.time
}
  $$;
};

-- 3. JSON Structure + Import Mapping (for transformed output)
create json structure Module.JSON_Weather
sample '{"temperature":12.8,"windSpeed":18.3,"latitude":52.52,"observationTime":"2024-01-15T14:00"}';

create import mapping Module.IMM_Weather
  with json structure Module.JSON_Weather
{
  create Module.CurrentWeather {
    Temperature = temperature,
    WindSpeed = windSpeed,
    Latitude = latitude,
    ObservationTime = observationTime
  }
};

-- 4. REST Client
create consumed rest service Module.WeatherAPI (
  BaseUrl: 'https://api.open-meteo.com/v1',
  authentication: none
)
{
  operation GetCurrent (
    method: get,
    path: '/forecast',
    query: ($latitude: decimal, $longitude: decimal, $current: string),
    headers: ('Accept': 'application/json'),
    response: json as $Result
  )
};

-- 5. Microflow (REST Client → Transform → Import)
create microflow Module.ACT_GetWeather ()
returns Module.CurrentWeather as $Weather
begin
  send rest request Module.WeatherAPI.GetCurrent;
  declare $RawJson string = $latestHttpResponse/content;
  $SimplifiedJson = transform $RawJson with Module.SimplifyWeather;
  $Weather = import from mapping Module.IMM_Weather($SimplifiedJson);
  return $Weather;
end;

Published REST: Authentication Parameters

The other direction — an API the app publishes — has one REST-specific trap: a custom-authentication microflow that takes a token. Every parameter besides System.HttpRequest / System.HttpResponse must be bound to a query or header value of the service, or mx check reports CE0336 ("not present in the custom authentication parameter list"). Bind them after the microflow:

mdl 1;
create or modify published rest service Shop.Api (
  Path: 'rest/shop/v1', Version: '1.0.0', ServiceName: 'Shop',
  Authentication: (
    basic,
    microflow Shop.Authenticate (
      $Token: header 'X-Api-Token',         -- HTTP name differs from $Token
      $Tenant: query,                       -- ?Tenant=
      $Lang: query 'lang' description 'UI language, defaults to en'
    )
  )
) { resource 'orders' { get '' microflow Shop.GetOrders; } };
  • The list is optional: an unlisted parameter is a query parameter under its own name, so microflow Shop.Authenticate alone is enough for ?Token=.
  • Never write a type — it is the microflow parameter's. Only query and header; an unknown $Param, the request/response, or a duplicate is MDL-REST04.
  • describe prints the list only when it differs from that default. mxcli syntax rest.published has the rest of the statement.

Mendix Validation Rules

Rule Error Fix
Every operation MUST have an Accept header CE7062 Auto-added by serializer if missing
POST/PUT/PATCH MUST have a body CE7064 Auto-added by serializer (empty JSON body)
Template placeholders must match parameters CE7056 {name} requires a parameter named name
No custom error handling on SEND REST REQUEST CE6035 Always uses abort-on-error semantics
Data Transformer requires 11.9+ version check checkFeature("integration", "data_transformer", ...)

BSON Types Reference

MDL Concept BSON Type
REST client document rest$ConsumedRestService
Operation rest$RestOperation
GET/DELETE method rest$RestOperationMethodWithoutBody
POST/PUT/PATCH method rest$RestOperationMethodWithBody
JSON body rest$JsonBody
Template body rest$StringBody
Export mapping body rest$ImplicitMappingBody
No response rest$NoResponseHandling
Import mapping response rest$ImplicitMappingResponseHandling
Header rest$HeaderWithValueTemplate
Path parameter rest$OperationParameter
Query parameter rest$QueryParameter
Data transformer DataTransformers$DataTransformer
JSLT step DataTransformers$JsltAction
Transform action microflows$TransformJsonAction
Send request action microflows$RestOperationCallAction
Inline HTTP action microflows$RestCallAction