Skip to content
This page View Markdown Open in ChatGPT Open in Claude

Call external API

Makes HTTP calls to any API from the server : REST with JSON, a form, an upload of files, SOAP and other XML, GraphQL, bytes. It says who asks (basic, bearer, API key, OAuth 2), follows redirects, tries again when a call failed, and gives the answer as JSON, as text, as bytes, or as a file it saved. One call, or a list of calls run in parallel or one after the other, with preProcess and postProcess rules which copy a value of one answer into the request of another.

Method POST
URL /api/system-api/admin/call-external-api : admin is the user path of your account
Body one call, an array of calls, or { type: "parallel" | "sequential", data: [ … ] }
Answer data : the response of the call, { statusCode, ok, headers, body, … }, or an array of them. The file itself, for one call with download
From code g.sys.system.callExternalApi

One call

Body
{
    "url": "https://api.example.com/customers",
    "method": "POST",
    "timeout": 5000,
    "headers": { "authorization": "Bearer …" },
    "queryParams": { "select": "first_name" },
    "body": { "first_name": "James", "last_name": "Bond" }
}
Answer
{ "success": true, "statusCode": 200, "data": { "statusCode": 201, "ok": true, "headers": { "content-type": "application/json" }, "body": { "id": 42, "first_name": "James" } } }
Key Meaning
url Required. The full URL.
method GET (default), HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH.
timeout Milliseconds one request may take. 10 minutes when it is not given.
headers The headers of the request. A name is the same header whatever its case.
queryParams, pathParams, queryArrayFormat The values of the query and of the path : see the URL.
body, bodyType What to send, and in which form : see the body.
auth Who asks : see authentication.
responseType How the body of the answer is given : see the answer.
download Saves the body of the answer as a file : see download a file.
followRedirects, retry, throwOnError, allowFailure, maxResponseSize See redirects, retries and failures.
id A name for this call, used by preProcess and postProcess of other calls.
preProcess, postProcess Rules { from, to } run before and after the call : from is a path starting with the id of a call (saveApi.output.body.data.id), to a path in a call (setApi.body.customerId).
output Filled with the answer of the call, for the rules of the others.
  • A call with only the keys of the first example works as it always did : every other key is optional.

The URL

Body : the values of the path and of the query
{
    "url": "https://{region}.api.example.com/customers/:customerId/orders",
    "pathParams": { "region": "eu", "customerId": 42 },
    "queryParams": { "status": ["paid", "sent"], "page": 2 },
    "queryArrayFormat": "repeat"
}
  • This calls https://eu.api.example.com/customers/42/orders?status=paid&status=sent&page=2.
  • pathParams fills {name} anywhere before the query, and :name where a part of the path starts. Each value is written as a part of a path : a/b is a%2Fb. A name without a value stays as it is written.
  • queryParams are added after the ones the URL has already. A text, a number and a boolean are sent as they are, an object as the text of its JSON.
  • queryArrayFormat says how a list is written :
queryArrayFormat { "ids": [1, 2] } is sent as
json (default) ids=[1,2]
repeat ids=1&ids=2
brackets ids[]=1&ids[]=2
comma ids=1,2

Authentication

auth says who asks. It takes the place of an authorization header of the call.

auth What is sent
{ "type": "basic", "username": "…", "password": "…" } Authorization: Basic …
{ "type": "bearer", "token": "…" } Authorization: Bearer …. prefix replaces the word Bearer.
{ "type": "apiKey", "name": "x-api-key", "value": "…" } The header x-api-key. With "in": "query", the query parameter x-api-key.
{ "type": "oauth2", "tokenUrl": "…", "clientId": "…", "clientSecret": "…", "scope": "…" } Authorization: Bearer <token>, with a token asked of tokenUrl.
Body : OAuth 2 client credentials
{
    "url": "https://api.example.com/reports",
    "auth": {
        "type": "oauth2",
        "tokenUrl": "https://login.example.com/oauth/token",
        "clientId": "CLIENT_ID",
        "clientSecret": "CLIENT_SECRET",
        "scope": "reports.read"
    }
}
  • The token of oauth2 is asked for with the client_credentials grant, kept in memory until 30 seconds before it expires, and used by every call with the same auth. When the API answers 401, a new token is asked for and the call is sent again, once.
  • grantType names another grant, and params what else the token request holds : { "grantType": "password", "params": { "username": "…", "password": "…" } }. audience is sent when it is given. "credentialsIn": "header" sends the id and the secret of the client as Authorization: Basic and not in the form.
  • Keep the secrets out of your code : read them with g.sys.system.getSecret, or copy the token of a call into the next one with postProcess ("to": "ordersApi.auth.token").

The body

bodyType says in which form the body is sent, and gives the request its content-type when the call names none.

bodyType body content-type
json An object or an array, sent as JSON. A text is sent as it is. application/json
form An object : { "a": 1, "b": "x y" } is sent as a=1&b=x+y. A list is its key once for each item, an object the text of its JSON. application/x-www-form-urlencoded
multipart An object : every key is a part of the form, a field or a file. multipart/form-data, with its boundary
text A text. text/plain; charset=utf-8
xml A text. application/xml; charset=utf-8
binary Bytes : a Buffer, their Base64 text, or a file. application/octet-stream, or the one the name of the file says
  • Without bodyType, an object is sent as JSON, or as a form when the content-type header of the call is application/x-www-form-urlencoded or multipart/form-data. A text is sent as it is, a Buffer as its bytes.
  • A GET or a HEAD without a body is sent without one. Every other JSON call without a body sends {}.
Body : a form
{
    "url": "https://api.example.com/oauth/token",
    "method": "POST",
    "bodyType": "form",
    "body": { "grant_type": "refresh_token", "refresh_token": "…" }
}
Body : SOAP
{
    "url": "https://soap.example.com/service",
    "method": "POST",
    "bodyType": "xml",
    "headers": { "content-type": "text/xml; charset=utf-8", "SOAPAction": "urn:GetRate" },
    "body": "<soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'><soap:Body><GetRate/></soap:Body></soap:Envelope>",
    "responseType": "text"
}
Body : GraphQL
{
    "url": "https://api.example.com/graphql",
    "method": "POST",
    "auth": { "type": "bearer", "token": "…" },
    "body": { "query": "query ($id: ID!) { customer(id: $id) { name } }", "variables": { "id": 42 } }
}

Files

A file is a value of a multipart body, or the whole binary body.

Value The file
{ "file": "reports/a.pdf" } A file of the uploads folder (uploads/reports/a.pdf in a sandbox). fileName and contentType replace its name and what the end of its name says it is.
{ "content": "…", "fileName": "note.txt" } A text sent as a file. With "encoding": "base64", the bytes the Base64 text stands for. Without fileName it is a field, with the contentType you give it.
A file of g.req.files A file a form sent to your custom API, sent on under its own name and content type.
A Buffer Bytes made by your code.
Body : a form of several parts, with two files
{
    "url": "https://api.example.com/documents",
    "method": "POST",
    "bodyType": "multipart",
    "body": {
        "title": "Contract",
        "tags": ["signed", "2026"],
        "document": { "file": "contracts/42.pdf" },
        "note": { "content": "Signed on site", "fileName": "note.txt" }
    }
}
Body : the bytes of a file as the whole body
{
    "url": "https://storage.example.com/bucket/42.pdf",
    "method": "PUT",
    "bodyType": "binary",
    "body": { "file": "contracts/42.pdf" }
}
  • Only files of the uploads folder are read : a path which leads out of it is refused.
  • A list as the value of a key sends one part for each item, under the same name. An object which is no file is sent as a field of JSON.
  • The body of a call is read whole before it is sent : 128 MB at most.

The answer

Key Meaning
statusCode The HTTP status of the answer.
ok true when the status is 200 to 299.
headers The headers of the answer, their names in small letters.
body The body of the answer : see responseType. null for a download which was saved.
url, redirected When redirects are followed : the URL the answer came from, and whether a redirect was followed.
attempts With retry : how many times the request was sent.
file With download : the file which was saved, { path, name, size, contentType }.
error With allowFailure : why the call failed, { message, code }.
responseType body
auto (default) JSON when the body is JSON, else its text. Bytes when it is no text : a picture, a PDF, an archive.
json JSON. A body which is no JSON is an error which shows its start.
text The text of the body, whatever it holds.
base64 The bytes of the body, as Base64 text.
buffer The bytes of the body : a Buffer in JavaScript and TypeScript, Base64 text in the other languages and over HTTP.
  • A text is read in the character set its content-type names (charset=iso-8859-1, windows-1256, shift_jis, utf-16 …), UTF-8 without one.
  • A body which came compressed (content-encoding: gzip, deflate, br) is unpacked.
  • A status of 400 or above is an answer as any other, unless the call asks for throwOnError.

Download a file

download saves the body of the answer as a file of the uploads folder, piece by piece as it arrives where the server itself sends the request, and answers with file. The body is not kept in memory, and body is null.

download Where the file goes
true A place of its own in the uploads folder, with the ending of the file : 3f0c…9a.pdf.
"invoices/42.pdf" That place in the uploads folder. A file which is there gives way.
"invoices/" That folder, under the name of the file.
{ "path": "invoices/42.pdf", "fileName": "Invoice 42.pdf" } The place, and the name the file has for who it is sent on to.
Answer of a call with download
{
    "statusCode": 200, "ok": true, "headers": { "content-type": "application/pdf" }, "body": null,
    "url": "https://cdn.example.com/invoices/42.pdf", "redirected": true,
    "file": { "path": "invoices/42.pdf", "name": "invoice-42.pdf", "size": 48213, "contentType": "application/pdf" }
}
  • file.name is the name the server gives the file (content-disposition), else the last part of its URL, else download.
  • Redirects are followed, 10 at most, unless the call says "followRedirects": false.
  • An answer whose status is not 200 to 299 is no file : it is given as any other answer, with its statusCode and its body, and without file.
  • A file can be 2 GB ; maxResponseSize names another limit, in bytes.
  • From code, the file is in the uploads folder of the sandbox : read it at uploads/ + file.path, or send it to the caller of your custom API by returning __am__downloadFilePath. A file which is not sent on stays until the uploads are cleaned (uploadedFileRemoveOlderThanThisTimeInSeconds, 30 minutes by default).
  • Over HTTP, the answer of one call with download is the file itself, with its content-type and content-disposition: attachment; filename="…", and the file is removed from the server once it was sent. A list of calls answers JSON, and leaves its files in the uploads folder.
Download a file through API Maker
curl -X POST 'https://example.com/api/system-api/admin/call-external-api' \
    -H 'x-am-authorization: TOKEN' -H 'content-type: application/json' \
    -d '{ "url": "https://api.example.com/invoices/42/pdf", "download": true }' \
    --output invoice.pdf

Redirects, retries and failures

Key Meaning
followRedirects true follows 10 redirects at most, a number that many. Default : false, the redirect is the answer. As a browser does : after a 303, and after a 301 or a 302 of a POST, the request is a GET without a body. Another server is not sent the authorization and cookie headers.
retry Sends the request again when it failed : a number of tries after the first one (10 at most), or the object below.
throwOnError true : a status of 400 or above is thrown as an error with the status as its code, and the statusCode, headers and body of the answer.
allowFailure true : a call which failed is not thrown. Its answer is { "statusCode": 0, "ok": false, "error": { "message": "…" } }, and the calls after it run.
maxResponseSize The most bytes of the answer. 64 MB when it is not given, 2 GB for a download.
timeout Milliseconds one request may take : each try has that long.
Key of retry Default Meaning
count 3 How many times the request is sent again.
delay 300 Milliseconds before the second try.
factor 2 What the wait is multiplied by from one try to the next.
maxDelay 30000 The longest wait, in milliseconds.
statusCodes [408, 429, 500, 502, 503, 504] The statuses which are tried again.
onNetworkError true Whether a request which got no answer is tried again : no connection, a timeout.
Body : a call which is tried again, and thrown when it still fails
{
    "url": "https://api.example.com/payments",
    "method": "POST",
    "body": { "amount": 1200 },
    "retry": { "count": 4, "delay": 500, "statusCodes": [429, 503] },
    "throwOnError": true
}
  • A retry-after header of the answer is waited for, up to maxDelay.
  • When every try failed, the answer is the last one. attempts says how many times the request was sent.
  • A POST which is sent again can be done twice by the API it calls : name the statuses which are safe to try again, or send a key the API recognises a repeated request by.

In parallel, in sequence

Body : a save, then two calls at once with its result
[
    { "id": "saveApi", "url": "https://api.example.com/customers", "method": "POST", "body": { "first_name": "James" },
      "postProcess": [ { "from": "saveApi.output.body.id", "to": "setApi.body.customerId" } ] },
    { "type": "parallel", "data": [
        { "id": "setApi", "url": "https://api.example.com/preferences", "method": "PUT", "body": {} },
        { "id": "notify", "url": "https://hooks.example.com/new-customer", "method": "POST", "body": { "source": "shop" }, "allowFailure": true }
    ] }
]
  • The top level array runs in sequence ; type: "parallel" runs its data at once, type: "sequential" one after the other. Groups nest.
  • The answer is an array with one entry per call, nested like the body.
  • In sequence, a call which fails ends the list, unless it has allowFailure. In parallel, the place of a call which failed is empty, or holds its error with allowFailure.
  • A rule can fill any key of a call : "to": "ordersApi.auth.token", "to": "ordersApi.pathParams.customerId".

From code

const resp = await g.sys.system.callExternalApi<{ id: number }>({ url: 'https://api.example.com/customers/42' });
if (resp.statusCode === 200) g.logger.log(resp.body.id);
resp = await g.sys.system.call_external_api('https://api.example.com/customers/42')
if resp.status_code == 200:
    g.logger.log(resp.body.id)
$resp = $g->sys->system->callExternalApi('https://api.example.com/customers/42');
if ($resp->statusCode === 200) {
    $g->logger->log($resp->body->id);
}
var resp = await g.Sys.System.CallExternalApi(new CallExternalApi("https://api.example.com/customers/42"));
if (resp.StatusCode == 200) g.Logger.Log(resp.Body?["id"]);
resp, err := g.Sys.System.CallExternalApi(am.CallExternalApi{Url: "https://api.example.com/customers/42"})
if err != nil {
    return nil, err
}
if resp.StatusCode == 200 {
    g.Logger.Log(am.AsM(resp.Body).Int("id"))
}
let resp = g.sys.system.call_external_api(CallExternalApi::new("https://api.example.com/customers/42"))?;
if resp.status_code == 200 {
    g.logger.log(&resp.body["id"]);
}
ExternalApiResponse resp = g.sys.system.callExternalApi(new CallExternalApi("https://api.example.com/customers/42"));
if (resp.getStatusCode() == 200) g.logger.log(JsonObject.from(resp.getBody()).getLong("id"));
val resp = g.sys.system.callExternalApi(CallExternalApi("https://api.example.com/customers/42"))
if (resp.statusCode == 200L) g.logger.log(JsonObject.from(resp.body).getLong("id"))

A custom API which downloads an invoice from another server and sends it to its own caller :

const saved = await g.sys.system.callExternalApi({
    url: 'https://api.example.com/invoices/42/pdf',
    auth: { type: 'bearer', token: 'TOKEN' },
    download: true,
    throwOnError: true,
});
// the file is in the uploads folder now : your code reads it at 'uploads/' + saved.file.path
return { __am__downloadFilePath: saved.file.path, __am__downloadFolderFileName: saved.file.name };
saved = await g.sys.system.call_external_api(
    'https://api.example.com/invoices/42/pdf',
    auth={'type': 'bearer', 'token': 'TOKEN'},
    download=True,
    throw_on_error=True,
)
# the file is in the uploads folder now : your code reads it at 'uploads/' + saved.file.path
return {'__am__downloadFilePath': saved.file.path, '__am__downloadFolderFileName': saved.file.name}
$saved = $g->sys->system->callExternalApi(
    'https://api.example.com/invoices/42/pdf',
    auth: ['type' => 'bearer', 'token' => 'TOKEN'],
    download: true,
    throwOnError: true,
);
// the file is in the uploads folder now : your code reads it at 'uploads/' . $saved->file->path
return ['__am__downloadFilePath' => $saved->file->path, '__am__downloadFolderFileName' => $saved->file->name];
var saved = await g.Sys.System.CallExternalApi(new CallExternalApi("https://api.example.com/invoices/42/pdf")
{
    Auth = new { type = "bearer", token = "TOKEN" },
    Download = true,
    ThrowOnError = true,
});
// the file is in the uploads folder now : your code reads it at "uploads/" + saved.File.GetString("path")
return new JsonObject { ["__am__downloadFilePath"] = saved.File.GetString("path"), ["__am__downloadFolderFileName"] = saved.File.GetString("name") };
saved, err := g.Sys.System.CallExternalApi(am.CallExternalApi{
    Url:          "https://api.example.com/invoices/42/pdf",
    Auth:         am.M{"type": "bearer", "token": "TOKEN"},
    Download:     true,
    ThrowOnError: true,
})
if err != nil {
    return nil, err
}
// the file is in the uploads folder now : your code reads it at "uploads/" + saved.File.String("path")
return am.M{"__am__downloadFilePath": saved.File.String("path"), "__am__downloadFolderFileName": saved.File.String("name")}, nil
let saved = g.sys.system.call_external_api(
    CallExternalApi::new("https://api.example.com/invoices/42/pdf")
        .auth(json!({ "type": "bearer", "token": "TOKEN" }))
        .download(true)
        .throw_on_error(true),
)?;
// the file is in the uploads folder now : your code reads it at "uploads/" + the path of the file
return Ok(json!({ "__am__downloadFilePath": saved.file["path"], "__am__downloadFolderFileName": saved.file["name"] }));
ExternalApiResponse saved = g.sys.system.callExternalApi(new CallExternalApi("https://api.example.com/invoices/42/pdf")
    .auth(Map.of("type", "bearer", "token", "TOKEN"))
    .download(true)
    .throwOnError(true));
// the file is in the uploads folder now : your code reads it at "uploads/" + saved.getFile().getString("path")
return Map.of("__am__downloadFilePath", saved.getFile().getString("path"), "__am__downloadFolderFileName", saved.getFile().getString("name"));
val saved = g.sys.system.callExternalApi(
    CallExternalApi("https://api.example.com/invoices/42/pdf")
        .auth(mapOf("type" to "bearer", "token" to "TOKEN"))
        .download(true)
        .throwOnError(true),
)
// the file is in the uploads folder now : your code reads it at "uploads/" + saved.file.getString("path")
return mapOf("__am__downloadFilePath" to saved.file.getString("path"), "__am__downloadFolderFileName" to saved.file.getString("name"))
  • More examples in every language : forms, files, XML, OAuth 2, retries and downloads.
  • Custom APIs can also use any HTTP client of npm from the sandbox ; this API adds the forms, the files, the authentication, the retries, the parallel and sequential plans and the data copying without code.

Where the request leaves from

  • Called over HTTP, the request leaves from the server of API Maker.
  • Called from your code, it leaves from where the code runs : its sandbox. The names and the addresses the sandbox reaches are the ones the call reaches.
  • The files a call sends and saves are the ones of the uploads folder, which the server and the sandboxes share.

Access and settings

  • Over HTTP, a system API answers once its settings give it apiAccessType: TOKEN_ACCESS (the token of an API user whose group grants this system API, in x-am-authorization) or IS_PUBLIC. Without settings it is NO_ACCESS : your code calls it through g.sys, the admin panel tests it, and an HTTP call is refused.
  • The settings can also cache the answer or require person tokens (authProviders) ; pre and post hooks run around it like around any API : a pre hook which adds the auth of the call keeps the secrets of the other server out of the reach of the caller.
  • The request headers apply : x-am-response-case, x-am-content-type-response, x-am-internationalization, x-am-tenant-username…

Errors

Code When
400 The body is wrong : the message names the missing or invalid key (Invalid value for bodyType. Allowed values are [json, form, multipart, text, xml, binary].), a file which is not in the uploads folder, or a download which leads out of it.
401 No valid API user token, or the API is NO_ACCESS : You are not authorized to access this API.
403 No group grants this system API.
The status of the other server With throwOnError : POST https://api.example.com/payments answered with the status 503. A status API Maker does not answer with itself is answered as 500, with the status in the code of the error.
500 The request got no answer, or its answer can not be given : connect ECONNREFUSED 10.0.0.5:443, getaddrinfo ENOTFOUND api.example.com, timeout of 5000ms exceeded, length limit exceeded, Too many redirects : 10 were followed, …, The answer is no JSON : ….