I Connected 47 Apps With 200 Lines of Code. Here's How.
Last year, my Zapier bill crossed £600. The workflows I'd built over three years had accumulated: email to task conversion, GitHub notifications to Slack summaries, calendar events triggering focus mode, expense receipts to accounting software. Each workflow added cost, and Zapier's per-task pricing meant automation success increased my bill.
The breaking point came when I needed a workflow Zapier couldn't handle. I wanted to aggregate data from three sources, apply custom logic, and route to different destinations based on content analysis. Zapier's linear triggers and actions couldn't express the workflow. Multi-step Zaps got close but couldn't handle the conditional branching I needed.
So I built a personal API instead. A lightweight Node.js server running on Heroku's free tier that receives webhooks, processes data with custom logic, and calls other APIs. Two hundred lines of code replaced my entire Zapier setup—and enabled workflows Zapier couldn't express.
This guide provides the complete technical implementation. If you're comfortable with JavaScript and API documentation, you can build this in a weekend. The ROI is substantial: £600+ annual savings plus unlimited workflow flexibility.
Why Build Instead of Buy
Zapier, Make (formerly Integromat), and similar tools have their place. For simple, linear workflows that non-technical users need to create, they're excellent. Pay money, save time, enable people who can't code.
But they have real limitations:
Per-task pricing scales with automation success. The more you automate, the more you pay. Complex workflows with many steps consume tasks quickly.
Linear workflow model restricts complexity. Trigger → Action → Action works for simple cases. Conditional branching, aggregation, and custom logic strain the model.
Limited customisation. You're constrained to the actions and data transformations the platform provides. Custom logic requires workarounds or isn't possible.
Dependency on third party. If Zapier changes pricing, deprecates features, or has downtime, your automation stops.
Building your own personal API eliminates these constraints:
Zero marginal cost. Once built, additional workflows cost nothing. Process 10 or 10,000 events daily for the same hosting cost (often £0).
Unlimited flexibility. Any logic you can express in code, you can implement. No platform constraints.
Full control. You own the code, the data, and the workflow definitions.
Learning investment. Building understanding of APIs and automation creates transferable skills.
The trade-off is obvious: building requires technical skill and time investment. But if you're a developer or technical professional, the skills exist. The investment is primarily the initial build.
Architecture Overview
The personal API is a simple server that:
- Receives incoming webhooks from services that support them
- Processes data with custom logic
- Calls APIs of destination services
- Optionally stores state for complex workflows
Technology Stack
Node.js + Express: Lightweight, JavaScript-based, excellent for API handling. Huge ecosystem of packages.
MongoDB: State storage when workflows need to remember things across events. MongoDB Atlas free tier provides 512MB storage—more than enough.
Heroku: Hosting platform with free tier (now Eco tier at ~$5/month) that handles deployment, HTTPS, and scaling. Alternatives: Railway, Render, Fly.io.
Native Service APIs: Direct API calls to services rather than intermediary platforms. Notion API, GitHub API, Slack API, Google APIs, etc.
System Flow
[Service A: Webhook] → [Your Server: Process] → [Service B: API Call]
↓
[MongoDB: Store State]
↓
[Service C: API Call (conditional)]
The server is the orchestration layer. Services send webhooks when events occur. Your server decides what to do and calls appropriate APIs.
Setting Up the Foundation
Step 1: Initialize the Project
Create a new directory and initialise:
mkdir personal-api
cd personal-api
npm init -y
npm install express axios dotenv mongoose
Create the basic server structure:
// index.js
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const app = express();
app.use(express.json());
// MongoDB connection (optional, for stateful workflows)
if (process.env.MONGODB_URI) {
mongoose.connect(process.env.MONGODB_URI)
.then(() => console.log('MongoDB connected'))
.catch(err => console.error('MongoDB error:', err));
}
// Health check endpoint
app.get('/', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Webhook endpoints will be added here
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Create environment file:
# .env
PORT=3000
MONGODB_URI=mongodb+srv://... # From MongoDB Atlas
NOTION_API_KEY=secret_...
SLACK_WEBHOOK_URL=https://hooks.slack.com/...
GITHUB_TOKEN=ghp_...
CHAOS_API_KEY=...
Step 2: Deploy to Heroku
Create a Procfile:
web: node index.js
Deploy:
git init
git add .
git commit -m "Initial commit"
heroku create your-personal-api
heroku config:set NOTION_API_KEY=secret_...
# Set all environment variables
git push heroku main
Your server is now running at https://your-personal-api.herokuapp.com/.
Building Integrations: 12 Working Examples
Here are 12 real integrations I built, with code you can adapt.
Integration 1: Email [TASK] to Chaos Task Creation
When I email myself with [TASK] in the subject, create a task in Chaos.
Email provider (Gmail) can forward matching emails to a webhook via Google Apps Script:
// Google Apps Script - runs periodically
function checkForTaskEmails() {
const threads = GmailApp.search('subject:[TASK] is:unread');
threads.forEach(thread => {
const messages = thread.getMessages();
const latest = messages[messages.length - 1];
const payload = {
subject: latest.getSubject().replace('[TASK]', '').trim(),
body: latest.getPlainBody(),
from: latest.getFrom(),
date: latest.getDate().toISOString()
};
UrlFetchApp.fetch('https://your-personal-api.herokuapp.com/webhook/email-task', {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload)
});
latest.markRead();
});
}
Server endpoint to receive and process:
// index.js - add this endpoint
const axios = require('axios');
app.post('/webhook/email-task', async (req, res) => {
try {
const { subject, body, from, date } = req.body;
// Extract due date if mentioned (basic parsing)
const dueMatch = body.match(/due:\s*(\d{4}-\d{2}-\d{2})/i);
const dueDate = dueMatch ? dueMatch[1] : null;
// Create task in Chaos via API
await axios.post('https://api.chaos.app/v1/tasks', {
title: subject,
description: `From email: ${from}\n\n${body}`,
due_date: dueDate,
source: 'email'
}, {
headers: {
'Authorization': `Bearer ${process.env.CHAOS_API_KEY}`,
'Content-Type': 'application/json'
}
});
res.json({ success: true, task: subject });
} catch (error) {
console.error('Email task error:', error);
res.status(500).json({ error: error.message });
}
});
Integration 2: GitHub PR Summary to Slack
Aggregate daily PR activity and post summary to Slack.
// Scheduled job - call this endpoint via cron
app.get('/jobs/github-pr-summary', async (req, res) => {
try {
const repos = ['myorg/repo1', 'myorg/repo2', 'myorg/repo3'];
const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
let allPRs = [];
for (const repo of repos) {
const response = await axios.get(
`https://api.github.com/repos/${repo}/pulls?state=all&sort=updated&since=${since}`,
{
headers: {
'Authorization': `token ${process.env.GITHUB_TOKEN}`,
'Accept': 'application/vnd.github.v3+json'
}
}
);
allPRs = allPRs.concat(response.data.map(pr => ({
...pr,
repo: repo.split('/')[1]
})));
}
// Group by status
const opened = allPRs.filter(pr => pr.state === 'open' && new Date(pr.created_at) > new Date(since));
const merged = allPRs.filter(pr => pr.merged_at && new Date(pr.merged_at) > new Date(since));
const closed = allPRs.filter(pr => pr.state === 'closed' && !pr.merged_at);
// Format Slack message
const message = {
text: `*Daily PR Summary*\n\n` +
`*Opened (${opened.length}):*\n${opened.map(pr => `• <${pr.html_url}|${pr.repo}: ${pr.title}> by ${pr.user.login}`).join('\n') || 'None'}\n\n` +
`*Merged (${merged.length}):*\n${merged.map(pr => `• <${pr.html_url}|${pr.repo}: ${pr.title}>`).join('\n') || 'None'}`
};
// Post to Slack
await axios.post(process.env.SLACK_WEBHOOK_URL, message);
res.json({ success: true, prs: allPRs.length });
} catch (error) {
console.error('GitHub summary error:', error);
res.status(500).json({ error: error.message });
}
});
Schedule via Cron-job.org (free) to hit this endpoint daily at 9am.
Integration 3: Chaos Task Complete to Notion Status Update
When task completes in Chaos, update corresponding Notion database entry.
app.post('/webhook/chaos-task-complete', async (req, res) => {
try {
const { task_id, title, completed_at, metadata } = req.body;
// Find corresponding Notion page by title
const searchResponse = await axios.post(
'https://api.notion.com/v1/databases/' + process.env.NOTION_DATABASE_ID + '/query',
{
filter: {
property: 'Task',
title: { equals: title }
}
},
{
headers: {
'Authorization': `Bearer ${process.env.NOTION_API_KEY}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json'
}
}
);
if (searchResponse.data.results.length === 0) {
return res.json({ success: true, message: 'No matching Notion entry' });
}
const pageId = searchResponse.data.results[0].id;
// Update Notion page status
await axios.patch(
`https://api.notion.com/v1/pages/${pageId}`,
{
properties: {
'Status': { select: { name: 'Complete' } },
'Completed Date': { date: { start: completed_at } }
}
},
{
headers: {
'Authorization': `Bearer ${process.env.NOTION_API_KEY}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json'
}
}
);
res.json({ success: true, notion_page: pageId });
} catch (error) {
console.error('Chaos to Notion error:', error);
res.status(500).json({ error: error.message });
}
});
Integration 4: Calendar Event to Auto-Block Focus Time
When meeting scheduled, automatically block preparation and recovery time.
app.post('/webhook/calendar-event-created', async (req, res) => {
try {
const { event_id, title, start_time, end_time, attendees } = req.body;
// Skip small/internal meetings
if (attendees.length < 3 || title.toLowerCase().includes('standup')) {
return res.json({ success: true, skipped: true });
}
const startDate = new Date(start_time);
const endDate = new Date(end_time);
// Create 15-min prep block before meeting
const prepStart = new Date(startDate.getTime() - 15 * 60 * 1000);
// Create 10-min recovery block after meeting
const recoveryStart = endDate;
const recoveryEnd = new Date(endDate.getTime() + 10 * 60 * 1000);
// Create calendar events via Google Calendar API
const calendarApi = 'https://www.googleapis.com/calendar/v3/calendars/primary/events';
await axios.post(calendarApi, {
summary: `Prep: ${title}`,
start: { dateTime: prepStart.toISOString() },
end: { dateTime: startDate.toISOString() },
colorId: '8' // Grey
}, {
headers: { 'Authorization': `Bearer ${process.env.GOOGLE_ACCESS_TOKEN}` }
});
await axios.post(calendarApi, {
summary: `Recovery: ${title}`,
start: { dateTime: recoveryStart.toISOString() },
end: { dateTime: recoveryEnd.toISOString() },
colorId: '8'
}, {
headers: { 'Authorization': `Bearer ${process.env.GOOGLE_ACCESS_TOKEN}` }
});
res.json({ success: true, blocks_created: 2 });
} catch (error) {
console.error('Calendar blocking error:', error);
res.status(500).json({ error: error.message });
}
});
Integration 5: Expense Receipt to Accounting
When receipt image arrives via email, extract data and log to accounting.
app.post('/webhook/expense-receipt', async (req, res) => {
try {
const { subject, body, attachments } = req.body;
// Parse amount from subject or body (basic regex)
const amountMatch = (subject + body).match(/[£$€](\d+\.?\d{0,2})/);
const amount = amountMatch ? parseFloat(amountMatch[1]) : null;
// Parse vendor from subject
const vendor = subject.replace(/receipt|invoice|order/gi, '').trim();
// Log to accounting system (example: Xero API)
await axios.post('https://api.xero.com/api.xro/2.0/Receipts', {
Receipts: [{
Contact: { Name: vendor || 'Unknown Vendor' },
User: { UserID: process.env.XERO_USER_ID },
LineAmountTypes: 'Inclusive',
LineItems: [{
Description: subject,
UnitAmount: amount,
AccountCode: '429' // General expenses
}]
}]
}, {
headers: {
'Authorization': `Bearer ${process.env.XERO_ACCESS_TOKEN}`,
'Content-Type': 'application/json'
}
});
res.json({ success: true, amount, vendor });
} catch (error) {
console.error('Expense receipt error:', error);
res.status(500).json({ error: error.message });
}
});
Additional Integrations (6-12)
The pattern continues for additional integrations:
- Stripe payment → Slack notification: Webhook from Stripe, format message, post to Slack.
- New RSS item → Notion reading list: Scheduled job fetches RSS, adds new items to Notion database.
- Twitter mention → Task: Webhook from Twitter (via Pipedream or IFTTT bridge), creates follow-up task.
- Slack reaction → Archive message: When specific emoji added, save message to Notion archive.
- Weather alert → Calendar block: Weather API check, create "Weather warning" event if conditions severe.
- Website form → CRM: Webhook from form provider, create contact in CRM via API.
- Time tracking → Invoice: Aggregate Toggl entries, format and send to invoicing system.
Each follows the same pattern: receive data, apply logic, call destination API.
Security Considerations
Webhook Verification
Services often include signatures to verify webhooks are legitimate:
const crypto = require('crypto');
function verifySlackSignature(req) {
const signature = req.headers['x-slack-signature'];
const timestamp = req.headers['x-slack-request-timestamp'];
const body = JSON.stringify(req.body);
const sigBasestring = `v0:${timestamp}:${body}`;
const mySignature = 'v0=' + crypto
.createHmac('sha256', process.env.SLACK_SIGNING_SECRET)
.update(sigBasestring)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(mySignature),
Buffer.from(signature)
);
}
app.post('/webhook/slack', (req, res) => {
if (!verifySlackSignature(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook...
});
Environment Variables
Never commit secrets to code. Use environment variables for all API keys, tokens, and secrets.
heroku config:set NOTION_API_KEY=secret_xxx
heroku config:set SLACK_SIGNING_SECRET=xxx
HTTPS
Heroku provides HTTPS automatically. Ensure all webhook URLs use HTTPS.
Rate Limiting
Protect against abuse:
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // 100 requests per window
});
app.use(limiter);
Troubleshooting Common Issues
Webhooks Not Arriving
Verify webhook URL is correct and publicly accessible. Check service's webhook logs (most platforms show delivery status). Ensure server is responding with 200 status. Check for firewall or platform-specific restrictions.
Authentication Errors
API tokens expire. Implement refresh logic for OAuth tokens. Check scopes—some APIs require specific permissions. Verify token is being sent in correct header format.
Rate Limits
Most APIs have rate limits. Implement:
- Exponential backoff for retries
- Request queuing for bulk operations
- Caching to reduce redundant requests
async function makeApiCallWithRetry(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (error.response?.status === 429 && i < maxRetries - 1) {
const delay = Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
}
Deployment Issues
Check Heroku logs: heroku logs --tail
Verify environment variables are set: heroku config
Ensure package.json has correct start script.
When NOT to Build This
Building a personal API isn't for everyone. Don't build if:
You're not comfortable with JavaScript/Node.js. The time investment to learn exceeds the value for most non-developers.
Workflows are simple and linear. Zapier handles simple trigger → action workflows efficiently. Don't over-engineer.
Team collaboration matters. Personal API serves one person. Team workflows need shared tooling.
You need guaranteed uptime. Heroku free tier has limitations. Mission-critical automation needs paid hosting.
Time investment isn't available. Initial build takes 10-30 hours depending on complexity. Ongoing maintenance adds more.
ROI Calculation
For my situation:
Previous Zapier cost: £600/year Current hosting cost: £0 (Heroku free tier) to £60/year (if using paid tier) Build time investment: ~25 hours initially Ongoing maintenance: ~2 hours/month
At professional hourly rate (£40/hour), the 25-hour build represents £1,000 of time. But this is learning investment—the skills transfer to future projects.
Annual savings: £540-600 Capability expansion: Unlimited—workflows that Zapier couldn't handle now work Control: Complete—no platform dependency
The ROI is positive after year one and improves every subsequent year.
Key Takeaways
A personal API—lightweight Node.js server connecting apps via native APIs—provides unlimited flexibility for £0 hosting cost.
Architecture: Express server receives webhooks, processes with custom logic, calls destination APIs, optionally stores state in MongoDB.
Tech stack: Node.js + Express + Axios + MongoDB Atlas + Heroku. All free or minimal cost.
The 12 integration examples demonstrate the pattern: email to task, GitHub to Slack, calendar to focus blocks, and more.
Security requirements: webhook signature verification, environment variables for secrets, HTTPS, rate limiting.
Don't build if: non-technical, workflows are simple, team collaboration needed, or time investment unavailable.
ROI: £600+/year savings plus capability expansion plus full control. Positive after year one.
The 200-line codebase handles what previously required £600/year of platform fees. For technical professionals, building beats buying.
Chaos provides API access for integration into custom workflows, enabling the kind of personal automation this guide describes—connecting your task management into whatever systems matter for your productivity.