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 callsapp.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 shipmentapp.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 confirmationapp.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 completeapp.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 stepapp.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
| Scenario | Steps | Chain? |
|---|---|---|
| Order processing | Charge, ship, notify | Yes |
| User onboarding | Verify, provision, welcome email | Yes |
| Data enrichment | Fetch from CRM, analytics, billing | Yes |
| Bulk email send | Single API call per recipient | No, use fan-out |
| Independent notifications | Email + SMS + push | No, 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.