# Custom APIs

> Write a function in API Maker, in TypeScript, Python, PHP, C#, Go, Rust, Java or Kotlin, and it is live as an API - path and method, settings for caching, access, validation, files and timeouts, the global object g to reach your databases, file uploads and downloads, any content type, hooks, versions and tests.

Source: https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.html

A custom API is a function behind a path and a method, written in any of the [languages](https://docs.apimaker.dev/v4/docs/languages/index.html) 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`](https://docs.apimaker.dev/v4/docs/pre-defined-terms/global-object-g.html).

| | |
|---|---|
| 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](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html), 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`](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html) 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](https://docs.apimaker.dev/v4/docs/languages/index.html) ; the basic info is always TypeScript.

**Basic info**

```typescript
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;
```

**TypeScript**

**Code**

```typescript
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;
```

**Python**

**Code**

```python
from am import G


async def main(g: G):
    return {'hello': g.req.query.name or 'world'}
```

**PHP**

**Code**

```php
<?php
function main(AmG $g)
{
    return ['hello' => $g->req->query->name ?? 'world'];
}
```

**C#**

**Code**

```csharp
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        return new { hello = g.Req.Query.GetString("name", "world") };
    }
}
```

**Go**

**Code**

```go
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
}
```

**Rust**

**Code**

```rust
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") }))
}
```

**Java**

**Code**

```java
import am.G;
import java.util.Map;

class Api {
    static Object main(G g) {
        return Map.of("hello", g.req.getQuery().getString("name", "world"));
    }
}
```

**Kotlin**

**Code**

```kotlin
import am.G

fun main(g: G): Any? {
    return mapOf("hello" to g.req.query.getString("name", "world"))
}
```

```bash
curl "$AM/api/custom-api/admin/hello-world?name=Bob" -H "x-am-authorization: $TOKEN"
```

```json
{ "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](https://docs.apimaker.dev/v4/docs/languages/index.html#one-language).

- 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](https://docs.apimaker.dev/v4/docs/authorization/AMDB.html) 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](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-reset-redis-cache-custom-api.html) 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](https://docs.apimaker.dev/v4/docs/features/security-features.html#encrypted-request-payloads). |
| `reqBodySchema`, `reqQueryParametersSchema` | A [schema](https://docs.apimaker.dev/v4/docs/schema/schema.html) 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](https://docs.apimaker.dev/v4/docs/i18/i18.html) 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](https://docs.apimaker.dev/v4/docs/settings/customApiSettings.html#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](https://docs.apimaker.dev/v4/docs/settings/customApiSettings.html) for every key with its example.

## Validate the request with a schema

**Basic info**

```typescript
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](https://docs.apimaker.dev/v4/docs/apis-all/system-apis/system-generated-is-valid-data-for-custom-api.html) system API runs the same check without calling the API.

## Reach your data and the rest of API Maker

**TypeScript**

```typescript
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;
```

**Python**

```python
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**

```php
<?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];
}
```

**C#**

```csharp
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 };
    }
}
```

**Go**

```go
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
}
```

**Rust**

```rust
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 }))
}
```

**Java**

```java
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);
    }
}
```

**Kotlin**

```kotlin
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](https://docs.apimaker.dev/v4/docs/apis-all/response-format.html) 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](https://docs.apimaker.dev/v4/docs/utility-class/utility-class.html).
- Every method of `g` has a page in the [code examples](https://docs.apimaker.dev/v4/examples/index.html).

## Errors and status codes

**TypeScript**

```typescript
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;
```

**Python**

```python
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**

```php
<?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];
}
```

**C#**

```csharp
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 };
    }
}
```

**Go**

```go
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
}
```

**Rust**

```rust
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 }))
}
```

**Java**

```java
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);
    }
}
```

**Kotlin**

```kotlin
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` :

**TypeScript**

```typescript
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;
```

**Python**

```python
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]
```

**PHP**

```php
<?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;
}
```

**C#**

```csharp
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();
    }
}
```

**Go**

```go
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
}
```

**Rust**

```rust
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<_>>()))
}
```

**Java**

```java
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();
    }
}
```

**Kotlin**

```kotlin
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.

**TypeScript**

```typescript
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;
```

**Python**

```python
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**

```php
<?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',
    ];
}
```

**C#**

```csharp
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",
        };
    }
}
```

**Go**

```go
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
}
```

**Rust**

```rust
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",
    }))
}
```

**Java**

```java
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");
    }
}
```

**Kotlin**

```kotlin
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.

**TypeScript**

```typescript
import * as T from 'types';

async function main(g: T.IAMGlobal) {
    g.res.contentType = T.EContentType.HTML;
    return '<h1>Hello</h1>';
}
module.exports = main;
```

**Python**

```python
from am import G


async def main(g: G):
    g.res.content_type = 'text/html'
    return '<h1>Hello</h1>'
```

**PHP**

```php
<?php
function main(AmG $g)
{
    $g->res->contentType = 'text/html';
    return '<h1>Hello</h1>';
}
```

**C#**

```csharp
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        g.Res.ContentType = "text/html";
        return "<h1>Hello</h1>";
    }
}
```

**Go**

```go
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
	g.Res.SetContentType("text/html")
	return "<h1>Hello</h1>", nil
}
```

**Rust**

```rust
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>"))
}
```

**Java**

```java
import am.G;

class Api {
    static Object main(G g) {
        g.res.setContentType("text/html");
        return "<h1>Hello</h1>";
    }
}
```

**Kotlin**

```kotlin
import am.G

fun main(g: G): Any? {
    g.res.contentType = "text/html"
    return "<h1>Hello</h1>"
}
```

**TypeScript**

**A tiny gif**

```typescript
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;
```

**Python**

**A tiny gif**

```python
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='
```

**PHP**

**A tiny gif**

```php
<?php
function main(AmG $g)
{
    $g->res->contentType = 'image/gif';   // another type : what is returned is base64
    return 'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=';
}
```

**C#**

**A tiny gif**

```csharp
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=";
    }
}
```

**Go**

**A tiny gif**

```go
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
}
```

**Rust**

**A tiny gif**

```rust
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="))
}
```

**Java**

**A tiny gif**

```java
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=";
    }
}
```

**Kotlin**

**A tiny gif**

```kotlin
import am.G

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

- `g.res.headers` adds response headers. See [Content types](https://docs.apimaker.dev/v4/examples/res/res.html#content-types).

## Around the code

> Diagram : The life of a request : app → Caddy → token and groups → pre hooks → the API → post hooks → reply
>
> The life of a request in API Maker : the app calls through Caddy, API Maker checks the token and the groups, runs the pre hooks, the API itself against the database or the cache, the post hooks, and replies in JSON, XML or YAML.

- **Hooks** : pre and post hooks on the custom API itself, see [Pre hooks](https://docs.apimaker.dev/v4/docs/apis-all/hooks/preHook-api.html).
- **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`](https://docs.apimaker.dev/v4/docs/test-cases/test-cases.html#testing-a-custom-api) 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](https://docs.apimaker.dev/v4/docs/apis-security/api-group-permission.html).

## 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](https://docs.apimaker.dev/v4/docs/guides/browser-automation.html). The runtime must be installed on the server (`node`, `python3`, `php`…), in a [version API Maker runs](https://docs.apimaker.dev/v4/docs/languages/index.html#versions) : [API Maker Launchpad](https://docs.apimaker.dev/v4/docs/getting-started/local-run.html#languages-for-native-apis) 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](https://docs.apimaker.dev/v4/docs/languages/index.html#g-sys-in-the-sandbox)) : `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](https://docs.apimaker.dev/v4/docs/am-resources/api-maker-configurations.html#process-environment)), 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.
