Add a contact
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_numberandnameare filled in from the request, and values already stored are never overwritten. - The contact was removed from this campaign earlier: it is restored as
pendingwith 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
namefilled in. Itscampaign_idcan 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(orfull). - 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_ERRORwhen the cap is reached, even if the contact already exists.
Authentication
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.
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
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
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.
Email address (Invalid email). Required for email campaigns.
E.164 WhatsApp number. Used as the dedup key when there is no phoneNumber.
Contact’s name; the first word feeds contact.first_name.
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.
Response
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>.

