Reference
Rate limits & requirements
These rules apply to every request to the Sync Users & Subscribers connector - the no-code embed, the REST API, and the MCP server. Follow them or requests are rejected: traffic over a limit returns 429, oversized or unsafe requests are refused, and each key is scoped so a leaked one cannot read your data. This page lists the exact limits, the response headers that report your usage, and the requirement and reason behind each one.
Rate limits
Limits are enforced per API key, not per IP address. One key's traffic never slows another customer down, and a high-volume site behind a single egress IP is not throttled by its own neighbours. Every limit resets on a rolling window.
| Operation | Per minute | Per hour |
|---|---|---|
| Read (GET contacts, search, whoami) | 600 / minute | 5,000 / hour |
| Create one contact (POST /contacts) | 60 / minute | 5,000 / hour |
| Batch create (POST /contacts/batch) | 30 / minute | 5,000 / hour |
If you need higher limits for a bulk import or a large migration, use POST /contacts/batch (up to 100 contacts per call) instead of looping single creates, or get in touch and we will lift the ceiling on your key.
Reading your limit from the response
Every response carries your current standing in the headers below, so you never have to guess. They are exposed cross-origin, so even a browser embed using a publishable key can read them.
| Header | Meaning |
|---|---|
| X-RateLimit-Limit-short | Your ceiling for the 1-minute window. |
| X-RateLimit-Remaining-short | Requests left in the current minute. |
| X-RateLimit-Reset-short | Seconds until the 1-minute window resets. |
| X-RateLimit-Limit-medium | Your ceiling for the 1-hour window. |
| X-RateLimit-Remaining-medium | Requests left in the current hour. |
| Retry-After | On a 429, the seconds to wait before retrying. |
Handling a 429
When you exceed a window the API returns 429 Too Many Requests with a Retry-After header in seconds. Back off and retry rather than hammering. A small exponential backoff that honours Retry-After covers it:
async function syncContact(payload, key) {
for (let attempt = 0; attempt < 5; attempt++) {
const res = await fetch("https://app.zoye.io/public/v1/contacts", {
method: "POST",
headers: {
Authorization: "Bearer " + key,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (res.status !== 429) return res; // success or a real error
// Respect Retry-After, falling back to exponential backoff.
const retryAfter = Number(res.headers.get("Retry-After")) || 2 ** attempt;
await new Promise((r) => setTimeout(r, retryAfter * 1000));
}
throw new Error("Rate limited - try again later");
}Payload limits
| Limit | Value |
|---|---|
| Request body size | 64 KB per request. Larger bodies are rejected. |
| Batch size | Up to 100 contacts per POST /contacts/batch call. |
| Custom fields per contact org | 100 total. Unknown field names become custom fields by label. |
| Auto-created custom fields per request | 10. Beyond that, create the fields in the app first. |
Key security
How you handle your keys is the single biggest factor in keeping your workspace safe.
Do
- Use a publishable key (
zk_pub_) in the browser. It can only create contacts (write-only, no read), so it is safe to put in page source. The no-code embed uses this automatically. - Keep secret keys (
zk_live_) server-side only. A secret key can read and search your contacts. Store it in an environment variable, never in client code, a repo, or a mobile app bundle. - Use one key per source. Name each key after where it lives (Marketing site, Webflow, Checkout). Each becomes its own labelled chip on your Leads page, and you can revoke one without touching the others.
- Rotate and revoke from the app. If a key leaks, revoke it in the Sync Users & Subscribers connector and mint a new one. Revocation takes effect immediately.
- Turn on bot protection for public forms (Cloudflare Turnstile) so a scraped publishable key cannot be used to flood your Leads with junk.
Don't
- Don't put a secret key in the browser or anywhere a visitor can view source. Use the publishable key there.
- Don't loop single creates for a bulk import. Use the batch endpoint - it is faster and has its own limit.
- Don't retry a 429 immediately in a tight loop. Honour
Retry-Afterand back off. - Don't hard-code a key you committed to git. Treat any committed key as leaked - revoke it.
Webhook safety
Outbound webhooks follow the same safety-first posture:
- Public endpoints only. Webhook URLs must point at a public address. Private, localhost, and cloud-metadata addresses are rejected, so a webhook can never be turned into a probe of an internal network.
- Always verify the signature. Check
X-Zoye-Signature(HMAC-SHA256) before acting on a payload, and ignore anything that does not match. - Respond fast and be idempotent. Acknowledge with a 2xx right away, do slow work in the background, and de-duplicate on
X-Zoye-Deliverysince retries can arrive more than once.
New to the connector? Start with the Quickstart, then pick a method: No-code embed, REST API, or MCP server.