Skip to navigation

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. contact.created for each new contact row ({ contact_id, campaign_id, name, phone_number, email, whatsapp_number, status, source, created_at }, with source api:manual); not sent when an existing contact is returned. Delivered only to webhooks that name contact.created (it is opt-in only: the "*" wildcard does not include it). Also audit.log_recorded for webhooks subscribed to it. See Webhooks.

Access

  • Required scope: write (or full).
  • Rate limit: General API — 120 requests/min per workspace on Starter or with no active plan, 200 on Growth, 300 on Business, 600 on Enterprise. The 5/min contacts-import limit does not apply to this route. See 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.

ScopeGET / HEADPOST / PUT / PATCH / DELETEAdmin-only routes
readYesNoNo
writeYesYesNo
fullYesYesYes

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.

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