Skip to main content
Complete use cases you can run for real with an API key. All calls use the 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.
1

Discover your channels (inboxes)

List the inboxes connected to your company.
2

Get a conversation

Sending a message happens inside a conversation. List conversations to get a conversationId (or create one with POST /v1/conversations).
cURL
3

Send a text message

POST /v1/messages/send with the conversationId and the message object.
Use contentType: "TEXT" for text messages.
4

Read the conversation history

GET /v1/messages/conversation/:id returns the messages; attachments are at GET /v1/messages/conversation/:id/attachments.
cURL

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).
1

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.
Optional fields: custom_headers (for you to validate the origin) and status (ACTIVE/INACTIVE).
2

Receive the callbacks

When an event occurs, Nuvia makes a 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.
Respond 2xx within 10 seconds and process in the background. There’s no retry: if your response fails or times out, the event is lost. Details in Delivery guarantees.
3

React to the event

From the callback, you can act via API: reply with POST /v1/messages/send (Case 1) or download attachments with GET /v1/messages/conversation/:id/attachments.
4

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.
The campaign steps (the messages and intervals of the sequence) are built in the Nuvia dashboard. There’s no steps route for API keys. Through the API you create and configure the campaign, enroll contacts, activate, and track.
1

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).
2

Enroll the audience

Pick the route by where the contacts come from. All of them require the campaign-enrollments:create scope:Contacts already enrolled in the campaign are skipped, so repeating the call doesn’t duplicate enrollments.
cURL
Enrolling by list reads the list’s contacts at the time of the call. Contacts added to the list later don’t join the campaign on their own: enroll again or use from-contact.
3

Check before sending

Use the previews (they create nothing): POST /v1/campaigns/audience-preview counts the audience’s contacts and POST /v1/campaigns/preview-message resolves the message variables.
4

Activate and track

Activate with 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.
cURL
Track with GET /v1/campaigns/:id/analytics and GET /v1/campaigns/:id/step-analytics, and export the contacts with GET /v1/campaigns/:id/enrollments/export.
Shortcut: to send a WhatsApp template to several contacts without building a campaign, use 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).
1

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.
A campaign whose trigger isn’t EXTERNAL_TRIGGER is completed automatically when it has no enrollment in progress left. A single moment like that is enough, for example the first lead replying before the CRM enrolls the second one. From then on, enrollment returns 409, and a completed MANUAL campaign can’t be reactivated.For sequences fed by the CRM, create the campaign with trigger_type: EXTERNAL_TRIGGER and audience_config.type: EXTERNAL. This kind of campaign stays active to receive new enrollments until you pause it.
2

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.
Enrollment always starts at the first step of the sequence; there’s no way to enroll from a specific step. The first send respects the campaign’s sending window.
3

Read the result

The response has status equal to enrolled or skipped:
With 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:
The skipped response doesn’t include the enrollmentId. To recover it, query the campaign’s enrollments (step “Track the enrollments”).
Errors: 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).
4

Store the IDs in the CRM

Store the 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:
  • fields writes to the contact, not to the enrollment. The enrollment has no deal field.
  • external_id is one per contact, and the last one sent wins.
  • Message variables use the contact’s current value.
If a person has more than one deal in your CRM, keep the enrollment → deal link on your side. Don’t send deal-specific data in fields: deal B’s enrollment overwrites deal A’s value on the contact.
5

Handle retries and timeouts

The route has no idempotency key. The protection is a Nuvia rule: a contact has at most one enrollment per campaign. After a timeout, resend the same call and treat skipped with already_enrolled (or already_in_progress, with enroll_again) as success.
Send the calls for the same contact in sequence, never in parallel. Simultaneous calls for the same phone number can duplicate the contact or fail with 5xx. On a 5xx, wait a few seconds and try again.
6

Track the enrollments

Each enrollment has a status:An enrollment that went through every step, or that stopped on a sending failure that a retry won’t fix, ends with no reason. Handle a missing, null, or new 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:The enrollment listing doesn’t filter by contactId or enrollmentId. That’s why the enrollmentId stored in the CRM is your reference.
7

Receive replies via webhook

Message events arrive through the company’s webhook. For a CRM integration, the main ones:
  • Lead reply: MESSAGE_RECEIVED with sender_type: CONTACT and message_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_CHANGED doesn’t mean a reply. A campaign step can move the conversation to a funnel step on the send itself, before the lead replies.
Message events don’t carry the campaign ID or the 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.
There’s no opt-out event. Nuvia doesn’t turn words like “STOP” or “UNSUBSCRIBE” into an event of their own: if your process handles opt-outs, identify the lead’s message on your side.
8

Avoid simultaneous sequences

Nuvia doesn’t prevent the same contact from being in different campaigns at the same time. The lead’s reply ends all of them, but until then they can receive both sequences. To avoid it:
  • In the CRM: before enrolling, check whether the contact already has an enrollment in progress, using the enrollmentId values 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.
1

Check for duplicates by phone

Before creating, resolve the phone numbers to see which already exist: POST /v1/contacts/find-by-phones.
2

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, with created: true.
  • 200: existing contact reused, with created: false and matched_by (email, linkedin, or phone). Found by phone, the fields you send are updated. Found by email or LinkedIn, only empty fields are filled in, and ignored_fields lists what wasn’t saved.
  • 409 CONTACT_IDENTITY_CONFLICT: the identifiers belong to different contacts. Nothing is saved, and conflicts returns the contact for each identifier.
An existing contact’s 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.
3

Update and list

Update a contact with 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).
1

Read the owner mapping

GET /v1/hubspot/owner-mappings returns your company’s HubSpot owner ↔ Nuvia user pairs.
Response:
The 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.
2

Cache the mapping and find the user

The mapping rarely changes. Query 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.
Handle an owner that isn’t in the mapping, for example a new seller who hasn’t been mapped on the screen yet: log the case and don’t assign the conversation blindly.
Don’t call GET /v1/hubspot/owners, /properties, or /pipelines on every event. These three routes query HubSpot in real time and spend your company’s HubSpot portal API quota, the same quota the sync uses. owner-mappings is read from Nuvia and doesn’t use that quota. Without HubSpot connected, the three return 404: “Integração com HubSpot não encontrada. Conecte sua conta primeiro.” (HubSpot integration not found. Connect your account first.)
3

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.

Other public surfaces

Consumable via API key and covered in the Reference: Agents, Tables and lists, Conversations, Knowledge base, and HubSpot (read-only access to the integration settings). See the map in API sections.

Next steps

API Key guide

Scopes, key creation, and revocation.

API reference

All endpoints, parameters, and responses.