# 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:

```sh
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:

```text
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

```json
{
  "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

```json
{
  "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.
