references/schemas/api-request.yml
ApiRequest:
type: object
description: Request configuration for a builder-mode API endpoint.
properties:
name:
type: string
maxLength: 200
description: Display name.
examples: ["Get Customer", "Create Order"]
description:
type: string
maxLength: 10240
description: Optional description.
examples: ["Retrieves customer information by ID"]
relativeURI:
type: string
maxLength: 131072
pattern: "^\\/[a-zA-Z0-9:_*\\/\\-\\.]*$"
description: |-
URI path relative to the version. Full endpoint becomes
`/{version}{relativeURI}`. Use colon notation for path params: `/customers/:id`.
examples: ["/customers/:id", "/orders", "/products/:productId/variants/:variantId"]
x-celigo-ai-guidance:
- |-
Full endpoint URL will be: `/{version}{relativeURI}`
Can include path parameters using colon notation: `/customers/:id`
First path segment must be alphanumeric, dash, or underscore only.
method:
type: string
enum: ["GET", "POST", "PUT", "PATCH", "DELETE"]
x-enumDescriptions:
GET: Retrieve data from the endpoint.
POST: Submit data to the endpoint.
PUT: Replace data at the endpoint.
PATCH: Partially update data at the endpoint.
DELETE: Remove data at the endpoint.
description: HTTP method for the API endpoint.
examples: ["GET", "POST"]
headers:
type: array
description: Expected request headers.
items:
type: object
properties:
key:
type: string
maxLength: 256
pattern: "^[a-zA-Z0-9_-]+$"
description: Header name
examples: ["Content-Type", "X-API-Key"]
description:
type: string
maxLength: 10240
description: Description of the header's purpose
examples: ["API key for authentication"]
x-celigo-ai-guidance:
- |-
Documents headers that clients should send. Headers are optional by default.
Only alphanumeric characters, hyphens, and underscores are allowed in header names.
pathParams:
type: array
description: Path parameters defined in the `relativeURI`.
items:
type: object
properties:
key:
type: string
maxLength: 256
description: Parameter name (without the colon prefix)
examples: ["id", "customerId"]
description:
type: string
maxLength: 10240
description: Description of the parameter
examples: ["Unique identifier of the customer"]
x-celigo-ai-guidance:
- |-
Documents the parameters that are part of the URL path (e.g., :id, :customerId).
These should match the parameters defined in the relativeURI.
queryParams:
type: array
description: Expected query string parameters.
items:
type: object
properties:
key:
type: string
maxLength: 256
pattern: "^[a-zA-Z0-9_\\-:\\/]*$"
description: Query parameter name
examples: ["limit", "offset", "filter"]
dataType:
type: string
enum: ["string", "number", "boolean"]
x-enumDescriptions:
string: Parameter value is a text string.
number: Parameter value is a numeric value.
boolean: Parameter value is true or false.
description: Expected data type of the parameter
examples: ["string"]
description:
type: string
maxLength: 10240
description: Description of the parameter
examples: ["Maximum number of records to return"]
bodySchema:
type: object
description: |-
JSON Schema describing the expected request body structure. Every
object-typed schema node must declare at least one property for the
API Builder to render it; omit this field when the endpoint has no
body contract.
additionalProperties: true
examples:
- type: object
properties:
name:
type: string
email:
type: string
format: email
x-celigo-ai-guidance:
- |-
Every object-typed schema node — the root, a nested property, or an
array's `items` — must carry a non-empty `properties` map. The API
persists a property-less object node without error, but the API
Builder editor crashes rendering it ("Cannot convert undefined or
null to object"), so the mistake only surfaces when the editor opens.
- |-
When the endpoint has no body contract (e.g. the source spec declares
an empty request body), omit `bodySchema` entirely — absent renders
fine. Do not substitute a catch-all `{"type": "object"}`, and do not
pre-fill `properties: {}` — the API strips empty objects on save, so
the empty map never survives the round-trip.
mockRequest:
type: object
description: Mock request data for testing the API without live calls.
properties:
body:
type: object
description: Sample request body
headers:
type: object
description: Sample headers
pathParams:
type: object
description: Sample path parameters
queryParams:
type: object
description: Sample query parameters
additionalProperties: false
x-celigo-ai-guidance:
- |-
Provides sample data to test the API without making actual calls.
Can include body, headers, pathParams, and queryParams.
transform:
type: object
description: Optional transformation applied to the incoming request before processing.
properties:
_scriptId:
type: string
format: objectId
x-celigo-refModel: scripts
description: Reference to a script for custom transformation logic.
examples: ["689212a2c42d988978e27a11"]
function:
type: string
description: Function name in the script to execute.
examples: ["transformRequest"]
x-celigo-ai-guidance:
- |-
Transforms the request data before it is processed by routers or responses.
Useful for normalizing data or extracting specific fields.
required:
- relativeURI
- method
x-celigo-ai-guidance:
- |-
Defines how the API endpoint receives data:
- HTTP method and URI path
- Expected parameters (path, query, headers)
- Request body schema
- Optional request transformation logic
references/schemas/api-response.yml
ApiResponse:
type: object
description: |-
Response configuration in a builder-mode API. Each API requires exactly one
`success` and one `fail` response; additional `custom` responses are optional.
properties:
id:
type: string
description: Unique identifier for this response, referenced by the response router.
examples: ["success_response", "error_response", "custom_validation_response"]
name:
type: string
maxLength: 200
description: Display name.
examples: ["Success Response", "Error Response", "Validation Error"]
description:
type: string
maxLength: 10240
description: Optional description of when this response is used.
examples: ["Returned when customer is successfully created"]
type:
type: string
enum: ["success", "fail", "custom"]
x-enumDescriptions:
success: Default response returned when the API operation completes successfully.
fail: Default response returned when the API operation encounters an error.
custom: Additional response for specific scenarios, routed via the response router.
description: Response type.
examples: ["success"]
x-celigo-ai-guidance:
- |-
- **success**: Default response for successful operations (required, exactly one)
- **fail**: Default response for errors (required, exactly one)
- **custom**: Additional responses for specific scenarios (optional, multiple allowed)
statusCode:
type: integer
minimum: 100
maximum: 599
description: HTTP status code to return.
examples: [200, 201, 400, 404, 500]
headers:
type: array
description: Response headers to include.
items:
type: object
properties:
key:
type: string
maxLength: 256
pattern: "^[a-zA-Z0-9_-]+$"
description: Header name
examples: ["Content-Type", "X-Request-Id"]
value:
type: string
maxLength: 256
description: Header value (can include handlebars templates)
examples: ["application/json", "{{requestId}}"]
description:
type: string
maxLength: 10240
description: Description of the header
examples: ["Content type of the response body"]
inputFilter:
type: object
description: Filter criteria for response selection by the response router.
properties:
version:
type: string
enum: ["1"]
x-enumDescriptions:
"1": Version 1 of the Celigo expression-based filter format.
description: Version of the filter format used by `rules`.
rules:
type: array
description: Celigo expression-based filter rules.
items: {}
examples:
- ["equals", ["boolean", ["context", "success"]], true]
- [
"and",
["equals", ["get", "statusCode"], 200],
["exists", ["get", "data"]],
]
x-celigo-ai-guidance:
- |-
Array-based DSL where the first element is an operator (e.g., "equals", "and", "or"),
followed by operands which can be nested expressions.
bodySchema:
type: object
description: |-
JSON Schema describing the response body structure. Every object-typed
schema node must declare at least one property for the API Builder to
render it; omit this field when the response body has no defined shape.
additionalProperties: true
examples:
- type: object
properties:
id:
type: string
name:
type: string
createdAt:
type: string
format: date-time
x-celigo-ai-guidance:
- |-
Every object-typed schema node — the root, a nested property, or an
array's `items` — must carry a non-empty `properties` map. The API
persists a property-less object node without error, but the API
Builder editor crashes rendering it ("Cannot convert undefined or
null to object"), so the mistake only surfaces when the editor opens.
- |-
When copying a schema from an external spec, prune property-less
object nodes (e.g. an array's `items` that is a bare
`{"type": "object"}`) instead of reproducing them. Array-typed roots
(which carry `items`, not `properties`) and type-less nullable
leaves render fine. Do not pre-fill `properties: {}` — the API
strips empty objects on save.
mockInput:
type:
- object
- string
maxLength: 0
description: |-
Mock data for testing this response, in the integrator.io canonical
record-page format: `{"page_of_records": [{"record": {...}}, ...]}`.
The server rejects any other object shape and any non-empty string
with a 422; the empty string `""` (a UI draft artifact) is accepted
and stored verbatim.
required:
- page_of_records
properties:
page_of_records:
type: array
description: Pages of mock records fed to this response's mappings.
items:
type: object
required:
- record
properties:
record:
type: object
description: One mock input record (freeform payload).
success:
type: boolean
description: When true, the mock record follows the success path.
testMode:
type: boolean
description: When true, the mock record is treated as a test-mode record.
additionalProperties: true
mappings:
type: array
description: Field mappings to transform processing results into the response body.
items:
type: object
required:
- dataType
properties:
generate:
type: string
description: Target field path in the response
examples: ["data.customerId"]
dataType:
type: string
enum:
- string
- number
- boolean
- object
- stringarray
- numberarray
- booleanarray
- objectarray
- arrayarray
x-lowercase: true
x-enumDescriptions:
string: Single string value
number: Single numeric value
boolean: Single boolean value
object: Nested object value
stringarray: Array of strings
numberarray: Array of numbers
booleanarray: Array of booleans
objectarray: Array of objects
arrayarray: Array of arrays
description: Data type of the value this mapping writes into the response body.
examples: ["string"]
x-celigo-ai-guidance:
- |-
Validator-enforced on every mapping entry: writes fail with
`422` ("Path 'dataType' is required."), repeated once per
entry that omits it.
extract:
type: string
description: Source field path from input data
examples: ["record.id"]
hardCodedValue:
type: string
description: |-
Static value written to the target field instead of extracting
from input data.
x-celigo-ai-guidance:
- |-
The field name is exactly `hardCodedValue` (capital C). The
server does not reject unknown mapping keys: a mapping sent
with `hardcodedValue` saves successfully with the value
silently dropped — no error, and the target field is then
simply absent from the API's responses.
lookups:
type: array
description: Static key-value lookup tables for value transformation.
items:
type: object
properties:
name:
type: string
description: Name of the lookup
examples: ["statusCodeMap"]
map:
type: object
description: Key-value mapping object
default:
type: string
description: Default value if key not found
examples: ["unknown"]
allowFailures:
type: boolean
description: When true, processing continues even if this lookup fails.
hooks:
type: object
description: Custom scripts to run during response processing.
properties:
preMap:
type: object
description: Script to run before applying mappings.
properties:
_scriptId:
type: string
format: objectId
x-celigo-refModel: scripts
description: Reference to the script resource.
examples: ["689212a2c42d988978e27a11"]
function:
type: string
description: Function name to execute.
examples: ["preMapResponse"]
postMap:
type: object
description: Script to run after applying mappings.
properties:
_scriptId:
type: string
format: objectId
x-celigo-refModel: scripts
description: Reference to the script resource.
examples: ["689212a2c42d988978e27a11"]
function:
type: string
description: Function name to execute.
examples: ["postMapResponse"]
x-celigo-ai-guidance:
- |-
Defines how to format and return data to the caller. Each API must have:
- Exactly one 'success' response (for successful operations)
- Exactly one 'fail' response (for errors)
- Zero or more 'custom' responses (for specific scenarios)
SKILL.md
---
name: building-apis
description: Build Celigo APIs -- custom HTTP endpoints that let external systems push or query data synchronously through Celigo integrations. Use when creating APIs, proxying authenticated requests, or exposing lookup/write operations as a REST interface that returns a structured response.
---
<!-- TIER:1 -->
# Building APIs
An API is a **RESTful endpoint** that exposes integration logic for external consumption. External systems call the API over HTTP; the API processes the request through lookups and imports, then returns a structured response. Concerns when building an API:
- **Mode selection** -- builder (visual configuration) vs script (full JavaScript control)
- **Request definition** -- HTTP method, URI path, parameters, body schema, request transformation
- **Processing pipeline** -- routers and page processors (lookups + imports) that execute business logic
- **Response routing** -- directing processed data to the correct response definition based on success/failure or custom conditions
- **Response shaping** -- status codes, field mappings, body schema, hooks (preMap, postMap) on each response
- **Response mapping** -- extracting fields from each page processor's response back into the record for downstream steps. Configured on each `pageProcessors[]` entry, same as in flows. For lookup exports the response has `data[]` and `errors[]` (use `data[0].fieldName` for single results). For imports the response is via `_json` (use `_json.fieldName`)
- **postResponseMap hook** -- JavaScript processing after response mapping, configured on `pageProcessors[]` entries
Used across integrations alongside flows and tools. APIs do not have their own authentication -- incoming requests authenticate via the Celigo API token; outbound calls to external systems use the connections referenced by exports/imports in the pipeline.
## The Request IS the Source Record
APIs are invoked by an external HTTP caller -- there is no upstream export, no scheduler, no listener feeding them. That has three design consequences:
- **The request stage is the input shape.** Whatever the caller sends (body, path params, query params, headers) is what downstream processing sees as the record. There is no upstream pipeline to reshape it first -- use the request `transform` if the envelope needs reshaping before routing.
- **The response stage is the output.** Whatever the selected response definition produces is exactly what the caller receives. Nothing runs after it.
- **No self-starting.** APIs have no `schedule`, no listener, and none of the flow runtime controls (`proceedOnFailure`, `skipRetries`, chaining). "Every night at 2 AM, do X" is a flow -- possibly one that *calls* the API, but the schedule lives on the flow. Retry-after-failure is the caller's decision.
When a flow needs to invoke an API, it does so as an ordinary HTTP caller (an HTTP export/import pointing at the API's URL). There is no special flow-step-to-API wiring.
A top-level `disabled: true` takes the API offline without deleting it -- callers get a 404 until it's re-enabled.
## API Modes
### Builder Mode (`type: "builder"`)
Visual configuration with discrete components:
```
API (type: "builder")
+-- request -- method, relativeURI, params, bodySchema, mockRequest, transform
+-- routers[] -- processing pipeline (same structure as flow routers)
| +-- branches[]
| +-- inputFilter -- when to use this branch (s-expression rules)
| +-- pageProcessors[] -- lookups (exports) and imports
| +-- nextRouterId -- chain to next router, or "apiRouter" to finish
+-- responseRouter -- id="apiRouter", routes processed data to a response
+-- responses[] -- success, fail, custom -- each with statusCode, inputFilter, mappings
```
The incoming HTTP request **replaces the export** as data source. Routers and page processors work identically to flows.
#### API Execution Pipeline (Builder Mode)
When an API receives a request:
1. **Request received** -- method + path matched against the API endpoint definition
2. **Request transform** (optional) -- reshapes the incoming request body before routing
3. **Router evaluation** -- `routeRecordsUsing` evaluates branch input filter conditions
4. **Branch selection** -- first matching branch processes the request
5. **Page processors** -- each processor in the branch executes sequentially (export lookups, import writes)
6. **Response mapping** -- `responseMapping` on each processor carries data forward to the next processor
7. **Response router** -- `responseRouter` (id="apiRouter") selects which response template to use based on response input filters
8. **Response** -- selected response template returned to the caller with its statusCode, headers, and body
### Script Mode (`type: "script"`)
A single `handleRequest` JavaScript function receives the request object (method, headers, queryParams, body, pathParams) and returns `{statusCode, headers, body}`. Complete control with no visual configuration.
**Legacy APIs** (no `type` field, top-level `_scriptId` + `function`) exist in production but are not represented in the current spec. Distinguish by: if `type` is absent/null and `_scriptId` is present, it's legacy.
## Quick Reference
### Decision Matrix
| Scenario | Mode | Why |
|----------|------|-----|
| Standard lookup/write with structured response | Builder | Visual debugging, test runs, structured responses |
| Multiple response shapes based on success/failure | Builder | Response router + inputFilter handles this declaratively |
| Complex conditional logic or custom auth validation | Script | Full JavaScript control over request/response |
| Dynamic routing that can't be expressed as input filters | Script | `handleRequest` can implement arbitrary logic |
| Proxy through an authenticated connection | Builder | Wire the connection's export/import as a page processor |
| Simple webhook receiver that transforms and forwards | Builder | Single router, single branch, one import |
### Minimum Required Fields
| Mode | Required Fields |
|------|----------------|
| Builder | `name`, `type: "builder"`, `builder.request` (method + relativeURI) |
| Script | `name`, `type: "script"`, `script._scriptId`, `script.function` |
| Legacy | `name`, `_scriptId`, `function` (no `type` field) |
### Schema Index
All schemas are in [references/schemas/](references/schemas/):
| Schema | What it defines |
|--------|----------------|
| [request.yml](references/schemas/request.yml) | Top-level API fields (name, type, version, disabled, builder/script refs) |
| [response.yml](references/schemas/response.yml) | API response shape |
| [builder.yml](references/schemas/builder.yml) | Builder configuration (request, routers, responseRouter, responses refs) |
| [api-request.yml](references/schemas/api-request.yml) | Request config (method, relativeURI, params, bodySchema, mockRequest, transform) |
| [api-response.yml](references/schemas/api-response.yml) | Response definitions (id, name, type, statusCode, inputFilter, mappings, hooks) |
| [response-router.yml](references/schemas/response-router.yml) | Response router (id="apiRouter", routeRecordsUsing) |
| [router.yml](references/schemas/router.yml) | Routers (branches, inputFilter, pageProcessors) |
| [script.yml](references/schemas/script.yml) | Script config (_scriptId, function) |
| [apim.yml](references/schemas/apim.yml) | APIM metadata (publication status) |
| [shipworks.yml](references/schemas/shipworks.yml) | Legacy ShipWorks auth |
## Related Skills
- [configuring-exports > Quick Reference](../configuring-exports/SKILL.md#quick-reference) -- building lookup exports used as page processors in the API pipeline
- [configuring-imports > Quick Reference](../configuring-imports/SKILL.md#quick-reference) -- building imports used as page processors in the API pipeline
- [building-flows > How to Build a Flow](../building-flows/SKILL.md#how-to-build-a-flow) -- flows share the same router/branch/pageProcessor pipeline mechanics
- [writing-scripts > Quick Reference](../writing-scripts/SKILL.md#quick-reference) -- writing `handleRequest` (script-mode APIs), `preMap`/`postMap` hooks, and `postResponseMap`
- [writing-handlebars > Quick Reference](../writing-handlebars/SKILL.md#quick-reference) -- dynamic expressions in request bodies, URIs, and response mappings
- [configuring-filters > Quick Reference](../configuring-filters/SKILL.md#quick-reference) -- input filters on router branches to conditionally route records
<!-- TIER:2 -->
## How to Build an API
### 1. Plan what the API needs to do
Before creating anything, understand the requirements: what endpoint the caller needs, what data it sends, what systems are involved, what the response should look like. This determines everything -- mode, pipeline shape, which connections/exports/imports are needed.
### 2. Decide the mode
Use **builder** for most APIs -- it provides visual debugging, test runs, and structured responses. Use **script** only when the processing logic is too dynamic for the visual pipeline (e.g., complex conditional responses, custom auth validation, dynamic routing).
### 3. Check for existing resources
Look for connections, exports, and imports that can be reused before creating new ones.
```bash
# Search across all resource types in the account
celigo account search "<keyword>"
# Show what an existing API uses (exports, imports, connections)
celigo account dependencies api <id>
# Find orphaned resources that could be reused
celigo account lint
# Search for APIs already in the account for patterns
celigo apis list | grep -i "<keyword>"
# Check existing exports/imports that could serve as pipeline steps
celigo exports list | grep -i "<system-name>"
celigo imports list | grep -i "<system-name>"
# Search marketplace for pre-built integration templates
celigo templates marketplace
```
The account index auto-refreshes when stale (>4 hours). Force a fresh snapshot with `celigo account snapshot`.
### 4. Create the supporting resources (bottom-up)
APIs reference exports and imports as page processors -- these must exist before you can attach them. Build order:
1. **Connections** -- create or reuse connections to the target systems
2. **Exports** -- for lookups that query external systems (use `configuring-exports` skill)
3. **Imports** -- for writes to external systems (use `configuring-imports` skill)
### 5. Define the request (builder mode)
Choose the HTTP method and URI path. GET and POST are most common; PUT and PATCH are rare.
- Path parameters use colon notation: `/customers/:id`
- Document query parameters, path parameters, headers, and body schema
- Add a `mockRequest` for testing the pipeline without live calls
- Optionally add a request `transform` (expression-based or script-based) to reshape incoming data before processing
### 6. Build the processing pipeline
The pipeline is made of routers, branches, and page processors. See [router.yml](references/schemas/router.yml) for the full schema.
Every builder API needs at least one router -- it's the container that holds branches, and branches hold the page processors that do the actual work. Use multiple branches when different request conditions need different processing paths (e.g., branch by HTTP method, request field value, or record type). Use multiple routers when you need sequential stages of processing where each stage can branch independently.
For pass-through routers (single branch, no filters, just linear steps before a branching router), omit `routeRecordsTo` and `routeRecordsUsing` -- including them makes it appear as a filter-based branch in the UI. The API defaults are sufficient.
Input filters use s-expression syntax: `["operator", ["type", ["extract", "field"]], value]`. Type wrappers (`string`, `number`, `boolean`) are required around `extract` and `context` accessors. Logical combinators: `["and", cond1, cond2]`, `["or", cond1, cond2]`.
The last branch in the chain must set `nextRouterId: "apiRouter"` to reach the response router.
### 7. Configure responses
Every builder API needs exactly one `success` response and one `fail` response. Add `custom` responses for specific scenarios (e.g., 404 not found, 422 validation error).
Each response has:
- `statusCode` (HTTP status code)
- `inputFilter` to determine when it's selected (typically `["equals", ["boolean", ["context", "success"]], true]` for success)
- `mappings` to shape the response body from the processed record
- Optional `bodySchema` for documentation, `headers`, `lookups`, and `hooks` (preMap, postMap)
### 8. Configure the response router
Set `id: "apiRouter"` and choose routing method:
- `input_filters` (default) -- evaluates each response's `inputFilter`
- `script` -- custom JavaScript returns the response `id` to use
### 9. Build the JSON
Reference the [Schema Index](#schema-index) above for exact field schemas.
Every API needs at minimum: `name`, `type`, and either `builder` (with `request`) or `script` (with `_scriptId` and `function`).
## The Response and Routing Model
Once the routers finish processing, the API selects which response to return and shapes its body. This is where APIs diverge most from flows -- the routing is narrower, and "mapping" happens at two distinct layers.
### Branch selection and router chaining
APIs support a single routing strategy: `first_matching_branch`. Within a router, each record is evaluated against the branches in order and taken by the *first* branch whose `inputFilter` matches; that record then follows only that branch. (Flows also offer `all_matching_branches`, which fans one record out to every matching branch -- APIs never do this. A record takes exactly one branch per router.)
Each branch's `nextRouterId` decides where the record goes after that branch's page processors finish:
- **Another router's `id`** -- chain into that router for a further stage of processing.
- **`"apiRouter"`** -- hand off to the response router (whose reserved `id` is always `apiRouter`) to finish.
Chaining lets you express sequential stages where each stage branches independently; the last branch in the chain sets `nextRouterId: "apiRouter"` to reach the response router.
### Response selection -- success, fail, custom
Every builder API has exactly one `success` response, exactly one `fail` response, and zero or more `custom` responses (the response's `type` field). The response router (`id: "apiRouter"`) picks one after processing completes:
- **`success`** -- the happy path, returned when processing completed and no `custom` response matched. Conventionally a 2xx `statusCode` (`200`, or `201` when the API created something).
- **`fail`** -- the error path. Processing errors (a lookup returned a 500, an import got a 4xx, a script threw) are routed here automatically. Conventionally a 4xx/5xx `statusCode` (`400` or `500`); its `mappings` surface the error message and any context the caller needs.
- **`custom`** -- a non-error, non-default response selected by its own `inputFilter`. Reach for one when the outcome fits neither `success` nor `fail`, when the `statusCode` differs, or when the body shape differs. Typical cases:
- **`404` not found** -- the lookup ran but returned zero records (filter: the results array is empty).
- **`409` conflict** -- the destination rejected a create because the record already exists.
- **`202` accepted** -- processing started a background job; tell the caller "received, working on it."
- **Conditional body** -- a different shape driven by a query parameter (e.g. `?format=summary` vs `?format=full`).
In `input_filters` mode the response router returns the first response whose `inputFilter` matches, so list `custom` responses ahead of `success` to let their specific conditions win. In `script` mode a JavaScript function inspects the record and returns the response `id` to use.
### `statusCode` vs the response `type`
These are independent and often conflated:
- The response **`type`** (`success` / `fail` / `custom`) is Celigo's internal classification -- it drives which response the response router selects.
- The **`statusCode`** is the HTTP status the caller receives -- it lives on the response definition.
A `custom` response can carry any `statusCode` (the "not found" response returns `404`; the "async accepted" response returns `202`), and `success` is conventionally 2xx but doesn't have to be. So "return a `404` when the customer isn't found" means adding a `custom` response with `statusCode: 404` and an `inputFilter` that matches when the lookup's results array is empty -- not editing the `success` response.
### The two mapping layers
"Mapping" refers to two different things at two layers, and conflating them is the most common source of confusion when building APIs.
**1. Page-processor `responseMapping` (record enrichment).** Configured on a lookup or import inside a router branch -- the same shape as a flow's page-processor `responseMapping`. It pulls fields off that page processor's response and merges them onto the record so downstream routers, page processors, and response mappings can see them. It does **not** shape the HTTP body.
```
{
"fields": [
{"extract": "id", "generate": "customerId"},
{"extract": "accountStatus", "generate": "status"}
]
}
```
**2. Response-stage `mappings` (HTTP body).** Configured on a `success` / `fail` / `custom` response (alongside its `lookups` and `hooks`). It reads the now-enriched record and builds the HTTP response body returned to the caller.
```
{
"mappings": [
{"extract": "customerId", "generate": "data.id"},
{"extract": "status", "generate": "data.status"}
]
}
```
The two work together: the lookup's `responseMapping` merges `customerId` onto the record, then the response's `mappings` place `customerId` into the body's `data.id`. A field the page processor returned must first be carried onto the record by a `responseMapping` before a response `mapping` can extract it. When unsure which layer you need, ask: does this step *add* the field to the record (page-processor `responseMapping`) or *read* the field off the record into the body (response `mappings`)?
## CLI Commands
```bash
# CRUD
celigo apis list
celigo apis get <id>
celigo apis create < api.json
celigo apis update <id> < api.json
celigo apis set <id> key=value [key2=value2 ...]
celigo apis delete <id>
# Clone (builder-mode only)
celigo apis clone <id> --api-version <version> [--name <name>] [--description <desc>] [--environment <envId>]
# Pipeline management
celigo apis add-processor <id> <exportOrImportId> [--router <routerId>] [--branch <branchName>]
celigo apis remove-processor <id> <exportOrImportId> [--router <routerId>] [--branch <branchName>]
# Logs
celigo apis logs <id>
celigo apis log-detail <id> <key>
# Test run
celigo apis test-run <id>
celigo apis test-run-step-results <id> <runId> <exportOrImportId>
celigo apis test-run-step-logs <id> <runId> <exportOrImportId>
# Debug (for exports/imports within the API pipeline)
celigo apis debug-requests <id> <exportOrImportId> [--since <minutes>]
celigo apis debug-request-detail <id> <exportOrImportId> <key>
# Discovery
celigo account search "<keyword>"
celigo templates marketplace
```
<!-- TIER:3 -->
## Pre-Submit Checklist
Before creating or updating an API, verify:
- [ ] `name` is set and descriptive
- [ ] `type` is `"builder"` or `"script"` (not omitted, which creates a legacy API)
- [ ] Builder mode: `builder.request.method` and `builder.request.relativeURI` are set
- [ ] Builder mode: at least one router with at least one branch exists
- [ ] Builder mode: last branch has `nextRouterId: "apiRouter"`
- [ ] Builder mode: both `success` and `fail` responses are defined
- [ ] Builder mode: success response `inputFilter` uses `["equals", ["boolean", ["context", "success"]], true]`
- [ ] Script mode: `script._scriptId` and `script.function` reference a valid script
- [ ] All `_exportId` and `_importId` references in page processors point to existing resources
- [ ] Router IDs are unique within the API
- [ ] `version` is set (it becomes part of the endpoint URL: `/{version}{relativeURI}`)
- [ ] Input filter expressions wrap `extract`/`context` accessors in type wrappers (`string`, `number`, `boolean`)
## Gotchas
1. **PUT erases omitted fields.** Always GET first, modify, then PUT. The `set` command handles this automatically.
2. **APIs only support `first_matching_branch` routing.** Unlike flows which also support `all_matching_branches`, API routers always stop at the first matching branch.
3. **Omitting `inputFilter` type wrappers silently fails.** Use `["boolean", ["context", "success"]]`, not bare `["context", "success"]` -- the filter will never match without the wrapper.
4. **Clone only works for builder-mode APIs.** Script and legacy APIs cannot be cloned via the CLI.
5. **Missing a success or fail response causes undefined behavior.** The response router won't know where to route.
6. `**version` becomes part of the URL path.** The full endpoint is `/{version}{relativeURI}`. Changing the version changes the URL that callers must use.
7. **Page-processor `responseMapping` and response `mappings` are different layers.** `responseMapping` enriches the record with fields from a page processor's response; a response's `mappings` shape the HTTP body from that record. A field the lookup returned won't reach the body unless a `responseMapping` first carries it onto the record.
8. **APIs don't start themselves.** No `schedule`, no listeners, no flow-level runtime controls, no abstract/instance templating. Scheduled or event-driven work belongs in a flow that calls the API.
9. **`disabled: true` returns 404 to callers.** Use it to pause an API without deleting it; re-enable with `disabled: false`.
## Common Errors
| Error | Cause | Fix |
|-------|-------|-----|
| `404` on API endpoint | Wrong `version` or `relativeURI` in the request | Verify the full URL is `/{version}{relativeURI}` and both match the API definition |
| `422` validation error on create/update | Missing required fields or invalid field values | Check the [Pre-Submit Checklist](#pre-submit-checklist); verify `type` is set |
| Response always returns the `fail` response | Success `inputFilter` is malformed or missing type wrapper | Use `["equals", ["boolean", ["context", "success"]], true]` exactly |
| Response body is empty | Response `mappings` not configured or field paths don't match | Verify mapping extract paths match the actual processed record structure |
| Pipeline step silently skipped | `inputFilter` on a branch evaluates to false for all records | Debug with `celigo apis test-run-step-results` to see each step's input/output |
| `Clone failed` error | Attempting to clone a script-mode or legacy API | Clone is builder-mode only; recreate script APIs manually |
| Page processor returns no data | Export/import `_id` reference is wrong or resource is disabled | Verify the referenced resource exists and is enabled with `celigo exports get` / `celigo imports get` |
| `Router ID not found` error | `nextRouterId` references a non-existent router ID | Ensure all `nextRouterId` values match a real router `id` or `"apiRouter"` |
| Response body missing a field the lookup returned | The page-processor `responseMapping` never carried the field onto the record | Add a `responseMapping` entry (`{"fields": [{"extract": ..., "generate": ...}]}`) on the page processor so the response `mappings` have it to extract |