<!-- 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 pr…

— the record root. An object record is addressed as\n '$.field'; a grouped (array) record as '$[0].field' / '$[*].field'\n - Used for precisely targeting data in structured JSON objects\n - Examples: '$.customer.firstName', '$.items[0].price', '$.addresses[*].street',\n '$[*].Tax' (every row of a grouped record)\n - Wildcards like [*] extract multiple values/objects\n - A key that contains a dot needs bracket notation: `$['P.O. Number']` (or\n `$[\"P.O. Number\"]`, `$.[P.O. Number]`). `$.P.O. Number` is read as a nested\n path: it maps nothing and the record still succeeds. Spaces, hyphens and `#`\n need no brackets (`$.Ship To Name`, `$.Tracking #`)\n - Filter expressions such as `$.items[?(@.active)]` fail the record with\n \"Eval [?(expr)] prevented in JSONPath expression.\"\n\n ```json\n \"extract\": \"$.customer.addresses[*]\" // Extracts all addresses\n ```\n\n **2. Handlebars Template Syntax**\n - Contains '{{' and '}}' pattern\n - Evaluated by the AFE 2.0 handlebars template engine\n - Can include logic, formatting, and computation\n - Access input record fields with {{record.fieldName}} notation; a grouped\n (array) record binds as `rows` instead — {{rows.0.fieldName}} / {{#each rows}}\n - Examples: \"{{record.firstName}} {{record.lastName}}\", \"{{#if record.isActive}}Active{{else}}Inactive{{/if}}\"\n - Valid on scalars and on `object` rows, where the rendered output must\n parse into an object. On an array row the template goes in a\n `buildArrayHelper` entry instead: on an `objectarray` row's own\n `extract` it is refused with 422 (\"buildArrayHelper\" or \"hardCodedValue\"\n must be present for object array datatype.), and on a primitive-array\n row's own `extract` it yields nothing (the field is left out) without an\n error\n\n ```json\n \"extract\": \"{{record.price}} {{record.currency}}\" // Combines two fields\n ```\n\n **3. Hard-Coded Value (literal string)**\n - Does NOT start with '

<!-- 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 pr…

\n - Does NOT contain handlebars '{{' syntax\n - System treats it as a literal string value\n - VERY COMMON for setting static/constant values\n - Examples: \"Active\", \"USD\", \"Completed\", \"true\"\n\n ```json\n \"extract\": \"primary\" // Sets field value to the literal string \"primary\"\n \"extract\": \"true\" // Sets field value to the literal string \"true\"\n \"extract\": \"N/A\" // Sets field value to the literal string \"N/A\"\n ```\n\n This third pattern is the simplest and most efficient way to set hard-coded values in your mappings.\n AI agents should use this pattern whenever a field needs a static value that doesn't come from\n the input record or require computation.\n\n **Important implementation details**\n\n - JSON path patterns ALWAYS execute from the TOP-LEVEL root of the input record\n - The system maintains this context even in deeply nested mappings\n - For object mappings without child mappings, extract should return a complete object\n - An `object` row takes `extract` (to copy a whole object) or child `mappings`, not\n both: a row with both fails with 422 (`\"extract\" should not be present if\n \"mappings\" field is present`). A `buildArrayHelper` entry is different: there\n `extract` and `mappings` work together.\n - The root is the record as it arrives: an object record roots at `$.field`; a\n grouped record (an array of rows — grouped exports, file key columns, NetSuite\n grouped saved searches) roots at the array, so its rows are addressed as\n `$[0].field` (one row) or `$[*].field` (every row). A root that contradicts the\n record's shape resolves to nothing without an error.\n\n For most simple field-to-field mappings, prefer JSON path syntax for its clarity and performance.\n For hard-coded values, simply use the literal string as the extract value.\n x-celigo-ai-guidance:\n - |-\n ## GROUPED (ARRAY) RECORDS\n Decide the JSONPath root from the record's shape before writing\n any extract. When the record is an array of rows, `$.field`,\n `$.properties.field` and `$.rows[*].field` all resolve to\n nothing — the processor still reports success and the request\n body ships as `{}`. Address rows as `$[0].field` (that row's\n value, typed) or `$[*].field` (every row: joined with commas\n into a `string` destination; rejected with\n `incompatible_destination_conversion` into a `number`\n destination). Column labels with spaces work as\n `$[*][Order Number]`, `$[*].[Order Number]`, or\n `$[*]['Order Number']`. For one destination element per row\n the two array kinds take different helper forms: a primitive\n array (`stringarray` / `numberarray`) uses `buildArrayHelper:\n [{\"extract\": \"$[*].field\"}]`; an `objectarray` uses\n `buildArrayHelper: [{\"extract\": \"$[*]\", \"mappings\": [...]}]`\n with the children addressing the row relatively (`$.field`,\n `{{rows.field}}`). An `extract` placed directly on the\n array-typed field yields nothing. Handlebars\n extracts bind the array record as `rows` (`{{rows.0.field}}`),\n an object record as `record`. The reverse holds too: `$[*]` on\n an object record iterates the object's values, not rows.\n - |-\n ## SIZE LIMIT\n The platform's import save validator caps this field at **1024\n characters**. Saves with a longer `extract` are rejected\n with `[size_limit_exceeded] The maximum size of the field:\n mappings.\u003cindex>.extract in the Import document should be\n 1024 characters`. This typically only matters for\n Handlebars templates that build complex strings inline -- if\n a template is approaching the cap, prefer one of:\n\n - **Decompose into nested mappings** (set `dataType:\n \"object\"` and use child `mappings` -- each child gets\n its own 1024-char budget).\n - **Move the logic to a `preMap` hook** so the heavy\n transformation runs in script and the mapping just reads\n `record.\u003ccomputed>`.\n - **Use a lookup** (`lookupName` + a small `lookups` entry)\n for value translation that would otherwise need a long\n `{{#if ...}}{{else if ...}}` chain.\n examples: [\"$.fullName\", \"{{record.fullName}}\", \"primary\", \"$.lineItems[*].unitPrice\"]\n extractDateFormat:\n type: string\n description: |\n Specifies the format pattern of the input date string to ensure proper parsing.\n\n Used on string-typed mappings whose `extract` yields a date. Uses\n Moment.js-compatible formatting tokens to describe how the incoming date\n string is structured.\n examples: [\"YYYY-MM-DD\", \"MM/DD/YYYY\", \"DD-MMM-YYYY HH:mm:ss\"]\n extractDateTimezone:\n type: string\n description: |\n Specifies the timezone of the input date string using Olson/IANA timezone identifiers.\n\n Used on string-typed mappings whose `extract` yields a date; tells the system\n how to interpret timestamp values from the input system.\n examples: [\"UTC\", \"America/New_York\", \"Europe/London\"]\n generateDateFormat:\n type: string\n description: |\n Specifies the output format pattern when generating a date string or converting\n from a Date type to String type.\n\n Uses Moment.js-compatible formatting tokens to define the structure of the resulting\n date string.\n examples: [\"YYYY-MM-DD\", \"MM/DD/YYYY\", \"DD-MMM-YYYY HH:mm:ss\"]\n generateDateTimezone:\n type: string\n description: |\n Specifies the timezone to apply when generating or converting timestamp values\n using Olson/IANA timezone identifiers.\n\n Controls timezone conversion when producing date output.\n examples: [\"UTC\", \"America/New_York\", \"Europe/London\"]\n default:\n type: [string, 'null']\n description: |\n Specifies a fallback value to use when extract returns empty/null or when conditional\n logic fails and no other mapping supplies a value.\n\n Explicit JSON `null` is itself a valid fallback: the destination field is written as\n JSON null when the extract yields nothing (the Mapper UI's \"Use null as default value\"\n action). Omitting the key entirely means no fallback — the field is left out of the\n output when the extract is missing or an empty string (the UI's \"Do nothing\" action),\n while a null source is still written as null unless `conditional.when` is\n `extract_not_empty`. Failing the record instead is `conditional.when:\n fail_record_when_empty`, which excludes a default.\n x-celigo-ai-guidance:\n - |-\n `\"default\": null` and `\"default\": \"\"` are distinct saved values, not\n absent keys: null renders the destination field as JSON null and the\n empty string renders it as `\"\"` when the extract yields nothing.\n Preserve them verbatim when editing or migrating a mapping — dropping\n the key changes runtime behavior from \"use null/empty string as\n default value\" to \"do nothing\" (a missing or `\"\"` source is left out\n of the output, while a null source is written as null unless\n `conditional.when` is `extract_not_empty`).\n - |-\n The four \"Action to take if source field has no value\" choices map to:\n do nothing → no `default` key (add `conditional.when:\n extract_not_empty` to leave a null source out too; without it a null\n source is written as null); use null → `\"default\": null`; use empty string or\n a custom value → `\"default\": \"\u003cvalue>\"`; cause the record to fail →\n `conditional.when: fail_record_when_empty` with no `default` key.\n examples: [\"N/A\", \"0\", \"false\"]\n lookupName:\n type: string\n description: |\n **Purpose**\n References a lookup table for transforming values during the mapping process.\n\n **Usage**\n\n The lookupName refers to a named lookup defined in the lookups array of the same resource.\n On an HTTP import that is `http.lookups`: a `lookups` array sent only at the top level\n is dropped on save. On an import, a name that matches no lookup fails every record\n with \"Unable to find lookup in mapping configs - \u003cname>\", and a key the lookup\n doesn't hold follows the lookup's `allowFailures`: false fails the record; true, or\n the field left out, lets the record succeed with the lookup's `default`, or without\n the mapped field when there is none.\n\n ```json\n {\n \"generate\": \"countryName\",\n \"dataType\": \"string\",\n \"extract\": \"$.countryCode\",\n \"lookupName\": \"countryCodeToName\"\n }\n ```\n\n During processing, the system:\n 1. Extracts the value from the input record (e.g., \"US\")\n 2. Finds the lookup table with the specified name\n 3. Uses the extracted value as a key in the lookup\n 4. Returns the corresponding value (e.g., \"United States\")\n\n **Benefits**\n\n - **Standardization**: Ensures consistent value translation across mappings\n - **Centralization**: Define translations once and reference them in multiple places\n - **Maintainability**: Update all mappings by changing the lookup definition\n - **Readability**: Makes mappings more descriptive and self-documenting\n\n The specific lookup capabilities depend on the context where mappings are used.\n examples: [\"countryCodeToName\", \"statusMapping\", \"departmentCodes\"]\n hardCodedValue:\n type: string\n description: |\n Constant written to the destination instead of an extracted value. On a primitive\n array (`stringarray`, `numberarray`, `booleanarray`) the string is split on commas,\n verbatim and untrimmed, into elements of the array's type (`\"A,B\"` gives `[\"A\", \"B\"]`).\n On an `object` it must be a JSON object string and on an `objectarray` a JSON array\n string (an object string becomes one element); the save parses it and refuses non-JSON\n or the wrong shape with 422, and an array row with a constant needs no `buildArrayHelper`.\n examples: [\"USD\", \"A,B\", \"[{\\\"code\\\": \\\"A\\\"}, {\\\"code\\\": \\\"B\\\"}]\"]\n description:\n type: string\n description: |\n Optional free-text annotation that appears in the Mapper sidebar to provide context about\n the mapping's purpose for collaboration and documentation.\n\n Has no functional impact on the mapping behavior.\n examples: [\"Maps customer name\", \"Converts price to target currency\", \"Formats date for target system\"]\n sourceDataType:\n type: string\n description: |\n Declares the JSON type of the value produced by `extract`, before any\n conversion to `dataType`. Same enum as `dataType`. Set on leaf mappings\n only — parent mappings (with child `mappings` or `buildArrayHelper`)\n have no extracted value of their own; the children carry their own\n `sourceDataType`.\n\n For date fields use `string` (JSON represents dates as strings); the\n parsing/formatting lives in `extractDateFormat` / `generateDateFormat` /\n `extractDateTimezone` / `generateDateTimezone`.\n enum:\n - string\n - number\n - boolean\n - object\n - stringarray\n - numberarray\n - booleanarray\n - objectarray\n - arrayarray\n x-enumDescriptions:\n string: Source value is a text string.\n number: Source value is numeric.\n boolean: Source value is true or false.\n object: Source value is a JSON object.\n stringarray: Source value is an array of strings.\n numberarray: Source value is an array of numbers.\n booleanarray: Source value is an array of booleans.\n objectarray: Source value is an array of objects.\n arrayarray: Source value is an array of arrays.\n examples: [\"string\", \"number\", \"boolean\", \"object\"]\n mappings:\n type: array\n description: |\n **Purpose**\n Enables recursive definition of nested object structures through child mapping objects.\n\n **Guidance**\n This is the KEY FIELD that implements the recursive pattern at the core of this schema:\n\n **When to Use**\n - REQUIRED when dataType = \"object\" (unless you are copying an entire object from the input record)\n - REQUIRED in buildArrayHelper.mappings when defining complex object array elements\n - NEVER used with simple types (string, number, boolean)\n\n **Behavior**\n - Each mapping in this array becomes a property of the parent object\n - The full Mappings schema is repeated recursively at each level\n - Can be nested to any depth for complex hierarchical structures\n\n **Context Handling**\n - Each level of nesting changes the mapping CONTEXT for 'generate'\n - The extraction CONTEXT always remains the original input record\n - This means child mappings can pull data from anywhere in the input record\n\n **Common Patterns**\n\n **Nested Objects**\n ```json\n {\n \"generate\": \"customer\",\n \"dataType\": \"object\",\n \"mappings\": [\n {\n \"generate\": \"contact\",\n \"dataType\": \"object\",\n \"mappings\": [\n {\"generate\": \"email\", \"dataType\": \"string\", \"extract\": \"$.customerEmail\"}\n ]\n }\n ]\n }\n ```\n\n **Multiple Fields in Object**\n ```json\n {\n \"generate\": \"address\",\n \"dataType\": \"object\",\n \"mappings\": [\n {\"generate\": \"street\", \"dataType\": \"string\", \"extract\": \"$.address.line1\"},\n {\"generate\": \"city\", \"dataType\": \"string\", \"extract\": \"$.address.city\"},\n {\"generate\": \"country\", \"dataType\": \"string\", \"extract\": \"$.address.country\"}\n ]\n }\n ```\n\n IMPORTANT: When analyzing or generating mappings, ALWAYS check if parent.dataType = \"object\"\n or if you're inside buildArrayHelper.mappings for objectarray elements. These are the only\n valid contexts for the mappings array.\n items:\n $ref: '#/Mappings/items'\n examples: [[{\"generate\": \"firstName\", \"extract\": \"$.name.first\", \"dataType\": \"string\", \"status\": \"Active\"}, {\"generate\": \"lastName\", \"extract\": \"$.name.last\", \"dataType\": \"string\", \"status\": \"Active\"}]]\n buildArrayHelper:\n type: array\n x-celigo-ai-guidance:\n - |-\n Entry-level `extract` accepts an iterating array path\n (`$.items[*]`), `\"$\"` (the record root), or omission — a\n constructed entry (child `mappings` with the extract omitted or\n `\"$\"`; the two spellings are equivalent) contributes exactly one\n element, and entries append in order. On a grouped (array)\n record the record itself is the array to iterate: `\"$[*]\"`\n yields one `objectarray` element per row (children address the\n row as `$.field`), and `\"$[*].field\"` yields one primitive-array\n value per row. Indexed or comma-joined\n entry extracts (`\"$[0]\"`, `\"$[0],$[0]\"`) are not valid: the\n processor renders the entire destination array as null with no\n validation error. To emit a fixed number of static elements, add\n that many sibling constructed entries — never index the extract.\n - |-\n An entry's `extract` can be a Handlebars template. On an\n `objectarray` it must render a JSON object (one element) or a JSON\n array (one element per item). On a primitive array each entry\n renders one element, so a serialized array becomes one string\n element. A bare literal entry extract is refused with 422 (\"extract\n only supports json path or handlebars expression\"); set a constant\n with the row's `hardCodedValue` instead.\n description: |\n **Purpose**\n Configures how to construct arrays in the output record, handling various array types and inputs.\n\n **Guidance**\n This is the mechanism for building array data types:\n\n **When to Use**\n - REQUIRED when dataType ends with \"array\" (stringarray, objectarray, etc.), unless the\n row sets `hardCodedValue`: an array row's own `extract` is never read (see `dataType`)\n - Each entry in this array contributes elements to the output array\n - Multiple entries allow combining data from different input arrays\n\n **Array Type Handling**\n\n **For Simple Arrays (stringarray, numberarray, booleanarray)**\n - Only the 'extract' field is used to pull values\n - JSON path with wildcards (e.g., $.items[*].name) returns multiple values;\n on a grouped (array) record the record itself is the array, so the\n path roots at it ($[*].name — one value per row)\n - Each result is converted to the appropriate primitive type\n ```json\n {\n \"generate\": \"productNames\",\n \"dataType\": \"stringarray\",\n \"buildArrayHelper\": [\n {\"extract\": \"$.products[*].name\"}\n ]\n }\n ```\n\n **For Object Arrays (objectarray) - three patterns**\n\n 1. Extract Only (existing objects):\n ```json\n {\n \"generate\": \"contacts\",\n \"dataType\": \"objectarray\",\n \"buildArrayHelper\": [\n {\"extract\": \"$.account.primaryContacts[*]\"}, // Pull primary contact objects\n {\"extract\": \"$.account.secondaryContacts[*]\"}, // Pull secondary contact objects\n {\"extract\": \"$.vendor.contactPersons[*]\"}, // Pull vendor contact objects\n {\"extract\": \"$.subsidiaries[*].mainContact\"} // Pull main contact from each subsidiary\n ]\n }\n ```\n\n 2. Mappings Only (constructed object):\n ```json\n {\n \"generate\": \"contactInfo\",\n \"dataType\": \"objectarray\",\n \"buildArrayHelper\": [\n {\n \"mappings\": [ // Creates one object in the array\n {\"generate\": \"type\", \"dataType\": \"string\", \"extract\": \"primary\"},\n {\"generate\": \"email\", \"dataType\": \"string\", \"extract\": \"$.primaryEmail\"}\n ]\n },\n {\n \"mappings\": [ // Creates another object in the array\n {\"generate\": \"type\", \"dataType\": \"string\", \"extract\": \"secondary\"},\n {\"generate\": \"email\", \"dataType\": \"string\", \"extract\": \"$.secondaryEmail\"}\n ]\n }\n ]\n }\n ```\n Each constructed entry contributes exactly one element, in entry\n order; writing the entry with `\"extract\": \"$\"` is equivalent to\n omitting the extract (both anchor the element at the record root).\n A fixed number of static elements is built from that many sibling\n constructed entries.\n\n 3. Extract AND Mappings (transform input arrays):\n ```json\n {\n \"generate\": \"lineItems\",\n \"dataType\": \"objectarray\",\n \"buildArrayHelper\": [\n {\n \"extract\": \"$.order.items[*]\", // For each item in the array\n \"mappings\": [ // Transform to this structure using the composite object\n {\"generate\": \"sku\", \"dataType\": \"string\", \"extract\": \"$.order.items.productId\"}, // Notice: items is singular\n {\"generate\": \"quantity\", \"dataType\": \"number\", \"extract\": \"$.order.items.qty\"}, // Notice: items is singular\n {\"generate\": \"orderNumber\", \"dataType\": \"string\", \"extract\": \"$.order.id\"}, // Access parent data\n {\"generate\": \"customerName\", \"dataType\": \"string\", \"extract\": \"$.customerName\"} // Access root data\n ]\n }\n ]\n }\n ```\n\n **Arrays of Arrays**\n - The mapper has no working type for them: `arrayarray` saves, but every record\n fails with `Destination datatype is a required field.` Build an array of arrays\n in a script hook instead.\n\n **Important details**\n\n - When both extract and mappings are provided, the system creates special composite objects\n that maintain hierarchical context during processing\n - This enables accessing both the current array element AND its parent context\n - An ITERATING entry's extract MUST be a JSON path that iterates an array:\n '$.items[*]' when the array is a field of an object record, or '$[*]'\n (objectarray) / '$[*].field' (primitive arrays) when the record itself is a\n grouped array of rows. A CONSTRUCTED entry (child mappings building one\n element) instead omits extract or sets it to exactly \"$\" — never rewrite \"$\"\n into an indexed or comma-joined form ('$[0]', '$[0],$[0]'): those render the\n entire destination array as null without raising a validation error\n - Each array helper entry acts independently, potentially adding multiple elements\n\n The buildArrayHelper is the most complex part of the mappings system - always analyze the\n dataType first to understand which pattern is appropriate.\n items:\n type: object\n properties:\n extract:\n type: string\n description: |\n JSON path expression that identifies the input array or values to extract.\n\n For objectarray with mappings, this defines which input objects to iterate through.\n The JSON path must return either a single object or an array of objects.\n\n For a CONSTRUCTED entry (child mappings building exactly one element from\n record-root fields and constants), omit this field or set it to exactly \"$\" —\n the two spellings are equivalent. Indexed or comma-joined forms ('$[0]',\n '$[0],$[0]') are not valid at the entry level: the processor renders the\n entire destination array as null without raising a validation error.\n\n The system creates special composite objects during processing to maintain\n hierarchical relationships, allowing easy access to both the current array item\n and its parent contexts.\n\n An iterating entry keeps the `[*]`: `$.line_items` without it yields one empty\n element per item, with no error. Child extracts drop the `[*]` but keep the\n array's path (`$.line_items.sku`); `$.line_items[*].sku` maps nothing, and an\n element-relative `$.sku` reads the record root instead of the element.\n examples: [\"$.items[*]\", \"$.order.lineItems[*]\", \"$.parents[*].children[*]\", \"$\"]\n name:\n type: string\n maxLength: 200\n description: |\n Display label for this source object in the mapper, so the sources of\n an object array can be told apart instead of all showing as ` building-apis — Skillopedia .\n Has no effect at runtime; stored only when non-empty.\n x-celigo-ai-guidance:\n - |-\n Carry an existing `name` through every edit — it is the user's label\n for the tab, and an entry sent without it loses the label. Duplicate\n names across entries are allowed. Set one only when the user names\n the source object; never invent labels.\n examples: [\"Billing address\", \"Shipping address\"]\n sourceDataType:\n type: string\n description: |\n Declares the JSON type of the input array being iterated, to ensure\n proper type handling during array construction. Same enum as `dataType`.\n enum:\n - string\n - number\n - boolean\n - object\n - stringarray\n - numberarray\n - booleanarray\n - objectarray\n - arrayarray\n x-enumDescriptions:\n string: Input array contains string elements.\n number: Input array contains numeric elements.\n boolean: Input array contains boolean elements.\n object: Input array contains object elements.\n stringarray: Input array contains arrays of strings.\n numberarray: Input array contains arrays of numbers.\n booleanarray: Input array contains arrays of booleans.\n objectarray: Input array contains arrays of objects.\n arrayarray: Input array contains arrays of arrays.\n examples: [\"string\", \"number\", \"boolean\", \"object\"]\n default:\n type: [string, 'null']\n description: |\n Specifies a fallback value when the extracted array element is empty or\n not found in the input data.\n\n Explicit JSON `null` is itself a valid fallback (the element is written as\n JSON null); omitting the key means no fallback is applied.\n examples: [\"N/A\", \"0\", \"false\"]\n conditional:\n type: object\n description: |\n Defines conditional rules for including each array element in the result.\n x-celigo-ai-guidance:\n - |-\n Judged against this entry's own `extract` (the array source). The\n design scopes the UI's tab-level \"Action to take if source field has\n no value\" to copy-as-is tabs (no child `mappings`); the API accepts\n `fail_record_when_empty` on a mapped entry as well, and the mapper\n processor honors it there — the record fails when that array is\n missing or empty (`errors[]` entry `code: extract_empty`, `fatal:\n true`, message `\u003cgenerate>: Source field has no value`; probed\n 2026-10-09). Say so when writing it on a mapped entry: the user may\n not find the setting in the mapper's tab. To fail on an empty field\n inside each element, set the action on that child mapping instead.\n properties:\n when:\n type: string\n description: |\n Condition judged on the entry's extract — skip the elements the\n extract does not return, or fail the whole record when the source\n array is missing or empty.\n enum:\n - extract_not_empty\n - fail_record_when_empty\n x-enumDescriptions:\n extract_not_empty: Include an array element only when the extract returns a non-empty value for it\n fail_record_when_empty: Fail the record when the entry's source array is missing or empty\n examples: [\"extract_not_empty\"]\n mappings:\n type: array\n description: |\n Contains recursive mapping definitions for complex array element transformations.\n\n **Composite object mechanism**\n\n When both 'extract' and 'mappings' are used together, the system implements a sophisticated\n \"composite object\" approach that is crucial for AI agents to understand:\n\n 1. The system starts with the complete input record\n\n 2. For each array element matched by the extract path, it creates a modified version of\n the input record where:\n - Array paths in the extract JSON path are REPLACED with single objects\n - Each array ([]) in the path is converted to a single object ({})\n - This preserves the hierarchical relationship between nested arrays\n\n **Example**\n\n Given an input record:\n ```json\n {\n \"customer\": {\n \"name\": \"John Doe\",\n \"orders\": [\n {\n \"id\": \"O-001\",\n \"items\": [\n {\"sku\": \"ABC\", \"qty\": 2},\n {\"sku\": \"XYZ\", \"qty\": 1}\n ]\n },\n {\n \"id\": \"O-002\",\n \"items\": [\n {\"sku\": \"DEF\", \"qty\": 3}\n ]\n }\n ]\n }\n }\n ```\n\n With extract path: `$.customer.orders[*].items[*]`\n\n For each item, the system creates a composite object like:\n ```json\n {\n \"customer\": {\n \"name\": \"John Doe\",\n \"orders\": { // Note: Array replaced with single object\n \"id\": \"O-001\",\n \"items\": { // Note: Array replaced with single object\n \"sku\": \"ABC\",\n \"qty\": 2\n }\n }\n }\n }\n ```\n\n Then in your mappings, you can access:\n - The current item: `$.customer.orders.items.sku`\n - The parent order: `$.customer.orders.id`\n - Top-level data: `$.customer.name`\n\n This approach allows for precise mapping from deeply nested structures while maintaining\n access to all contextual parent data, without requiring complex array index management.\n\n **Implementation guidance**\n\n When implementing the composite object mechanism:\n\n 1. Analyze the extract path to identify all array patterns (`[*]` or `[number]`)\n 2. For each array in the path, understand that it will be replaced with a single object\n 3. In the mappings, use paths that reference these arrays as if they were objects\n 4. Remember that every mapping still has access to the full input record context\n 5. This mechanism is especially powerful when mapping hierarchical data like:\n - Order → Line Items → Taxes/Discounts\n - Customer → Addresses → Address Lines\n - Invoice → Line Items → Serial Numbers\n\n The extract path effectively tells the system \"iterate through these arrays\",\n while the composite object mechanism ensures you can still access both the\n current array item AND its parent context during mapping.\n items:\n $ref: '#/Mappings/items'\n examples: [[{\"generate\": \"itemName\", \"extract\": \"$.name\", \"dataType\": \"string\", \"status\": \"Active\"}, {\"generate\": \"itemPrice\", \"extract\": \"$.price\", \"dataType\": \"number\", \"status\": \"Active\"}]]\n # The save rejects the fail-record action paired with a default on an\n # entry ([unsupported_mapping]); the entry schema has no lookupName or\n # hardCodedValue, so this is the only pair at this level.\n if:\n properties:\n conditional:\n properties:\n when:\n const: fail_record_when_empty\n required: [when]\n required: [conditional]\n then:\n not:\n required: [default]\n status:\n type: string\n description: |\n **Purpose**\n Controls whether the mapping is applied. Send it on every mapping entry.\n\n **Guidance**\n Only `Active` rows run. `Draft` and `Inactive` rows keep their\n configuration, are saved with minimal validation, and are skipped at\n runtime and in preview; a non-Active parent skips its whole subtree\n whatever its children say. Imports (on save and in the mapper processor)\n and API response mappings reject a row missing this field (validation\n error: \"Mapping object must have status field present.\"); a Tool's\n output mapping saves and runs one, which is why `required` doesn't\n list it.\n x-celigo-ai-guidance:\n - |-\n Emit `\"Active\"` for mappings that should run; `\"Draft\"` is also\n accepted and saves the mapping while bypassing the Active-only\n field validations. The value is case-sensitive. The API rejects:\n - Missing field: `[status_field_absent] Mapping object must have status field present.`\n - Wrong case (e.g. `\"active\"`): `[invalid_status_enums] active is not a valid enum for status field.`\n - |-\n `\"Inactive\"` is the user's \"Set inactive\" on a row: use it only when\n asked to disable or park a row, never to mean incomplete (that is\n `\"Draft\"`). Setting a parent `Inactive` means no `Active` row may\n remain under it — the save rejects an `Active` row under an\n `Inactive` parent (`[inactive_parent_active_child]` on `mappings`,\n no message text) and never cascades or promotes on its own; `Draft`\n descendants are kept as they are, and a `Draft` parent does not gate\n its children. The UI's Set inactive on a parent marks the parent and\n every child `Inactive`. Re-enabling a child\n means re-enabling its ancestors in the same payload. Edits that keep\n existing rows must carry their stored status forward — rewriting a\n `Draft` or `Inactive` row to `Active` turns a row the user parked\n back on.\n enum:\n - Active\n - Draft\n - Inactive\n x-enumDescriptions:\n Active: The mapping is enabled and applied during processing\n Draft: In-progress mapping; saved without the Active-only field validations and skipped at runtime\n Inactive: Disabled by the user; configuration kept, skipped at runtime and in preview\n examples: [\"Active\"]\n conditional:\n type: object\n description: |\n **Purpose**\n Defines conditional processing rules for the entire mapping.\n\n **Guidance**\n These conditions determine whether the mapping is applied based on record\n state or field content:\n\n **When to Use**\n - When a mapping should only be applied in specific circumstances\n - To implement conditional logic without using complex handlebars expressions\n - For creating mappings that only run during create or update operations\n\n **Available Conditions**\n\n - `record_created`: Apply only when creating a new record\n Useful for setting initial values that should not be overwritten during updates\n\n - `record_updated`: Apply only when updating an existing record\n Useful for transformation logic that should only run during updates\n\n - `extract_not_empty`: Apply only when the extract field returns a value\n Useful for conditional mapping based on input data availability\n\n - `fail_record_when_empty`: Fail the record when the extract field returns\n no value (the UI's \"Cause record to fail\" action when the source field\n has no value)\n\n **Example**\n ```json\n {\n \"generate\": \"statusMessage\",\n \"dataType\": \"string\",\n \"status\": \"Active\",\n \"extract\": \"$.status.message\",\n \"conditional\": {\n \"when\": \"extract_not_empty\" // Only map when status.message exists\n }\n }\n ```\n x-celigo-ai-guidance:\n - |-\n `when` holds one value, and two different settings share it: the\n composite-import operation gate (`record_created` / `record_updated`,\n the UI's \"Only perform mapping when\") and the empty-source action\n (`extract_not_empty` / `fail_record_when_empty`, the UI's \"Action to\n take if source field has no value\"). A row cannot carry both — writing\n one replaces the other and the save succeeds, so a composite import\n row that needs both has to choose. Never author `fail_record_when_empty`\n on a row that already carries `record_created` or `record_updated`.\n properties:\n when:\n type: string\n description: |\n Condition that gates this mapping — either an operation gate on a\n composite (create-or-update) import or the action to take when the\n extract returns no value. Empty means null, undefined or an empty\n string; `0`, `false` and whitespace are values.\n x-celigo-ai-guidance:\n - |-\n `fail_record_when_empty` is the fourth \"Action to take if source field\n has no value\" and is mutually exclusive with the other three: send it\n with no `default` key. A `default`, a `lookupName` or a `hardCodedValue`\n on the same row — or a `default` on a `buildArrayHelper` entry — is\n rejected at save with `[unsupported_mapping] \"\u003ckey>\" and conditional.when\n \"fail_record_when_empty\" cannot be present in mapping simultaneously.`\n (the lookup variant adds `Use lookup \"allowFailures\" instead.`);\n lookups fail through `allowFailures: false`, and a hard-coded row\n always has a value. At runtime an empty extract fails the record with\n an `errors[]` entry `{code: \"extract_empty\", fatal: true, source:\n \"mapping\", message: \"\u003cgenerate>: Source field has no value\"}`\n (\"Input field has no value\" in a Transformation 2.0); `0` and `false`\n are values and map through.\n enum:\n - record_created\n - record_updated\n - extract_not_empty\n - fail_record_when_empty\n x-enumDescriptions:\n record_created: Apply this mapping only when creating a new record\n record_updated: Apply this mapping only when updating an existing record\n extract_not_empty: Apply this mapping only when the extract field returns a non-empty value\n fail_record_when_empty: Fail the record when the extract field returns no value\n examples: [\"record_created\", \"record_updated\", \"extract_not_empty\", \"fail_record_when_empty\"]\n required:\n - dataType\n if:\n properties:\n conditional:\n properties:\n when:\n const: fail_record_when_empty\n required: [when]\n required: [conditional]\n then:\n not:\n anyOf:\n - required: [default]\n - required: [lookupName]\n - required: [hardCodedValue]\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":52991,"content_sha256":"52bc801b81ac5f934dda6ea0e194361e1aad6592840f911cf948db2fbb4ca41d"},{"filename":"references/flow-schemas/response-mapping.yml","content":"ResponseMapping:\n type: object\n description: |\n Merges fields from a step's response back onto the in-flight record so\n downstream steps can read them. Applies to both lookup steps\n (`type: \"export\"`) and import steps (`type: \"import\"`); without it, the\n step's response is discarded after the call.\n\n `fields` and `lists` are plain arrays — exactly the shape GET returns.\n\n Extract paths do NOT read the raw application response — the platform\n wraps every response in a canonical per-record envelope first, and\n extracts evaluate against that envelope:\n\n - **Lookup steps**: `{\"statusCode\": 200, \"data\": [\u003cresult records>],\n \"errors\": []}` — `data` holds the records the lookup step returns,\n AFTER the export's own `transform` and output `filter` have run, so\n extracts must start from it and name transformed fields by their\n transformed name: `data[0].name` copies a field of the first result,\n `data` copies the whole result array. A bare result-record field name\n (e.g. `name`) resolves to nothing and merges nothing, and so does the\n raw response path a transform consumed. A lookup that groups its\n results returns each group as an array inside `data`\n (`\"data\": [[\u003crow>, \u003crow>]]`): `data[0][0].name` (or `data.0.0.name`)\n reads the first row of the first group, and `data[0]` copies the group.\n - **Import steps**: `{\"id\": \"\u003cdestination record id>\", \"statusCode\": 200,\n \"ignored\": false, \"errors\": [], \"_json\": \u003craw destination response>,\n ...\u003cresponse body fields flattened at the root>}` — use `id` for the\n destination-assigned id and `_json.\u003cpath>` (or the flattened\n `\u003cpath>`) for fields inside the destination's response body. The full\n envelope (including `id`) is present on real runs and in flow-builder\n previews; flow TEST runs never execute imports, and their envelope is\n built from the import's saved `mockResponse`, or from the placeholder\n `{\"id\": \"1234567890\", \"mockResponse\": \"replace me\"}` (numbered\n `1234567890-1`, `1234567890-2`, … when a page holds several records)\n — an `extract: \"id\"` in a test run merges that mock id, never a\n destination record id.\n\n An `extract` may also be a Handlebars expression. It evaluates against\n the same envelope as a plain path, so every path inside `{{ }}` starts\n where the plain paths start — `{{data.0.name}}`,\n `{{#compare data.0.count \"==\" 1}}yes{{else}}no{{/compare}}`,\n `{{#if data.length}}found{{/if}}` on a lookup; `{{_json.order.id}}` or\n `{{id}}` on an import. Index arrays with dots inside Handlebars\n (`data.0.name` or `data.[0].name`; `data[0].name` renders nothing), and\n expect a string result (`\"1\"`, `\"false\"`). There is no `record` object in\n this context: `{{record.\u003canything>}}` renders nothing. Prefer a plain\n path when the value is simply copied.\n x-celigo-ai-guidance:\n - |-\n Handlebars is allowed in `extract` but only rooted at the envelope —\n `{{data.0.x}}` on a lookup, `{{_json.x}}` / `{{id}}` on an import. The\n `record.` root belongs to import-mapping templates; here every\n `{{record.…}}` form renders nothing, silently, and `{{data[0].x}}`\n (bracket index inside Handlebars) renders nothing too. Use Handlebars\n only for a derived value (a flag, a concatenation); copy values with a\n plain path.\n - |-\n A lookup's `data[]` is the step's output after its `transform` and\n `filter`, so author extracts against the transformed records — never\n against the raw response the transform consumed, and never against a\n saved `mockOutput` as stored (it is the raw response; the transform\n runs on top of it in a test run).\n - |-\n Send `fields` and `lists` as bare arrays, exactly as GET returns them.\n Never wrap either in an object (e.g. `\"fields\": {\"type\": [...]}`): the\n API does not reject that shape — it returns 200 and silently persists\n `fields: [null]` / `lists: [{\"fields\": []}]`, erasing the step's\n response mappings.\n - |-\n responseMapping is fully replaced on PUT: omitting it resets the step\n to empty `{\"fields\": [], \"lists\": []}`. Always send the complete\n intended mapping for every step.\n - |-\n To carry several lookup results onto the record, use `extract: \"data\"`\n (the whole result array) or a `lists` entry whose extracts use\n `data[*].x` — never `data[*].x` in `fields[].extract`, which merges\n nothing: the record simply never gains the field.\n examples:\n - fields:\n - extract: \"id\"\n generate: \"ContactId\"\n lists: []\n - fields:\n - extract: \"data[0].customer.name\"\n generate: \"customerName\"\n lists:\n - generate: \"matchedOrders\"\n fields:\n - extract: \"data[*].id\"\n generate: \"orderId\"\n - extract: \"data[*].total\"\n generate: \"amount\"\n properties:\n fields:\n type: array\n description: |\n Field mappings that copy individual values from the response\n envelope onto the in-flight record.\n items:\n type: object\n properties:\n extract:\n type: string\n x-celigo-agent:\n - handlebars\n description: |\n Path within the canonical response envelope to copy the value\n from, or a Handlebars expression over that same envelope. For\n lookup steps, paths start from the envelope's `data` array\n (`data[0].x` and `data.0.x` are equivalent; `data` alone\n copies every result) — a lookup result field written without\n the `data` prefix resolves to nothing. For import steps, use\n the envelope fields `id`, `statusCode`, `ignored`, `_json.\u003cpath>`\n for the raw response body, or the body field itself at the\n envelope root (`orderNumber` and `_json.orderNumber` read the\n same value). A `[*]` wildcard is not honored here — `data[*].x`\n merges nothing; iterate arrays with `lists` instead. Inside\n Handlebars, index with dots (`{{data.0.x}}`, never\n `{{data[0].x}}`) and never use a `record.` root.\n examples:\n [\"data[0].customer.name\", \"data.0.fields.Product Name\", \"data\", \"id\", \"_json.orderNumber\", \"{{#if data.length}}found{{else}}missing{{/if}}\"]\n generate:\n type: string\n description: |\n Field path on the in-flight record where the extracted value\n is stored, using dot notation for nesting. Downstream steps\n read the value at this path.\n examples:\n [\"customerName\", \"supplierDetails.supplierName\", \"netsuite_order_id\"]\n lists:\n type: array\n description: |\n Array mappings that build a target array on the in-flight record\n from an array inside the response envelope — one output item per\n element of the array the item extracts mark with `[*]` (`data[*].x`\n for lookups, `_json.\u003carrayPath>[*].x` for imports); an empty source\n array yields an empty target array. Not needed for one-to-many\n (fan-out) merge-back, which uses plain field mappings.\n items:\n type: object\n properties:\n generate:\n type: string\n description: |\n Array field created on the in-flight record to hold the\n mapped items.\n examples: [\"orderItems\", \"addresses\", \"contacts\"]\n fields:\n type: array\n description: |\n Field mappings applied to each item of the source array to\n produce the corresponding target-array item.\n items:\n type: object\n properties:\n extract:\n type: string\n description: |\n Full envelope path to the source value, with `[*]`\n marking the array to iterate — `data[*].productId` for a\n lookup, `_json.items[*].sku` for an import. A path without\n `[*]` (e.g. `statusCode`) repeats the same envelope value\n on every item; an item-relative name (`productId`)\n resolves to nothing and produces empty objects.\n examples: [\"data[*].productId\", \"data[*].quantity\", \"_json.items[*].sku\"]\n generate:\n type: string\n description: Target field within each mapped array item.\n examples: [\"itemId\", \"qty\", \"unitPrice\"]\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":8714,"content_sha256":"29e8e545086c95dbbfd13e76e0cfd2efd4b017292d49a52201279e972a7b2f9e"},{"filename":"references/pagination.md","content":"# Paginating a Builder API\n\nCursor pagination returns a large lookup result one page per call instead of draining the whole source. It's off until you turn it on, and only builder-mode APIs have it. Field rules are under `pagination` in [base.yml](schemas/base.yml).\n\n## What it pages\n\n- The first lookup (export step) in `builder.routers` is the volume driver: each call returns one source page of that export.\n- That export must page on its own: an HTTP export with `http.paging` (such as `method: \"page\"`), or a Salesforce or NetSuite export with paging. Otherwise turning pagination on fails with 422 `api_pagination_source_not_paged` (an HTTP lookup without paging: \"The lookup must have paging configured.\"), `api_pagination_adapter_not_supported` (any other adaptor) or `api_pagination_no_volume_driver` (no lookup).\n- The success response needs a body. A `204` returns nothing, so the cursor is lost with it.\n- Script-mode APIs never paginate: a create or update drops `pagination`, and patching `pagination.enabled` answers 422 `api_pagination_builder_only`.\n\n## Turning it on and off\n\n- `celigo apis set \u003cid> pagination.enabled=true` (a PATCH of `/pagination/enabled`). A full update that omits `pagination` keeps the stored value; send `\"pagination\": {\"enabled\": null}` to clear it.\n- Turn pagination off before you remove paging from the volume-driver export. If the export loses its paging while pagination is on, the export save still succeeds, but every call then fails with 400 `api_pagination_source_not_paged` and every save of the API is refused with the same code as a 422.\n- Test runs never paginate: their success response carries no `pagination` object, and a `Celigo-Next-Cursor` header in `mockRequest.headers` isn't checked.\n\n## What callers do\n\n- A paged response keeps its own keys at the root and gains `\"pagination\": {\"has_more\": true, \"next_cursor\": \"...\"}`. The last page has `\"has_more\": false` and no `next_cursor`.\n- For the next page, the caller resends the identical request -- same method, path, query string and body -- with the cursor in the `Celigo-Next-Cursor` header. A `next_cursor` in the body is ordinary payload (it returns the first page again), and a changed path, query or body fails with 400 `cursor_filter_mismatch`.\n- Other cursor faults answer 400: `invalid_cursor_format`, `cursor_signature_invalid`, `cursor_version_unsupported`, `cursor_api_mismatch`, and `cursor_not_supported_for_api` when pagination is off. An expired cursor answers 410 `cursor_expired`.\n- Saving the volume-driver export, even unchanged, ends every walk in progress: the next call with an old cursor returns 200 with an empty last page, `{\"pagination\": {\"has_more\": false}}`. Restart from the first page. Saving the API itself leaves cursors valid.\n- A response hook that turns the body into something other than an object fails the call with 422 `api_pagination_response_not_object`.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2914,"content_sha256":"f3d04179120fe0d7af56e6cda1bd33ece4624579d9c4fb004b832b69be5afe6a"},{"filename":"references/schemas/api-request.yml","content":"ApiRequest:\n type: object\n description: Request configuration for a builder-mode API endpoint.\n properties:\n name:\n type: string\n maxLength: 200\n description: Display name.\n examples: [\"Get Customer\", \"Create Order\"]\n description:\n type: string\n maxLength: 10240\n description: Optional description.\n examples: [\"Retrieves customer information by ID\"]\n relativeURI:\n type: string\n maxLength: 131072\n pattern: \"^\\\\/[a-zA-Z0-9:_*\\\\/\\\\-\\\\.]*$\"\n description: |-\n URI path relative to the version. Callers reach the API at\n `https://api.integrator.io/apis/{version}{relativeURI}` (on the account's regional\n host outside the US), with the account's API token. Use colon notation for path\n params: `/customers/:id`.\n examples: [\"/customers/:id\", \"/orders\", \"/products/:productId/variants/:variantId\"]\n x-celigo-ai-guidance:\n - |-\n The public endpoint is `https://api.integrator.io/apis/{version}{relativeURI}`, for\n example `/apis/v1/customers/:id`; `relativeURI` already starts with `/`, so add no\n slash after the version. Without the `/apis` prefix the request goes to the\n platform's own API and answers `resource_not_found`.\n Can include path parameters using colon notation: `/customers/:id`\n First path segment must be alphanumeric, dash, or underscore only.\n method:\n type: string\n enum: [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"]\n x-enumDescriptions:\n GET: Retrieve data from the endpoint.\n POST: Submit data to the endpoint.\n PUT: Replace data at the endpoint.\n PATCH: Partially update data at the endpoint.\n DELETE: Remove data at the endpoint.\n description: HTTP method for the API endpoint.\n examples: [\"GET\", \"POST\"]\n headers:\n type: array\n description: Expected request headers.\n items:\n type: object\n properties:\n key:\n type: string\n maxLength: 256\n pattern: \"^[a-zA-Z0-9_-]+$\"\n description: Header name\n examples: [\"Content-Type\", \"X-API-Key\"]\n description:\n type: string\n maxLength: 10240\n description: Description of the header's purpose\n examples: [\"API key for authentication\"]\n x-celigo-ai-guidance:\n - |-\n Documents headers that clients should send. Headers are optional by default.\n Only alphanumeric characters, hyphens, and underscores are allowed in header names.\n pathParams:\n type: array\n description: Path parameters defined in the `relativeURI`.\n items:\n type: object\n properties:\n key:\n type: string\n maxLength: 256\n description: Parameter name (without the colon prefix)\n examples: [\"id\", \"customerId\"]\n description:\n type: string\n maxLength: 10240\n description: Description of the parameter\n examples: [\"Unique identifier of the customer\"]\n x-celigo-ai-guidance:\n - |-\n Documents the parameters that are part of the URL path (e.g., :id, :customerId).\n These should match the parameters defined in the relativeURI.\n queryParams:\n type: array\n description: Expected query string parameters.\n items:\n type: object\n properties:\n key:\n type: string\n maxLength: 256\n pattern: \"^[a-zA-Z0-9_\\\\-:\\\\/]*$\"\n description: Query parameter name\n examples: [\"limit\", \"offset\", \"filter\"]\n dataType:\n type: string\n enum: [\"string\", \"number\", \"boolean\"]\n x-enumDescriptions:\n string: Parameter value is a text string.\n number: Parameter value is a numeric value.\n boolean: Parameter value is true or false.\n description: Expected data type of the parameter\n examples: [\"string\"]\n description:\n type: string\n maxLength: 10240\n description: Description of the parameter\n examples: [\"Maximum number of records to return\"]\n bodySchema:\n type: object\n description: |-\n JSON Schema describing the expected request body structure. Every\n object-typed schema node must declare at least one property for the\n API Builder to render it; omit this field when the endpoint has no\n body contract.\n additionalProperties: true\n examples:\n - type: object\n properties:\n name:\n type: string\n email:\n type: string\n format: email\n x-celigo-ai-guidance:\n - |-\n Every object-typed schema node — the root, a nested property, or an\n array's `items` — must carry a non-empty `properties` map. The API\n persists a property-less object node without error, but the API\n Builder editor crashes rendering it (\"Cannot convert undefined or\n null to object\"), so the mistake only surfaces when the editor opens.\n - |-\n When the endpoint has no body contract (e.g. the source spec declares\n an empty request body), omit `bodySchema` entirely — absent renders\n fine. Do not substitute a catch-all `{\"type\": \"object\"}`, and do not\n pre-fill `properties: {}` — the API strips empty objects on save, so\n the empty map never survives the round-trip.\n mockRequest:\n type: object\n description: |-\n Sample request saved with the API for testing. `POST /v1/apis/{_id}/test/run` doesn't\n read it; send it as that call's `mockRequest`. A test run still makes outside calls: a\n lookup with no saved `mockOutput` calls its source live, and the run saves that response\n as the lookup's `mockOutput`, which later test runs answer from.\n properties:\n body:\n type: object\n description: Sample request body\n headers:\n type: object\n description: Sample headers\n pathParams:\n type: object\n description: Sample path parameters\n queryParams:\n type: object\n description: Sample query parameters\n additionalProperties: false\n x-celigo-ai-guidance:\n - |-\n Saving a mockRequest doesn't make test runs use it: a test run without its own\n mockRequest starts from an empty record. A test run isn't free of outside calls\n either: a lookup without a saved mockOutput runs live (and the run then saves its\n response as that mockOutput), and only imports are kept from sending (each returns\n its mockResponse or a placeholder). Can include body, headers, pathParams, and\n queryParams; only the body reaches the record the steps see.\n transform:\n type: object\n description: |-\n Optional transformation applied to the incoming request before processing. The\n expression form (`type: \"expression\"` with Transformation 2.0 rules in `expression`)\n saves and runs. In a test run its rules see only the request body, not `pathParams`,\n `queryParams` or headers.\n properties:\n type:\n type: string\n description: Selects the transform form; `expression` runs the rules in `expression`.\n examples: [\"expression\"]\n expression:\n type: object\n description: Transformation 2.0 rules applied to the request body.\n properties:\n version:\n type: string\n description: Rule format; `\"2\"` selects the rules in `rulesTwoDotZero`.\n examples: [\"2\"]\n rulesTwoDotZero:\n type: object\n description: The rules applied to the request body.\n properties:\n mode:\n type: string\n enum: [\"create\", \"modify\"]\n x-enumDescriptions:\n create: Builds a new record from the mappings, dropping unmapped fields.\n modify: Edits the incoming record, keeping unmapped fields.\n description: How the mappings apply to the record; `modify` keeps the body's own fields beside the mapped ones.\n examples: [\"modify\"]\n mappings:\n type: array\n description: Mapper 2.0 rows that write fields onto the record.\n items:\n type: object\n required:\n - dataType\n properties:\n generate:\n type: string\n description: Record field the row writes.\n examples: [\"customerId\"]\n extract:\n type: string\n description: JSONPath into the request body that the value is read from.\n examples: [\"$.id\"]\n dataType:\n type: string\n enum:\n - string\n - number\n - boolean\n - object\n - stringarray\n - numberarray\n - booleanarray\n - objectarray\n - arrayarray\n x-celigo-deprecated-enum-values:\n - arrayarray\n x-enumDescriptions:\n string: Single string value\n number: Single numeric value\n boolean: Single boolean value\n object: Nested object value\n stringarray: Array of strings\n numberarray: Array of numbers\n booleanarray: Array of booleans\n objectarray: Array of objects\n arrayarray: \"Deprecated: not a type the mapper recognizes; a row that uses it fails\"\n description: Data type the row writes.\n examples: [\"string\"]\n sourceDataType:\n type: string\n description: Data type of the extracted value.\n examples: [\"string\"]\n status:\n type: string\n description: Row status; `Active` rows run.\n examples: [\"Active\"]\n mappings:\n type: array\n description: Child rows of an `object` row, with the same shape as this row.\n items:\n $ref: '#/ApiRequest/properties/transform/properties/expression/properties/rulesTwoDotZero/properties/mappings/items'\n _scriptId:\n type: string\n format: objectId\n x-celigo-refModel: scripts\n description: Reference to a script for custom transformation logic.\n examples: [\"689212a2c42d988978e27a11\"]\n function:\n type: string\n description: Function name in the script to execute.\n examples: [\"transformRequest\"]\n x-celigo-ai-guidance:\n - |-\n Transforms the request data before it is processed by routers or responses.\n Useful for normalizing data or extracting specific fields.\n required:\n - relativeURI\n - method\n\n x-celigo-ai-guidance:\n - |-\n Defines how the API endpoint receives data:\n - HTTP method and URI path\n - Expected parameters (path, query, headers)\n - Request body schema\n - Optional request transformation logic\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":11948,"content_sha256":"dddd5fd9c6664b5b22407b392ef928cc78dd6a1f0500cb62054f398aef958096"},{"filename":"references/schemas/api-response.yml","content":"ApiResponse:\n type: object\n description: |-\n Response configuration in a builder-mode API. Each API requires exactly one\n `success` and one `fail` response; additional `custom` responses are optional.\n properties:\n id:\n type: string\n description: Unique identifier for this response, referenced by the response router.\n examples: [\"success_response\", \"error_response\", \"custom_validation_response\"]\n name:\n type: string\n maxLength: 200\n description: Display name.\n examples: [\"Success Response\", \"Error Response\", \"Validation Error\"]\n description:\n type: string\n maxLength: 10240\n description: Optional description of when this response is used.\n examples: [\"Returned when customer is successfully created\"]\n type:\n type: string\n enum: [\"success\", \"fail\", \"custom\"]\n x-enumDescriptions:\n success: The happy-path response; returned when it's the first response, in list order, whose `inputFilter` matches.\n fail: The error response; returned when a step fails and that step's `proceedOnFailure` is off.\n custom: Additional response for specific scenarios, selected by its own `inputFilter`.\n description: |-\n Response type. `success` isn't a fallback checked after the `custom` responses: list\n `custom` responses before it (see `inputFilter`).\n examples: [\"success\"]\n x-celigo-ai-guidance:\n - |-\n - **success**: Response for successful operations (required, exactly one)\n - **fail**: Response for errors (required, exactly one)\n - **custom**: Additional responses for specific scenarios (optional, multiple allowed), listed before `success`\n statusCode:\n type: integer\n minimum: 100\n maximum: 599\n description: HTTP status code to return.\n examples: [200, 201, 400, 404, 500]\n headers:\n type: array\n description: Response headers to include.\n items:\n type: object\n properties:\n key:\n type: string\n maxLength: 256\n pattern: \"^[a-zA-Z0-9_-]+$\"\n description: Header name\n examples: [\"Content-Type\", \"X-Request-Id\"]\n value:\n type: string\n maxLength: 256\n description: Header value (can include handlebars templates)\n examples: [\"application/json\", \"{{requestId}}\"]\n description:\n type: string\n maxLength: 10240\n description: Description of the header\n examples: [\"Content type of the response body\"]\n inputFilter:\n type: object\n description: |-\n Filter criteria for response selection by the response router, which returns the first\n response in `responses` whose filter matches. A response without an `inputFilter` matches\n every request, so a `success` listed first answers requests a later `custom` filter\n matches; the typed `context.success` filter doesn't prevent that, because\n `context.success` is true whenever no step failed, including when a lookup found nothing.\n A step that fails with `proceedOnFailure` off selects the `fail` response.\n\n Send `version` and `rules` directly on `inputFilter`, not inside the\n `{type: \"expression\", expression: {...}}` wrapper that export and import filters use. A\n wrapped filter is saved without an error, but the response is stored with no\n `inputFilter`, so it matches every request that reaches it.\n properties:\n version:\n type: string\n enum: [\"1\"]\n x-enumDescriptions:\n \"1\": Version 1 of the Celigo expression-based filter format.\n description: Version of the filter format used by `rules`.\n rules:\n type: array\n description: Celigo expression-based filter rules.\n items: {}\n examples:\n - [\"empty\", [\"string\", [\"extract\", \"customerId\"]]]\n - [\"and\", [\"notempty\", [\"string\", [\"extract\", \"orderId\"]]], [\"greaterthan\", [\"number\", [\"extract\", \"amount\"]], 1000]]\n x-celigo-ai-guidance:\n - |-\n Array-based DSL where the first element is an operator (e.g., \"equals\", \"and\", \"or\"),\n followed by operands which can be nested expressions.\n bodySchema:\n type: object\n description: |-\n JSON Schema describing the response body structure. Every object-typed\n schema node must declare at least one property for the API Builder to\n render it; omit this field when the response body has no defined shape.\n additionalProperties: true\n examples:\n - type: object\n properties:\n id:\n type: string\n name:\n type: string\n createdAt:\n type: string\n format: date-time\n x-celigo-ai-guidance:\n - |-\n Every object-typed schema node — the root, a nested property, or an\n array's `items` — must carry a non-empty `properties` map. The API\n persists a property-less object node without error, but the API\n Builder editor crashes rendering it (\"Cannot convert undefined or\n null to object\"), so the mistake only surfaces when the editor opens.\n - |-\n When copying a schema from an external spec, prune property-less\n object nodes (e.g. an array's `items` that is a bare\n `{\"type\": \"object\"}`) instead of reproducing them. Array-typed roots\n (which carry `items`, not `properties`) and type-less nullable\n leaves render fine. Do not pre-fill `properties: {}` — the API\n strips empty objects on save.\n mockInput:\n type:\n - object\n - string\n maxLength: 0\n description: |-\n Mock data for testing this response, in the integrator.io canonical\n record-page format: `{\"page_of_records\": [{\"record\": {...}}, ...]}`.\n The server rejects any other object shape and any non-empty string\n with a 422; the empty string `\"\"` (a UI draft artifact) is accepted\n and stored verbatim.\n required:\n - page_of_records\n properties:\n page_of_records:\n type: array\n description: Pages of mock records fed to this response's mappings.\n items:\n type: object\n required:\n - record\n properties:\n record:\n type: object\n description: One mock input record (freeform payload).\n success:\n type: boolean\n description: When true, the mock record follows the success path.\n testMode:\n type: boolean\n description: When true, the mock record is treated as a test-mode record.\n additionalProperties: true\n mappings:\n type: array\n description: |-\n Mapper 2.0 rows that build the response body from the record the steps produced,\n under the same rules as the shared Mapper 2.0 schema an import's `mappings` use.\n items:\n type: object\n required:\n - dataType\n properties:\n generate:\n type: string\n description: |-\n Name of the response-body field this row writes. A dot doesn't nest:\n `customer.id` makes one key named `customer.id`. To nest, use an `object` row\n with child `mappings`.\n examples: [\"customerId\"]\n dataType:\n type: string\n enum:\n - string\n - number\n - boolean\n - object\n - stringarray\n - numberarray\n - booleanarray\n - objectarray\n - arrayarray\n x-celigo-deprecated-enum-values:\n - arrayarray\n x-lowercase: true\n x-enumDescriptions:\n string: Single string value\n number: Single numeric value\n boolean: Single boolean value\n object: Nested object value\n stringarray: Array of strings\n numberarray: Array of numbers\n booleanarray: Array of booleans\n objectarray: Array of objects\n arrayarray: \"Deprecated: not a type the mapper recognizes; a row that uses it fails\"\n description: Data type of the value this mapping writes into the response body.\n examples: [\"string\"]\n x-celigo-ai-guidance:\n - |-\n Validator-enforced on every mapping entry: writes fail with\n `422` (\"Path 'dataType' is required.\"), repeated once per\n entry that omits it.\n extract:\n type: string\n description: |-\n Where the value comes from, as a JSONPath into the record (`$.id`). A bare string\n such as `id` or `record.id` is written as that literal text, not read from the\n record.\n examples: [\"$.id\"]\n mappings:\n type: array\n description: |-\n Child rows of an `object` row, with the same shape as this row. Each writes one\n field inside the object this row generates.\n items:\n $ref: '#/ApiResponse/properties/mappings/items'\n hardCodedValue:\n type: string\n description: |-\n Static value written to the target field instead of extracting\n from input data.\n x-celigo-ai-guidance:\n - |-\n The field name is exactly `hardCodedValue` (capital C). The\n server does not reject unknown mapping keys: a mapping sent\n with `hardcodedValue` saves successfully with the value\n silently dropped — no error, and the target field is then\n simply absent from the API's responses.\n status:\n # Response mappings are Mapper 2.0 entries validated by the same rules as an\n # import's `mappings`: an entry without `status` is rejected with 422 \"Mapping\n # object must have status field present.\" (live-verified on POST /v1/apis).\n $ref: '../common-schemas/mappings.yml#/Mappings/items/properties/status'\n lookups:\n type: array\n description: Static key-value lookup tables for value transformation.\n items:\n type: object\n properties:\n name:\n type: string\n description: Name of the lookup\n examples: [\"statusCodeMap\"]\n map:\n type: object\n description: Key-value mapping object\n default:\n type: string\n description: Default value if key not found\n examples: [\"unknown\"]\n allowFailures:\n type: boolean\n description: When true, processing continues even if this lookup fails.\n hooks:\n type: object\n description: Custom scripts to run during response processing.\n properties:\n preMap:\n type: object\n description: Script to run before applying mappings.\n properties:\n _scriptId:\n type: string\n format: objectId\n x-celigo-refModel: scripts\n description: Reference to the script resource.\n examples: [\"689212a2c42d988978e27a11\"]\n function:\n type: string\n description: Function name to execute.\n examples: [\"preMapResponse\"]\n postMap:\n type: object\n description: Script to run after applying mappings.\n properties:\n _scriptId:\n type: string\n format: objectId\n x-celigo-refModel: scripts\n description: Reference to the script resource.\n examples: [\"689212a2c42d988978e27a11\"]\n function:\n type: string\n description: Function name to execute.\n examples: [\"postMapResponse\"]\n x-celigo-ai-guidance:\n - |-\n Defines how to format and return data to the caller. Each API must have:\n - Exactly one 'success' response (for successful operations)\n - Exactly one 'fail' response (for errors)\n - Zero or more 'custom' responses (for specific scenarios)\n The response router returns the first response whose `inputFilter` matches, and a\n response without one matches every request, so list the 'custom' responses ahead of\n 'success'.\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":12642,"content_sha256":"6d612f55cfe5854f6185c3ef62fae867b9af0265dedfa491ff01f76502111392"},{"filename":"references/schemas/apim.yml","content":"Apim:\n type: array\n readOnly: true\n description: Publication status of this API in external API management systems.\n items:\n type: object\n properties:\n apiId:\n type: string\n description: Identifier assigned by the external API management system (a UUID, not a Celigo API _id).\n examples: [\"44754dc4-15fa-4f52-b54d-c415fa5f525c\"]\n flowId:\n type: string\n description: Associated flow identifier in the external API management system (a UUID, not a Celigo flow _id). Empty when no flow is associated.\n examples: [\"14b8def5-143e-4598-b8de-f5143e559832\"]\n status:\n type: string\n enum: [\"oaspending\", \"published\"]\n x-enumDescriptions:\n oaspending: OpenAPI spec generation or update is pending for this API.\n published: The API has been published to the external API management system.\n description: Publication status.\n definitionVersion:\n type: string\n enum: [\"v4\"]\n x-enumDescriptions:\n v4: Version 4 of the API definition format used by the management system.\n description: API definition format version.\n\n x-celigo-ai-guidance:\n - |-\n Tracks the publication status of this API to external API management systems\n and portals. Used for generating and syncing OpenAPI specifications.\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":1349,"content_sha256":"dd0b9194fdaa43e71eb34e7ea2d6af9f7890e9c24774cd62f9526aeaf70f7409"},{"filename":"references/schemas/base.yml","content":"APIBase:\n type: object\n description: Writable fields shared by the request and response schemas.\n properties:\n name:\n type: string\n minLength: 1\n maxLength: 100\n description: Display name.\n examples: [\"Customer API\", \"Order Management API\"]\n _integrationId:\n type: [string, \"null\"]\n format: objectId\n x-celigo-refModel: integrations\n description: |-\n Integration this API belongs to. **Builder mode only** — a script-mode\n API is always account-level and the server silently drops this field.\n Send `null` (or omit) to keep the API account-level; on reads the field\n is omitted when unset, never `null`.\n examples: [\"5e9a8f7c6b3d2a0011c4e5f6\"]\n x-celigo-ai-guidance:\n - To move the API to a different integration, PUT with a changed _integrationId; the platform registers the API's step connections in the target integration on save.\n - |-\n PUT is a full replace: omitting _integrationId (or sending null) on an\n integration-owned API moves it back to the account level and clears\n _apiGroupingId with it.\n _apiGroupingId:\n type: [string, \"null\"]\n format: objectId\n description: |-\n One of the owning integration's `apiGroupings`, subdividing its APIs.\n Requires `_integrationId` in the same write — sent without it, the server\n silently drops both — so builder mode only. `PUT /v1/apis/updateApiGrouping`\n regroups many APIs at once; this field carries the same value on a single\n API. On reads the field is omitted when unset, never `null`.\n examples: [\"60a1b2c3d4e5f60012345678\"]\n x-celigo-ai-guidance:\n - |-\n The id must be one of the `apiGroupings` of the integration named by _integrationId\n in the same write; any other id fails with 422 `apigrouping_not_belong` — including\n a PUT that changes _integrationId but keeps the previous grouping. When moving an\n API, send the target integration's grouping or `null` (Unassigned).\n - |-\n `PATCH /v1/apis/{_id}` rejects `/_apiGroupingId` (422, not a whitelisted property);\n regroup with a full PUT or with `PUT /v1/apis/updateApiGrouping`.\n description:\n type: string\n description: Optional description of the API's purpose.\n examples: [\"API for managing customer records in Salesforce\"]\n x-celigo-ai-guidance:\n - Use this to document the API's purpose, expected inputs/outputs, and any special considerations.\n type:\n type: string\n enum: [\"builder\", \"script\"]\n default: script\n x-enumDescriptions:\n builder: Visual configuration with request/response mapping and transformations.\n script: Custom JavaScript function for complete control over API behavior.\n description: |-\n API mode. Cannot be changed after creation. Defaults to `script` when\n omitted on create; legacy script APIs created before builder mode may\n omit it on reads as well.\n examples: [\"builder\"]\n version:\n type: string\n default: \"v1\"\n pattern: \"^[a-zA-Z0-9\\\\-_\\\\.]+$\"\n description: Version segment of the public URL (`/apis/{version}{relativeURI}`).\n examples: [\"v1\", \"v2\", \"v1.0\"]\n disabled:\n type: boolean\n default: false\n description: When true, the API rejects all incoming requests.\n examples: [false]\n timeoutPeriod:\n type: integer\n minimum: 0\n maximum: 120\n description: |-\n Request-timeout override in seconds (1–120). `0` is a sentinel meaning \"use the\n 120-second default\" — the server rewrites it on write, so a stored value is never 0.\n examples: [120, 30]\n logging:\n type: object\n description: |-\n Execution-logging settings for the API. Builder-mode APIs are\n initialized to `{\"mode\": \"basic\"}` at creation (when the account\n license has logging enabled), so the field is present on responses\n from the start.\n\n POST and PUT bodies cannot set or change this field — the server\n ignores it silently (the write succeeds and the stored value is\n untouched). `PATCH /v1/apis/{_id}` with the `/logging/mode` and\n `/logging/debugUntil` paths is the only way to change it.\n x-celigo-ai-guidance:\n - |-\n Never send logging changes via PUT: the request returns 200 with\n the stored value unchanged — no error, nothing persisted. PATCH\n `/logging/mode` and `/logging/debugUntil` are the only writers.\n properties:\n mode:\n type: string\n enum: [basic, standard, detailed, accountLevel, noLogging]\n x-enumDescriptions:\n basic: Log request metadata and the trace view's step list, with the least per-record payload.\n standard: Log request metadata plus headers and summary step information.\n detailed: Full capture including per-step trace logs, viewable in the run-history trace view.\n accountLevel: Defer to the account's logging settings.\n noLogging: Disable request logging entirely — no run-history entries are produced.\n description: |-\n Which logging level applies to requests handled by this API. Every\n mode that logs records the step list that\n `GET /v1/apis/{_id}/requests/{executionId}` returns, `basic` included;\n the level decides how much per-record payload the log-data query can\n return. Payload-capturing modes (`standard`, `detailed`) require payload\n storage to be enabled for the account — without it, changing the\n mode fails with `422` (code `payload_storage_required`).\n debugUntil:\n type: string\n format: date-time\n description: |-\n While this timestamp is in the future, requests are captured in\n full debug mode regardless of `mode`. Clears itself once the\n window passes. Setting it requires payload storage to be enabled\n for the account.\n examples: [\"2026-05-15T14:00:00.000Z\"]\n traceKeyTemplate:\n type: string\n maxLength: 1024\n description: |-\n Handlebars template that computes each request's trace key from the\n request payload, used to correlate run-history entries with source\n records. Absent from responses until set.\n examples: [\"{{record.orderId}}\"]\n pagination:\n type: object\n description: |-\n Cursor pagination for builder-mode APIs. The first lookup (export\n step) in `builder.routers` is the volume driver: when pagination is\n enabled, each call to the public endpoint returns one source page of\n that lookup instead of draining it. The mapped response body keeps its\n own keys at the root and gains one reserved `pagination` object,\n `{\"has_more\": boolean, \"next_cursor\": string}` — no `data` wrapper.\n `next_cursor` is present only while `has_more` is true, and a page\n that maps to no records is just `{\"pagination\": {\"has_more\": false}}`.\n A root-level `pagination` mapping in the success response is\n overwritten by the platform object at runtime.\n\n Callers fetch the next page by resending the request unchanged with\n the cursor in the `Celigo-Next-Cursor` request header — for every\n HTTP method; a `next_cursor` field in the request body is ordinary\n payload, not a cursor. Resending a cursor returns the same page again.\n Cursor faults fail the invocation with `400`:\n `invalid_cursor_format`, `cursor_signature_invalid` (the cursor was\n altered), `cursor_version_unsupported`, `cursor_api_mismatch` (the\n cursor was issued by a different API), `cursor_filter_mismatch` (the\n path, query, or body differs from the request that issued the cursor),\n or `cursor_not_supported_for_api` (a cursor sent to an API whose\n pagination is off). Cursors expire and are then rejected with `410\n cursor_expired`; a response hook that turns the mapped body into a\n non-object fails with `422 api_pagination_response_not_object`.\n\n Saving the volume-driver export — even with no changes — invalidates\n every cursor issued so far: the next call with one returns `200` with\n an empty final page, `{\"pagination\": {\"has_more\": false}}`, as if the\n source were exhausted; restart from the first page without a cursor.\n Saving the API document itself leaves issued cursors valid.\n A success response whose `statusCode` is `204` returns no body, so the\n `pagination` object and cursor are lost with it — a paginated API\n needs a body-bearing success status.\n\n Absent from responses until set. A PUT that omits `pagination` keeps\n the stored value; send `{\"enabled\": null}` to clear it (the field is\n then absent again).\n x-celigo-ai-guidance:\n - |-\n When calling a paginated API, read `pagination.next_cursor` from the\n response and resend it only in the `Celigo-Next-Cursor` header with\n the identical path, query string, and body; putting it in the body\n returns the first page again, and adding or changing a query\n parameter fails with 400 `cursor_filter_mismatch`. Loop until\n `has_more` is false — the final page carries no `next_cursor`. Saving\n the volume-driver export mid-walk ends the walk early without an\n error: the next cursor call returns an empty\n `{\"pagination\": {\"has_more\": false}}` page, so restart from the first\n page after any save of that export.\n - |-\n Script-mode APIs never paginate: POST and PUT silently drop\n `pagination` (it never reads back), and `PATCH /pagination/enabled`\n on one fails with 422 `api_pagination_builder_only`.\n properties:\n enabled:\n type: boolean\n description: |-\n When true, invocations return one volume-driver page per request\n plus the reserved `pagination` object, and accept the\n `Celigo-Next-Cursor` header. Saving `true` requires a builder-mode\n API whose first lookup is an HTTP, Salesforce, or NetSuite export\n with source paging configured; otherwise the write fails with\n `422` `api_pagination_no_volume_driver` (no lookup at all),\n `api_pagination_adapter_not_supported` (any other adaptor), or\n `api_pagination_source_not_paged` (lookup without paging). The\n same check repeats on every later save of the API and at every\n invocation while the stored value is true: if the volume driver\n loses its paging afterwards (the export save itself succeeds),\n every call fails with `400 api_pagination_source_not_paged` and\n every PUT or PATCH of the API — including one that omits\n `pagination` — is refused with the `422` until paging is restored\n or this flag is set to false. When false, a `Celigo-Next-Cursor`\n header is rejected with `400 cursor_not_supported_for_api`.\n examples: [true]\n x-celigo-ai-guidance:\n - |-\n Before enabling, give the first lookup export paging (for HTTP,\n `http.paging.method` such as `page`): an HTTP lookup without it\n fails the save with 422 `api_pagination_source_not_paged`, an FTP,\n webhook, or other non-HTTP/Salesforce/NetSuite lookup with 422\n `api_pagination_adapter_not_supported`, and an API with no lookup\n with 422 `api_pagination_no_volume_driver`.\n - |-\n Before removing paging from the volume-driver export of an API\n whose `enabled` is true, PATCH `/pagination/enabled` to false\n first (that PATCH succeeds even while the lookup is unpaged).\n Otherwise the export save returns 200 but every call to the API\n fails 400 `api_pagination_source_not_paged` and every save of\n the API fails 422 with the same code.\n script:\n $ref: \"./script.yml#/Script\"\n builder:\n $ref: \"./builder.yml#/Builder\"\n shipworks:\n $ref: \"./shipworks.yml#/Shipworks\"\n draft:\n type: boolean\n description: |-\n When true, this API is a draft that auto-deletes when its expiry passes\n (`draftExpiresAt` in the response). Set at creation; an update can clear the\n flag but never set it.\n x-celigo-ai-guidance:\n - |-\n POST with `draft: true` arms the draft TTL (the server computes `draftExpiresAt`).\n PUT with `draft: false` clears the flag and the expiry, promoting the draft to a\n permanent API. PUT with `draft: true` on a non-draft API is silently ignored, and\n a PUT that omits the field leaves an existing draft flag unchanged.\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":13067,"content_sha256":"8bf93fbdc413853710aad503ac6f480099dab72f51d4e563dfc0265e0be322d2"},{"filename":"references/schemas/builder.yml","content":"Builder:\n type: object\n description: Builder-mode configuration defining request structure, routing, and response mapping.\n properties:\n request:\n $ref: \"./api-request.yml#/ApiRequest\"\n routers:\n type: array\n description: Optional routers for conditional processing before the response stage.\n items:\n $ref: \"./router.yml#/Router\"\n x-celigo-ai-guidance:\n - |-\n Routers allow you to direct requests to different processing branches\n based on criteria. This enables complex business logic within the API.\n responseRouter:\n $ref: \"./response-router.yml#/ResponseRouter\"\n responses:\n type: array\n description: |-\n Response configurations. Must include exactly one `success` and one `fail`\n response; additional `custom` responses are optional. Order matters: the response\n router returns the first response whose `inputFilter` matches, and a response without\n one matches every request.\n items:\n $ref: \"./api-response.yml#/ApiResponse\"\n\n x-celigo-ai-guidance:\n - |-\n Must include exactly one 'success' response and one 'fail' response.\n Additional custom responses can be added for specific scenarios; list them before\n 'success', which otherwise answers the requests their filters match.\n x-celigo-ai-guidance:\n - |-\n The builder mode allows you to define your API using visual configuration:\n - Define the request structure (method, path, parameters)\n - Transform incoming requests\n - Route requests through processing logic\n - Map and transform responses\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":1649,"content_sha256":"47db1bae1e81a7f8584b5a6f5aafcab5e2721aa2c719f6705e129fb405b23723"},{"filename":"references/schemas/request.yml","content":"Request:\n type: object\n description: |-\n Request body for creating or updating an API.\n\n For builder-mode APIs, populate the `builder` object; the `script` field is\n ignored. `builder.request` can be omitted on create — the API has no route\n until `relativeURI` and `method` are set — but when it is present it must\n carry both, or the write fails with 422 `invalid_api_builder_request`. For\n script-mode APIs, populate `script` with `_scriptId` and `function`; the\n `builder` field is ignored. On PUT, send the complete object — omitted\n fields revert to defaults.\n\n Set `type` explicitly to `builder` or `script`. The server infers `script`\n when `type` is omitted, but new APIs must declare it.\n required:\n - name\n - type\n allOf:\n - $ref: \"./base.yml#/APIBase\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":813,"content_sha256":"1a620717a72712dee945c932976efb57adaf81f5cb12c4196effd5c087b926ab"},{"filename":"references/schemas/response-router.yml","content":"ResponseRouter:\n type: object\n description: |-\n Final routing step that selects which response configuration to return.\n Must have `id: \"apiRouter\"`.\n properties:\n id:\n type: string\n enum: [\"apiRouter\"]\n x-enumDescriptions:\n apiRouter: Identifies this as the final router that determines which API response to return.\n description: Must be `\"apiRouter\"`.\n examples: [\"apiRouter\"]\n x-celigo-ai-guidance:\n - |-\n Must be \"apiRouter\" to indicate this is the final router before\n returning the API response.\n routeRecordsUsing:\n type: string\n enum: [\"input_filters\", \"script\"]\n x-enumDescriptions:\n input_filters: Evaluate routing criteria using declarative filter expressions.\n script: Evaluate routing criteria using a custom JavaScript function.\n description: Method used to determine which response to use.\n examples: [\"input_filters\"]\n x-celigo-ai-guidance:\n - |-\n - **input_filters**: Use declarative filter expressions on each response\n - **script**: Use custom JavaScript to return the response id\n script:\n type: object\n description: Script configuration when `routeRecordsUsing` is `\"script\"`.\n properties:\n _scriptId:\n type: string\n format: objectId\n x-celigo-refModel: scripts\n description: Reference to the script resource.\n examples: [\"60a2c4e6f321d800129a1a3c\"]\n function:\n type: string\n description: Function name that returns the response id.\n examples: [\"determineResponse\", \"routeToResponse\"]\n x-celigo-ai-guidance:\n - |-\n The response router determines which response configuration to use based on\n the processing results. This allows you to return different responses based\n on different conditions or outcomes.\n\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":1898,"content_sha256":"57186b1a6007bd054b232781f28d00a4778ac196047acbfbd78f6d67b04d7d0f"},{"filename":"references/schemas/response.yml","content":"API:\n required:\n - _id\n - name\n - createdAt\n - lastModified\n description: |-\n API resource. Shape varies by mode: builder-mode APIs carry `type`, `version`,\n `disabled`, and `builder`; script-mode APIs additionally carry `script` plus\n top-level `_scriptId` / `function` copies. Legacy script APIs (pre-builder era)\n omit `type`, `version`, `disabled`, and `builder` entirely.\n allOf:\n - $ref: './base.yml#/APIBase'\n - type: object\n properties:\n _id:\n type: string\n format: objectId\n readOnly: true\n x-celigo-refModel: apis\n description: Unique identifier for the API.\n examples: [\"68ae4264b5f755d2dd3796b4\"]\n _scriptId:\n type: string\n format: objectId\n readOnly: true\n x-celigo-refModel: scripts\n description: |-\n Top-level copy of `script._scriptId`. Present on script-mode and legacy\n script APIs for backward compatibility.\n examples: [\"689212a2c42d988978e27a11\"]\n function:\n type: string\n readOnly: true\n description: |-\n Top-level copy of `script.function`. Present on script-mode and legacy\n script APIs for backward compatibility.\n examples: [\"handleRequest\"]\n createdAt:\n type: string\n format: date-time\n readOnly: true\n description: Timestamp when the API was created.\n examples: [\"2025-08-26T23:25:24.107Z\"]\n lastModified:\n type: string\n format: date-time\n readOnly: true\n description: Timestamp when the API was last modified.\n examples: [\"2026-04-07T03:48:20.795Z\"]\n _templateId:\n type: string\n format: objectId\n readOnly: true\n x-celigo-refModel: templates\n description: Template this API was created from. Present only on template-installed APIs.\n examples: [\"60a2c4e6f321d800129a1a3c\"]\n sandbox:\n type: boolean\n deprecated: true\n readOnly: true\n description: Environment flag. Present on older accounts.\n draftExpiresAt:\n type: string\n format: date-time\n readOnly: true\n description: |-\n Timestamp when a draft API auto-deletes. Server-computed when `draft` is set at\n creation.\n examples: [\"2026-01-15T09:30:00.000Z\"]\n apim:\n $ref: \"./apim.yml#/Apim\"\n x-celigo-ai-guidance:\n - |-\n Contains the complete API configuration including metadata, script/builder settings,\n and integration-specific configurations.\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":2684,"content_sha256":"e07fea5faaccd4478ba77fc4c7f46907c59c8d3418353cce6f398afd6ad8c3fe"},{"filename":"references/schemas/router.yml","content":"Router:\n type: object\n description: |-\n Conditional routing within a builder-mode API. Unlike flows, APIs only\n support `first_matching_branch` routing.\n\n A router comes in two shapes, told apart by whether `routeRecordsTo` and\n `routeRecordsUsing` are set: a **branching router** sets both and has two or\n more branches, each taking the requests its `inputFilter` (or the router's\n `script`) matches — the node you see in the API builder; a **pass-through\n router** sets neither and has exactly one branch that every request goes\n straight through — not drawn, it holds the steps before the first branching\n router and merges branches back into shared steps (branches rejoin only by\n pointing `nextRouterId` at the same router).\n properties:\n id:\n type: string\n maxLength: 36\n pattern: '^[A-Za-z0-9_-]+

<!-- 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 pr…

\n description: |-\n Unique identifier for this router within the API. Use at most 36 letters, digits,\n underscores and hyphens: a longer id fails the save with 422 (\"Router id is missing or\n contains more than 36 characters or invalid characters. Please correct the router\n id.\"), as on flow routers.\n examples: [\"router_001\", \"validation_router\"]\n name:\n type: string\n description: Display name.\n examples: [\"Customer Type Router\", \"Validation Router\"]\n routeRecordsTo:\n type: string\n enum: [\"first_matching_branch\"]\n x-enumDescriptions:\n first_matching_branch: Routes the request to the first branch whose criteria match.\n description: |-\n Routing strategy; APIs only support `first_matching_branch`. Set together with\n `routeRecordsUsing` on a branching router; leave both unset on a one-branch\n pass-through router.\n examples: [\"first_matching_branch\"]\n x-celigo-ai-guidance:\n - Set this and `routeRecordsUsing` together on every router that decides between branches; setting only one of the two is rejected with 422 `invalid_flow_router_config`.\n - Leave both unset on a one-branch pass-through router (the steps before the first branching router, or a merge point). Setting them there still runs, but the builder shows a router node with one branch and no rules. A router with neither field and more than one branch is rejected with 422 `invalid_flow_router_branch`.\n routeRecordsUsing:\n type: string\n enum: [\"input_filters\", \"script\"]\n x-enumDescriptions:\n input_filters: Evaluate routing criteria using declarative filter expressions.\n script: Evaluate routing criteria using a custom JavaScript function.\n description: |-\n How a branching router decides which branch a request matches. Set together with\n `routeRecordsTo`; leave both unset on a one-branch pass-through router.\n examples: [\"input_filters\"]\n x-celigo-ai-guidance:\n - Set this and `routeRecordsTo` together on every router that decides between branches, and leave both unset on a one-branch pass-through router; the rejections for a half-set pair or a multi-branch pass-through are listed on `routeRecordsTo`.\n script:\n type: object\n description: |\n Script configuration when routeRecordsUsing=\"script\".\n properties:\n _scriptId:\n type: string\n format: objectId\n x-celigo-refModel: scripts\n description: Reference to the script resource.\n examples: [\"60a2c4e6f321d800129a1a3c\"]\n function:\n type: string\n description: Function name that returns the branch name.\n examples: [\"routeRequest\", \"determineBranch\"]\n branches:\n type: array\n description: Processing branches, evaluated in order.\n items:\n type: object\n properties:\n name:\n type: string\n description: Branch name\n examples: [\"Premium customers\"]\n inputFilter:\n type: object\n description: |-\n Filter criteria for branch selection. Send `version` and `rules` directly on\n `inputFilter` — not inside the `{type: \"expression\", expression: {...}}` wrapper\n that export and import filters use. A wrapped filter is saved without an error,\n but the branch is stored with no `inputFilter` and takes every request.\n x-celigo-ai-guidance:\n - Read the API back after saving a branching router and check that each filtered branch still has its `inputFilter`; a filter sent in the export-filter wrapper is dropped on save without an error.\n properties:\n version:\n type: string\n enum: [\"1\"]\n x-enumDescriptions:\n \"1\": Version 1 of the Celigo expression-based filter format.\n description: Version of the filter format used by `rules`.\n rules:\n type: array\n description: Celigo expression-based filter rules.\n items: {}\n examples:\n - [\"equals\", [\"string\", [\"extract\", \"customerType\"]], \"premium\"]\n - [\"and\", [\"notempty\", [\"string\", [\"extract\", \"orderId\"]]], [\"greaterthan\", [\"number\", [\"extract\", \"amount\"]], 1000]]\n x-celigo-ai-guidance:\n - |-\n Array-based DSL where the first element is an operator (e.g., \"equals\", \"and\", \"or\"),\n followed by operands which can be nested expressions.\n nextRouterId:\n type: string\n description: |-\n Next router to chain to, or \"apiRouter\" for the response router. Every branch needs\n one: a branch without it is saved, but every test run of the API is then refused\n with 400 \"One or more branches are not merged back. Review the setup to ensure all\n branches converge.\", whichever branch the request takes. An id that names no router\n fails the save with 422 \"Invalid nextRouterId is defined on the branch. Please\n correct the branch configuration.\"\n examples: [\"apiRouter\"]\n pageProcessors:\n type: array\n description: |-\n Processing steps to execute in this branch. An export is a step at most once in an\n API: using it again, on another branch or in the same one, fails the save with 422\n \"The export \u003cid> is already included as a lookup in your API and cannot be added\n again.\"\n items:\n type: object\n properties:\n type:\n type: string\n enum: [\"export\", \"import\"]\n x-enumDescriptions:\n export: Uses an export resource for data lookup or enrichment.\n import: Uses an import resource to send data to a destination system.\n description: Type of processor\n _exportId:\n type: string\n format: objectId\n x-celigo-refModel: exports\n description: Export resource reference (when type=\"export\").\n examples: [\"6878f4a43bc01652c09cdccc\"]\n _importId:\n type: string\n format: objectId\n x-celigo-refModel: imports\n description: Import resource reference (when type=\"import\").\n examples: [\"6878f4a43bc01652c09cddee\"]\n responseMapping:\n # The same step shape as a flow's pageProcessors[] (shared server-side), so the\n # envelope rules and the bare-array `fields`/`lists` warning apply unchanged.\n # A step saved without one is stored with {\"fields\": [], \"lists\": []}.\n $ref: '../flow-schemas/response-mapping.yml#/ResponseMapping'\n hooks:\n type: object\n description: Custom scripts for processing\n proceedOnFailure:\n type: boolean\n description: |-\n When true, the next step still runs after this step fails, and the request\n can still end in the `success` response (`context.success` stays true).\n When false, a failure stops the branch and the API returns its `fail`\n response.\n x-celigo-ai-guidance:\n - |-\n Each branch has its own criteria and processing steps. Requests are\n evaluated against branch criteria in the order defined.\n required:\n - branches\n\n x-celigo-ai-guidance:\n - |-\n Routers evaluate the request and route it to different processing branches.\n This enables complex business logic and conditional processing within your API.\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":8870,"content_sha256":"88bf6e050745a6f7a14b9751f340ea6b96cacc126b7a01fd63793e7be7725e07"},{"filename":"references/schemas/script.yml","content":"Script:\n type: object\n description: |-\n Script-mode configuration. Callers reach the API at `/v1/apis/{_id}/request`, and the\n referenced function is called with one `options` object: `method`, `headers` (the\n request's headers, without `Authorization`), `queryString` (the parsed query string),\n `body`, and `rawBody` (the body as received, a string). On a GET, `body` is absent and\n `rawBody` is empty. It returns `{statusCode, headers, body}`: `headers` is optional,\n and `body` may be any JSON value, an array included.\n properties:\n _scriptId:\n type: string\n format: objectId\n x-celigo-refModel: scripts\n description: Reference to the script resource containing the handler function.\n examples: [\"60a2c4e6f321d800129a1a3c\"]\n function:\n type: string\n description: Name of the function to invoke within the script.\n examples: [\"handleRequest\", \"processOrder\"]\n required:\n - _scriptId\n - function\n\n x-celigo-ai-guidance:\n - |-\n In script mode, you provide a custom JavaScript function that receives the `options`\n object (`method`, `headers` without `Authorization`, `queryString`, `body`, `rawBody`)\n and returns the response. This gives you complete control over the API behavior.\n - |-\n The function gets no `pathParams` or `queryParams`: read the query from `queryString`.\n A return without `statusCode`, or with a `null` or missing `body`, answers the caller\n 501 (\"'StatusCode' returned by the script cannot be undefined.\" or \"'body' returned by\n the script cannot be undefined or null.\"), and an error the function throws answers\n 422 with its message. Any other `statusCode` is sent as returned.\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":1727,"content_sha256":"d5e596fe4b952b3cd76c6be8c8b92b10cfad55fe92ee7b304027a76ed8bcbd85"},{"filename":"references/schemas/shipworks.yml","content":"Shipworks:\n type: object\n description: ShipWorks shipping-management credentials. Legacy feature.\n properties:\n username:\n type: string\n description: ShipWorks username.\n examples: [\"shipworks_user\"]\n password:\n type: string\n description: ShipWorks password. Masked as `\"******\"` in GET responses.\n examples: [\"******\"]\n x-celigo-ai-guidance:\n - |-\n When retrieving, this will be masked as \"******\" if a password is set.\n When creating/updating, provide the actual password value.\n required:\n - username\n - password\n\n x-celigo-ai-guidance:\n - |-\n ShipWorks is a shipping management platform. This configuration stores\n authentication credentials for ShipWorks-specific API integrations.\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":777,"content_sha256":"93aedca129d58973937427af38b912f712eae211b56dc591aa3b9cd1e23761b6"}],"content_json":{"type":"doc","content":[{"type":"paragraph","content":[{"text":"\u003c!-- TIER:1 -->","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"Building APIs","type":"text"}]},{"type":"paragraph","content":[{"text":"An API is a ","type":"text"},{"text":"RESTful endpoint","type":"text","marks":[{"type":"strong"}]},{"text":" 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:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Mode selection","type":"text","marks":[{"type":"strong"}]},{"text":" -- builder (visual configuration) vs script (full JavaScript control)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Request definition","type":"text","marks":[{"type":"strong"}]},{"text":" -- HTTP method, URI path, parameters, body schema, request transformation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Processing pipeline","type":"text","marks":[{"type":"strong"}]},{"text":" -- routers and page processors (lookups + imports) that execute business logic","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Response routing","type":"text","marks":[{"type":"strong"}]},{"text":" -- directing processed data to the correct response definition based on success/failure or custom conditions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Response shaping","type":"text","marks":[{"type":"strong"}]},{"text":" -- status codes, field mappings, body schema, hooks (preMap, postMap) on each response","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Response mapping","type":"text","marks":[{"type":"strong"}]},{"text":" -- extracting fields from each page processor's response back into the record for downstream steps. Configured on each ","type":"text"},{"text":"pageProcessors[]","type":"text","marks":[{"type":"code_inline"}]},{"text":" entry, same as in flows. For lookup exports the response has ","type":"text"},{"text":"data[]","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"errors[]","type":"text","marks":[{"type":"code_inline"}]},{"text":" (use ","type":"text"},{"text":"data[0].fieldName","type":"text","marks":[{"type":"code_inline"}]},{"text":" for single results). For imports use ","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the created record's ID and ","type":"text"},{"text":"_json.fieldName","type":"text","marks":[{"type":"code_inline"}]},{"text":" for response-body fields","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"postResponseMap hook","type":"text","marks":[{"type":"strong"}]},{"text":" -- JavaScript processing after response mapping, configured on ","type":"text"},{"text":"pageProcessors[]","type":"text","marks":[{"type":"code_inline"}]},{"text":" entries","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Used across integrations alongside flows and tools. APIs do not have their own authentication -- callers send a Celigo API token with full access or one scoped to the API (","type":"text"},{"text":"_apiIds","type":"text","marks":[{"type":"code_inline"}]},{"text":"; see ","type":"text"},{"text":"managing-api-tokens > Access Scope Decision Matrix","type":"text","marks":[{"type":"link","attrs":{"href":"../managing-api-tokens/SKILL.md#access-scope-decision-matrix","title":null}}]},{"text":") to ","type":"text"},{"text":"https://api.integrator.io/apis/{version}{relativeURI}","type":"text","marks":[{"type":"code_inline"}]},{"text":"; outbound calls to external systems use the connections referenced by exports/imports in the pipeline.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"The Request IS the Source Record","type":"text"}]},{"type":"paragraph","content":[{"text":"APIs are invoked by an external HTTP caller -- there is no upstream export, no scheduler, no listener feeding them. That has three design consequences:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The request body is the input shape.","type":"text","marks":[{"type":"strong"}]},{"text":" What the caller sends in the body is what downstream processing sees as the record. Path params, query params and headers aren't on ","type":"text"},{"text":"record","type":"text","marks":[{"type":"code_inline"}]},{"text":": only the ones the request declares reach the request ","type":"text"},{"text":"transform","type":"text","marks":[{"type":"code_inline"}]},{"text":" (as ","type":"text"},{"text":"{{pathParams.\u003ckey>}}","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"{{queryParams.\u003ckey>}}","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"{{headers.\u003ckey>}}","type":"text","marks":[{"type":"code_inline"}]},{"text":"), and later steps see only what the transform writes onto the record. There is no upstream pipeline to reshape the body first -- use the request ","type":"text"},{"text":"transform","type":"text","marks":[{"type":"code_inline"}]},{"text":" if it needs reshaping before routing.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The response stage is the output.","type":"text","marks":[{"type":"strong"}]},{"text":" Whatever the selected response definition produces is exactly what the caller receives. Nothing runs after it.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No self-starting.","type":"text","marks":[{"type":"strong"}]},{"text":" APIs have no ","type":"text"},{"text":"schedule","type":"text","marks":[{"type":"code_inline"}]},{"text":", no listener, and none of the flow-level runtime controls (","type":"text"},{"text":"skipRetries","type":"text","marks":[{"type":"code_inline"}]},{"text":", chaining). \"Every night at 2 AM, do X\" is a flow -- possibly one that ","type":"text"},{"text":"calls","type":"text","marks":[{"type":"em"}]},{"text":" the API, but the schedule lives on the flow. Retry-after-failure is the caller's decision. A page processor's ","type":"text"},{"text":"proceedOnFailure","type":"text","marks":[{"type":"code_inline"}]},{"text":" does apply (see ","type":"text"},{"text":"Response selection","type":"text","marks":[{"type":"link","attrs":{"href":"#response-selection----success-fail-custom","title":null}}]},{"text":").","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"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.","type":"text"}]},{"type":"paragraph","content":[{"text":"A top-level ","type":"text"},{"text":"disabled: true","type":"text","marks":[{"type":"code_inline"}]},{"text":" takes the API offline without deleting it -- callers get a 404 until it's re-enabled.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"API Modes","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Builder Mode (","type":"text"},{"text":"type: \"builder\"","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]},{"type":"paragraph","content":[{"text":"Visual configuration with discrete components:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"API (type: \"builder\")\n+-- request -- method, relativeURI, params, bodySchema, mockRequest, transform\n+-- routers[] -- optional processing pipeline (same structure as flow routers)\n| +-- branches[]\n| +-- inputFilter -- when to use this branch (s-expression rules)\n| +-- pageProcessors[] -- lookups (exports) and imports\n| +-- nextRouterId -- chain to next router, or \"apiRouter\" to finish\n+-- responseRouter -- id=\"apiRouter\", routes processed data to a response\n+-- responses[] -- success, fail, custom -- each with statusCode, inputFilter, mappings","type":"text"}]},{"type":"paragraph","content":[{"text":"The incoming HTTP request ","type":"text"},{"text":"replaces the export","type":"text","marks":[{"type":"strong"}]},{"text":" as data source. Routers and page processors work identically to flows.","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"API Execution Pipeline (Builder Mode)","type":"text"}]},{"type":"paragraph","content":[{"text":"When an API receives a request:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Request received","type":"text","marks":[{"type":"strong"}]},{"text":" -- method + path matched against the API endpoint definition","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Request transform","type":"text","marks":[{"type":"strong"}]},{"text":" (optional) -- reshapes the incoming request body before routing","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Router evaluation","type":"text","marks":[{"type":"strong"}]},{"text":" -- ","type":"text"},{"text":"routeRecordsUsing","type":"text","marks":[{"type":"code_inline"}]},{"text":" evaluates branch input filter conditions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Branch selection","type":"text","marks":[{"type":"strong"}]},{"text":" -- first matching branch processes the request","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Page processors","type":"text","marks":[{"type":"strong"}]},{"text":" -- each processor in the branch executes sequentially (export lookups, import writes)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Response mapping","type":"text","marks":[{"type":"strong"}]},{"text":" -- ","type":"text"},{"text":"responseMapping","type":"text","marks":[{"type":"code_inline"}]},{"text":" on each processor carries data forward to the next processor","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Response router","type":"text","marks":[{"type":"strong"}]},{"text":" -- ","type":"text"},{"text":"responseRouter","type":"text","marks":[{"type":"code_inline"}]},{"text":" (id=\"apiRouter\") selects which response template to use based on response input filters","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Response","type":"text","marks":[{"type":"strong"}]},{"text":" -- selected response template returned to the caller with its statusCode, headers, and body","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Script Mode (","type":"text"},{"text":"type: \"script\"","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]},{"type":"paragraph","content":[{"text":"A single JavaScript function (","type":"text"},{"text":"handleRequest","type":"text","marks":[{"type":"code_inline"}]},{"text":") gets ","type":"text"},{"text":"options.method","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"headers","type":"text","marks":[{"type":"code_inline"}]},{"text":" (without ","type":"text"},{"text":"Authorization","type":"text","marks":[{"type":"code_inline"}]},{"text":"), ","type":"text"},{"text":"queryString","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"body","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"rawBody","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- no ","type":"text"},{"text":"pathParams","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"queryParams","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- and returns ","type":"text"},{"text":"{statusCode, headers, body}","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"writing-scripts > Routing and Handlers","type":"text","marks":[{"type":"link","attrs":{"href":"../writing-scripts/SKILL.md#routing-and-handlers","title":null}}]},{"text":"). Callers reach it at ","type":"text"},{"text":"/v1/apis/\u003c_id>/request","type":"text","marks":[{"type":"code_inline"}]},{"text":". Complete control with no visual configuration.","type":"text"}]},{"type":"paragraph","content":[{"text":"Legacy script APIs","type":"text","marks":[{"type":"strong"}]},{"text":", made before builder mode, can still turn up in an account: they have no ","type":"text"},{"text":"type","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"version","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"disabled","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"builder","type":"text","marks":[{"type":"code_inline"}]},{"text":", only the top-level ","type":"text"},{"text":"_scriptId","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"function","type":"text","marks":[{"type":"code_inline"}]},{"text":" that script-mode APIs also carry as read-only copies. If ","type":"text"},{"text":"type","type":"text","marks":[{"type":"code_inline"}]},{"text":" is absent and ","type":"text"},{"text":"_scriptId","type":"text","marks":[{"type":"code_inline"}]},{"text":" is present, it's legacy. You can't create one: a create without ","type":"text"},{"text":"type","type":"text","marks":[{"type":"code_inline"}]},{"text":" makes a script-mode API (422 \"Script type apis must have valid scriptId and function.\" when it has no ","type":"text"},{"text":"script","type":"text","marks":[{"type":"code_inline"}]},{"text":"), and the mode can't be changed later.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Reference","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Decision Matrix","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Scenario","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Mode","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Why","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Standard lookup/write with structured response","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Builder","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Visual debugging, test runs, structured responses","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Multiple response shapes based on success/failure","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Builder","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Response router + inputFilter handles this declaratively","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Complex conditional logic or custom auth validation","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Script","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Full JavaScript control over request/response","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Dynamic routing that can't be expressed as input filters","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Script","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"handleRequest","type":"text","marks":[{"type":"code_inline"}]},{"text":" can implement arbitrary logic","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Proxy through an authenticated connection","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Builder","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Wire the connection's export/import as a page processor","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Simple webhook receiver that transforms and forwards","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Builder","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Single router, single branch, one import","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Minimum Required Fields","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Mode","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Required Fields","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Builder","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"type: \"builder\"","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"builder.request","type":"text","marks":[{"type":"code_inline"}]},{"text":" (method + relativeURI)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Script","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"type: \"script\"","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"script._scriptId","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"script.function","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Schema Index","type":"text"}]},{"type":"paragraph","content":[{"text":"All schemas are in ","type":"text"},{"text":"references/schemas/","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/","title":null}}]},{"text":":","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Schema","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"What it defines","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"request.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/request.yml","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Top-level API fields (name, type, version, disabled, builder/script refs)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"response.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/response.yml","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"API response shape","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"builder.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/builder.yml","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Builder configuration (request, routers, responseRouter, responses refs)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"api-request.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/api-request.yml","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Request config (method, relativeURI, params, bodySchema, mockRequest, transform)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"api-response.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/api-response.yml","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Response definitions (id, name, type, statusCode, inputFilter, mappings, hooks)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"response-router.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/response-router.yml","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Response router (id=\"apiRouter\", routeRecordsUsing)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"router.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/router.yml","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Routers (branches, inputFilter, pageProcessors)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"script.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/script.yml","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Script config (_scriptId, function)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"apim.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/apim.yml","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"APIM metadata (publication status)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"shipworks.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/shipworks.yml","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Legacy ShipWorks auth","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Related Skills","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"configuring-exports > Quick Reference","type":"text","marks":[{"type":"link","attrs":{"href":"../configuring-exports/SKILL.md#quick-reference","title":null}}]},{"text":" -- building lookup exports used as page processors in the API pipeline","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"configuring-imports > Quick Reference","type":"text","marks":[{"type":"link","attrs":{"href":"../configuring-imports/SKILL.md#quick-reference","title":null}}]},{"text":" -- building imports used as page processors in the API pipeline","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"building-flows > How to Build a Flow","type":"text","marks":[{"type":"link","attrs":{"href":"../building-flows/SKILL.md#how-to-build-a-flow","title":null}}]},{"text":" -- flows share the same router/branch/pageProcessor pipeline mechanics","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"writing-scripts > Quick Reference","type":"text","marks":[{"type":"link","attrs":{"href":"../writing-scripts/SKILL.md#quick-reference","title":null}}]},{"text":" -- writing ","type":"text"},{"text":"handleRequest","type":"text","marks":[{"type":"code_inline"}]},{"text":" (script-mode APIs), ","type":"text"},{"text":"preMap","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"postMap","type":"text","marks":[{"type":"code_inline"}]},{"text":" hooks, and ","type":"text"},{"text":"postResponseMap","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"writing-handlebars > Quick Reference","type":"text","marks":[{"type":"link","attrs":{"href":"../writing-handlebars/SKILL.md#quick-reference","title":null}}]},{"text":" -- dynamic expressions in request bodies, URIs, and response mappings","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"configuring-filters > Quick Reference","type":"text","marks":[{"type":"link","attrs":{"href":"../configuring-filters/SKILL.md#quick-reference","title":null}}]},{"text":" -- input filters on router branches to conditionally route records","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"\u003c!-- TIER:2 -->","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"How to Build an API","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. Plan what the API needs to do","type":"text"}]},{"type":"paragraph","content":[{"text":"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.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Decide the mode","type":"text"}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"builder","type":"text","marks":[{"type":"strong"}]},{"text":" for most APIs -- it provides visual debugging, test runs, and structured responses. Use ","type":"text"},{"text":"script","type":"text","marks":[{"type":"strong"}]},{"text":" only when the processing logic is too dynamic for the visual pipeline (e.g., complex conditional responses, custom auth validation, dynamic routing).","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. Check for existing resources","type":"text"}]},{"type":"paragraph","content":[{"text":"Look for connections, exports, and imports that can be reused before creating new ones.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Search across all resource types in the account\nceligo account search \"\u003ckeyword>\"\n\n# Show what an existing API uses -- the local index records no edges out of APIs\nceligo apis connections \u003cid>\nceligo apis get \u003cid>\n\n# Find orphaned resources that could be reused\nceligo account lint\n\n# Search for APIs already in the account for patterns\nceligo apis list | grep -i \"\u003ckeyword>\"\n\n# Check existing exports/imports that could serve as pipeline steps\nceligo exports list | grep -i \"\u003csystem-name>\"\nceligo imports list | grep -i \"\u003csystem-name>\"\n\n# Search marketplace for pre-built integration templates\nceligo templates marketplace","type":"text"}]},{"type":"paragraph","content":[{"text":"The account index auto-refreshes when stale (older than 15 minutes by default). Force a fresh snapshot with ","type":"text"},{"text":"celigo account snapshot","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4. Create the supporting resources (bottom-up)","type":"text"}]},{"type":"paragraph","content":[{"text":"APIs reference exports and imports as page processors -- these must exist before you can attach them. Build order:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Connections","type":"text","marks":[{"type":"strong"}]},{"text":" -- create or reuse connections to the target systems","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Exports","type":"text","marks":[{"type":"strong"}]},{"text":" -- for lookups that query external systems (use ","type":"text"},{"text":"configuring-exports","type":"text","marks":[{"type":"code_inline"}]},{"text":" skill)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Imports","type":"text","marks":[{"type":"strong"}]},{"text":" -- for writes to external systems (use ","type":"text"},{"text":"configuring-imports","type":"text","marks":[{"type":"code_inline"}]},{"text":" skill)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"5. Define the request (builder mode)","type":"text"}]},{"type":"paragraph","content":[{"text":"Choose the HTTP method (","type":"text"},{"text":"GET","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"POST","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"PUT","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"PATCH","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"DELETE","type":"text","marks":[{"type":"code_inline"}]},{"text":"; GET and POST are most common) and URI path.","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Path parameters use colon notation: ","type":"text"},{"text":"/customers/:id","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Declare the query parameters, path parameters and headers the API reads (","type":"text"},{"text":"queryParams","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"pathParams","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"headers","type":"text","marks":[{"type":"code_inline"}]},{"text":"; header keys in lowercase): only declared ones reach the request ","type":"text"},{"text":"transform","type":"text","marks":[{"type":"code_inline"}]},{"text":". Add the body schema too (every object node in a ","type":"text"},{"text":"bodySchema","type":"text","marks":[{"type":"code_inline"}]},{"text":" needs properties; Gotcha 11)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Save a ","type":"text"},{"text":"mockRequest","type":"text","marks":[{"type":"code_inline"}]},{"text":" as a sample request, but send the test request explicitly when you test (step 10)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Optionally add a request ","type":"text"},{"text":"transform","type":"text","marks":[{"type":"code_inline"}]},{"text":" (expression-based or script-based) to reshape incoming data before processing","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"6. Build the processing pipeline","type":"text"}]},{"type":"paragraph","content":[{"text":"The pipeline is made of routers, branches, and page processors. See ","type":"text"},{"text":"router.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/router.yml","title":null}}]},{"text":" for the full schema.","type":"text"}]},{"type":"paragraph","content":[{"text":"Routers are optional: an API that only reshapes the request can go straight to the response router, with the responses mapping request fields. Once the API calls other systems, add a 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.","type":"text"}]},{"type":"paragraph","content":[{"text":"For pass-through routers (single branch, no filters, just linear steps before a branching router), omit ","type":"text"},{"text":"routeRecordsTo","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"routeRecordsUsing","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- including them makes it appear as a filter-based branch in the UI. Leaving both unset is the pass-through shape; the server fills in no default.","type":"text"}]},{"type":"paragraph","content":[{"text":"A branch's ","type":"text"},{"text":"inputFilter","type":"text","marks":[{"type":"code_inline"}]},{"text":", like a response's, is ","type":"text"},{"text":"{\"version\": \"1\", \"rules\": [...]}","type":"text","marks":[{"type":"code_inline"}]},{"text":", and its ","type":"text"},{"text":"rules","type":"text","marks":[{"type":"code_inline"}]},{"text":" use s-expression syntax: ","type":"text"},{"text":"[\"operator\", [\"type\", [\"extract\", \"field\"]], value]","type":"text","marks":[{"type":"code_inline"}]},{"text":". Type wrappers (","type":"text"},{"text":"string","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"number","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"boolean","type":"text","marks":[{"type":"code_inline"}]},{"text":") are required around ","type":"text"},{"text":"extract","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"context","type":"text","marks":[{"type":"code_inline"}]},{"text":" accessors. Logical combinators: ","type":"text"},{"text":"[\"and\", cond1, cond2]","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"[\"or\", cond1, cond2]","type":"text","marks":[{"type":"code_inline"}]},{"text":". Don't send the ","type":"text"},{"text":"{\"type\": \"expression\", \"expression\": {...}}","type":"text","marks":[{"type":"code_inline"}]},{"text":" form that export and import filters use (Gotcha 13).","type":"text"}]},{"type":"paragraph","content":[{"text":"Every branch sets ","type":"text"},{"text":"nextRouterId","type":"text","marks":[{"type":"code_inline"}]},{"text":": another router's ","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":"\"apiRouter\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" to reach the response router. A branch without one saves, but every test run is then refused with 400 \"One or more branches are not merged back. Review the setup to ensure all branches converge.\", whichever branch the request takes.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"7. Configure responses","type":"text"}]},{"type":"paragraph","content":[{"text":"Every builder API needs exactly one ","type":"text"},{"text":"success","type":"text","marks":[{"type":"code_inline"}]},{"text":" response and one ","type":"text"},{"text":"fail","type":"text","marks":[{"type":"code_inline"}]},{"text":" response. Add ","type":"text"},{"text":"custom","type":"text","marks":[{"type":"code_inline"}]},{"text":" responses for specific scenarios (e.g., 404 not found, 422 validation error), and list them before ","type":"text"},{"text":"success","type":"text","marks":[{"type":"code_inline"}]},{"text":": the first response in ","type":"text"},{"text":"responses[]","type":"text","marks":[{"type":"code_inline"}]},{"text":" whose ","type":"text"},{"text":"inputFilter","type":"text","marks":[{"type":"code_inline"}]},{"text":" matches is the one returned, and a response without an ","type":"text"},{"text":"inputFilter","type":"text","marks":[{"type":"code_inline"}]},{"text":" matches every request.","type":"text"}]},{"type":"paragraph","content":[{"text":"Each response has:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"statusCode","type":"text","marks":[{"type":"code_inline"}]},{"text":" (HTTP status code)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"inputFilter","type":"text","marks":[{"type":"code_inline"}]},{"text":" to determine when it's selected (optional; see ","type":"text"},{"text":"Response selection","type":"text","marks":[{"type":"link","attrs":{"href":"#response-selection----success-fail-custom","title":null}}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"mappings","type":"text","marks":[{"type":"code_inline"}]},{"text":" to shape the response body from the processed record. The body is a JSON object, so put a list under a property (","type":"text"},{"text":"{\"records\": [...]}","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Optional ","type":"text"},{"text":"bodySchema","type":"text","marks":[{"type":"code_inline"}]},{"text":" for documentation, ","type":"text"},{"text":"headers","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"lookups","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"hooks","type":"text","marks":[{"type":"code_inline"}]},{"text":" (preMap, postMap)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Optional ","type":"text"},{"text":"mockInput","type":"text","marks":[{"type":"code_inline"}]},{"text":" for trying the mappings, in the record-page format ","type":"text"},{"text":"{\"page_of_records\": [{\"record\": {...}}]}","type":"text","marks":[{"type":"code_inline"}]},{"text":"; any other object is refused with 422","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"To return a large lookup result one page per call, see ","type":"text"},{"text":"references/pagination.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/pagination.md","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"8. Configure the response router","type":"text"}]},{"type":"paragraph","content":[{"text":"Set ","type":"text"},{"text":"id: \"apiRouter\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" and choose routing method:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"input_filters","type":"text","marks":[{"type":"code_inline"}]},{"text":" (default) -- evaluates each response's ","type":"text"},{"text":"inputFilter","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"script","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- custom JavaScript returns the response ","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]},{"text":" to use","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"9. Build the JSON","type":"text"}]},{"type":"paragraph","content":[{"text":"Reference the ","type":"text"},{"text":"Schema Index","type":"text","marks":[{"type":"link","attrs":{"href":"#schema-index","title":null}}]},{"text":" above for exact field schemas.","type":"text"}]},{"type":"paragraph","content":[{"text":"Every API needs at minimum: ","type":"text"},{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"type","type":"text","marks":[{"type":"code_inline"}]},{"text":", and either ","type":"text"},{"text":"builder","type":"text","marks":[{"type":"code_inline"}]},{"text":" (with ","type":"text"},{"text":"request","type":"text","marks":[{"type":"code_inline"}]},{"text":") or ","type":"text"},{"text":"script","type":"text","marks":[{"type":"code_inline"}]},{"text":" (with ","type":"text"},{"text":"_scriptId","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"function","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]},{"type":"paragraph","content":[{"text":"A complete builder API with one lookup, a 404 for \"not found\", a nested success body and the fail response. ","type":"text"},{"text":"request","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"routers","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"responseRouter","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"responses","type":"text","marks":[{"type":"code_inline"}]},{"text":" are siblings under ","type":"text"},{"text":"builder","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"name\": \"Get item stock\",\n \"type\": \"builder\",\n \"version\": \"v1\",\n \"disabled\": false,\n \"_integrationId\": \"\u003cintegration id>\",\n \"builder\": {\n \"request\": {\"method\": \"POST\", \"relativeURI\": \"/items/stock\"},\n \"routers\": [{\n \"id\": \"lookup_item\",\n \"branches\": [{\n \"name\": \"Look up the item\",\n \"pageProcessors\": [{\n \"type\": \"export\",\n \"_exportId\": \"\u003clookup export id>\",\n \"responseMapping\": {\"fields\": [{\"extract\": \"data[0]\", \"generate\": \"item\"}]}\n }],\n \"nextRouterId\": \"apiRouter\"\n }]\n }],\n \"responseRouter\": {\"id\": \"apiRouter\", \"routeRecordsUsing\": \"input_filters\"},\n \"responses\": [\n {\n \"id\": \"notFound\", \"name\": \"Not found\", \"type\": \"custom\", \"statusCode\": 404,\n \"inputFilter\": {\"version\": \"1\", \"rules\": [\"empty\", [\"string\", [\"extract\", \"item.sku\"]]]},\n \"mappings\": [{\"generate\": \"error\", \"dataType\": \"string\", \"hardCodedValue\": \"Item not found\", \"status\": \"Active\"}]\n },\n {\n \"id\": \"success\", \"name\": \"Success\", \"type\": \"success\", \"statusCode\": 200,\n \"mappings\": [{\n \"generate\": \"item\", \"dataType\": \"object\", \"status\": \"Active\",\n \"mappings\": [\n {\"generate\": \"sku\", \"dataType\": \"string\", \"extract\": \"$.item.sku\", \"status\": \"Active\"},\n {\"generate\": \"onHand\", \"dataType\": \"number\", \"extract\": \"$.item.onHand\", \"status\": \"Active\"}\n ]\n }]\n },\n {\"id\": \"fail\", \"name\": \"Fail\", \"type\": \"fail\", \"statusCode\": 500, \"mappings\": []}\n ]\n }\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"The lookup's first record lands on the record as ","type":"text"},{"text":"item","type":"text","marks":[{"type":"code_inline"}]},{"text":", the custom 404 comes first so it answers when the lookup found nothing, and ","type":"text"},{"text":"success","type":"text","marks":[{"type":"code_inline"}]},{"text":" (no filter) answers everything else.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"10. Test the API","type":"text"}]},{"type":"paragraph","content":[{"text":"Send the test request explicitly as ","type":"text"},{"text":"{\"mockRequest\": {\"body\": {...}}}","type":"text","marks":[{"type":"code_inline"}]},{"text":", in a file passed to ","type":"text"},{"text":"celigo apis test-run \u003cid> --file \u003cpath>","type":"text","marks":[{"type":"code_inline"}]},{"text":". To test with the sample saved on the API, read the API first and send its ","type":"text"},{"text":"builder.request.mockRequest","type":"text","marks":[{"type":"code_inline"}]},{"text":" as that ","type":"text"},{"text":"mockRequest","type":"text","marks":[{"type":"code_inline"}]},{"text":": a run without one starts from an empty record, and the saved one isn't replayed. The body must be a JSON object: an array is refused with 400 \"Mock request body should be a plain object.\" Only the body becomes the record. A test request's ","type":"text"},{"text":"pathParams","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"queryParams","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"headers","type":"text","marks":[{"type":"code_inline"}]},{"text":" reach the request ","type":"text"},{"text":"transform","type":"text","marks":[{"type":"code_inline"}]},{"text":" the way a real call's do: the keys the request declares, and no others. The run's ","type":"text"},{"text":"metadata","type":"text","marks":[{"type":"code_inline"}]},{"text":" names the response it selected: ","type":"text"},{"text":"success","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"fail","type":"text","marks":[{"type":"code_inline"}]},{"text":" or a custom response's ","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]},{"text":" is one of its keys.","type":"text"}]},{"type":"paragraph","content":[{"text":"A lookup export without a saved ","type":"text"},{"text":"mockOutput","type":"text","marks":[{"type":"code_inline"}]},{"text":" calls its live system in a test run, and the run then saves that response on the export as its ","type":"text"},{"text":"mockOutput","type":"text","marks":[{"type":"code_inline"}]},{"text":". Every later test run that uses the export -- from any API or Tool, whatever the request -- answers the lookup from that saved ","type":"text"},{"text":"mockOutput","type":"text","marks":[{"type":"code_inline"}]},{"text":" instead of calling the system; real calls aren't affected. So when test runs keep returning the same lookup result for different requests, read the export's ","type":"text"},{"text":"mockOutput","type":"text","marks":[{"type":"code_inline"}]},{"text":", and remove it to test against live data again. Imports never send in a test run: each returns its saved ","type":"text"},{"text":"mockResponse","type":"text","marks":[{"type":"code_inline"}]},{"text":" or a placeholder (","type":"text"},{"text":"configuring-imports > Testing an Import Without Writing","type":"text","marks":[{"type":"link","attrs":{"href":"../configuring-imports/SKILL.md#testing-an-import-without-writing","title":null}}]},{"text":"). So a passing run proves the request shaping and routing but not the writes, and it never shows up in request history. Only a real call to the endpoint does.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"The Response and Routing Model","type":"text"}]},{"type":"paragraph","content":[{"text":"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.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Branch selection and router chaining","type":"text"}]},{"type":"paragraph","content":[{"text":"APIs support a single routing strategy: ","type":"text"},{"text":"first_matching_branch","type":"text","marks":[{"type":"code_inline"}]},{"text":". Within a router, each record is evaluated against the branches in order and taken by the ","type":"text"},{"text":"first","type":"text","marks":[{"type":"em"}]},{"text":" branch whose ","type":"text"},{"text":"inputFilter","type":"text","marks":[{"type":"code_inline"}]},{"text":" matches; that record then follows only that branch. (Flows also offer ","type":"text"},{"text":"all_matching_branches","type":"text","marks":[{"type":"code_inline"}]},{"text":", which fans one record out to every matching branch -- APIs never do this. A record takes exactly one branch per router.)","type":"text"}]},{"type":"paragraph","content":[{"text":"Each branch's ","type":"text"},{"text":"nextRouterId","type":"text","marks":[{"type":"code_inline"}]},{"text":" decides where the record goes after that branch's page processors finish:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Another router's ","type":"text","marks":[{"type":"strong"}]},{"text":"id","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" -- chain into that router for a further stage of processing.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"apiRouter\"","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" -- hand off to the response router (whose reserved ","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]},{"text":" is always ","type":"text"},{"text":"apiRouter","type":"text","marks":[{"type":"code_inline"}]},{"text":") to finish.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Chaining lets you express sequential stages where each stage branches independently; every branch ends at another router or at ","type":"text"},{"text":"\"apiRouter\"","type":"text","marks":[{"type":"code_inline"}]},{"text":", the response router. An id that names no router fails the save with 422 \"Invalid nextRouterId is defined on the branch. Please correct the branch configuration.\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Response selection -- success, fail, custom","type":"text"}]},{"type":"paragraph","content":[{"text":"Every builder API has exactly one ","type":"text"},{"text":"success","type":"text","marks":[{"type":"code_inline"}]},{"text":" response, exactly one ","type":"text"},{"text":"fail","type":"text","marks":[{"type":"code_inline"}]},{"text":" response, and zero or more ","type":"text"},{"text":"custom","type":"text","marks":[{"type":"code_inline"}]},{"text":" responses (the response's ","type":"text"},{"text":"type","type":"text","marks":[{"type":"code_inline"}]},{"text":" field). The response router (","type":"text"},{"text":"id: \"apiRouter\"","type":"text","marks":[{"type":"code_inline"}]},{"text":") picks one after processing completes:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"success","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" -- the happy path. It isn't a fallback checked after the ","type":"text"},{"text":"custom","type":"text","marks":[{"type":"code_inline"}]},{"text":" responses: like any response, it's returned when it's the first in the list whose ","type":"text"},{"text":"inputFilter","type":"text","marks":[{"type":"code_inline"}]},{"text":" matches. Conventionally a 2xx ","type":"text"},{"text":"statusCode","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"200","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":"201","type":"text","marks":[{"type":"code_inline"}]},{"text":" when the API created something).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"fail","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" -- the error path. A step that fails (a lookup returned a 500, an import got a 4xx, a script threw) stops the request, which gets the ","type":"text"},{"text":"fail","type":"text","marks":[{"type":"code_inline"}]},{"text":" response. Set ","type":"text"},{"text":"proceedOnFailure: true","type":"text","marks":[{"type":"code_inline"}]},{"text":" on a page processor to let the next step run after it fails instead; the request can then still end in ","type":"text"},{"text":"success","type":"text","marks":[{"type":"code_inline"}]},{"text":", because ","type":"text"},{"text":"context.success","type":"text","marks":[{"type":"code_inline"}]},{"text":" stays true. Conventionally a 4xx/5xx ","type":"text"},{"text":"statusCode","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"400","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"500","type":"text","marks":[{"type":"code_inline"}]},{"text":"); its ","type":"text"},{"text":"mappings","type":"text","marks":[{"type":"code_inline"}]},{"text":" surface the error message and any context the caller needs.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"custom","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" -- a non-error, non-default response selected by its own ","type":"text"},{"text":"inputFilter","type":"text","marks":[{"type":"code_inline"}]},{"text":". Reach for one when the outcome fits neither ","type":"text"},{"text":"success","type":"text","marks":[{"type":"code_inline"}]},{"text":" nor ","type":"text"},{"text":"fail","type":"text","marks":[{"type":"code_inline"}]},{"text":", when the ","type":"text"},{"text":"statusCode","type":"text","marks":[{"type":"code_inline"}]},{"text":" differs, or when the body shape differs. Typical cases:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"404","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" not found","type":"text","marks":[{"type":"strong"}]},{"text":" -- the lookup ran but returned zero records (filter: the results array is empty).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"409","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" conflict","type":"text","marks":[{"type":"strong"}]},{"text":" -- the destination rejected a create because the record already exists.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"202","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" accepted","type":"text","marks":[{"type":"strong"}]},{"text":" -- processing started a background job; tell the caller \"received, working on it.\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Conditional body","type":"text","marks":[{"type":"strong"}]},{"text":" -- a different shape driven by a query parameter (e.g. ","type":"text"},{"text":"?format=summary","type":"text","marks":[{"type":"code_inline"}]},{"text":" vs ","type":"text"},{"text":"?format=full","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]}]}]}]}]},{"type":"paragraph","content":[{"text":"In ","type":"text"},{"text":"input_filters","type":"text","marks":[{"type":"code_inline"}]},{"text":" mode the response router returns the first response, in list order, whose ","type":"text"},{"text":"inputFilter","type":"text","marks":[{"type":"code_inline"}]},{"text":" matches, and a response without an ","type":"text"},{"text":"inputFilter","type":"text","marks":[{"type":"code_inline"}]},{"text":" matches every request. So list ","type":"text"},{"text":"custom","type":"text","marks":[{"type":"code_inline"}]},{"text":" responses ahead of ","type":"text"},{"text":"success","type":"text","marks":[{"type":"code_inline"}]},{"text":". A ","type":"text"},{"text":"success","type":"text","marks":[{"type":"code_inline"}]},{"text":" listed first answers every request that didn't fail, even one a later custom filter matches, and giving it the typed filter ","type":"text"},{"text":"[\"equals\", [\"boolean\", [\"context\", \"success\"]], true]","type":"text","marks":[{"type":"code_inline"}]},{"text":" doesn't change that: ","type":"text"},{"text":"context.success","type":"text","marks":[{"type":"code_inline"}]},{"text":" is true whenever no step failed, including when a lookup found nothing. In ","type":"text"},{"text":"script","type":"text","marks":[{"type":"code_inline"}]},{"text":" mode a JavaScript function inspects the record and returns the response ","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]},{"text":" to use.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"statusCode","type":"text","marks":[{"type":"code_inline"}]},{"text":" vs the response ","type":"text"},{"text":"type","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"These are independent and often conflated:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The response ","type":"text"},{"text":"type","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" (","type":"text"},{"text":"success","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"fail","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"custom","type":"text","marks":[{"type":"code_inline"}]},{"text":") is Celigo's internal classification -- it drives which response the response router selects.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The ","type":"text"},{"text":"statusCode","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" is the HTTP status the caller receives -- it lives on the response definition.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"A ","type":"text"},{"text":"custom","type":"text","marks":[{"type":"code_inline"}]},{"text":" response can carry any ","type":"text"},{"text":"statusCode","type":"text","marks":[{"type":"code_inline"}]},{"text":" (the \"not found\" response returns ","type":"text"},{"text":"404","type":"text","marks":[{"type":"code_inline"}]},{"text":"; the \"async accepted\" response returns ","type":"text"},{"text":"202","type":"text","marks":[{"type":"code_inline"}]},{"text":"), and ","type":"text"},{"text":"success","type":"text","marks":[{"type":"code_inline"}]},{"text":" is conventionally 2xx but doesn't have to be. So \"return a ","type":"text"},{"text":"404","type":"text","marks":[{"type":"code_inline"}]},{"text":" when the customer isn't found\" means adding a ","type":"text"},{"text":"custom","type":"text","marks":[{"type":"code_inline"}]},{"text":" response with ","type":"text"},{"text":"statusCode: 404","type":"text","marks":[{"type":"code_inline"}]},{"text":" and an ","type":"text"},{"text":"inputFilter","type":"text","marks":[{"type":"code_inline"}]},{"text":" that matches when the lookup's results array is empty -- not editing the ","type":"text"},{"text":"success","type":"text","marks":[{"type":"code_inline"}]},{"text":" response.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"The two mapping layers","type":"text"}]},{"type":"paragraph","content":[{"text":"\"Mapping\" refers to two different things at two layers, and conflating them is the most common source of confusion when building APIs.","type":"text"}]},{"type":"paragraph","content":[{"text":"1. Page-processor ","type":"text","marks":[{"type":"strong"}]},{"text":"responseMapping","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" (record enrichment).","type":"text","marks":[{"type":"strong"}]},{"text":" Configured on a lookup or import inside a router branch -- the same shape as a flow's page-processor ","type":"text"},{"text":"responseMapping","type":"text","marks":[{"type":"code_inline"}]},{"text":". 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 ","type":"text"},{"text":"not","type":"text","marks":[{"type":"strong"}]},{"text":" shape the HTTP body.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"{\n \"fields\": [\n {\"extract\": \"data[0].id\", \"generate\": \"customerId\"},\n {\"extract\": \"data[0].accountStatus\", \"generate\": \"status\"}\n ]\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"2. Response-stage ","type":"text","marks":[{"type":"strong"}]},{"text":"mappings","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" (HTTP body).","type":"text","marks":[{"type":"strong"}]},{"text":" Configured on a ","type":"text"},{"text":"success","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"fail","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"custom","type":"text","marks":[{"type":"code_inline"}]},{"text":" response (alongside its ","type":"text"},{"text":"lookups","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"hooks","type":"text","marks":[{"type":"code_inline"}]},{"text":"). It reads the now-enriched record and builds the HTTP response body returned to the caller.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"{\n \"mappings\": [\n {\n \"generate\": \"data\",\n \"dataType\": \"object\",\n \"status\": \"Active\",\n \"mappings\": [\n {\"extract\": \"$.customerId\", \"generate\": \"id\", \"dataType\": \"string\", \"status\": \"Active\"},\n {\"extract\": \"$.status\", \"generate\": \"status\", \"dataType\": \"string\", \"status\": \"Active\"}\n ]\n }\n ]\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"Response mappings are Mapper 2.0 rows. Every row needs ","type":"text"},{"text":"dataType","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"status","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- a save without ","type":"text"},{"text":"dataType","type":"text","marks":[{"type":"code_inline"}]},{"text":" fails with 422 ","type":"text"},{"text":"Path 'dataType' is required","type":"text","marks":[{"type":"code_inline"}]},{"text":", once per row that omits it, and a row without ","type":"text"},{"text":"status","type":"text","marks":[{"type":"code_inline"}]},{"text":" fails with 422 ","type":"text"},{"text":"Mapping object must have status field present.","type":"text","marks":[{"type":"code_inline"}]},{"text":" An ","type":"text"},{"text":"extract","type":"text","marks":[{"type":"code_inline"}]},{"text":" reads a field only as a ","type":"text"},{"text":"$.","type":"text","marks":[{"type":"code_inline"}]},{"text":" path or Handlebars: a bare string, ","type":"text"},{"text":"record.customerId","type":"text","marks":[{"type":"code_inline"}]},{"text":" included, is written into the body as that literal text. A dotted ","type":"text"},{"text":"generate","type":"text","marks":[{"type":"code_inline"}]},{"text":" such as ","type":"text"},{"text":"data.id","type":"text","marks":[{"type":"code_inline"}]},{"text":" makes one key with a dot in its name, so build nested objects as an ","type":"text"},{"text":"object","type":"text","marks":[{"type":"code_inline"}]},{"text":" row with child ","type":"text"},{"text":"mappings","type":"text","marks":[{"type":"code_inline"}]},{"text":", as above. A literal goes in ","type":"text"},{"text":"hardCodedValue","type":"text","marks":[{"type":"code_inline"}]},{"text":" (capital C): a row keyed ","type":"text"},{"text":"hardcodedValue","type":"text","marks":[{"type":"code_inline"}]},{"text":" saves without its value and writes nothing.","type":"text"}]},{"type":"paragraph","content":[{"text":"The two work together: the lookup's ","type":"text"},{"text":"responseMapping","type":"text","marks":[{"type":"code_inline"}]},{"text":" merges ","type":"text"},{"text":"customerId","type":"text","marks":[{"type":"code_inline"}]},{"text":" onto the record, then the response's ","type":"text"},{"text":"mappings","type":"text","marks":[{"type":"code_inline"}]},{"text":" place ","type":"text"},{"text":"customerId","type":"text","marks":[{"type":"code_inline"}]},{"text":" into the body's ","type":"text"},{"text":"data.id","type":"text","marks":[{"type":"code_inline"}]},{"text":". A field the page processor returned must first be carried onto the record by a ","type":"text"},{"text":"responseMapping","type":"text","marks":[{"type":"code_inline"}]},{"text":" before a response ","type":"text"},{"text":"mapping","type":"text","marks":[{"type":"code_inline"}]},{"text":" can extract it. When unsure which layer you need, ask: does this step ","type":"text"},{"text":"add","type":"text","marks":[{"type":"em"}]},{"text":" the field to the record (page-processor ","type":"text"},{"text":"responseMapping","type":"text","marks":[{"type":"code_inline"}]},{"text":") or ","type":"text"},{"text":"read","type":"text","marks":[{"type":"em"}]},{"text":" the field off the record into the body (response ","type":"text"},{"text":"mappings","type":"text","marks":[{"type":"code_inline"}]},{"text":")?","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Observing Calls","type":"text"}]},{"type":"paragraph","content":[{"text":"Look for an API's calls in its request history, the run index (","type":"text"},{"text":"celigo apis request-history \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":"; builder-mode APIs only). It covers the last 5 minutes unless you pass a window (","type":"text"},{"text":"--time-gte","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--time-lte","type":"text","marks":[{"type":"code_inline"}]},{"text":"), and a new call can take a while to appear. Then open that execution's trace with ","type":"text"},{"text":"celigo apis execution-logs \u003cid> \u003cexecutionId>","type":"text","marks":[{"type":"code_inline"}]},{"text":"; step-level trace data is recorded only under ","type":"text"},{"text":"detailed","type":"text","marks":[{"type":"code_inline"}]},{"text":" logging or a debug window. A response step is listed under its ","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]},{"text":", and its stage data can be read only when that id is the builder's own 11-character one: a response ","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]},{"text":" you chose (","type":"text"},{"text":"success","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"notFound","type":"text","marks":[{"type":"code_inline"}]},{"text":") is refused with 400 ","type":"text"},{"text":"invalid_exp_or_imp_id","type":"text","marks":[{"type":"code_inline"}]},{"text":", so read the step before it. The older request-log store (","type":"text"},{"text":"celigo apis logs","type":"text","marks":[{"type":"code_inline"}]},{"text":") may hold nothing for current calls, and test runs appear in neither, so an empty result from one store doesn't prove the API wasn't called.","type":"text"}]},{"type":"paragraph","content":[{"text":"Change an API's logging only by patching ","type":"text"},{"text":"logging.mode","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"logging.debugUntil","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"celigo apis set \u003cid> logging.mode=detailed","type":"text","marks":[{"type":"code_inline"}]},{"text":"); a create or a full update ignores ","type":"text"},{"text":"logging","type":"text","marks":[{"type":"code_inline"}]},{"text":" without an error. ","type":"text"},{"text":"noLogging","type":"text","marks":[{"type":"code_inline"}]},{"text":" records no request history, and ","type":"text"},{"text":"standard","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"detailed","type":"text","marks":[{"type":"code_inline"}]},{"text":" and a debug window need payload storage on the account (422 ","type":"text"},{"text":"payload_storage_required","type":"text","marks":[{"type":"code_inline"}]},{"text":"). The modes are listed under ","type":"text"},{"text":"logging","type":"text","marks":[{"type":"code_inline"}]},{"text":" in ","type":"text"},{"text":"base.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/base.yml","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"A step's own requests and responses (","type":"text"},{"text":"celigo apis debug-requests \u003cid> \u003cexportOrImportId>","type":"text","marks":[{"type":"code_inline"}]},{"text":") are captured only while that export or import has ","type":"text"},{"text":"debugUntil","type":"text","marks":[{"type":"code_inline"}]},{"text":" set: run ","type":"text"},{"text":"celigo exports enable-debug \u003cexportId> --duration \u003cminutes>","type":"text","marks":[{"type":"code_inline"}]},{"text":" (at most 60; ","type":"text"},{"text":"imports enable-debug","type":"text","marks":[{"type":"code_inline"}]},{"text":" for an import) before the calls you want to see.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Publishing to API Management","type":"text"}]},{"type":"paragraph","content":[{"text":"API Management (APIM) is a separate Celigo product that publishes an API, export or import as a managed API with its own console. A published API's read-only ","type":"text"},{"text":"apim","type":"text","marks":[{"type":"code_inline"}]},{"text":" array records the publication (","type":"text"},{"text":"apiId","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"status","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"oaspending","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"published","type":"text","marks":[{"type":"code_inline"}]},{"text":"; ","type":"text"},{"text":"apim.yml","type":"text","marks":[{"type":"link","attrs":{"href":"references/schemas/apim.yml","title":null}}]},{"text":"). No CLI command group or Celigo MCP tool publishes: do it in the UI, or over the REST API's ","type":"text"},{"text":"apim","type":"text","marks":[{"type":"code_inline"}]},{"text":" routes. ","type":"text"},{"text":"POST /v1/apim/\u003cexports|imports|apis>/\u003cid>/manageAPI","type":"text","marks":[{"type":"code_inline"}]},{"text":" publishes a resource as a new APIM API (a ","type":"text"},{"text":"PUT","type":"text","marks":[{"type":"code_inline"}]},{"text":" with ","type":"text"},{"text":"apiId","type":"text","marks":[{"type":"code_inline"}]},{"text":" publishes it to an existing one), ","type":"text"},{"text":"POST /v1/apim/\u003capiId>/\u003cexports|imports|apis>/\u003cid>/openAPIspec","type":"text","marks":[{"type":"code_inline"}]},{"text":" pushes its OpenAPI spec, and ","type":"text"},{"text":"POST /v1/apim/setup","type":"text","marks":[{"type":"code_inline"}]},{"text":" provisions APIM for the account (","type":"text"},{"text":"PUT /v1/apim/validate","type":"text","marks":[{"type":"code_inline"}]},{"text":" checks a subdomain first), in its production environment only. The routes need the ","type":"text"},{"text":"apim:operations","type":"text","marks":[{"type":"code_inline"}]},{"text":" permission, most of them an APIM license too; a workspace user's APIM access is the ","type":"text"},{"text":"allowAccessToAPIM","type":"text","marks":[{"type":"code_inline"}]},{"text":" flag (","type":"text"},{"text":"managing-users","type":"text","marks":[{"type":"link","attrs":{"href":"../managing-users/SKILL.md","title":null}}]},{"text":"). The CLI can only read: ","type":"text"},{"text":"celigo platform get v1/apim/settings","type":"text","marks":[{"type":"code_inline"}]},{"text":" returns the account's APIM subdomain and service account (","type":"text"},{"text":"{}","type":"text","marks":[{"type":"code_inline"}]},{"text":" on an account without APIM), and ","type":"text"},{"text":"celigo platform get v1/apim/apis","type":"text","marks":[{"type":"code_inline"}]},{"text":" answers 403 ","type":"text"},{"text":"APIM license is not enabled.","type":"text","marks":[{"type":"code_inline"}]},{"text":" without the license.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"CLI Commands","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# CRUD\nceligo apis list\nceligo apis get \u003cid>\nceligo apis create \u003c api.json\nceligo apis update \u003cid> \u003c api.json\nceligo apis set \u003cid> key=value [key2=value2 ...]\nceligo apis dependencies \u003cid> # check before deleting\nceligo apis audit \u003cid> # change history\nceligo apis delete \u003cid>\n\n# Clone (builder-mode only): check the route first, and name the target integration\nceligo apis clone \u003cid> --api-version \u003cversion> --dry-run\nceligo apis clone \u003cid> --api-version \u003cversion> --integration \u003cintegrationId> [--name \u003cname>] [--description \u003cdesc>] [--environment \u003cenvId>]\n\n# Pipeline management\nceligo apis add-processor \u003cid> \u003cexportOrImportId> [--router \u003crouterId>] [--branch \u003cbranchName>]\nceligo apis remove-processor \u003cid> \u003cexportOrImportId> [--router \u003crouterId>] [--branch \u003cbranchName>]\nceligo apis connections \u003cid>\nceligo apis replace-connection \u003cid> \u003cconnectionId> \u003cnewConnectionId> # every step at once; returns {replacedNodeCount}\n\n# API groups\nceligo apis set-group \u003capiGroupingId> \u003capiIds...>\nceligo apis unset-group \u003capiIds...>\n\n# Request history and logging\nceligo apis request-history \u003cid> [--time-gte \u003ciso>] [--time-lte \u003ciso>]\nceligo apis execution-logs \u003cid> \u003cexecutionId> [--details]\nceligo apis set \u003cid> logging.mode=\u003cbasic|standard|detailed|accountLevel|noLogging>\nceligo apis delete-execution-logs \u003cid> --time-gte \u003ciso> --time-lte \u003ciso> # irreversible\nceligo apis logs \u003cid> # older request-log store\nceligo apis log-detail \u003cid> \u003ckey>\n\n# Test run (the --file body is {\"mockRequest\": {...}})\nceligo apis test-run \u003cid> --file \u003cpath>\nceligo apis test-run-step-results \u003cid> \u003crunId> \u003cexportOrImportId>\nceligo apis test-run-step-logs \u003cid> \u003crunId> \u003cexportOrImportId>\nceligo --jq '.mockOutput' exports get \u003cexportId> # the saved response test runs answer this lookup from\nceligo exports set \u003cexportId> mockOutput=null # clear it so the next test run calls the system\n\n# Debug (set debugUntil on the step first: celigo exports enable-debug \u003cexportId>)\nceligo apis debug-requests \u003cid> \u003cexportOrImportId> [--since \u003cminutes>]\nceligo apis debug-request-detail \u003cid> \u003cexportOrImportId> \u003ckey>\n\n# Discovery and export\nceligo account search \"\u003ckeyword>\"\nceligo templates marketplace\nceligo apis graph \u003cref> [--as mermaid] # the API's canvas, from a pulled local tree\nceligo apis download \u003cid> [-o \u003cpath>] # template ZIP of the API and what it references\n\n# API Management: read-only here; publishing is in the UI or the REST API's apim routes\nceligo platform get v1/apim/settings","type":"text"}]},{"type":"paragraph","content":[{"text":"\u003c!-- TIER:3 -->","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Pre-Submit Checklist","type":"text"}]},{"type":"paragraph","content":[{"text":"Before creating or updating an API, verify:","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":" is set and descriptive","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"type","type":"text","marks":[{"type":"code_inline"}]},{"text":" is ","type":"text"},{"text":"\"builder\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"\"script\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" (omitted, the API is created in script mode, which can't be changed later)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Builder mode: ","type":"text"},{"text":"builder.request.method","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"builder.request.relativeURI","type":"text","marks":[{"type":"code_inline"}]},{"text":" are set","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Builder mode, full update: the body carries ","type":"text"},{"text":"disabled","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"false","type":"text","marks":[{"type":"code_inline"}]},{"text":" keeps the API online) and the API's ","type":"text"},{"text":"_integrationId","type":"text","marks":[{"type":"code_inline"}]},{"text":" (and ","type":"text"},{"text":"_apiGroupingId","type":"text","marks":[{"type":"code_inline"}]},{"text":", if it has one)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Builder mode: lookups and imports sit in a router branch (an API with no processing can omit ","type":"text"},{"text":"routers","type":"text","marks":[{"type":"code_inline"}]},{"text":"), and no lookup export is a step twice","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Every response ","type":"text"},{"text":"mappings[]","type":"text","marks":[{"type":"code_inline"}]},{"text":" row has a ","type":"text"},{"text":"dataType","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"status: \"Active\"","type":"text","marks":[{"type":"code_inline"}]},{"text":", reads fields with ","type":"text"},{"text":"$.","type":"text","marks":[{"type":"code_inline"}]},{"text":" paths or Handlebars, nests objects with child ","type":"text"},{"text":"mappings","type":"text","marks":[{"type":"code_inline"}]},{"text":" rather than a dotted ","type":"text"},{"text":"generate","type":"text","marks":[{"type":"code_inline"}]},{"text":", and puts a literal in ","type":"text"},{"text":"hardCodedValue","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Builder mode: every branch has a ","type":"text"},{"text":"nextRouterId","type":"text","marks":[{"type":"code_inline"}]},{"text":" (another router's ","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":"\"apiRouter\"","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Builder mode: both ","type":"text"},{"text":"success","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"fail","type":"text","marks":[{"type":"code_inline"}]},{"text":" responses are defined","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Builder mode: ","type":"text"},{"text":"custom","type":"text","marks":[{"type":"code_inline"}]},{"text":" responses come before ","type":"text"},{"text":"success","type":"text","marks":[{"type":"code_inline"}]},{"text":" in ","type":"text"},{"text":"responses[]","type":"text","marks":[{"type":"code_inline"}]},{"text":" (the first match wins, and a response without an ","type":"text"},{"text":"inputFilter","type":"text","marks":[{"type":"code_inline"}]},{"text":" matches every request)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Builder mode: ","type":"text"},{"text":"request","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"routers","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"responseRouter","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"responses","type":"text","marks":[{"type":"code_inline"}]},{"text":" sit directly under ","type":"text"},{"text":"builder","type":"text","marks":[{"type":"code_inline"}]},{"text":", and each lookup that feeds the body has a ","type":"text"},{"text":"responseMapping","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"{\"fields\": [{\"extract\": \"data[0]...\", \"generate\": ...}]}","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Script mode: ","type":"text"},{"text":"script._scriptId","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"script.function","type":"text","marks":[{"type":"code_inline"}]},{"text":" reference a valid script","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"All ","type":"text"},{"text":"_exportId","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"_importId","type":"text","marks":[{"type":"code_inline"}]},{"text":" references in page processors point to existing resources","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Router IDs are unique within the API","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"version","type":"text","marks":[{"type":"code_inline"}]},{"text":" is set (it becomes part of the endpoint URL: ","type":"text"},{"text":"/apis/{version}{relativeURI}","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Every ","type":"text"},{"text":"inputFilter","type":"text","marks":[{"type":"code_inline"}]},{"text":", on a branch or a response, is ","type":"text"},{"text":"{\"version\": \"1\", \"rules\": [...]}","type":"text","marks":[{"type":"code_inline"}]},{"text":", with ","type":"text"},{"text":"extract","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"context","type":"text","marks":[{"type":"code_inline"}]},{"text":" accessors in type wrappers (","type":"text"},{"text":"string","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"number","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"boolean","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Every object node in a ","type":"text"},{"text":"bodySchema","type":"text","marks":[{"type":"code_inline"}]},{"text":" has at least one property, or the API has no ","type":"text"},{"text":"bodySchema","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Gotchas","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PUT erases omitted fields.","type":"text","marks":[{"type":"strong"}]},{"text":" Always GET first, modify, then PUT. The ","type":"text"},{"text":"set","type":"text","marks":[{"type":"code_inline"}]},{"text":" command handles this automatically. That includes ","type":"text"},{"text":"_integrationId","type":"text","marks":[{"type":"code_inline"}]},{"text":": an update without it moves the API to account level and clears its ","type":"text"},{"text":"_apiGroupingId","type":"text","marks":[{"type":"code_inline"}]},{"text":". PATCH reaches only ","type":"text"},{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"description","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"disabled","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"timeoutPeriod","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"pagination.enabled","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"logging","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"traceKeyTemplate","type":"text","marks":[{"type":"code_inline"}]},{"text":" (any other path is refused with 422), so a change inside ","type":"text"},{"text":"builder","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- reordering responses, editing a router -- is always a full update, and so is moving an API into a group: ","type":"text"},{"text":"_apiGroupingId","type":"text","marks":[{"type":"code_inline"}]},{"text":" must be one of its integration's ","type":"text"},{"text":"apiGroupings","type":"text","marks":[{"type":"code_inline"}]},{"text":" (422 ","type":"text"},{"text":"apigrouping_not_belong","type":"text","marks":[{"type":"code_inline"}]},{"text":"), or regroup several APIs with ","type":"text"},{"text":"celigo apis set-group","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"APIs only support ","type":"text","marks":[{"type":"strong"}]},{"text":"first_matching_branch","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" routing.","type":"text","marks":[{"type":"strong"}]},{"text":" Unlike flows which also support ","type":"text"},{"text":"all_matching_branches","type":"text","marks":[{"type":"code_inline"}]},{"text":", API routers always stop at the first matching branch.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Omitting ","type":"text","marks":[{"type":"strong"}]},{"text":"inputFilter","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" type wrappers silently fails.","type":"text","marks":[{"type":"strong"}]},{"text":" Use ","type":"text"},{"text":"[\"boolean\", [\"context\", \"success\"]]","type":"text","marks":[{"type":"code_inline"}]},{"text":", not bare ","type":"text"},{"text":"[\"context\", \"success\"]","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- the filter will never match without the wrapper.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A clone needs a free route and copies the API's steps.","type":"text","marks":[{"type":"strong"}]},{"text":" Only builder-mode APIs clone (others answer 400 ","type":"text"},{"text":"clone_not_supported","type":"text","marks":[{"type":"code_inline"}]},{"text":"). The clone keeps the source's method and ","type":"text"},{"text":"relativeURI","type":"text","marks":[{"type":"code_inline"}]},{"text":", so give it an unused ","type":"text"},{"text":"--api-version","type":"text","marks":[{"type":"code_inline"}]},{"text":" and check with ","type":"text"},{"text":"--dry-run","type":"text","marks":[{"type":"code_inline"}]},{"text":" first (","type":"text"},{"text":"canClone: false","type":"text","marks":[{"type":"code_inline"}]},{"text":" means the route is taken). It copies the exports, imports and scripts its steps use -- the copied lookups keep their saved ","type":"text"},{"text":"mockOutput","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- and it's created outside any integration unless you pass ","type":"text"},{"text":"--integration","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Missing a success or fail response causes undefined behavior.","type":"text","marks":[{"type":"strong"}]},{"text":" The response router won't know where to route.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"version","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" becomes part of the URL path.","type":"text","marks":[{"type":"strong"}]},{"text":" Callers reach the API at ","type":"text"},{"text":"https://api.integrator.io/apis/{version}{relativeURI}","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"api.eu.","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"api.au.","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"api.ca.integrator.io","type":"text","marks":[{"type":"code_inline"}]},{"text":" for accounts in those regions); without ","type":"text"},{"text":"/apis","type":"text","marks":[{"type":"code_inline"}]},{"text":" the request goes to the platform's own API and answers ","type":"text"},{"text":"resource_not_found","type":"text","marks":[{"type":"code_inline"}]},{"text":". Changing the version changes the URL that callers must use.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Page-processor ","type":"text","marks":[{"type":"strong"}]},{"text":"responseMapping","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" and response ","type":"text","marks":[{"type":"strong"}]},{"text":"mappings","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" are different layers.","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"responseMapping","type":"text","marks":[{"type":"code_inline"}]},{"text":" enriches the record with fields from a page processor's response; a response's ","type":"text"},{"text":"mappings","type":"text","marks":[{"type":"code_inline"}]},{"text":" shape the HTTP body from that record. A field the lookup returned won't reach the body unless a ","type":"text"},{"text":"responseMapping","type":"text","marks":[{"type":"code_inline"}]},{"text":" first carries it onto the record.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"APIs don't start themselves.","type":"text","marks":[{"type":"strong"}]},{"text":" No ","type":"text"},{"text":"schedule","type":"text","marks":[{"type":"code_inline"}]},{"text":", no listeners, no flow-level runtime controls, no abstract/instance templating. Scheduled or event-driven work belongs in a flow that calls the API.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A full update without ","type":"text","marks":[{"type":"strong"}]},{"text":"disabled","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" takes the API offline.","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"disabled: true","type":"text","marks":[{"type":"code_inline"}]},{"text":" pauses an API without deleting it (callers get a 404). Send ","type":"text"},{"text":"disabled","type":"text","marks":[{"type":"code_inline"}]},{"text":" on every full update of a builder API (","type":"text"},{"text":"false","type":"text","marks":[{"type":"code_inline"}]},{"text":" to keep it online): a body that omits it saves without error, but the endpoint answers 404 ","type":"text"},{"text":"api_not_found","type":"text","marks":[{"type":"code_inline"}]},{"text":" until ","type":"text"},{"text":"disabled: false","type":"text","marks":[{"type":"code_inline"}]},{"text":" is sent again.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Deleting an API doesn't check dependents.","type":"text","marks":[{"type":"strong"}]},{"text":" The delete succeeds and the endpoint stops at once, so anything that depends on the API, such as an MCP server publishing it, breaks. Check ","type":"text"},{"text":"celigo apis dependencies \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":" first; a deleted API stays in the recycle bin for 30 days (","type":"text"},{"text":"celigo recycle-bin restore \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A ","type":"text","marks":[{"type":"strong"}]},{"text":"bodySchema","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" object without properties breaks the API Builder.","type":"text","marks":[{"type":"strong"}]},{"text":" Every object node -- the root, a nested property, an array's ","type":"text"},{"text":"items","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- needs a non-empty ","type":"text"},{"text":"properties","type":"text","marks":[{"type":"code_inline"}]},{"text":". A property-less object saves without error, but the editor can't render the API, and ","type":"text"},{"text":"properties: {}","type":"text","marks":[{"type":"code_inline"}]},{"text":" is stripped on save. Leave ","type":"text"},{"text":"bodySchema","type":"text","marks":[{"type":"code_inline"}]},{"text":" out when there's no contract, and fill in or remove the bare nested objects a schema generated from a sample leaves.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hooks on an API's steps don't see the integration's settings.","type":"text","marks":[{"type":"strong"}]},{"text":" A lookup's ","type":"text"},{"text":"postResponseMap","type":"text","marks":[{"type":"code_inline"}]},{"text":" hook got ","type":"text"},{"text":"options.settings","type":"text","marks":[{"type":"code_inline"}]},{"text":" holding only that export's own ","type":"text"},{"text":"settings.export","type":"text","marks":[{"type":"code_inline"}]},{"text":", and the lookup's Handlebars ","type":"text"},{"text":"settings","type":"text","marks":[{"type":"code_inline"}]},{"text":" held the same, though the integration had settings. Put anything else a hook needs on the record (","type":"text"},{"text":"writing-scripts > Common Options Available to All Hooks","type":"text","marks":[{"type":"link","attrs":{"href":"../writing-scripts/SKILL.md#common-options-available-to-all-hooks","title":null}}]},{"text":").","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A filter in the export-filter form is dropped.","type":"text","marks":[{"type":"strong"}]},{"text":" A branch or response ","type":"text"},{"text":"inputFilter","type":"text","marks":[{"type":"code_inline"}]},{"text":" sent as ","type":"text"},{"text":"{\"type\": \"expression\", \"expression\": {...}}","type":"text","marks":[{"type":"code_inline"}]},{"text":" saves without an error and is stored as no filter, so that branch takes every request, and that response answers every request that reaches it. Send ","type":"text"},{"text":"{\"version\": \"1\", \"rules\": [...]}","type":"text","marks":[{"type":"code_inline"}]},{"text":" and read the API back.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A lookup export is a step only once per API.","type":"text","marks":[{"type":"strong"}]},{"text":" Using the same export on two branches, or twice in one, fails the save with 422 \"The export \u003cid> is already included as a lookup in your API and cannot be added again.\" Put a lookup that every branch needs before the branching router, or give the second branch its own export.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common Errors","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cause","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fix","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"404","type":"text","marks":[{"type":"code_inline"}]},{"text":" on API endpoint, or ","type":"text"},{"text":"resource_not_found","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Wrong path, ","type":"text"},{"text":"version","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"relativeURI","type":"text","marks":[{"type":"code_inline"}]},{"text":" in the request","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Call ","type":"text"},{"text":"https://api.integrator.io/apis/{version}{relativeURI}","type":"text","marks":[{"type":"code_inline"}]},{"text":" (your region's host outside the US), with ","type":"text"},{"text":"version","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"relativeURI","type":"text","marks":[{"type":"code_inline"}]},{"text":" as the API defines them","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"404","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"api_not_found","type":"text","marks":[{"type":"code_inline"}]},{"text":" on an endpoint that answered before","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The API is disabled, or a full update left ","type":"text"},{"text":"disabled","type":"text","marks":[{"type":"code_inline"}]},{"text":" out","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Send ","type":"text"},{"text":"disabled: false","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"celigo apis set \u003cid> disabled=false","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Callers get ","type":"text"},{"text":"400","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"request_body_invalid_format","type":"text","marks":[{"type":"code_inline"}]},{"text":" \"Single record supported in payload.\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The request body isn't a JSON object (an array, say)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Send one object per call","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Callers get ","type":"text"},{"text":"413","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"request_body_exceeds_limit","type":"text","marks":[{"type":"code_inline"}]},{"text":" \"Api request cannot exceed 5 MB.\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The body and the declared parameters together are over 5 MB","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Send less per call","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Callers get ","type":"text"},{"text":"501","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"api_generic_error","type":"text","marks":[{"type":"code_inline"}]},{"text":", and a test run answers ","type":"text"},{"text":"400","type":"text","marks":[{"type":"code_inline"}]},{"text":" naming the cause","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The API is incomplete: a branch without ","type":"text"},{"text":"nextRouterId","type":"text","marks":[{"type":"code_inline"}]},{"text":", a response without ","type":"text"},{"text":"statusCode","type":"text","marks":[{"type":"code_inline"}]},{"text":", a request without its method or ","type":"text"},{"text":"relativeURI","type":"text","marks":[{"type":"code_inline"}]},{"text":", or a step that isn't set up","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fix what the test run names","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Callers get ","type":"text"},{"text":"422","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"api_generic_error","type":"text","marks":[{"type":"code_inline"}]},{"text":" \"An error occurred while processing the request. Contact API publisher and try again later.\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The run failed in a way no configured response handles; the details aren't in the response","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Find the call in its request history (","type":"text"},{"text":"celigo apis logs \u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":" when the API has no logging mode), or test-run the same request","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Callers get ","type":"text"},{"text":"422","type":"text","marks":[{"type":"code_inline"}]},{"text":" with an empty body","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No response's ","type":"text"},{"text":"inputFilter","type":"text","marks":[{"type":"code_inline"}]},{"text":" matched the record","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Leave ","type":"text"},{"text":"success","type":"text","marks":[{"type":"code_inline"}]},{"text":" without a filter, or add a response for the case","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Callers get ","type":"text"},{"text":"429","type":"text","marks":[{"type":"code_inline"}]},{"text":" \"Rate limit exceeded. Please try again after N seconds.\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The account's API calls are over the rate limit","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Retry after the wait it names","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Callers get ","type":"text"},{"text":"504","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"api_timeout","type":"text","marks":[{"type":"code_inline"}]},{"text":" \"API execution took longer than expected. Please try again.\", as a JSON array rather than an ","type":"text"},{"text":"errors","type":"text","marks":[{"type":"code_inline"}]},{"text":" object","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The run took longer than the API's ","type":"text"},{"text":"timeoutPeriod","type":"text","marks":[{"type":"code_inline"}]},{"text":" (120 seconds at most)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Speed up the slow step, or raise ","type":"text"},{"text":"timeoutPeriod","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"celigo apis set \u003cid> timeoutPeriod=120","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422","type":"text","marks":[{"type":"code_inline"}]},{"text":" validation error on create/update","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Missing required fields or invalid field values","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Check the ","type":"text"},{"text":"Pre-Submit Checklist","type":"text","marks":[{"type":"link","attrs":{"href":"#pre-submit-checklist","title":null}}]},{"text":"; verify ","type":"text"},{"text":"type","type":"text","marks":[{"type":"code_inline"}]},{"text":" is set","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422","type":"text","marks":[{"type":"code_inline"}]},{"text":" \"Script type apis must have valid scriptId and function.\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The create left out ","type":"text"},{"text":"type","type":"text","marks":[{"type":"code_inline"}]},{"text":", so the API was taken as script mode","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Send ","type":"text"},{"text":"\"type\": \"builder\"","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A branch takes every request, or a custom response answers every request, and its ","type":"text"},{"text":"inputFilter","type":"text","marks":[{"type":"code_inline"}]},{"text":" reads back ","type":"text"},{"text":"null","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The filter was sent as ","type":"text"},{"text":"{\"type\": \"expression\", \"expression\": {...}}","type":"text","marks":[{"type":"code_inline"}]},{"text":", which is stored as no filter","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Send ","type":"text"},{"text":"{\"version\": \"1\", \"rules\": [...]}","type":"text","marks":[{"type":"code_inline"}]},{"text":", then read the API back","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A hard-coded field is missing from the response body","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The row's key is ","type":"text"},{"text":"hardcodedValue","type":"text","marks":[{"type":"code_inline"}]},{"text":" (lowercase c), which saves without its value","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"hardCodedValue","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Response always returns the ","type":"text"},{"text":"fail","type":"text","marks":[{"type":"code_inline"}]},{"text":" response","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A page processor fails on every request: with ","type":"text"},{"text":"proceedOnFailure","type":"text","marks":[{"type":"code_inline"}]},{"text":" off, a failed step selects ","type":"text"},{"text":"fail","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Find the failing step in the test run's step results (","type":"text"},{"text":"celigo apis test-run-step-results","type":"text","marks":[{"type":"code_inline"}]},{"text":") and fix it, or set ","type":"text"},{"text":"proceedOnFailure: true","type":"text","marks":[{"type":"code_inline"}]},{"text":" on a step whose failure the API should tolerate","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A custom response (a 404, say) is never returned; those requests get ","type":"text"},{"text":"success","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"success","type":"text","marks":[{"type":"code_inline"}]},{"text":" is listed before it. Responses are checked in list order, and a ","type":"text"},{"text":"success","type":"text","marks":[{"type":"code_inline"}]},{"text":" without a filter, or with the typed ","type":"text"},{"text":"context.success","type":"text","marks":[{"type":"code_inline"}]},{"text":" filter, matches every request that didn't fail","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Move the custom response ahead of ","type":"text"},{"text":"success","type":"text","marks":[{"type":"code_inline"}]},{"text":" in ","type":"text"},{"text":"responses[]","type":"text","marks":[{"type":"code_inline"}]},{"text":", as a full update: read the API, reorder, and send the whole document back with ","type":"text"},{"text":"disabled","type":"text","marks":[{"type":"code_inline"}]},{"text":" (PATCH can't reach ","type":"text"},{"text":"builder","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Every test run returns the same lookup result, whatever the request","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The lookup export has a saved ","type":"text"},{"text":"mockOutput","type":"text","marks":[{"type":"code_inline"}]},{"text":" (the first test run that called it saved one), and test runs answer from it","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Read the export's ","type":"text"},{"text":"mockOutput","type":"text","marks":[{"type":"code_inline"}]},{"text":"; remove it to call the live system again","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Response body is empty","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Response ","type":"text"},{"text":"mappings","type":"text","marks":[{"type":"code_inline"}]},{"text":" not configured or field paths don't match","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Verify mapping extract paths match the actual processed record structure","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"Mapping object must have status field present.","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A response mapping row without ","type":"text"},{"text":"status","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add ","type":"text"},{"text":"\"status\": \"Active\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" to every row","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Response body holds a field's name (","type":"text"},{"text":"\"customerId\"","type":"text","marks":[{"type":"code_inline"}]},{"text":") instead of its value, or a key with a dot in it","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A bare ","type":"text"},{"text":"extract","type":"text","marks":[{"type":"code_inline"}]},{"text":" is a literal, and a dotted ","type":"text"},{"text":"generate","type":"text","marks":[{"type":"code_inline"}]},{"text":" is one key","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Read the field with a ","type":"text"},{"text":"$.","type":"text","marks":[{"type":"code_inline"}]},{"text":" path (","type":"text"},{"text":"$.customerId","type":"text","marks":[{"type":"code_inline"}]},{"text":"), and nest objects with child ","type":"text"},{"text":"mappings","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Pipeline step silently skipped","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"inputFilter","type":"text","marks":[{"type":"code_inline"}]},{"text":" on a branch evaluates to false for all records","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Debug with ","type":"text"},{"text":"celigo apis test-run-step-results","type":"text","marks":[{"type":"code_inline"}]},{"text":" to see each step's input/output","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"400","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"clone_not_supported","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cloning a script-mode or legacy API","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Clone is builder-mode only; recreate script APIs manually","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422","type":"text","marks":[{"type":"code_inline"}]},{"text":" \"The clone API already exists with the same version, method and relative uri.\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The clone keeps the source's method and ","type":"text"},{"text":"relativeURI","type":"text","marks":[{"type":"code_inline"}]},{"text":", and that ","type":"text"},{"text":"--api-version","type":"text","marks":[{"type":"code_inline"}]},{"text":" is taken","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Pick another version; ","type":"text"},{"text":"celigo apis clone \u003cid> --api-version \u003cv> --dry-run","type":"text","marks":[{"type":"code_inline"}]},{"text":" answers ","type":"text"},{"text":"canClone","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Page processor returns no data","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The lookup's request matched nothing in the source, or answered from a saved ","type":"text"},{"text":"mockOutput","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Read the step's request and response in the test run (","type":"text"},{"text":"celigo apis test-run-step-results","type":"text","marks":[{"type":"code_inline"}]},{"text":"), and the export's ","type":"text"},{"text":"mockOutput","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422","type":"text","marks":[{"type":"code_inline"}]},{"text":" \"\u003cid> is not a valid builder.routers.0.branches.0.pageProcessors.0._exportId\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The step names an export (or import) that doesn't exist","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use an id from ","type":"text"},{"text":"celigo exports list","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"celigo imports list","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422","type":"text","marks":[{"type":"code_inline"}]},{"text":" \"Invalid nextRouterId is defined on the branch. Please correct the branch configuration.\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A ","type":"text"},{"text":"nextRouterId","type":"text","marks":[{"type":"code_inline"}]},{"text":" names no router in the API","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Point it at a router ","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"\"apiRouter\"","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"400","type":"text","marks":[{"type":"code_inline"}]},{"text":" \"One or more branches are not merged back. Review the setup to ensure all branches converge.\" on a test run","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A branch has no ","type":"text"},{"text":"nextRouterId","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Set ","type":"text"},{"text":"nextRouterId","type":"text","marks":[{"type":"code_inline"}]},{"text":" on every branch","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422","type":"text","marks":[{"type":"code_inline"}]},{"text":" \"Duplicate router found with the id ...\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Two routers share an ","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Give each router its own ","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]},{"text":" and update the ","type":"text"},{"text":"nextRouterId","type":"text","marks":[{"type":"code_inline"}]},{"text":" values that point at it","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"422","type":"text","marks":[{"type":"code_inline"}]},{"text":" \"The export \u003cid> is already included as a lookup in your API and cannot be added again.\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"One export is a step twice, on two branches or in one","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Put the shared lookup before the branching router, or give the second branch its own export","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Response body missing a field the lookup returned","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The page-processor ","type":"text"},{"text":"responseMapping","type":"text","marks":[{"type":"code_inline"}]},{"text":" never carried the field onto the record","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add a ","type":"text"},{"text":"responseMapping","type":"text","marks":[{"type":"code_inline"}]},{"text":" entry (","type":"text"},{"text":"{\"fields\": [{\"extract\": ..., \"generate\": ...}]}","type":"text","marks":[{"type":"code_inline"}]},{"text":") on the page processor so the response ","type":"text"},{"text":"mappings","type":"text","marks":[{"type":"code_inline"}]},{"text":" have it to extract","type":"text"}]}]}]}]}]},"metadata":{"date":"2026-11-02","name":"building-apis","author":"@skillopedia","source":{"stars":4,"repo_name":"ai","origin_url":"https://github.com/celigo/ai/blob/HEAD/skills/building-apis/SKILL.md","repo_owner":"celigo","body_sha256":"2fa1bd990849e8c31b183ba43e504c1e55206183a99bf371d3a35887b07847f9","cluster_key":"ead56594ce94d7edf2903799bbb3818687ea7eb8f201bfa52bdfacb0333ff516","clean_bundle":{"format":"clean-skill-bundle-v1","source":"celigo/ai/skills/building-apis/SKILL.md","text_bytes":122043,"source_commit":"2f3853ed93eece259b8c78ee74ef19035dfae052","skills_sh_rank":6254,"attachment_count":14,"text_attachments":14,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/building-apis/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"integrations-apis","category_label":"Integrations"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"integrations-apis","import_tag":"skills-sh-top20k-2026-11","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, exposing lookup/write operations as a REST interface that returns a structured response, or publishing an API to API Management."}},"renderedAt":1793671648046}

<!-- 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 pr…