A practical field guide from Automation Ace.
The short answer
To connect two apps with Cloudflare Workers, the Worker sits in the middle: App A sends data in (a webhook, or a scheduled pull from its API), the Worker validates and maps the fields, then calls App B's API with authentication, a search-then-create (upsert) to avoid duplicates, and retries for rate limits and temporary errors. It is the same job a Zap does, written as code.
If you have never deployed a Worker, do your first API automation first.
The architecture
App A ── webhook or scheduled pull ──▶ Worker ──▶ App B API
│
├─ auth check on inbound
├─ validate + map fields
├─ search → update or create
├─ retry 429 / 5xx
└─ log result
- Inbound: a webhook from App A (see receiving webhooks) or a scheduled pull from App A's API.
- Outbound: calls to App B's API with its credentials stored as secrets.
Example: website form to CRM
// src/index.js — form submission (App A) → CRM contact (App B)
export default {
async fetch(request, env, ctx) {
if (request.method !== 'POST') return new Response('Method not allowed', { status: 405 });
if (request.headers.get('X-Form-Token') !== env.FORM_TOKEN) {
return new Response('Unauthorized', { status: 401 });
}
const submission = await request.json();
// 1) Validate and map fields from App A to App B's schema
const email = (submission.email || '').trim().toLowerCase();
if (!email) return new Response('Missing email', { status: 400 });
const contact = {
email,
first_name: submission.first_name?.trim() || '',
last_name: submission.last_name?.trim() || '',
source: 'website-form',
};
// 2) Upsert: search first, then update or create (prevents duplicates)
const existing = await crm(env, `/contacts?email=${encodeURIComponent(email)}`);
const result = existing.results?.length
? await crm(env, `/contacts/${existing.results[0].id}`, 'PATCH', contact)
: await crm(env, '/contacts', 'POST', contact);
return Response.json({ ok: true, id: result.id });
},
};
// Small API client with auth and retry on 429/5xx
async function crm(env, path, method = 'GET', body) {
for (let attempt = 1; attempt <= 3; attempt++) {
const res = await fetch(`https://api.example-crm.com/v1${path}`, {
method,
headers: { Authorization: `Bearer ${env.CRM_API_KEY}`, 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined,
});
if (res.ok) return res.json();
if (res.status === 429 || res.status >= 500) {
const wait = Number(res.headers.get('Retry-After')) * 1000 || attempt * 1000;
await new Promise(r => setTimeout(r, wait));
continue;
}
throw new Error(`CRM ${method} ${path} failed: ${res.status} ${await res.text()}`);
}
throw new Error(`CRM ${method} ${path} failed after retries`);
}
Key design decisions
- Authentication on both sides: protect your inbound endpoint and store App B's key as a secret. OAuth apps need token refresh logic, which is more work in code than in Zapier. See API authentication and the OAuth glossary entry.
- Field mapping: normalize case, trim whitespace, and convert dates and numbers. See data mapping and handling blank values.
- Upsert, not insert: search by a unique key before creating. See avoiding duplicate records.
- Retries and rate limits: back off on 429 and 5xx, honoring
Retry-After. See 429 errors. - Pagination: when pulling lists, follow cursors or page numbers until done.
- Observability: log IDs and outcomes, and alert on failures.
One-way vs two-way sync
One-way sync (A to B) is straightforward. Two-way sync needs loop prevention and conflict rules: tag records updated by the automation, compare timestamps, and decide which system wins. Store the ID mapping between apps in KV or D1. See preventing concurrent updates and why CRM records do not sync.
When to use a Worker instead of a Zap
- High volume where task costs add up.
- Complex mapping or logic that would need several Code steps.
- An API with no Zapier integration, or actions the integration does not expose. Compare API by Zapier.
- Strict response-time or security requirements.
For long, multistep syncs with waits and per-step retries, use Cloudflare Workflows. For professional help, see custom API integrations and API integration for non-technical teams.
Frequently asked questions
How do I connect two apps with Cloudflare Workers?
Receive data from App A with a webhook or a scheduled API pull, validate and map the fields in the Worker, then call App B's API with stored credentials, searching before creating to avoid duplicates and retrying on rate limits.
How do I avoid creating duplicate records when syncing apps?
Search App B by a unique key such as email or external ID before creating a record, and update the existing record when one is found.
How should a Worker handle API rate limits?
Retry on 429 and 5xx responses with increasing delays, honor the Retry-After header when present, and spread large batches over time.
Can Cloudflare Workers handle OAuth APIs?
Yes, but you must implement the token exchange and refresh yourself and store tokens securely, for example in KV or D1. Zapier handles OAuth for you, which is one reason to keep some integrations there.
Should I build a two-way sync in a Worker?
Only with clear rules: prevent loops by tagging automated updates, store the ID mapping between apps, and decide which system wins on conflicts.
Disclaimer: Zapier features, plan availability, and settings can change. Confirm current details in Zapier's help documentation and Cloudflare's developer documentation and pricing pages. Limits and plan features change over time. Code samples are simplified starting points; add your own validation, error handling, and security review before production use before relying on a specific setting. This article may include links to apps, products, or services; some links may be affiliate links, which means Automation Ace may earn a commission at no extra cost to you.