logo

How to Chain Multiple API Calls

Many backend workflows require calling several APIs in sequence. Charging a payment, then creating a shipment, then sending a confirmation email. Fetching a user from a CRM (Customer Relationship Management), enriching with analytics data, then syncing to a billing provider. Each call depends on the previous step’s output.

Running this synchronously blocks your server for the total duration of every request, and a failure at any step forces a full restart.

New to async workflows? See the glossary for key terms: API chaining, task chaining, webhook, callback.

The Problem

// Synchronous chain - blocks for the total duration of all calls
app.post('/api/process-order', async (req, res) => {
const payment = await chargePayment(req.body.card, req.body.amount); // 3-5s
const shipment = await createShipment(payment.orderId, req.body.address); // 2-4s
const email = await sendConfirmation(req.body.email, shipment.trackingId); // 1-2s
res.json({ orderId: payment.orderId, trackingId: shipment.trackingId });
// User waited 6-11 seconds, and if sendConfirmation fails, the whole request fails
});

This approach creates several problems:

  • Timeouts: The chain exceeds gateway or serverless function limits
  • No partial recovery: If step 3 fails, you must re-run steps 1 and 2 before retrying
  • Wasted resources: Your server blocks on I/O for the full duration
  • No visibility: You lack any record of which step succeeded or failed

The Solution

Step 1: Identify the API chain

Map out your dependent calls and their data flow.

chargePayment(card, amount)
└─ returns { orderId, receiptUrl }
└─ createShipment(orderId, address)
└─ returns { trackingId, estimatedDelivery }
└─ sendConfirmation(email, trackingId)
└─ returns { messageId }

Each step consumes specific output from the one before, forming your chain.

Step 2: Queue the first API call

Replace the synchronous chain with a single queued task. Your endpoint responds instantly.

app.post('/api/process-order', async (req, res) => {
const workflowId = crypto.randomUUID();
await saveWorkflow({
id: workflowId,
type: 'order-processing',
status: 'started',
steps: { payment: 'pending', shipment: 'pending', confirmation: 'pending' },
context: req.body,
});
await aq.tasks.create({
targetUrl: 'https://your-app.com/api/steps/charge-payment',
payload: {
workflowId,
card: req.body.card,
amount: req.body.amount,
},
webhookUrl: 'https://your-app.com/api/chain/payment-complete',
retries: 2,
timeout: 30,
});
res.json({ workflowId, status: 'processing' });
});

Step 3: Build step handlers that trigger the next call

Each webhook endpoint processes the previous result and queues the next operation.

// After payment succeeds, create the shipment
app.post('/api/chain/payment-complete', async (req, res) => {
const { workflowId } = req.body.payload;
const { orderId, receiptUrl } = req.body.result;
await updateWorkflow(workflowId, {
'steps.payment': 'completed',
orderId,
receiptUrl,
});
const workflow = await getWorkflow(workflowId);
await aq.tasks.create({
targetUrl: 'https://your-app.com/api/steps/create-shipment',
payload: {
workflowId,
orderId,
address: workflow.context.address,
},
webhookUrl: 'https://your-app.com/api/chain/shipment-complete',
retries: 3,
timeout: 30,
});
res.status(200).json({ received: true });
});
// After shipment is created, send the confirmation
app.post('/api/chain/shipment-complete', async (req, res) => {
const { workflowId } = req.body.payload;
const { trackingId, estimatedDelivery } = req.body.result;
await updateWorkflow(workflowId, {
'steps.shipment': 'completed',
trackingId,
estimatedDelivery,
});
const workflow = await getWorkflow(workflowId);
await aq.tasks.create({
targetUrl: 'https://your-app.com/api/steps/send-confirmation',
payload: {
workflowId,
email: workflow.context.email,
trackingId,
orderId: workflow.orderId,
},
webhookUrl: 'https://your-app.com/api/chain/confirmation-sent',
retries: 3,
timeout: 15,
});
res.status(200).json({ received: true });
});
// Final step - mark workflow complete
app.post('/api/chain/confirmation-sent', async (req, res) => {
const { workflowId } = req.body.payload;
await updateWorkflow(workflowId, {
'steps.confirmation': 'completed',
status: 'completed',
});
res.status(200).json({ received: true });
});

Step 4: Add error handling and per-step retries

Each step carries its own retry and timeout settings. When all retries run out, handle the failure.

// Generic failure handler for any step
app.post('/api/chain/step-failed', async (req, res) => {
const { workflowId, step } = req.body.payload;
const { error } = req.body;
await updateWorkflow(workflowId, {
[`steps.${step}`]: 'failed',
status: 'failed',
error: error,
});
// Alert your team or trigger a compensation flow
await notifyOpsTeam(workflowId, step, error);
res.status(200).json({ received: true });
});

Configure retry counts per step based on each service’s reliability.

// Payment: retry cautiously (idempotency key required)
{ retries: 2, timeout: 30 }
// Shipment: retry more aggressively
{ retries: 3, timeout: 30 }
// Email: retry freely, sending twice is acceptable
{ retries: 5, timeout: 15 }

Step 5: Track workflow progress

Let clients poll for the current state of their workflow.

app.get('/api/workflows/:id', async (req, res) => {
const workflow = await getWorkflow(req.params.id);
res.json({
workflowId: workflow.id,
status: workflow.status,
steps: workflow.steps,
trackingId: workflow.trackingId || null,
orderId: workflow.orderId || null,
});
});

Example response:

{
"workflowId": "abc-123",
"status": "processing",
"steps": {
"payment": "completed",
"shipment": "completed",
"confirmation": "pending"
},
"trackingId": "TRACK-456",
"orderId": "ORD-789"
}

When to Use This Pattern

ScenarioStepsChain?
Order processingCharge, ship, notifyYes
User onboardingVerify, provision, welcome emailYes
Data enrichmentFetch from CRM, analytics, billingYes
Bulk email sendSingle API call per recipientNo, use fan-out
Independent notificationsEmail + SMS + pushNo, run in parallel

Use API chaining when steps are dependent and each needs the previous result. Use fan-out when steps are independent and can run concurrently.