How to Map API Responses into n8n Items
A programmatic n8n node should decide what one output item means before it maps an API response. A single resource can become one item. A response containing records can become one item per record. A wrapper such as { data: [...], meta: {...} } can stay together when the metadata is needed by the next step.
Every item a custom node returns needs a json object. When one input produces several outputs, keep those outputs linked to the input item that produced them. The NativeShip custom-node guide covers the package around the execute method; this article focuses on the output contract inside it.
Here is the same response mapped two ways. The API returns:
{ "data": [ { "id": "a1" }, { "id": "b2" } ], "meta": { "next": "c3" } }
| Contract | Output items | Each item's json |
pairedItem |
|---|---|---|---|
| One envelope item | 1 | { data: [...], meta: { next: 'c3' } } |
{ item: 0 } |
| One item per record | 2 | { id: 'a1' } and { id: 'b2' } |
{ item: 0 } on both |
Choose the output contract first
The same HTTP response can support different workflow designs. Choose the shape that matches the next node's job:
| API response | Output choice | Use it when |
|---|---|---|
| One object, such as one customer | One n8n item with that object in json |
The next step works on one resource |
| Array of records | One item per record | Downstream nodes should run once for each record |
Envelope with data and meta |
Keep one envelope item or expand records and carry selected metadata | The cursor, request ID, totals, or warnings affect later steps |
| Empty array | Return zero items or one explicit empty-result item | The workflow needs either a natural stop or a visible status |
| File or other binary body | Return a binary property and a JSON description | A downstream file-aware node needs the bytes |
Do not choose one shape for every operation. A Get operation usually returns one item. A List operation usually returns one item per record, unless the caller needs page metadata as one object. Document the choice in the node's description and examples so expressions such as {{$json.id}} keep the same meaning after an update.
Return the structure n8n expects
n8n passes data between nodes as an array of objects. Ordinary values live under json; binary values live under binary. The n8n data-structure guide notes that the Code node can add some wrapper syntax, but a custom node still needs to return the json key itself.
A one-record response can be returned like this:
return [[
{
json: {
id: 'cus_123',
name: 'Example customer',
status: 'active',
},
pairedItem: { item: itemIndex },
},
]];
The outer array represents the node's main output. The inner array contains the output items. The json object is the public data contract that expressions and later nodes will read.
If the response is already one JSON object, n8n's programmatic-node tutorial shows a helper pattern that turns it into execution data and associates it with the current input item:
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
{ itemData: { item: itemIndex } },
);
returnData.push.apply(returnData, executionData);
Use the helper when the response already has the shape you want. Use an explicit mapper when you need to unwrap data, expand records, preserve selected metadata, or reject an unexpected body.
Map an array into one item per record
An API often wraps a collection in a property such as data, results, or items. Map each record into its own n8n item when a downstream node should process records independently:
type JsonRecord = Record<string, unknown>;
function isRecord(value: unknown): value is JsonRecord {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function recordsFromResponse(response: unknown): JsonRecord[] {
if (Array.isArray(response)) {
return response.map((value, index) => {
if (!isRecord(value)) {
throw new Error(`Response item ${index + 1} must be an object`);
}
return value;
});
}
if (isRecord(response) && Array.isArray(response.data)) {
return response.data.map((value, index) => {
if (!isRecord(value)) {
throw new Error(`Response data item ${index + 1} must be an object`);
}
return value;
});
}
if (isRecord(response)) {
return [response];
}
throw new Error('API response must be an object or an array of objects');
}
const records = recordsFromResponse(responseData);
for (const record of records) {
returnData.push({
json: record as IDataObject,
pairedItem: { item: itemIndex },
});
}
This adapter handles a top-level array, a { data: [] } envelope, and a single object. It rejects scalar values instead of creating an item whose shape changes with the API response. If your service uses another wrapper, add that wrapper as an explicit rule rather than guessing the first array in the body.
If a specific operation promises a { data: [] } response, make that promise strict instead of accepting any object as one record:
function dataRecords(response: unknown): JsonRecord[] {
if (!isRecord(response) || !Array.isArray(response.data)) {
throw new Error('The list response must contain a data array');
}
return response.data.map((value, index) => {
if (!isRecord(value)) {
throw new Error(`Response data item ${index + 1} must be an object`);
}
return value;
});
}
Use the permissive adapter only for an operation whose contract allows a single object or an array. A strict adapter gives a clearer failure when the provider returns a different envelope after an API change.
An empty data array produces no output items in this example. That is useful when the next node should stop naturally. If users need to distinguish "the API returned no records" from "the node did not run," return one item with an explicit status and document that contract.
Preserve useful envelope metadata
Expanding a response can hide information such as a cursor, total count, warning, or request ID. Decide which metadata belongs with every record and which belongs in one envelope item.
| Metadata | Keep with each record? | Reason |
|---|---|---|
| API request ID | Usually yes when support needs it | A user can report the exact request that produced an item |
| Pagination cursor | Usually no when a separate pagination loop owns it (see the pagination guide) | Repeating it on every record adds noise and can confuse the next request |
| Total count | Sometimes | It can help a report, but repeating it is unnecessary if the workflow keeps an envelope |
| Warning or partial-result flag | Yes when it changes how the record should be used | Downstream logic can filter or alert on the condition |
| HTTP headers | Only selected headers | Keep an API-specific value, not a full header dump with secrets |
One way to retain selected metadata is to place it under a reserved field:
const responseMeta = isRecord(responseData) && isRecord(responseData.meta)
? responseData.meta
: {};
const records = recordsFromResponse(responseData);
for (const record of records) {
returnData.push({
json: {
...record,
_response: {
requestId: responseMeta.requestId,
warning: responseMeta.warning,
},
} as IDataObject,
pairedItem: { item: itemIndex },
});
}
Reserve a name such as _response only if it cannot collide with a field supplied by the service. A safer option for a public node is to use a documented namespace such as nativeShip or the service name. Keep the namespace stable across versions, because users may reference it in expressions and stored workflows.
If metadata is needed for pagination or a summary, keep the envelope as one output item instead:
returnData.push({
json: {
data: records,
meta: responseMeta,
} as IDataObject,
pairedItem: { item: itemIndex },
});
That shape asks downstream users to address {{$json.data}} and split the array later. It is a reasonable contract when page-level metadata matters more than one-item-per-record processing.
Keep input and output items linked
The programmatic execute() reference requires input and output pairing information in the returned data. The item-linking guide for node creators explains why a downstream .item expression can fail when a programmatic node loses that relationship.
For a one-to-many response, every record from the same request can point to the same input index:
const records = recordsFromResponse(responseData);
const executionData = records.map((record) => ({
json: record as IDataObject,
pairedItem: { item: itemIndex },
}));
returnData.push(...executionData);
If a node combines several inputs, use the input number as well as the item reference. If it filters or reorders existing items, preserve the relationship for every output that remains. If it creates a summary with no one-to-one source, define the source relationship and test the downstream expression rather than relying on an accidental index match. If a downstream node shows "Paired item data unavailable", the paired item troubleshooting guide walks through the usual causes and fixes.
Keep field names stable across operations
An API may return different names for the same concept across endpoints. Choose the n8n output names users should rely on, then keep the raw response only when it helps with debugging or advanced use:
| API response field | Stable node field | Mapping decision |
|---|---|---|
user_id |
id |
Normalize only when the node promises id for every user operation |
created_at |
createdAt |
Use one naming style across the node's operations |
attributes.email |
email |
Flatten when the common workflow needs the value directly |
links.self |
url |
Keep the original link under a documented raw or links field if users need both |
| Unknown provider fields | raw or the untouched response |
Preserve them only when the node's output contract says they are available |
Do not silently rename a field in a patch release if workflows already use it. If the API changes its response, either keep a compatibility field, add a new node version, or document the migration. The NativeShip node-versioning guide is the next reference when the output contract must change.
Offer a Simplify toggle for large records
Some APIs return records with dozens of fields, nested objects, and links. n8n's own nodes handle this with a boolean Simplify parameter that returns a short, flat version of each record by default and the full record when switched off:
{
displayName: 'Simplify',
name: 'simplify',
type: 'boolean',
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
displayOptions: { show: { operation: ['get', 'getAll'] } },
},
Then map the record once, in one place:
const simplify = this.getNodeParameter('simplify', itemIndex, true) as boolean;
const output = simplify
? {
id: record.id,
name: record.name,
email: (record.attributes as JsonRecord | undefined)?.email,
createdAt: record.created_at,
}
: record;
returnData.push({ json: output as IDataObject, pairedItem: { item: itemIndex } });
Pick the simplified fields from what common workflows actually use, keep the list to about ten, and document both shapes. The simplified fields become part of the node's public contract, so treat them as carefully as any other field name.
Handle a malformed successful response
An HTTP 200 does not guarantee that the body has the shape your node expects. Check the response before mapping it and surface a useful local error if the body cannot be converted into items:
import { NodeOperationError } from 'n8n-workflow';
let records: JsonRecord[];
try {
records = recordsFromResponse(responseData);
} catch (error) {
throw new NodeOperationError(this.getNode(), error as Error, {
description: 'The API response did not contain an object or an array of objects that this operation can return.',
itemIndex,
});
}
Do not include a full HTML error page, access token, or unbounded response body in the n8n error. Keep a short explanation and, if the service provides one, a safe request ID. Use the API-error path for an HTTP or service failure; use the operation error path for a response that reached the mapper but cannot satisfy the node's output contract. The n8n error-handling reference documents the two error classes.
Test the response contract before publishing
Run the mapping against the n8n and package versions you plan to support. Test both the API body and the item relationship:
| Test | Expected result |
|---|---|
| One object response | One item with a json object and the source input link |
| Top-level array | One item per object, all linked to the request's input item |
{ data: [] } |
The documented empty-result behavior occurs without a shape error |
{ data: [...], meta: {...} } |
Records and selected metadata appear in the promised locations |
Record contains a field named _response |
The namespace rule prevents silent metadata overwrite |
| Array contains a scalar or null | The node reports the response-shape problem before returning partial data |
| Multiple input items | Each response maps to the correct input index |
Downstream .item expression |
It resolves to the source item after one-to-many expansion |
| API returns HTML or an unexpected wrapper | The error is bounded and does not expose credentials or a full body |
| API changes a field name | The compatibility or migration behavior matches the documented node version |
Run these with npm run dev from the package root. From n8n 3.0, n8n-node dev starts n8n in a container, so Docker or Podman must be installed (or use --external-n8n with your own instance started with N8N_DEV_RELOAD=true). Use the NativeShip debugging guide to inspect the raw response, mapped output, item index, and downstream expression. The examples here are patterns to run and review. They do not prove that a generated package or a particular API returns the shown fields.
Frequently asked questions
A custom node returns an array of execution items. Each ordinary item needs a json object, and a file can be stored under binary. The fields inside json become the data that later nodes and expressions read.
Return one item per record when downstream nodes should process records independently. Keep one envelope item when page metadata or the full response wrapper is the main result. Choose and document one contract for each operation.
Check that data is an array of objects, map each object to an item when you need one item per record, and carry only the metadata that later steps need. Add explicit rules for other wrapper names instead of guessing.
It tells n8n which input item produced each output. Without that relationship, downstream expressions that refer back to earlier data can fail or resolve to the wrong item.
Keep raw data only when users need it for a documented reason. Stable, useful fields should be easy to read, while raw or provider-specific data can live under a named field that will not collide with normal response keys.
Stop the mapping, report that the response shape is unsupported, include the input item index, and keep secrets and the full response out of the error. Treat the response contract as a tested part of the operation.
Run single-object, array, empty-array, wrapped-metadata, malformed-record, multiple-input, item-linking, unexpected-body, and field-change cases on the supported n8n and package versions before publishing.