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
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.
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.
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").
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 {}.
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"}}}
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.
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.
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
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.
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".
A custom API which downloads an invoice from another server and sends it to its own caller :
constsaved=awaitg.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.pathreturn{__am__downloadFilePath:saved.file.path,__am__downloadFolderFileName:saved.file.name};
saved=awaitg.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.pathreturn{'__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->pathreturn ['__am__downloadFilePath' => $saved->file->path, '__am__downloadFolderFileName' => $saved->file->name];
varsaved=awaitg.Sys.System.CallExternalApi(newCallExternalApi("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")returnnewJsonObject{["__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,})iferr!=nil{returnnil,err}// the file is in the uploads folder now : your code reads it at "uploads/" + saved.File.String("path")returnam.M{"__am__downloadFilePath":saved.File.String("path"),"__am__downloadFolderFileName":saved.File.String("name")},nil
letsaved=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 filereturnOk(json!({"__am__downloadFilePath":saved.file["path"],"__am__downloadFolderFileName":saved.file["name"]}));
ExternalApiResponsesaved=g.sys.system.callExternalApi(newCallExternalApi("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")returnMap.of("__am__downloadFilePath",saved.getFile().getString("path"),"__am__downloadFolderFileName",saved.getFile().getString("name"));
valsaved=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")returnmapOf("__am__downloadFilePath"tosaved.file.getString("path"),"__am__downloadFolderFileName"tosaved.file.getString("name"))
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.
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.
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…
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 : ….