Add a contact

View as Markdown
Adds one contact to a campaign that is not `completed`/`archived`. It is created `pending`. At least one of `phoneNumber`, `email` or `whatsappNumber` is required. For more than a handful of contacts use `POST /api/campaigns/{campaignId}/contacts/bulk`. **Deduplication.** The contact is keyed by phone number inside the campaign: - The phone already exists in this campaign: the existing row is returned (still 201). Blank `email`, `whatsapp_number` and `name` are filled in from the request, and values already stored are never overwritten. - The contact was removed from this campaign earlier: it is restored as `pending` with the new values. - Email-only request (no phone or WhatsApp) whose address already belongs to a contact anywhere in the workspace (case-insensitive): that contact is returned, with a blank `name` filled in. Its `campaign_id` can then be a different campaign. Adding a contact to an `active` campaign does not queue it. Pause and activate the campaign to reach it. A voice campaign can only call contacts with a real phone number; email-only contacts end as `invalid` there. **Side effects.** Inserts the contact, records its consent provenance (`api:manual`, no evidence) and writes an audit entry. Nobody is contacted. **Idempotency.** Safe to retry: the same phone (or the same address, for email-only contacts) returns the same row. **Webhook events.** `audit.log_recorded` for webhooks subscribed to it. See [Webhooks](/webhooks). **Access** - **Required scope:** `write` (or `full`). - **Rate limit:** Contacts import — 5 requests/min per workspace, shared by every route under `/api/campaigns/{campaignId}/contacts`, on top of the general API limit. See [Rate limits](/rate-limits). - **Plan:** Counts against the workspace-wide contact cap (Starter 500, Growth 2,000, Business 20,000). Refused with 403 `BILLING_ERROR` when the cap is reached, even if the contact already exists.

Authentication

AuthorizationBearer
Workspace API key: `jl_` followed by 64 lowercase hex characters, created by the workspace owner in the dashboard (**Settings → API Keys**) and sent as `Authorization: Bearer jl_...`. The plaintext is shown once, at creation; Jelliu stores only a SHA-256 hash. A workspace can hold up to 25 active keys. | Scope | GET / HEAD | POST / PUT / PATCH / DELETE | Admin-only routes | | --- | --- | --- | --- | | `read` | Yes | No | No | | `write` | Yes | Yes | No | | `full` | Yes | Yes | Yes | Operations restricted to admins or owners reject keys without the `full` scope with `403`, and say so in their description. No key, whatever its scope, can mint or revoke API keys or rotate a webhook secret — that requires a signed-in owner session. A revoked key stops authenticating within about 10 seconds. See [Authentication](/authentication).

Path parameters

campaignIdstringRequiredformat: "uuid"

UUID of the campaign that receives the contact. An unknown, deleted or foreign campaign answers 404; a value that is not a UUID answers 400.

Request

This endpoint expects an object.
phoneNumberstringOptionalformat: "^\+[1-9]\d{1,14}$"

E.164 phone number with country code (Must be E.164 format). It is the dedup key in the campaign. Numbers shorter than 7 digits pass here but cannot be dialed and end as invalid.

emailstringOptionalformat: "email"<=320 characters

Email address (Invalid email). Required for email campaigns.

whatsappNumberstringOptionalformat: "^\+[1-9]\d{1,14}$"

E.164 WhatsApp number. Used as the dedup key when there is no phoneNumber.

namestringOptional<=200 characters

Contact’s name; the first word feeds contact.first_name.

metadatamap from strings to stringsOptional

Up to 20 keys matching ^[a-zA-Z0-9_-]+\$, string values up to 500 characters. The agent can read them, and tokens can use them as contact.metadata.<key>. Card-like and 9-digit ID-like numbers are redacted before storage.

crmExternalIdstringOptional<=500 characters
Id of the matching record in an external CRM.
crmProviderstringOptional<=50 characters
CRM the external id belongs to.

Response

The created, restored or existing contact.
dataobject

A contact row as returned by POST /api/campaigns/{campaignId}/contacts. Keys are snake_case. phone_number is always present: contacts saved with only a WhatsApp number use it, and email-only contacts get a synthetic key em:<16 hex>.

Errors

400
Bad Request Error
401
Unauthorized Error
403
Forbidden Error
404
Not Found Error
429
Too Many Requests Error