n8n API Integration Patterns: REST, GraphQL, and OAuth Workflows
API integrations are fundamental to workflow automation. This guide covers essential patterns for integrating REST APIs, GraphQL endpoints, and OAuth-protected services in n8n.
REST API Patterns
Basic HTTP Request Configuration
// HTTP Request node configuration
{
"method": "POST",
"url": "https://api.example.com/v1/users",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {
"timeout": 30000,
"redirect": {
"followRedirects": true,
"maxRedirects": 5
}
},
"headerParameters": {
"Content-Type": "application/json",
"X-Request-ID": "={{ $runId }}"
},
"body": {
"name": "={{ $json.name }}",
"email": "={{ $json.email }}"
}
}Dynamic URL Building
// Function node for dynamic URL construction
const baseUrl = $env.API_BASE_URL;
const version = 'v2';
const resource = $json.resourceType;
const resourceId = $json.resourceId;
// Build URL with query parameters
const params = new URLSearchParams();
if ($json.filters) {
Object.entries($json.filters).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
params.append(key, value);
}
});
}
if ($json.pagination) {
params.append('page', $json.pagination.page || 1);
params.append('limit', $json.pagination.limit || 50);
}
const queryString = params.toString();
const url = `${baseUrl}/${version}/${resource}${resourceId ? '/' + resourceId : ''}${queryString ? '?' + queryString : ''}`;
return [{
json: {
url,
method: $json.method || 'GET'
}
}];Response Handling and Error Management
// Function node for API response handling
const response = $json;
const statusCode = $response?.statusCode || response.statusCode;
// Define expected responses
const successCodes = [200, 201, 204];
const retryableCodes = [408, 429, 500, 502, 503, 504];
const clientErrorCodes = [400, 401, 403, 404];
// Success response
if (successCodes.includes(statusCode)) {
return [{
json: {
success: true,
data: response.data || response,
metadata: {
statusCode,
requestId: response.headers?.['x-request-id'],
rateLimit: {
remaining: parseInt(response.headers?.['x-ratelimit-remaining'] || '-1'),
reset: response.headers?.['x-ratelimit-reset']
}
}
}
}];
}
// Retryable error
if (retryableCodes.includes(statusCode)) {
const retryAfter = parseInt(response.headers?.['retry-after'] || '60');
return [{
json: {
success: false,
retryable: true,
statusCode,
error: response.message || 'Server error',
retryAfter,
retryCount: ($json.retryCount || 0) + 1
}
}];
}
// Client error
if (clientErrorCodes.includes(statusCode)) {
return [{
json: {
success: false,
retryable: false,
statusCode,
error: response.message || response.error,
details: response.errors || response.details
}
}];
}
// Unknown error
throw new Error(`Unexpected status code: ${statusCode}`);Pagination Handling
// Function node for cursor-based pagination
const allResults = [];
let hasMore = true;
let cursor = null;
let pageCount = 0;
const maxPages = 100;
// Store pagination state in workflow static data
const staticData = $getWorkflowStaticData('global');
cursor = staticData.nextCursor || null;
// This function will be called in a loop
const processPage = async () => {
const response = $json;
// Add results from current page
if (response.data && Array.isArray(response.data)) {
allResults.push(...response.data);
}
// Check for next page
hasMore = response.pagination?.hasMore || response.meta?.hasNextPage || false;
cursor = response.pagination?.nextCursor || response.meta?.endCursor || null;
// Update static data
staticData.nextCursor = cursor;
staticData.totalFetched = (staticData.totalFetched || 0) + response.data.length;
pageCount++;
return {
hasMore: hasMore && pageCount < maxPages,
cursor,
resultsThisPage: response.data.length,
totalResults: allResults.length
};
};
return [{
json: {
paginationState: await processPage(),
results: allResults
}
}];
// For offset-based pagination
const offsetPagination = () => {
const pageSize = 100;
const currentPage = $json.page || 1;
const totalCount = $json.totalCount;
const offset = (currentPage - 1) * pageSize;
const hasMore = offset + pageSize < totalCount;
return {
limit: pageSize,
offset,
hasMore,
nextPage: hasMore ? currentPage + 1 : null,
totalPages: Math.ceil(totalCount / pageSize)
};
};Batch API Requests
// Function node for batching API requests
const items = $input.all();
const batchSize = 50;
const batches = [];
// Split items into batches
for (let i = 0; i < items.length; i += batchSize) {
batches.push(items.slice(i, i + batchSize).map(item => item.json));
}
// Process each batch
const results = [];
for (const batch of batches) {
// Prepare batch request
const batchRequest = {
requests: batch.map((item, index) => ({
id: index.toString(),
method: 'POST',
url: '/api/v1/resource',
body: item
}))
};
results.push({
batch: batchRequest,
batchIndex: batches.indexOf(batch)
});
}
return results.map(r => ({ json: r }));GraphQL Integration
Basic GraphQL Query
// HTTP Request node for GraphQL
{
"method": "POST",
"url": "https://api.example.com/graphql",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer {{ $credentials.apiToken }}"
},
"body": {
"query": `
query GetUsers($first: Int!, $after: String) {
users(first: $first, after: $after) {
edges {
node {
id
name
email
createdAt
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
`,
"variables": {
"first": 50,
"after": "={{ $json.cursor || null }}"
}
}
}GraphQL Mutation with Variables
// Function node for GraphQL mutation
const mutation = `
mutation CreateOrder($input: CreateOrderInput!) {
createOrder(input: $input) {
order {
id
status
total
items {
productId
quantity
price
}
}
errors {
field
message
}
}
}
`;
const variables = {
input: {
customerId: $json.customerId,
items: $json.items.map(item => ({
productId: item.productId,
quantity: item.quantity
})),
shippingAddress: {
street: $json.address.street,
city: $json.address.city,
state: $json.address.state,
postalCode: $json.address.postalCode,
country: $json.address.country
}
}
};
return [{
json: {
query: mutation,
variables
}
}];GraphQL Error Handling
// Function node for GraphQL response handling
const response = $json;
// Check for GraphQL errors
if (response.errors && response.errors.length > 0) {
const errors = response.errors.map(err => ({
message: err.message,
path: err.path?.join('.'),
code: err.extensions?.code
}));
// Check if any errors are retriable
const retryableCodes = ['INTERNAL_SERVER_ERROR', 'TIMEOUT', 'RATE_LIMITED'];
const hasRetryable = errors.some(e => retryableCodes.includes(e.code));
return [{
json: {
success: false,
errors,
retryable: hasRetryable,
partialData: response.data // GraphQL can return partial data with errors
}
}];
}
// Success - extract and transform data
const data = response.data;
return [{
json: {
success: true,
data
}
}];GraphQL Subscriptions (via Webhook)
// Setting up GraphQL subscription via webhook
// This pattern uses a webhook endpoint to receive subscription updates
// Subscription setup (external service)
const subscriptionQuery = `
subscription OnOrderUpdated($customerId: ID!) {
orderUpdated(customerId: $customerId) {
id
status
updatedAt
}
}
`;
// In n8n, create a webhook node to receive subscription events
// The external GraphQL service will POST updates to this webhook
// Function node to process subscription event
const event = $json;
if (event.type !== 'orderUpdated') {
return []; // Ignore other events
}
const orderUpdate = event.data.orderUpdated;
return [{
json: {
orderId: orderUpdate.id,
newStatus: orderUpdate.status,
updatedAt: orderUpdate.updatedAt,
requiresAction: ['PAYMENT_FAILED', 'CANCELLED'].includes(orderUpdate.status)
}
}];OAuth Authentication Flows
OAuth 2.0 Authorization Code Flow
// Function node for OAuth state management
const crypto = require('crypto');
// Generate secure state parameter
const state = crypto.randomBytes(32).toString('hex');
// Store state for verification
const staticData = $getWorkflowStaticData('global');
staticData.oauthStates = staticData.oauthStates || {};
staticData.oauthStates[state] = {
created: Date.now(),
userId: $json.userId,
returnUrl: $json.returnUrl
};
// Clean up old states (older than 10 minutes)
const tenMinutesAgo = Date.now() - 600000;
Object.entries(staticData.oauthStates).forEach(([key, value]) => {
if (value.created < tenMinutesAgo) {
delete staticData.oauthStates[key];
}
});
// Build authorization URL
const params = new URLSearchParams({
client_id: $env.OAUTH_CLIENT_ID,
redirect_uri: $env.OAUTH_REDIRECT_URI,
response_type: 'code',
scope: 'read write',
state
});
return [{
json: {
authorizationUrl: `https://oauth.provider.com/authorize?${params}`,
state
}
}];OAuth Token Exchange
// Function node for token exchange
const code = $json.code;
const state = $json.state;
// Verify state
const staticData = $getWorkflowStaticData('global');
const savedState = staticData.oauthStates?.[state];
if (!savedState) {
throw new Error('Invalid or expired state parameter');
}
// Clean up used state
delete staticData.oauthStates[state];
// Prepare token request
return [{
json: {
tokenEndpoint: 'https://oauth.provider.com/token',
body: {
grant_type: 'authorization_code',
code,
redirect_uri: $env.OAUTH_REDIRECT_URI,
client_id: $env.OAUTH_CLIENT_ID,
client_secret: $env.OAUTH_CLIENT_SECRET
},
userId: savedState.userId,
returnUrl: savedState.returnUrl
}
}];Token Refresh Handler
// Function node for automatic token refresh
const tokenData = $json;
const now = Date.now();
// Check if token needs refresh (within 5 minutes of expiry)
const expiresAt = tokenData.expiresAt || (tokenData.issuedAt + tokenData.expiresIn * 1000);
const needsRefresh = expiresAt - now < 300000; // 5 minutes
if (!needsRefresh) {
return [{
json: {
accessToken: tokenData.accessToken,
needsRefresh: false
}
}];
}
// Token needs refresh
if (!tokenData.refreshToken) {
throw new Error('Token expired and no refresh token available');
}
return [{
json: {
needsRefresh: true,
refreshToken: tokenData.refreshToken,
tokenEndpoint: 'https://oauth.provider.com/token',
body: {
grant_type: 'refresh_token',
refresh_token: tokenData.refreshToken,
client_id: $env.OAUTH_CLIENT_ID,
client_secret: $env.OAUTH_CLIENT_SECRET
}
}
}];OAuth Token Storage
// Function node for secure token storage
const tokens = $json;
// Hash sensitive data for logging
const crypto = require('crypto');
const tokenHash = crypto
.createHash('sha256')
.update(tokens.accessToken)
.digest('hex')
.substring(0, 8);
// Prepare for storage (in database or secret manager)
const tokenRecord = {
userId: $json.userId,
provider: 'example_provider',
accessToken: tokens.accessToken, // Encrypt before storing!
refreshToken: tokens.refreshToken, // Encrypt before storing!
tokenType: tokens.tokenType || 'Bearer',
scope: tokens.scope,
expiresAt: Date.now() + (tokens.expiresIn * 1000),
issuedAt: Date.now(),
// Metadata for auditing
metadata: {
tokenHash,
grantType: tokens.grantType || 'authorization_code',
createdAt: new Date().toISOString()
}
};
return [{
json: tokenRecord
}];Rate Limiting and Throttling
Request Throttling
// Function node for request throttling
const Redis = require('ioredis');
const redis = new Redis($env.REDIS_URL);
const apiKey = $json.apiKey || 'default';
const endpoint = $json.endpoint;
// Rate limit configuration per API
const rateLimits = {
'stripe': { requests: 100, window: 1 }, // 100 req/sec
'sendgrid': { requests: 100, window: 1 }, // 100 req/sec
'shopify': { requests: 2, window: 1 }, // 2 req/sec
'hubspot': { requests: 100, window: 10 }, // 100 req/10sec
'default': { requests: 10, window: 1 } // 10 req/sec
};
const limit = rateLimits[apiKey] || rateLimits.default;
const key = `ratelimit:${apiKey}:${endpoint}`;
// Sliding window implementation
const now = Date.now();
const windowStart = now - (limit.window * 1000);
const multi = redis.multi();
multi.zremrangebyscore(key, '-inf', windowStart);
multi.zadd(key, now, `${now}-${Math.random()}`);
multi.zcard(key);
multi.expire(key, limit.window + 1);
const results = await multi.exec();
const requestCount = results[2][1];
if (requestCount > limit.requests) {
// Calculate wait time
const oldestInWindow = await redis.zrange(key, 0, 0, 'WITHSCORES');
const waitMs = oldestInWindow.length > 1
? parseInt(oldestInWindow[1]) + (limit.window * 1000) - now
: limit.window * 1000;
return [{
json: {
proceed: false,
waitMs,
retryAfter: new Date(now + waitMs).toISOString()
}
}];
}
return [{
json: {
proceed: true,
remaining: limit.requests - requestCount,
resetAt: new Date(now + limit.window * 1000).toISOString()
}
}];Adaptive Rate Limiting
// Function node for adaptive rate limiting based on response headers
const response = $json;
const headers = response.headers || {};
// Parse rate limit headers (different APIs use different formats)
const rateLimitInfo = {
limit: parseInt(headers['x-ratelimit-limit'] || headers['x-rate-limit-limit'] || '0'),
remaining: parseInt(headers['x-ratelimit-remaining'] || headers['x-rate-limit-remaining'] || '0'),
reset: parseInt(headers['x-ratelimit-reset'] || headers['x-rate-limit-reset'] || '0'),
retryAfter: parseInt(headers['retry-after'] || '0')
};
// Store rate limit info for adaptive throttling
const staticData = $getWorkflowStaticData('global');
staticData.rateLimits = staticData.rateLimits || {};
const apiKey = $json.apiKey || 'default';
staticData.rateLimits[apiKey] = {
...rateLimitInfo,
lastUpdated: Date.now()
};
// Calculate recommended delay
let recommendedDelay = 0;
if (rateLimitInfo.remaining === 0 && rateLimitInfo.reset) {
// Wait until reset
recommendedDelay = (rateLimitInfo.reset * 1000) - Date.now();
} else if (rateLimitInfo.remaining < rateLimitInfo.limit * 0.1) {
// Less than 10% remaining - slow down
recommendedDelay = 1000; // 1 second delay
} else if (rateLimitInfo.retryAfter) {
recommendedDelay = rateLimitInfo.retryAfter * 1000;
}
return [{
json: {
...response,
rateLimit: {
...rateLimitInfo,
recommendedDelay: Math.max(0, recommendedDelay)
}
}
}];Webhook Integration
Webhook Payload Processing
// Function node for webhook payload normalization
const payload = $json;
const headers = $request.headers;
// Identify webhook source
const webhookSource = (() => {
if (headers['x-github-event']) return 'github';
if (headers['x-shopify-hmac-sha256']) return 'shopify';
if (headers['stripe-signature']) return 'stripe';
if (headers['x-gitlab-event']) return 'gitlab';
return 'unknown';
})();
// Normalize payload based on source
const normalizedEvent = (() => {
switch (webhookSource) {
case 'github':
return {
type: headers['x-github-event'],
action: payload.action,
repository: payload.repository?.full_name,
sender: payload.sender?.login,
data: payload
};
case 'stripe':
return {
type: payload.type,
action: payload.type.split('.').pop(),
resourceId: payload.data?.object?.id,
livemode: payload.livemode,
data: payload.data?.object
};
case 'shopify':
return {
type: headers['x-shopify-topic'],
action: headers['x-shopify-topic']?.split('/').pop(),
shop: headers['x-shopify-shop-domain'],
data: payload
};
default:
return {
type: 'unknown',
data: payload
};
}
})();
return [{
json: {
source: webhookSource,
receivedAt: new Date().toISOString(),
event: normalizedEvent
}
}];Best Practices Summary
- Always handle errors - Implement comprehensive error handling with retry logic
- Respect rate limits - Track and adapt to API rate limits
- Use pagination correctly - Handle both cursor and offset-based pagination
- Secure credentials - Never log or expose API keys and tokens
- Validate responses - Check response structure before processing
- Implement timeouts - Set appropriate timeouts for all requests
- Log strategically - Log enough for debugging without exposing sensitive data
By following these patterns, you can build robust, production-ready API integrations in n8n.