Authorization: Bearer header. See Authentication and the API Key guide
to get and configure the key. Base: https://api.nuvia.ai, prefix /v1.
Each case lists the required API key scopes. Grant only those when creating the key.
Case 1: Send your first message
Scopes:inboxes:read, conversations:read, messages:read, messages:create.
Discover your channels (inboxes)
Get a conversation
conversationId (or create one with POST /v1/conversations).Send a text message
POST /v1/messages/send with the conversationId and the message object.contentType: "TEXT" for text messages.Read the conversation history
GET /v1/messages/conversation/:id returns the messages; attachments are at
GET /v1/messages/conversation/:id/attachments.Case 2: Receive events via webhooks
Register a URL of yours for Nuvia to notify in real time (messages, conversations, contacts). This case shows the end-to-end flow; the payload format, the full list of events, and the delivery guarantees are in the Webhooks guide. Scopes: to register and query,webhooks:create and webhooks:read; to manage,
webhooks:update and webhooks:delete (+ messages:create if you reply automatically).
Register a webhook
POST /v1/webhooks. There are 12 valid events: MESSAGE_SENT, MESSAGE_RECEIVED,
MESSAGE_DELIVERED, MESSAGE_READ, MESSAGE_FAILED, CONVERSATION_CREATED,
CONVERSATION_UPDATED, CONVERSATION_STEP_CHANGED, CONTACT_CREATED, CONTACT_UPDATED,
FOLLOWUP_SENT, and FIELD_UPDATED. See what each one delivers in the
event table.Your company has one webhook: if one is already configured, this call returns 409.
Update the existing one instead of creating another.custom_headers (for you to validate the origin) and status (ACTIVE/INACTIVE).Receive the callbacks
POST to your url, with the header
X-Webhook-Event and a three-field body:data carries the event’s resource: the message, the conversation, or the contact, in the
same format as the corresponding GET. FIELD_UPDATED is the exception and has its own
structure.React to the event
POST /v1/messages/send (Case 1) or download attachments with
GET /v1/messages/conversation/:id/attachments.Manage the configuration
GET /v1/webhooks returns one object with the company’s configuration (or null). It’s
not a list. Use that object’s id to update with PUT /v1/webhooks/:id or remove with
DELETE /v1/webhooks/:id. To pause without losing the configuration, prefer PUT with
status: "INACTIVE".Case 3: Campaigns and bulk sending
Scopes:campaigns:create, campaigns:read, campaigns:update, campaigns:delete, and
campaign-enrollments:create (+ messages:create for the bulk template shortcut).
A campaign reaches the contacts enrolled in it. Each enrollment links a contact to a campaign
and tracks where that contact is in the sequence. Through the API, creating the campaign enrolls
no one, and adding a contact to a list doesn’t either: enrollment is always an explicit step.
Create the campaign
POST /v1/campaigns. Required fields: name, channel_type, trigger_type, and
audience_config; optional ones include description, trigger_config, behavior_config,
sender_config, and exclusion_lists. The internal structures of these configuration objects
are in the API reference. Follow the schema there.The campaign starts as a draft. audience_config describes the audience, but creating the
campaign through the API enrolls no one. Enrollments come in the next step.Exclusion lists (exclusion_lists) can only be chosen while the campaign is a draft. After
activation, what changes is their content (see Case 4).Enroll the audience
campaign-enrollments:create scope:Check before sending
POST /v1/campaigns/audience-preview counts the
audience’s contacts and POST /v1/campaigns/preview-message resolves the message variables.Activate and track
POST /v1/campaigns/:id/activate (pause with /pause). The campaign needs at
least one active step. Activation queues the contacts already enrolled and returns how many in
enrollments_enqueued.GET /v1/campaigns/:id/analytics and GET /v1/campaigns/:id/step-analytics, and
export the contacts with GET /v1/campaigns/:id/enrollments/export.POST /v1/messages/send-bulk-template (scope messages:create). The step-by-step, with the
template parameters and Meta’s limits, is in the guide
Bulk template sending on WhatsApp.Case 4: Enroll contacts from your CRM
For integrations where your system decides who enters which sequence: the campaign is already active, and each contact is enrolled when the CRM asks for it. Nuvia does the sending, ends the sequence when the lead replies, and sends the events back for you to update the CRM. Scopes:campaign-enrollments:create, campaign-enrollments:read, campaigns:read, and
contacts:read (+ messages:read to read conversations and webhooks:create to receive
events).
Pick the campaign
GET /v1/campaigns lists the campaigns with status and channel; GET /v1/campaigns/:id returns
one. Store the campaign id for each sequence. Enrollment is only accepted on a campaign with
status ACTIVE.The campaign’s channel defines the contact’s required identifier: a LinkedIn campaign requires
linkedin_identifier; any other requires a phone number. Use one campaign per channel and let
the CRM decide which one to enroll in.Enroll the contact
POST /v1/campaigns/:id/enrollments/from-contact. The route finds the contact by phone (or,
without a phone, by linkedin_identifier), creates the contact if it doesn’t exist, and enrolls
it in the campaign.Read the result
status equal to enrolled or skipped:enroll_again, a re-enrollment comes with re_enrolled: true. When the enrollment is
skipped, the reason comes in reason, along with the contactId when the contact was found:404 when the campaign doesn’t exist or belongs to another company, 409 when it isn’t
ACTIVE, and 400 when the body is invalid (for example, with neither phone nor
linkedin_identifier).Store the IDs in the CRM
contactId, the enrollmentId, and the campaign id alongside the CRM record. They
link Nuvia’s events and queries to your deal or opportunity.In Nuvia, a person is one contact, with one CRM link:fieldswrites to the contact, not to the enrollment. The enrollment has no deal field.external_idis one per contact, and the last one sent wins.- Message variables use the contact’s current value.
fields: deal B’s enrollment overwrites deal A’s value on the
contact.Handle retries and timeouts
skipped with
already_enrolled (or already_in_progress, with enroll_again) as success.Track the enrollments
status:completion_reason value without
breaking.When the lead replies, Nuvia ends all of their active enrollments that already had activity
(a send, a failure, or an invite), in any campaign, with completion_reason: REPLIED. The
sequence stops on its own: your system doesn’t need to interrupt it, only move the lead to the
next sales action.There’s no webhook for enrollment created, status changed, or enrollment completed. To keep the
CRM up to date, query periodically:contactId or enrollmentId. That’s why the
enrollmentId stored in the CRM is your reference.Receive replies via webhook
- Lead reply:
MESSAGE_RECEIVEDwithsender_type: CONTACTandmessage_type: INCOMING. Filter by both fields: the event also fires for messages that don’t come from the lead, such as those sent from the WhatsApp Business app in coexistence. See who sent the message. - Delivery failure:
MESSAGE_FAILED. Failures that happen before the send attempt don’t trigger a webhook and only show up in the campaign’s executions. - Funnel step change:
CONVERSATION_STEP_CHANGEDdoesn’t mean a reply. A campaign step can move the conversation to a funnel step on the send itself, before the lead replies.
external_id. Use the contactId to find the
record in the CRM. The webhook makes a single delivery attempt, so keep the periodic query from
the previous step to reconcile missed events.Avoid simultaneous sequences
- In the CRM: before enrolling, check whether the contact already has an enrollment in
progress, using the
enrollmentIdvalues you stored. - In Nuvia: use exclusion lists. They’re chosen when the campaign is created (only while it’s
a draft), but the content of each list is checked before every send. Adding a contact to an
exclusion list after activation applies to the next sends, and their enrollment ends with
EXCLUSION_LIST.
Case 5: Sync contacts
Keep your external CRM and Nuvia in sync. Scopes:contacts:read, contacts:create, contacts:update, contacts:delete.
Check for duplicates by phone
POST /v1/contacts/find-by-phones.Create the missing contacts
POST /v1/contacts. Required: name and at least one identifier among phone (phone_number,
digits only, area code + number, without the country code, together with country_code, e.g.
55), email, and linkedin_identifier. Optional: profile fields such as job_title and
city, and additional_attributes ({ "slug": value }) for custom fields.If the company already has a contact with any of the identifiers, the route reuses that contact
instead of creating a duplicate. The lookup order is email, LinkedIn, and phone:201: contact created, withcreated: true.200: existing contact reused, withcreated: falseandmatched_by(email,linkedin, orphone). Found by phone, the fields you send are updated. Found by email or LinkedIn, only empty fields are filled in, andignored_fieldslists what wasn’t saved.409CONTACT_IDENTITY_CONFLICT: the identifiers belong to different contacts. Nothing is saved, andconflictsreturns the contact for each identifier.
name never changes: to change it, use PUT /v1/contacts/:id. To see what
the route would do without saving anything, use POST /v1/contacts/identity-check.Update and list
PUT /v1/contacts/:id (or just the name with PUT /v1/contacts/:id/name) and
list/paginate with GET /v1/contacts. To remove the link between a contact and an external CRM,
use DELETE /v1/contacts/:id/crm-link: this only undoes the link; the contact keeps
existing in Nuvia (it’s not a contact deletion).Case 6: Assign the conversation to the HubSpot owner
For automations that receive a HubSpot event with an owner (for example, the owner of a scheduled meeting) and need to assign the conversation in Nuvia to the matching user. The mapping between HubSpot owners and Nuvia users is configured on the HubSpot integration screen. The API only reads it. Scopes:hubspot:read and conversations:update (+ conversations:read to find the
conversation). An existing key doesn’t gain the scope: generate a new one with it selected (see the
API key guide).
Read the owner mapping
GET /v1/hubspot/owner-mappings returns your company’s HubSpot owner ↔ Nuvia user pairs.nuvia_user_id is the id of the Nuvia user you use to assign the conversation. If the
company doesn’t have HubSpot connected, the list comes back empty.Cache the mapping and find the user
owner-mappings once, keep it for a few minutes, and look up
the event’s owner by hubspot_owner_id. Don’t use the email: hubspot_owner_email is a copy
from when the mapping was saved and isn’t updated if the email changes in HubSpot.Assign the conversation
PUT /v1/conversations/:id/assign-owner with the nuvia_user_id in userId. The route sets
the conversation’s owner and doesn’t change who is answering: if the AI is handling it, the AI
keeps handling it. To hand the conversation to a person, use
PUT /v1/conversations/:id/assign-user.