Contact sync API
POST /api/v1/contacts/sync idempotently upserts contacts and their status on
one list. It is intended for server-to-server integrations; API keys must never
be exposed in browser code.
Authentication
Create a project-scoped key with the narrowest required scope:
pnpm api-key:create <project-slug> "Production contact sync" contacts:write
Add --expires-days <n> for a key that should self-revoke.
Send the resulting key once in the Authorization header:
Authorization: Bearer phk_...
The project in the request body must match the project that owns the key. An
organization key (pha_...) also works and may address any project in its
organization, by slug or id.
Key-authenticated requests are rate limited per key (fixed one-minute windows;
see lib/rate-limit.ts). Over-budget requests get 429 with a Retry-After
header — back off and retry rather than hammering.
Request
{
"projectId": "bioloja",
"listSlug": "newsletter",
"source": "website_signup",
"defaultLanguage": "pt-BR",
"triggerAutomations": true,
"contacts": [
{
"email": "reader@example.com",
"subscribed": true,
"subscribedAt": "2026-07-23T20:00:00.000Z",
"unsubscribedAt": null
}
]
}
The endpoint accepts between 1 and 5,000 contacts. Emails are normalized to lowercase. Repeating the same desired state is a no-op. Names, language, and tags are optional. Existing tags are preserved and incoming tags are merged.
Set triggerAutomations only for real-time user actions. Leave it false for
imports and reconciliation jobs so historical contacts do not receive
new-subscriber automations.
Hard-bounce and complaint suppressions are protected: a sync cannot
accidentally resubscribe them. The response reports these as
protectedSuppressions.
Response
{
"success": true,
"received": 1,
"valid": 1,
"created": 1,
"updated": 0,
"subscribed": 1,
"unsubscribed": 0,
"protectedSuppressions": 0
}
The legacy /api/contacts/sync endpoint remains available for internal clients
using WEBHOOK_SECRET, but new integrations should use the versioned endpoint
and project-scoped keys.