Skip to content
Blog

How to Build HTTP Requests in a Programmatic n8n Node

Build the request as a small, explicit contract. Put the path in the URL, filters and pagination controls in qs, representation details in headers, the API payload in body, and authentication in the n8n credential helper. Then send the options object with httpRequest or httpRequestWithAuthentication.

This separation makes an operation easier to review. It also gives you a place to validate each value before the request is sent. The NativeShip custom-node guide covers the package around the execute method; this article focuses on constructing the request inside it.

In short, each part of the request has one source:

Request part Comes from Example
Base URL Credential field or a constant https://api.example.com/v1
Path Operation plus an encoded ID parameter /records/rec_123
Query Filter, limit, and cursor parameters ?status=active&limit=50
Headers Operation needs Accept: application/json
Body Validated fields for create or update { "name": "Ada" }
Authentication Credential authenticate block Authorization: Bearer …

Start with the API request contract

Write down the request shape for each operation before adding code. A short request sheet for each operation answers these questions:

Part Question to answer Typical n8n field
Method Which HTTP verb does this operation use? operation or a fixed operation definition
Path Which resource path identifies the record or collection? resourceId, endpoint, or an operation route
Query Which filters, limits, cursors, or expansions belong in the query string? filters, pageSize, cursor
Headers Which representation or provider headers are required? Accept, Content-Type, idempotency key
Body What JSON, form, or binary payload does the API accept? body, additionalFields, or a binary property
Authentication Which credential type adds the service authentication? Credential declaration and helper call
Response Should the helper return the body or status, headers, and body? json, encoding, returnFullResponse

Keep one operation responsible for one request contract. A Get operation should not quietly send a body that belongs to Create, and a list operation should not place a cursor in a JSON payload when the API expects it in the query string.

Use n8n's HTTP request helper

n8n's HTTP request helper reference applies to programmatic-style nodes. It documents httpRequest for unauthenticated calls and httpRequestWithAuthentication for calls that use a credential:

const response = await this.helpers.httpRequest(options);

const authenticatedResponse = await this.helpers.httpRequestWithAuthentication.call(
  this,
  'serviceApi',
  options,
);

The helper options include url, headers, method, body, qs, arrayFormat, encoding, returnFullResponse, timeout, and json. Use the helper that ships with n8n instead of adding a separate HTTP client such as axios. n8n's verification guidelines require verified community nodes to have no runtime dependencies, and from n8n 3.0 unverified nodes are switched off by default (N8N_UNVERIFIED_PACKAGES_ENABLED=false), so an extra HTTP library can keep your node out of most users' instances.

A complete authenticated request can look like this:

import type { IHttpRequestOptions } from 'n8n-workflow';

const options: IHttpRequestOptions = {
  method: 'GET',
  url: `${baseUrl}/records/${encodeURIComponent(recordId)}`,
  qs: {
    include: 'owner',
  },
  headers: {
    Accept: 'application/json',
  },
  json: true,
};

const response = await this.helpers.httpRequestWithAuthentication.call(
  this,
  'serviceApi',
  options,
);

The exact credential name, base URL, path, and response type belong to the API you are integrating. Treat this as the request shape, then swap in the example values from the current API reference.

Read the base URL and parameters per item

Most APIs have one base URL, but some have regional hosts or self-hosted installs. Store the base URL on the credential when it varies by account, and read every node parameter with the item index so each input item can resolve its own expression:

import type {
  IExecuteFunctions,
  INodeExecutionData,
  IHttpRequestOptions,
  JsonObject,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';

export async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
  const items = this.getInputData();
  const returnData: INodeExecutionData[] = [];
  const credentials = await this.getCredentials('serviceApi');
  const baseUrl = String(credentials.baseUrl ?? 'https://api.example.com/v1').replace(/\/$/, '');

  for (let i = 0; i < items.length; i++) {
    try {
      const recordId = this.getNodeParameter('recordId', i) as string;
      const options: IHttpRequestOptions = {
        method: 'GET',
        url: `${baseUrl}/records/${encodeURIComponent(recordId)}`,
        headers: { Accept: 'application/json' },
        json: true,
      };
      const response = await this.helpers.httpRequestWithAuthentication.call(
        this,
        'serviceApi',
        options,
      );
      returnData.push({ json: response, pairedItem: { item: i } });
    } catch (error) {
      if (this.continueOnFail()) {
        returnData.push({ json: { error: (error as Error).message }, pairedItem: { item: i } });
        continue;
      }
      throw new NodeApiError(this.getNode(), error as JsonObject, { itemIndex: i });
    }
  }

  return [returnData];
}

Three details matter here. getNodeParameter('recordId', i) reads the value for item i, so an expression like {{ $json.id }} resolves per item. pairedItem: { item: i } keeps item linking intact for downstream expressions. And continueOnFail() lets a user choose to collect failed items instead of stopping the workflow.

Build the URL without mixing query data into the path

Keep path segments and query parameters separate. Encode a user-provided identifier before inserting it into a path:

const recordPath = `/records/${encodeURIComponent(recordId)}`;
const url = `${baseUrl.replace(/\/$/, '')}${recordPath}`;

const qs: Record<string, unknown> = {
  fields: selectedFields,
};

if (includeOwner) {
  qs.include = 'owner';
}

const options: IHttpRequestOptions = {
  method: 'GET',
  url,
  qs,
  json: true,
};

Do not concatenate ?page=...&limit=... by hand. The helper's qs option keeps query values separate from the path and handles the final query string. It also lets you choose how an array is serialized when the provider documents a particular format.

arrayFormat Example for ids: ['a', 'b']
indices ids[0]=a&ids[1]=b
brackets ids[]=a&ids[]=b
repeat ids=a&ids=b
comma ids=a,b

n8n uses indices by default, but that is not a universal API rule. Set arrayFormat when the service documents a different format or a real request test proves it:

const options: IHttpRequestOptions = {
  method: 'GET',
  url: `${baseUrl}/records`,
  qs: { ids: recordIds },
  arrayFormat: 'repeat',
  json: true,
};

If an optional query value is absent, omit it rather than sending a string such as undefined or an empty filter that changes the endpoint's behavior.

For list operations, the cursor or page value also lives in qs, and it changes on every call. The programmatic pagination guide shows how to loop over pages with the same request builder and stop cleanly.

Put representation headers in the right place

Headers describe how the request and response should be interpreted. Typical examples are:

  • Accept tells the service which response representation the node can read.
  • Content-Type describes the body format when the request has a body.
  • An idempotency or provider-specific header belongs in the operation that needs it.
  • Authentication headers belong in the credential definition, so the node does not expose or duplicate secrets in operation code.

For a JSON request, make the body and header agree:

const options: IHttpRequestOptions = {
  method: 'POST',
  url: `${baseUrl}/records`,
  headers: {
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: {
    name,
    email,
  },
  json: true,
};

The helper documentation says n8n adds the content type for FormData and URLSearchParams bodies. Set a header yourself when the provider needs a documented override. Do not copy a browser request's entire header list into a node. Browser-only headers, cookies, and tracing values can expose data or make the operation depend on a session that n8n does not have.

Send the body that matches the content type

The body option can be a JavaScript object, array, string, Buffer, FormData, or URLSearchParams. Choose the body type from the API contract:

API expects Body value Request details
JSON object Plain object or array Set the documented content type and use json: true when the helper should parse JSON responses
Form URL encoded URLSearchParams Let the helper set the form content type or set the provider's required value
Multipart file upload FormData and a file buffer Keep the binary property and field name explicit; let the helper handle the multipart content type
Raw text or XML String Set the provider's content type and do not ask the helper to treat the body as a JSON object
Binary upload Buffer Set the content type from the API and use the response encoding that matches the operation

For optional JSON fields, build the body from values that passed validation rather than copying the whole node parameter object:

const body: Record<string, unknown> = {
  name,
};

if (description !== undefined && description !== '') {
  body.description = description;
}

if (tags.length > 0) {
  body.tags = tags;
}

const options: IHttpRequestOptions = {
  method: 'POST',
  url: `${baseUrl}/records`,
  headers: {
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body,
  json: true,
};

This prevents an empty optional field from becoming an unintended null, empty array, or provider-specific default. Validate these values before the request is sent; treat this article as the request-construction step.

Let the credential helper handle authentication

Declare the credential in the node and pass its name to httpRequestWithAuthentication. The NativeShip credentials guide covers the credential file and authentication boundary, and how to add a credential test covers the request n8n runs when a user saves the credential.

const response = await this.helpers.httpRequestWithAuthentication.call(
  this,
  'serviceApi',
  options,
);

Do not add a second Authorization header in the operation when the credential already supplies it. Duplicate headers can make a working credential look like a request bug, and putting a token in a field or log can expose it to workflow users. The helper reference documents a Basic Auth auth option, but recommends using the credential helper for authenticated nodes.

The credential name in the helper call must match the name declared in the node's credential definition. Keep the name in one constant or the operation's routing definition when several operations share the same service credential.

Keep operation request builders explicit

When a node supports several operations, build the options object in a function that makes each request's differences visible:

function buildRequest(
  operation: string,
  baseUrl: string,
  recordId: string,
  input: Record<string, unknown>,
): IHttpRequestOptions {
  const rootUrl = baseUrl.replace(/\/$/, '');

  switch (operation) {
    case 'get':
      return {
        method: 'GET',
        url: `${rootUrl}/records/${encodeURIComponent(recordId)}`,
        headers: { Accept: 'application/json' },
        json: true,
      };
    case 'update':
      return {
        method: 'PUT',
        url: `${rootUrl}/records/${encodeURIComponent(recordId)}`,
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/json',
        },
        body: input,
        json: true,
      };
    case 'list':
      return {
        method: 'GET',
        url: `${rootUrl}/records`,
        qs: input,
        headers: { Accept: 'application/json' },
        json: true,
      };
    default:
      // In execute, wrap this in NodeOperationError so n8n shows the node name.
      throw new Error(`Unsupported operation: ${operation}`);
  }
}

This keeps the method, path, query, headers, and body together for each operation. It also gives the test matrix a stable place to inspect before a request is sent. The n8n programmatic-node tutorial shows the same flow: read input and parameters, build options, call the helper, and add the response to return data.

Decide whether you need the full response

By default, the helper returns the response body. Use returnFullResponse: true when the node needs the status code or selected response headers:

const response = await this.helpers.httpRequestWithAuthentication.call(
  this,
  'serviceApi',
  {
    method: 'POST',
    url: `${baseUrl}/records`,
    body,
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    json: true,
    returnFullResponse: true,
  },
);

The full response shape includes body, headers, statusCode, and statusMessage according to the helper reference. Return only the fields users need. A request ID can help support, while a full header object can contain cookies or provider data that does not belong in workflow output.

Choose the response encoding for the operation. JSON responses can use json: true; file operations may need encoding: 'arraybuffer' or another documented mode. For a file download, set encoding: 'arraybuffer', turn off json, and pass the buffer to this.helpers.prepareBinaryData so the file lands in the item's binary property. Do not parse a binary response as JSON because the operation happens to use the same helper.

Keep request errors separate from request construction

Build and validate the options before calling the helper. Once the helper runs, API status codes, authentication failures, rate limits, and service outages belong to the node's request error path. The n8n error-handling reference documents the difference between NodeApiError for external API failures and NodeOperationError for local configuration or transformation problems. The n8n custom node errors guide lists the error messages builders hit most often and what each one means.

Do not add retry loops to the request builder. A retry decision depends on the HTTP method, idempotency guarantee, response status, and provider guidance. Keep that policy in the rate-limit and error-handling parts of the node so the number of attempts remains visible.

Test the request shape before publishing

The examples here are patterns to run against the supported n8n and package versions. Inspect the actual request options with secrets redacted and compare them with the target API reference:

Test What to inspect
GET with a path ID Encoded path segment, method, Accept, and omitted body
GET with filters qs values, optional fields, and array serialization
POST JSON Body keys, Content-Type, json, and credential application
PUT with optional fields Empty values are omitted according to the API contract
DELETE Method, path, no accidental JSON body, and safe response handling
Form URL encoded request URLSearchParams and content type
Multipart upload Binary field name, boundary handling, and file metadata
Full response mode Body, status, selected headers, and no secret leakage
Multiple input items Each item resolves its own parameters and sends the intended request
API error or rate limit The error path preserves useful status context without duplicating retries

Run the node with npm run dev. From n8n 3.0, n8n-node dev starts n8n in a container, so install Docker or Podman first, or point it at your own instance with n8n-node dev --external-n8n and N8N_DEV_RELOAD=true. Use the NativeShip debugging guide to inspect the resolved options and response while stepping through a node. Record the n8n version, package version, credential type, operation, redacted request shape, response status, and output. Do not call an example tested until the target API and supported n8n version have been run.

Frequently asked questions

Use this.helpers.httpRequest for a request without n8n authentication, or this.helpers.httpRequestWithAuthentication.call(this, 'serviceApi', options) with the credential type name when the service uses a node credential. Don't add axios or another HTTP client, because verified nodes can't have runtime dependencies.

Put them in the qs object. Keep them separate from the path and set arrayFormat only when the target API documents how it expects arrays.

Build a plain object from validated fields, set the method and URL, set the JSON content type when required, and pass the object as body with the response option that matches the API.

Usually no. Declare the credential and let httpRequestWithAuthentication apply the authentication. A second manual header can conflict with the credential and can expose a token.

Use it when the node needs the status code or selected response headers, such as a request ID. If the operation only needs the body, keep the default body response and return only the useful fields.

Use FormData for multipart requests, URLSearchParams for form URL encoding, and a Buffer for binary uploads. Match the content type and response encoding to the service documentation.

Inspect the redacted request options for GET, filtered list, JSON create, update, delete, form, multipart, full-response, multiple-item, API-error, and rate-limit cases. Run them on the n8n and package versions you support, with npm run dev (Docker or Podman is needed from n8n 3.0).

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

Give your customers another way to use your product.

Get started