Building a Zapier Integration for a Tiny SaaS: The API Key Default I Almost Shipped A developer running a small SaaS that places an AI assistant on small business websites built a public Zapier integration in an afternoon, then spent most of a day correcting it, including removing a default request template that placed customers' API keys in query strings where they would land in access logs, proxy logs, analytics and error trackers. The writeup details stable lead IDs for Zapier's polling deduplication, HMAC-derived API keys computed from the account ID and a server secret, and the review requirements that took longer than the code itself. I run a small SaaS that puts an AI assistant on small business websites. It answers visitors and captures leads. Owners kept asking the same thing: "Can the leads go straight into my CRM / Google Sheet / Slack?" The answer to "connect to everything" is usually Zapier, so I built a public Zapier integration. It took an afternoon to get working and most of a day to get right. This post is about the gap between those two, including one default that would have leaked every customer's API key into URLs. Zapier's polling triggers need surprisingly little from you: GET /api/agent?zapier=me X-API-Key: aicf abc123 0123456789abcdef01234567 200 OK { "ok": true, "id": "abc123", "name": "Bright Smile Dental", "site": "brightsmile.com" } GET /api/agent?zapier=leads X-API-Key: aicf abc123 0123456789abcdef01234567 200 OK { "id": "abc123-1793812345678", "created at": "2026-11-05T09:30:12.000Z", "name": "Sara Khan", "email": "sara@example.com", "message": "Do you do teeth whitening on Saturdays?", "conversation": "Visitor: ...\nAssistant: ..." } The one thing that matters most in the second response is id . Zapier deduplicates polling results by id. If your ids are unstable array index, a random value per request , every poll looks like new data and users get duplicate Zaps. Mine combine the account id and the lead's creation timestamp, so the same lead always has the same id. I didn't want a new table of API keys to manage. Each key is derived from the account id with an HMAC over a server secret: js const crypto = require 'crypto' ; const apiKey = id = 'aicf ' + id + ' ' + crypto.createHmac 'sha256', process.env.SERVER SECRET .update 'api:' + id .digest 'hex' .slice 0, 24 ; // Verify: parse the id out of the key, recompute, compare. const m = key.match /^aicf a-z0-9 {4,24} a-f0-9 {24} $/i ; const valid = m && key === apiKey m 1 ; The prefix makes keys easy to recognise in logs and secret scanners. The trade off: rotating one customer's key means adding a per-account salt later. For a v1, that is fine. Zapier's platform UI has a request template that injects auth fields into every request. When you pick API key auth, it helpfully adds your key field in two places: X-API-KEY header, and ?api key={{bundle.authData.api key}} Everything works with both. Tests pass. Nothing warns you. But a key in a query string ends up in places a header never does: server access logs, proxy logs, analytics tools, error trackers that record URLs. My API only reads the header, so the query parameter added risk and zero function. The fix took a minute: open Advanced, Request Template , remove the URL param, keep the header. One quirk: the template wouldn't let me delete its last remaining row, so I cleared the key and value fields and saved. Worth checking yours. Lesson: read every field of a generated request, not just the ones you set yourself. {{bundle.inputData.name}} {{bundle.inputData.site}} turns "Account 4" into "Bright Smile Dental brightsmile.com ". Users with several clients will thank you. conversation . This is what users see when mapping fields, before they have any real data. Getting a public listing has requirements that code can't satisfy: Budget time for these. In my case the review requirements took longer than writing the integration. The working version took an afternoon. The right version needed three extra checks: stable ids, the key out of the URL, and a human look at every default. If you build one, I wrote up our API docs https://aicofounderhq.com/developers as a reference for the shape of a minimal polling API. Happy to answer questions in the comments.