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

Custom APIs

A custom API is a function behind a path and a method, written in any of the languages of API Maker. Save it in the admin panel (APIs & Logic → Custom API) or in your editor through the local client, and it answers at /api/custom-api/<user-path>/<path> right away, in a sandbox, with the whole of API Maker available through the global object g.

URL /api/custom-api/<user-path>/<path of the settings>
Method the requestMethod of the settings : GET, POST, PUT or DELETE
Body, query, params, headers, files g.req.body, g.req.query, g.req.params, g.req.headers, g.req.body.files
Answer what main returns, in data of the envelope, or a file, or any content type
Language TypeScript on Node.js or Bun, Python, PHP, C#, Go, Rust, Java or Kotlin : picked above the editor, codeLanguage of the API
Runs in the sandbox of the account for its language ; the machine of the server with runOnNativeProcess: true
Access apiAccessType : TOKEN_ACCESS (default), IS_PUBLIC, NO_ACCESS (only from other code)
From code g.sys.system.callExternalApi over HTTP, or g.sys.test.runCustomApi in a test

Hello world

Every custom API has two files : the basic info (its settings) and the code. The code is written in any language ; the basic info is always TypeScript.

Basic info
import * as T from 'types';

let customApi: T.ICustomApiSettingsTypes = {
    name: 'Hello World',
    path: '/hello-world',
    requestMethod: T.ERequestMethod.GET,
    apiAccessType: T.EAPIAccessType.TOKEN_ACCESS,
    errorList: [],
};
module.exports = customApi;
Code
1
2
3
4
5
6
7
import * as T from 'types';
import * as db from 'db-interfaces';

async function main(g: T.IAMGlobal) {
    return { hello: g.req.query.name || 'world' };
}
module.exports = main;
Code
1
2
3
4
5
from am import G


async def main(g: G):
    return {'hello': g.req.query.name or 'world'}
Code
1
2
3
4
5
<?php
function main(AmG $g)
{
    return ['hello' => $g->req->query->name ?? 'world'];
}
Code
1
2
3
4
5
6
7
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        return new { hello = g.Req.Query.GetString("name", "world") };
    }
}
Code
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    name := g.Req.Query().String("name")
    if name == "" {
        name = "world"
    }
    return am.M{"hello": name}, nil
}
Code
1
2
3
4
5
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    Ok(json!({ "hello": g.req.query()["name"].as_str().unwrap_or("world") }))
}
Code
1
2
3
4
5
6
7
8
import am.G;
import java.util.Map;

class Api {
    static Object main(G g) {
        return Map.of("hello", g.req.getQuery().getString("name", "world"));
    }
}
Code
1
2
3
4
5
import am.G

fun main(g: G): Any? {
    return mapOf("hello" to g.req.query.getString("name", "world"))
}
curl "$AM/api/custom-api/admin/hello-world?name=Bob" -H "x-am-authorization: $TOKEN"
{ "success": true, "statusCode": 200, "data": { "hello": "Bob" } }

Pick the language above the editor, and the editor offers its template. The list of the custom APIs shows the language of each with its logo. The hooks of a custom API are written in the language of its code.

  • The path and the method must match exactly : /hello-world with GET. Variables go in the query string or the body, not in the path.
  • path + requestMethod is unique in the account ; name is unique too and names the folder in Git.

The settings

Key Meaning
name, path, requestMethod Identity and URL.
apiAccessType TOKEN_ACCESS : the API user token, plus the person token when authProviders names one. IS_PUBLIC : no token. NO_ACCESS : not reachable over HTTP, only from other code and the testing page.
authProviders Names of the auth providers whose token the person must send. Absent : the providers of the default secret. [] : the API user token only.
enableCaching Cache the answer in Redis, per URL, body and headers that matter, until resetCacheOnModificationOf says otherwise or the reset custom API cache system API runs.
resetCacheOnModificationOf What resets the cache : 'DB:instance:database:table' when that table is written through API Maker, 'CA:name' when that custom API is called.
acceptOnlyEncryptedData Refuse plain bodies : the body must be an encrypted payload.
reqBodySchema, reqQueryParametersSchema A schema for the body and for the query params : converted and validated before the code runs, every error at once with 400.
errorList The messages your API throws, listed so they can be translated with i18n and shown in the docs of the API.
runOnNativeProcess Run on the machine of the server instead of in the sandbox : the packages of the folder API Maker runs in and the programs of the machine, no isolation. Use with care.
customApiTimeoutInSeconds How long the code and the hooks of the API may run, in seconds, wherever they run (the sandbox, runOnNativeProcess). Empty : sandboxReqTimeout of the configuration (13 seconds). A request which sends x-am-sandbox-timeout is given that time instead.
separateSandboxSettings A sandbox of its own for this API, with its own runtime, version, packages and count : separate sandbox.
fileUpload enable, allowFileUploadFields, and per field minFileSizeBytes, maxFileSizeBytes, allowedExtensionsArr.
swaggerDocs What the Swagger document of the API users shows for this API.

See Custom API settings for every key with its example.

Validate the request with a schema

Basic info
let customApi: T.ICustomApiSettingsTypes = {
    name: 'Create Order', path: '/orders', requestMethod: T.ERequestMethod.POST, errorList: [],
    reqBodySchema: {
        customer_id: { __type: T.EType.number, validations: { required: true } },
        items: [ { product_id: { __type: T.EType.string, validations: { required: true } }, qty: { __type: T.EType.number, validations: { min: 1 } } } ],
    },
    reqQueryParametersSchema: {
        dryRun: { __type: T.EType.boolean, conversions: { defaults: { defaultValue: false } } },
    },
};
  • The body and the query params reach your code converted (types, trims, defaults) and checked (required, min, enum…). A wrong request never reaches the code.
  • The is valid data for custom API system API runs the same check without calling the API.

Reach your data and the rest of API Maker

import * as T from 'types';
import * as db from 'db-interfaces';

async function main(g: T.IAMGlobal) {
    const person = g.req.auth.authAMDB;                          // the signed-in person
    const orders = await g.sys.db.query<db.mongodb.shop.IOrders>({
        instance: 'mongodb', database: 'shop', collection: 'orders',
        find: { customer_id: person.customer_id, status: 'PAID' },
        sort: '-created_at', limit: 10,
        deep: [ { s_key: 'items' } ],
    });
    const total = orders.reduce((sum, order) => sum + order.amount, 0);
    g.logger.log(`${orders.length} orders`);                     // reaches the log table and the caller's logs
    await g.sys.cache.setKey(`orders:${person.customer_id}`, JSON.stringify(orders), 60);
    return { orders, total };
}
module.exports = main;
import json

from am import DeepItem, G


async def main(g: G):
    person = g.req.auth.authAMDB                                 # the signed-in person
    orders = await g.sys.db.query(
        'mongodb', 'shop', 'orders',
        find={'customer_id': person.customer_id, 'status': 'PAID'},
        sort='-created_at', limit=10,
        deep=[DeepItem('items')],
    )
    total = sum(order.amount or 0 for order in orders)
    g.logger.log(f'{len(orders)} orders')                       # reaches the log table and the caller's logs
    await g.sys.cache.set_key(f'orders:{person.customer_id}', json.dumps(orders), 60)
    return {'orders': orders, 'total': total}
<?php
function main(AmG $g)
{
    $person = $g->req->auth->authAMDB;                           // the signed-in person
    $orders = $g->sys->db->query(
        'mongodb', 'shop', 'orders',
        find: ['customer_id' => $person->customer_id, 'status' => 'PAID'],
        sort: '-created_at', limit: 10,
        deep: [new DeepItem('items')],
    );
    $total = array_sum(array_map(fn ($order) => $order->amount ?? 0, $orders));
    $g->logger->log(count($orders) . ' orders');                 // reaches the log table and the caller's logs
    $g->sys->cache->setKey("orders:{$person->customer_id}", json_encode($orders), 60);
    return ['orders' => $orders, 'total' => $total];
}
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        var person = g.Req.Auth.GetObject("authAMDB");                  // the signed-in person
        var orders = await g.Sys.Db.Query(new Query("mongodb", "shop", "orders")
        {
            Find = new JsonObject { ["customer_id"] = person["customer_id"]?.DeepClone(), ["status"] = "PAID" },
            Sort = "-created_at", Limit = 10,
            Deep = new[] { new DeepItem("items") },
        });
        var total = orders.Sum(order => order.GetDouble("amount"));
        g.Logger.Log($"{orders.Count} orders");                          // reaches the log table and the caller's logs
        await g.Sys.Cache.SetKey($"orders:{person["customer_id"]}", JsonSerializer.Serialize(orders), 60);
        return new { orders, total };
    }
}
package code

import (
    "encoding/json"
    "fmt"

    "apimaker/am"
)

func Main(g *am.G) (any, error) {
    person := g.Req.Auth().Map("authAMDB") // the signed-in person
    orders, err := g.Sys.Db.Query(am.Query{
        Instance: "mongodb", Database: "shop", Collection: "orders",
        Find: am.M{"customer_id": person.Get("customer_id"), "status": "PAID"},
        Sort: "-created_at", Limit: 10,
        Deep: []am.DeepItem{am.DeepItem{SKey: "items"}},
    })
    if err != nil {
        return nil, err
    }
    total := 0.0
    for _, order := range orders {
        total += order.Float("amount")
    }
    g.Logger.Log(fmt.Sprintf("%d orders", len(orders))) // reaches the log table and the caller's logs
    kept, err := json.Marshal(orders)
    if err != nil {
        return nil, err
    }
    if _, err := g.Sys.Cache.SetKey(fmt.Sprintf("orders:%v", person.Get("customer_id")), string(kept), 60); err != nil {
        return nil, err
    }
    return am.M{"orders": orders, "total": total}, nil
}
use am::{DeepItem, Errors, G, Query, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    let person = g.req.auth()["authAMDB"].clone(); // the signed-in person
    let orders = g.sys.db.query(
        Query::new("mongodb", "shop", "orders")
            .find(json!({ "customer_id": person["customer_id"], "status": "PAID" }))
            .sort("-created_at").limit(10)
            .deep(vec![DeepItem::new("items")]),
    )?;
    let total: f64 = orders.iter().map(|order| order["amount"].as_f64().unwrap_or(0.0)).sum();
    g.logger.log(format!("{} orders", orders.len())); // reaches the log table and the caller's logs
    g.sys.cache.set_key(&format!("orders:{}", person["customer_id"]), &json!(orders).to_string(), Some(60))?;
    Ok(json!({ "orders": orders, "total": total }))
}
import am.DeepItem;
import am.G;
import am.Json;
import am.JsonObject;
import am.Query;
import java.util.List;
import java.util.Map;

class Api {
    static Object main(G g) {
        JsonObject person = g.req.getAuth().getObject("authAMDB");              // the signed-in person
        List<JsonObject> orders = g.sys.db.query(new Query("mongodb", "shop", "orders")
            .find(Map.of("customer_id", person.get("customer_id"), "status", "PAID"))
            .sort("-created_at").limit(10)
            .deep(List.of(new DeepItem("items"))));
        double total = orders.stream().mapToDouble(order -> order.getDouble("amount")).sum();
        g.logger.log(orders.size() + " orders");                                // reaches the log table and the caller's logs
        g.sys.cache.setKey("orders:" + person.get("customer_id"), Json.stringify(orders), 60);
        return Map.of("orders", orders, "total", total);
    }
}
import am.DeepItem
import am.G
import am.Json
import am.Query

fun main(g: G): Any? {
    val person = g.req.auth.getObject("authAMDB")                               // the signed-in person
    val orders = g.sys.db.query(
        Query("mongodb", "shop", "orders")
            .find(mapOf("customer_id" to person["customer_id"], "status" to "PAID"))
            .sort("-created_at").limit(10)
            .deep(listOf(DeepItem("items"))),
    )
    val total = orders.sumOf { it.getDouble("amount") }
    g.logger.log("${orders.size} orders")                                        // reaches the log table and the caller's logs
    g.sys.cache.setKey("orders:${person["customer_id"]}", Json.stringify(orders), 60)
    return mapOf("orders" to orders, "total" to total)
}
  • In TypeScript await every g.sys call (a coroutine of Python and a task of C# too) ; the other languages wait for the answer of each call. The whole envelope instead of an error comes with true as the second argument in TypeScript, and through full in the other languages (g.sys.db.full.query).
  • In TypeScript, import * as db from 'db-interfaces' gives the interfaces generated from your schemas, db.<instance>.<database>.I<Table> ; the editors of the other languages offer the same columns on the rows a call answers.
  • Code shared by many APIs (a price, a rule) goes into a utility class.
  • Every method of g has a page in the code examples.

Errors and status codes

import * as T from 'types';

async function main(g: T.IAMGlobal) {
    if (!g.req.body?.customer_id) {
        throw [{ code: T.EStatusCode.BAD_REQUEST, message: 'Please provide customer_id.' }];   // 400 with the message in errors
    }
    if (g.req.body.customer_id < 0) {
        g.res.statusCode = T.EStatusCode.BAD_REQUEST;                                       // 400 with your data
        return { field: 'customer_id', message: 'A customer_id is positive.' };
    }
    return { ok: true };
}
module.exports = main;
from am import Errors, G


async def main(g: G):
    if not g.req.body.customer_id:
        raise Errors('Please provide customer_id.', code=400)    # 400 with the message in errors
    if g.req.body.customer_id < 0:
        g.res.status_code = 400                                    # 400 with your data
        return {'field': 'customer_id', 'message': 'A customer_id is positive.'}
    return {'ok': True}
<?php
function main(AmG $g)
{
    if (empty($g->req->body->customer_id)) {
        throw new AmErrors('Please provide customer_id.', 400);   // 400 with the message in errors
    }
    if ($g->req->body->customer_id < 0) {
        $g->res->statusCode = 400;                                 // 400 with your data
        return ['field' => 'customer_id', 'message' => 'A customer_id is positive.'];
    }
    return ['ok' => true];
}
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        if (g.Req.Body["customer_id"] is null)
            throw new AmErrors("Please provide customer_id.", 400);   // 400 with the message in errors
        if (g.Req.Body.GetDouble("customer_id") < 0)
        {
            g.Res.StatusCode = 400;                                    // 400 with your data
            return new { field = "customer_id", message = "A customer_id is positive." };
        }
        return new { ok = true };
    }
}
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    body := g.Req.Body()
    if body.Get("customer_id") == nil {
        return nil, am.Fail(400, "Please provide customer_id.") // 400 with the message in errors
    }
    if body.Float("customer_id") < 0 {
        g.Res.SetStatusCode(400) // 400 with your data
        return am.M{"field": "customer_id", "message": "A customer_id is positive."}, nil
    }
    return am.M{"ok": true}, nil
}
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    let Some(customer_id) = g.req.body()["customer_id"].as_f64() else {
        return Err(am::fail(400, "Please provide customer_id.")); // 400 with the message in errors
    };
    if customer_id < 0.0 {
        g.res.set_status_code(400); // 400 with your data
        return Ok(json!({ "field": "customer_id", "message": "A customer_id is positive." }));
    }
    Ok(json!({ "ok": true }))
}
import am.AmErrors;
import am.G;
import java.util.Map;

class Api {
    static Object main(G g) {
        if (!g.req.getBody().has("customer_id")) throw new AmErrors("Please provide customer_id.", 400);   // 400 with the message in errors
        if (g.req.getBody().getDouble("customer_id") < 0) {
            g.res.setStatusCode(400);                                                                       // 400 with your data
            return Map.of("field", "customer_id", "message", "A customer_id is positive.");
        }
        return Map.of("ok", true);
    }
}
import am.AmErrors
import am.G

fun main(g: G): Any? {
    if (!g.req.body.has("customer_id")) throw AmErrors("Please provide customer_id.", 400)   // 400 with the message in errors
    if (g.req.body.getDouble("customer_id") < 0) {
        g.res.statusCode = 400                                                                // 400 with your data
        return mapOf("field" to "customer_id", "message" to "A customer_id is positive.")
    }
    return mapOf("ok" to true)
}
  • An error raised answers success: false with its message in errors, and its code as the status : throw [{ code, message }] in TypeScript, raise Errors(message, code=400) in Python, AmErrors in PHP, C#, Java and Kotlin, am.Fail(400, message) in Go, am::fail(400, message) in Rust. Any other exception answers 500 with its message. A message listed in errorList is translated by i18n.
  • g.res.statusCode sets the status of a normal answer ; g.res.warnings adds warnings.

Files

Upload. Send multipart/form-data with the files in the field files (or files1, files2… up to files41 to keep groups apart). They are on disk when the code runs, in g.req.body.files :

1
2
3
4
5
6
7
import * as T from 'types';

async function main(g: T.IAMGlobal) {
    const files: { originalname: string; mimetype: string; size: number; path: string; filename: string }[] = g.req.body.files || [];
    return files.map(f => ({ name: f.originalname, bytes: f.size }));
}
module.exports = main;
1
2
3
4
5
6
from am import G


async def main(g: G):
    files = g.req.body.files or []   # each : originalname, mimetype, size, path, filename
    return [{'name': f.originalname, 'bytes': f.size} for f in files]
1
2
3
4
5
6
7
8
9
<?php
function main(AmG $g)
{
    $answer = [];
    foreach ($g->req->body->files ?? [] as $f) {   // each : originalname, mimetype, size, path, filename
        $answer[] = ['name' => $f->originalname, 'bytes' => $f->size];
    }
    return $answer;
}
1
2
3
4
5
6
7
8
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        // each : originalname, mimetype, size, path, filename
        return g.Req.Body.GetObjects("files").Select(f => new { name = f.GetString("originalname"), bytes = f.GetInt64("size") }).ToList();
    }
}
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    answer := []am.M{}
    for _, each := range g.Req.Body().List("files") { // each : originalname, mimetype, size, path, filename
        f := am.AsM(each)
        answer = append(answer, am.M{"name": f.String("originalname"), "bytes": f.Int("size")})
    }
    return answer, nil
}
1
2
3
4
5
6
7
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    // each : originalname, mimetype, size, path, filename
    let files = g.req.body()["files"].as_array().cloned().unwrap_or_default();
    Ok(json!(files.iter().map(|f| json!({ "name": f["originalname"], "bytes": f["size"] })).collect::<Vec<_>>()))
}
import am.G;
import am.JsonObject;
import java.util.List;
import java.util.Map;

class Api {
    static Object main(G g) {
        // each : originalname, mimetype, size, path, filename
        List<JsonObject> files = g.req.getBody().getObjects("files");
        return files.stream().map(f -> Map.of("name", f.getString("originalname", ""), "bytes", f.getLong("size"))).toList();
    }
}
1
2
3
4
5
6
import am.G

fun main(g: G): Any? {
    // each : originalname, mimetype, size, path, filename
    return g.req.body.getObjects("files").map { mapOf("name" to it.getString("originalname"), "bytes" to it.getLong("size")) }
}
  • fileUpload.validations.files.maxFileSizeBytes and allowedExtensionsArr in the settings refuse a wrong file before the code runs. Uploaded files are cleaned from the uploads folder after uploadedFileRemoveOlderThanThisTimeInSeconds (30 minutes by default).

Download. Return an object with the __am__ keys of IDownloadResponse : the file (or a zip of several) is streamed to the caller.

import * as T from 'types';

async function main(g: T.IAMGlobal) {
    // a file written earlier in the uploads folder of the sandbox
    return <T.IDownloadResponse>{
        __am__downloadFilePath: 'reports/2026-09.pdf',
        __am__downloadFolderFileName: 'September report.pdf',
        __am__cleanupFileOrFolderPaths: 'reports/2026-09.pdf',
    };
}
module.exports = main;
from am import G


async def main(g: G):
    # a file written earlier in the uploads folder of the sandbox
    return {
        '__am__downloadFilePath': 'reports/2026-09.pdf',
        '__am__downloadFolderFileName': 'September report.pdf',
        '__am__cleanupFileOrFolderPaths': 'reports/2026-09.pdf',
    }
<?php
function main(AmG $g)
{
    // a file written earlier in the uploads folder of the sandbox
    return [
        '__am__downloadFilePath' => 'reports/2026-09.pdf',
        '__am__downloadFolderFileName' => 'September report.pdf',
        '__am__cleanupFileOrFolderPaths' => 'reports/2026-09.pdf',
    ];
}
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        // a file written earlier in the uploads folder of the sandbox
        return new JsonObject
        {
            ["__am__downloadFilePath"] = "reports/2026-09.pdf",
            ["__am__downloadFolderFileName"] = "September report.pdf",
            ["__am__cleanupFileOrFolderPaths"] = "reports/2026-09.pdf",
        };
    }
}
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    // a file written earlier in the uploads folder of the sandbox
    return am.M{
        "__am__downloadFilePath":         "reports/2026-09.pdf",
        "__am__downloadFolderFileName":   "September report.pdf",
        "__am__cleanupFileOrFolderPaths": "reports/2026-09.pdf",
    }, nil
}
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    // a file written earlier in the uploads folder of the sandbox
    Ok(json!({
        "__am__downloadFilePath": "reports/2026-09.pdf",
        "__am__downloadFolderFileName": "September report.pdf",
        "__am__cleanupFileOrFolderPaths": "reports/2026-09.pdf",
    }))
}
import am.G;
import java.util.Map;

class Api {
    static Object main(G g) {
        // a file written earlier in the uploads folder of the sandbox
        return Map.of(
            "__am__downloadFilePath", "reports/2026-09.pdf",
            "__am__downloadFolderFileName", "September report.pdf",
            "__am__cleanupFileOrFolderPaths", "reports/2026-09.pdf");
    }
}
import am.G

fun main(g: G): Any? {
    // a file written earlier in the uploads folder of the sandbox
    return mapOf(
        "__am__downloadFilePath" to "reports/2026-09.pdf",
        "__am__downloadFolderFileName" to "September report.pdf",
        "__am__cleanupFileOrFolderPaths" to "reports/2026-09.pdf",
    )
}
  • __am__downloadFileOrFolderPaths with several paths, or { fsSource, archiveDestination } objects, answers a zip.

Any content type

Set g.res.contentType and return the content : HTML, text, XML, YAML, or bytes as base64 for any other MIME type.

1
2
3
4
5
6
7
import * as T from 'types';

async function main(g: T.IAMGlobal) {
    g.res.contentType = T.EContentType.HTML;
    return '<h1>Hello</h1>';
}
module.exports = main;
1
2
3
4
5
6
from am import G


async def main(g: G):
    g.res.content_type = 'text/html'
    return '<h1>Hello</h1>'
1
2
3
4
5
6
<?php
function main(AmG $g)
{
    $g->res->contentType = 'text/html';
    return '<h1>Hello</h1>';
}
1
2
3
4
5
6
7
8
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        g.Res.ContentType = "text/html";
        return "<h1>Hello</h1>";
    }
}
1
2
3
4
5
6
7
8
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    g.Res.SetContentType("text/html")
    return "<h1>Hello</h1>", nil
}
1
2
3
4
5
6
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    g.res.set_content_type("text/html");
    Ok(json!("<h1>Hello</h1>"))
}
1
2
3
4
5
6
7
8
import am.G;

class Api {
    static Object main(G g) {
        g.res.setContentType("text/html");
        return "<h1>Hello</h1>";
    }
}
1
2
3
4
5
6
import am.G

fun main(g: G): Any? {
    g.res.contentType = "text/html"
    return "<h1>Hello</h1>"
}
A tiny gif
1
2
3
4
5
6
7
import * as T from 'types';

async function main(g: T.IAMGlobal) {
    g.res.contentType = 'image/gif' as T.EContentType;   // not in EContentType : the return is base64
    return 'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=';
}
module.exports = main;
A tiny gif
1
2
3
4
5
6
from am import G


async def main(g: G):
    g.res.content_type = 'image/gif'   # another type : what is returned is base64
    return 'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA='
A tiny gif
1
2
3
4
5
6
<?php
function main(AmG $g)
{
    $g->res->contentType = 'image/gif';   // another type : what is returned is base64
    return 'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=';
}
A tiny gif
1
2
3
4
5
6
7
8
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        g.Res.ContentType = "image/gif";   // another type : what is returned is base64
        return "R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=";
    }
}
A tiny gif
1
2
3
4
5
6
7
8
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    g.Res.SetContentType("image/gif") // another type : what is returned is base64
    return "R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=", nil
}
A tiny gif
1
2
3
4
5
6
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    g.res.set_content_type("image/gif"); // another type : what is returned is base64
    Ok(json!("R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA="))
}
A tiny gif
1
2
3
4
5
6
7
8
import am.G;

class Api {
    static Object main(G g) {
        g.res.setContentType("image/gif");   // another type : what is returned is base64
        return "R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=";
    }
}
A tiny gif
1
2
3
4
5
6
import am.G

fun main(g: G): Any? {
    g.res.contentType = "image/gif"   // another type : what is returned is base64
    return "R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA="
}

Around the code

The life of a request : app → Caddy → token and groups → pre hooks → the API → post hooks → reply Your app web · mobile · server or an AI agent Caddy HTTPS ends here :443 → :38246 API Maker one process, or a cluster behind Caddy Token & groups API gate · row gate 401 · 403 if not Pre hooks your code check · change · stop The API schema · generated custom · system Post hooks your code reshape · notify Reply JSON · XML YAML · text · file Redis cache answers again in µs Your databases MongoDB · SQL · Oracle The API asks the cache first when caching is on, then the database. Sandboxed : your code runs in Docker containers, apart from API Maker. x-am-authorization 200 · 201 with data, or 400 · 401 · 403 · 404 with every error at once
  • Hooks : pre and post hooks on the custom API itself, see Pre hooks.
  • Events and WebSockets : an event can trigger automatically after the API, and WebSocket clients subscribed to the API get its answer.
  • Versions : several versions of the code, one active ; switch back in one click.
  • Docs tab : notes for the people who use the API, shown in the API testing page and the Swagger of the API users.
  • Test cases : g.sys.test.runCustomApi runs the code with mocks and measures its coverage.
  • Git : src/custom_apis/<name>/ holds the basic info (<name>.config.ts), the code (<name>.ts, or .py, .go… in another language) and the docs.
  • Groups : an API user calls a custom API only when a group grants it, see API group permission.

Native process

runOnNativeProcess: true runs the function on the machine of the server instead of in Docker, in one runner of its runtime shared by every account : no sandbox start, the packages installed in the folder API Maker runs in, and the programs of the machine, such as a browser for Playwright. The runtime must be installed on the server (node, python3, php…), in a version API Maker runs : API Maker Launchpad installs it for you, on your computer and on your servers, or finds the one they have. The code is not isolated from the server, so keep it for the few APIs which need it.

That runner loads API Maker itself, as a sandbox does (g.sys is answered where the code runs) : g.sys.db.getAll(…), a key of the cache, a secret are function calls in the process of the runner, which reads the databases and Redis itself, for the account of each request. Only the answer of the API goes back to the server. This holds in every language ; a release has the libraries it needs, built for its machine. Where none is found for the machine of the server (a server built by hand on a Mac : AM_NATIVE_CORE in the configuration), the server answers those calls itself, as it says in its log.

Samples in every account

A new account comes with custom APIs under /default/… which show the features at work : deep populate, find and join, caching, a WebSocket emitter, S3 upload and download, a login which returns both tokens, a captcha, data isolation with hooks. Open them on the Custom API page and read the code.