Skip to content
Blog

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.

Ready to launch your n8n, Make, Zapier or Activepieces integration?

Give your customers another way to use your product.

Get started