IntegrationsThird-Party Integrations & Webhooks
Your backend won't live in isolation. Almost every real application integrates with external services: Stripe or Paystack for payments, SendGrid or Mailchimp for emails, Cloudinary for image storage, Twilio for SMS, Google for OAuth login, etc. Understanding how to call these external APIs — and how to let them call you back — is an essential production skill.
Making Outbound API Requests
When your server calls an external API, your server temporarily acts as a client. You send an HTTP request to an external server and wait for the response. In Node.js, the modern way to do this is with the built-in fetch API (Node 18+) or the popular axios library.
// Using the Fetch API (Node.js 18+ or install node-fetch)
const response = await fetch('https://api.exchangerate.host/latest?base=USD', {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
if (!response.ok) {
throw new Error(`External API failed: ${response.status}`);
}
const data = await response.json();
console.log(data.rates.GHS); // Current USD to GHS exchange rate
// Using axios (npm install axios) — slightly more ergonomic API
const axios = require('axios');
const { data } = await axios.get('https://api.exchangerate.host/latest', {
params: { base: 'USD' }, // Automatically appended as query params
headers: { 'Accept': 'application/json' },
timeout: 5000 // Fail if response takes > 5 seconds
});
console.log(data.rates.GHS);
Stripe Payment Integration — A Real Example
Stripe is the gold standard for payment APIs. Here's a complete payment flow: create a payment intent on your server, send the client secret to your frontend, and confirm the payment is complete via webhook.
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// Step 1: Frontend calls this to start the checkout process
app.post('/payments/create-intent', authenticate, async (req, res) => {
const { amount, currency } = req.body; // amount in smallest currency unit (e.g., cents)
try {
const paymentIntent = await stripe.paymentIntents.create({
amount: amount, // 2999 = $29.99
currency: currency, // 'usd', 'ghs', etc.
metadata: {
userId: req.user.userId // Track which user is paying
}
});
// Send the clientSecret to the frontend
// The frontend uses this with Stripe.js to render the payment form
res.json({ clientSecret: paymentIntent.client_secret });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
What Are Webhooks?
A webhook is the reverse of a regular API call. Instead of your server calling the external service to check if something happened, the external service calls your server whenever an event occurs.
Think of it as the difference between polling and subscribing:
❌Polling (inefficient)You: "Did the payment succeed?" ... wait 1 second ... "Did the payment succeed?" ... This is wasteful and delayed.
✅Webhook (efficient)Stripe: "I will call your URL automatically the moment the payment succeeds." One call, zero polling.
Webhook use cases you will implement constantly:
- Stripe/Paystack: Notify you when a payment succeeds, fails, or is refunded
- GitHub Actions: Trigger deployments when code is pushed
- SendGrid: Notify you when an email bounces or is opened
- Twilio: Notify you when an SMS is delivered
Building a Webhook Endpoint
A webhook endpoint is just a POST route on your server. The critical difference is signature verification: you must confirm that the request actually came from Stripe (or wherever), not from a malicious actor who found your webhook URL.
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// ⚠️ IMPORTANT: The webhook handler must receive the RAW body, not the parsed JSON.
// express.json() converts it to an object, but Stripe's signature check needs the raw bytes.
app.post('/webhooks/stripe',
express.raw({ type: 'application/json' }),
async (req, res) => {
const signature = req.headers['stripe-signature'];
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
let event;
try {
// Verify the signature — this proves it's really from Stripe
event = stripe.webhooks.constructEvent(req.body, signature, webhookSecret);
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).json({ error: 'Invalid signature' });
}
// Process the event based on its type
switch (event.type) {
case 'payment_intent.succeeded': {
const paymentIntent = event.data.object;
const userId = paymentIntent.metadata.userId;
// Fulfill the order: update subscription, send receipt, etc.
await db.createOrder({
userId,
amount: paymentIntent.amount,
stripeId: paymentIntent.id,
status: 'paid'
});
await sendReceiptEmail(userId, paymentIntent.amount);
console.log(`Payment succeeded: ${paymentIntent.id}`);
break;
}
case 'payment_intent.payment_failed': {
const paymentIntent = event.data.object;
await sendPaymentFailedEmail(paymentIntent.metadata.userId);
break;
}
default:
console.log(`Unhandled webhook event type: ${event.type}`);
}
// Always return 200 quickly. If you take too long, Stripe retries.
res.json({ received: true });
});
💡Idempotency: Stripe may send the same webhook event multiple times if your server times out. Always make webhook handlers idempotent — check if you've already processed this event ID before doing it again. Store processed event IDs in your database.
Sending Emails with Nodemailer
const nodemailer = require('nodemailer');
// Configure transporter (this uses SendGrid's SMTP, but any SMTP works)
const transporter = nodemailer.createTransport({
host: 'smtp.sendgrid.net',
port: 587,
secure: false,
auth: {
user: 'apikey',
pass: process.env.SENDGRID_API_KEY
}
});
// Helper function to send emails
async function sendEmail({ to, subject, html }) {
const info = await transporter.sendMail({
from: '"VoidX"
',
to,
subject,
html
});
console.log(`Email sent: ${info.messageId}`);
return info;
}
// Usage
await sendEmail({
to: 'user@example.com',
subject: 'Welcome to VoidX!',
html: 'Welcome!
Your account is ready.
'
});