Custom API Definition
Schema id: custom-api@1
Machine-readable schema: custom-api.schema.json
The canonical definition of a Custom API (Section 6.9): a versioned HTTP contract an environment publishes to systems outside the platform, owned by a Solution and travelling in its package. A definition is validated identically whether it comes from the Admin Control Center, an API call, an AI assistant, or a Solution import.
Shape
| Field | Type | Required | Notes |
|---|---|---|---|
logicalName | string | yes | Publisher-prefixed, lower snake-case. Stable across versions. |
displayName | string | yes | The OpenAPI operation's summary. |
publisherPrefix | string | yes | Also a segment of the served path. |
version | integer | yes | 1–999. Served as the v{version} path segment. |
route | string | yes | Below the version segment, no leading slash. A braced segment is a path parameter. |
verb | string | yes | Get, Post, Put, Patch, Delete. |
script | string | yes | The BS Lang this API runs. There is one implementation and this is it. |
requiredScope | string | yes | The scope a caller's App Registration must hold. |
description | string | null | no | The OpenAPI operation's description. |
requestSchema | object | null | no | A JSON Schema, checked on every call. Refused for a body-less verb. Null means the script's own getInput names are the contract (published in OpenAPI; on a portal call, a closed list). |
responseSchema | object | null | no | A JSON Schema, published in the OpenAPI document. |
responseContentType | string | no | application/json, text/plain or text/html — what the contract normally answers with, and what the generated OpenAPI publishes. |
isEnabled | boolean | no | A disabled definition is stored and exported, and returns 404. |
Identity is a pair
(logicalName, version) identifies the API; (publisherPrefix, version, verb, route) is its
address. par_approve_invoice v1 and v2 are one contract at two ages and answer side by side
while an integration partner migrates — so a second definition sharing a name and a version is a
duplicate, while one sharing the name with a new version is the next version. Two definitions may
never share an address, whatever they are called.
The served path
/api/{publisherPrefix}/v{version}/{route}
Reached today under an environment — /environments/{environmentId}/api/par/v1/invoices — because
a client-credentials token names a tenant rather than one of its environments. The shorter public
shape arrives with the Gateway, which maps a hostname onto an environment.
What runs
One thing: the script. A definition names a route and the BS Lang that answers it, and there
is nothing else to choose. It reads the request with getInput, answers with setOutput or
setBody, chooses a status and content type with setStatus and setContentType, sets headers
with setHeader, reaches other tables with the record functions, and refuses with
throwValidationError.
It used to choose between four implementation kinds — a direct table operation, an Automation Flow, a Function, and a custom plugin message. Three of them could be defined but never dispatched, so a call answered 501 naming the iteration it waited on, and only the fourth ran. That is gone. A contract whose behaviour is the maker's own code is simpler to explain, simpler to version, and the only one of the four that could express something the platform had not anticipated — and there is no 501 any more, because there is no unwired kind left to produce one.
Logic worth sharing between two APIs is a solution script, imported by name, not a fifth kind.
Rules a schema cannot state
Checked when the definition is saved, in addition to the schema above:
- The declared
responseContentTypeis one the platform can serve. - A
Read,UpdateorDeleteroute declares{id}— it is the only place the record's id can come from. - A body-less verb has no request schema. A body-carrying verb may leave it null.
- The request and response schemas compile as JSON Schema.
- The script parses as BS Lang, and every module it imports resolves to a solution script.
Example
{
"logicalName": "par_create_invoice",
"displayName": "Create an invoice",
"publisherPrefix": "par",
"description": "Creates an invoice from a validated body.",
"version": 1,
"route": "invoices",
"verb": "Post",
"requestSchema": {
"type": "object",
"additionalProperties": false,
"required": ["par_name", "par_amount"],
"properties": {
"par_name": { "type": "string", "maxLength": 200 },
"par_amount": { "type": "number", "exclusiveMinimum": 0 }
}
},
"responseContentType": "application/json",
"requiredScope": "modelapps.write",
"isEnabled": true,
"script": "var id = createRecord('par_invoice', {
par_name: getInput('par_name'),
par_amount: getInput('par_amount')
});
setOutput('id', id);"
}
The body is checked against requestSchema before the script runs, so getInput reads a shape
that has already been validated rather than one every script has to defend against itself.
An input can be an object or a list
getInput hands the script whatever the property holds — a string, a number, a boolean, or an
object or array carried whole, which a script walks by member (f.fileName) and by
length(). So a contract that describes a structured input in its requestSchema can actually
read one:
"par_document": {
"type": "object",
"additionalProperties": false,
"required": ["fileName", "contentType", "contentBase64"],
"properties": {
"fileName": { "type": "string", "maxLength": 260 },
"contentType": { "type": "string", "maxLength": 200 },
"sizeBytes": { "type": "integer" },
"contentBase64": { "type": "string", "maxLength": 5600000 }
}
}
Until portals round 10 an object arrived as its raw JSON text, which BS Lang has no way to parse — so a maker who declared the shape they wanted was worse off than one who flattened it into loose properties, which is the opposite of what a schema is for. Nesting is bounded by the JSON parser's own 64-level limit, before any of this is reached.
What it never does
A Custom API does not widen access. The script's record functions go through the same door as the platform's own screens and run as the caller, so their permission on a table and the data policy behind it decide what comes back. A curated contract gives an outside system a stable view, never a larger one.