# Custom APIs

> Write a TypeScript function in API Maker 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/v1/docs/apis-all/custom-apis/user-created-custom-api.html

A custom API is a TypeScript function behind a path and a method. Save it in the admin panel (`API Info → 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/v1/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/v1/docs/apis-all/response-format.html), or a file, or any content type |
| Runs in | the sandbox of the account ; the native process 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/v1/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**.

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

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

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

```json
{ "success": true, "statusCode": 200, "data": { "hello": "Bob" } }
```

- 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/v1/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/v1/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, `'TP:bundle:version'` when a third party API of that version writes. |
| `acceptOnlyEncryptedData` | Refuse plain bodies : the body must be an [encrypted payload](https://docs.apimaker.dev/v1/docs/features/security-features.html#encrypted-request-payloads). |
| `reqBodySchema`, `reqQueryParametersSchema` | A [schema](https://docs.apimaker.dev/v1/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/v1/docs/i18/i18.html) and shown in the docs of the API. |
| `runOnNativeProcess` | Run in the API Maker process instead of the sandbox : native modules and the packages of API Maker, no isolation. Use with care. |
| `customApiTimeoutInSeconds` | How long the code may run, in seconds (10 by default). Sent as `x-am-sandbox-timeout`. |
| `separateSandboxSettings` | A sandbox of its own for this API, with its own packages and memory. |
| `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/v1/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/v1/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
import * as T from 'types';
import * as db from 'db-interfaces';
import * as Pricing from 'utils/Pricing';

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 = Pricing.sum(orders);                           // a utility class of yours
    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;
```

- `await` every `g.sys` call. The second argument `true` returns the whole [envelope](https://docs.apimaker.dev/v1/docs/apis-all/response-format.html) instead of throwing.
- `import * as db from 'db-interfaces'` gives the interfaces generated from your schemas, `db.<instance>.<database>.I<Table>`.
- Every method of `g` has a page in the [code examples](https://docs.apimaker.dev/v1/examples/index.html).

## Errors and status codes

```typescript
async function main(g: T.IAMGlobal) {
    if (!g.req.body?.customer_id) throw new Error('Please provide customer_id.');   // 500 with the message

    g.res.statusCode = T.EStatusCode.BAD_REQUEST;                                  // 400 with your data
    return { field: 'customer_id', message: 'Please provide customer_id.' };
}
```

- A thrown error answers `success: false` with the message in `errors` ; 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
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 }));
}
```

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

- `__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
async function main(g: T.IAMGlobal) {
    g.res.contentType = T.EContentType.HTML;
    return '<h1>Hello</h1>';
}
```

**A tiny gif**

```typescript
async function main(g: T.IAMGlobal) {
    g.res.contentType = <any>'image/gif';   // not in EContentType : the return is base64
    return 'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=';
}
```

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

## Around the code

- **Hooks** : pre and post hooks on the custom API itself, see [Pre hooks](https://docs.apimaker.dev/v1/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/v1/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, the code 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/v1/docs/apis-security/api-group-permission.html).

## Native process

`runOnNativeProcess: true` runs the function inside API Maker itself : no sandbox start, access to the packages of API Maker and to native modules such as a browser for [Playwright](https://docs.apimaker.dev/v1/docs/guides/browser-automation.html). A bug there can hurt the whole server, so keep it for the few APIs which need it.

## 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.
