Next CapNext Cap

CRM Integration

Bidirectional ticket management and live chat — receive ticket events via webhooks and act on them from your own CRM.

Overview

The CRM Integration API lets you connect Next Cap directly to your existing CRM — Salesforce, HubSpot, Zendesk, or any custom system. With a single API key, you get access to both live chat (real-time WebSocket) and ticketing (REST API), enabling full bidirectional communication from your own dashboard.

Enterprise plan required. The CRM Integration API is available on the Enterprise plan only. Contact sales to upgrade.
Live ChatWebSocket

Real-time bidirectional connection via Socket.IO WebSocket. Receive messages, typing indicators, and conversation events as they happen. Send replies instantly from your CRM. WebSocket is the only way to integrate live chat — it requires a persistent connection for real-time data.

TicketingWebhook + REST

Webhooks (recommended) push ticket events to your server in real time — new tickets, status changes, replies, and assignments. Use the REST API to take action: reply, assign, close, reopen, and update tickets. Webhooks keep your CRM in sync without polling.

How It Works

Unlike the read-only External Data API, the CRM Integration API provides per-user authentication. Every API key is tied to a seat in your organization and inherits that seat's exact permissions and widget assignments. This means:

  • Every CRM key occupies a paid seat in your organization
  • Each key inherits the seat's permissions (e.g., live-chat:join, tickets:manage)
  • Each key only sees data from widgets the seat is assigned to
  • If the seat has live-chat permissions, the key can access live chat — if it has ticketing permissions, it can access tickets — one key, both features
  • If a seat is removed or deactivated, its CRM key stops working immediately

Authentication

CRM keys use the branded nextcap_ck_ prefix. The same key works for both REST and WebSocket:

REST API (Tickets & Conversations)

HTTP Headerbash
X-Api-Key: nextcap_ck_YOUR_KEY
# or
Authorization: Bearer nextcap_ck_YOUR_KEY

WebSocket (Live Chat)

Socket.IO Authjavascript
const socket = io("wss://api.nextcap.ai/crm-chat", {
auth: { apiKey: "nextcap_ck_YOUR_KEY" },
transports: ["websocket"],
});
CRM keys are server-to-server only. Browser-origin requests are blocked for both REST and WebSocket. Never expose CRM keys in client-side code.

Obtaining CRM Keys

CRM keys can only be created from the Next Cap Dashboard

There is no API endpoint for creating CRM keys. Only organization owners and admins can generate them through the dashboard interface. This is by design — API keys grant server-to-server access to your organization's data and must be provisioned securely.

There are two ways to create a CRM key, depending on your use case:

1

API-Only Seat Recommended for integrations

Create a dedicated CRM seat without inviting anyone or requiring sign-up. The seat gets a role, widget access, and an API key — all in one step. No email, no OTP, no dashboard login needed.

  1. Navigate to Settings → API Keys → CRM Integration.
  2. Click API-Only Seat.
  3. Enter a name for the seat (e.g., "Salesforce Integration") and a key name.
  4. Choose a role — this controls which permissions the key has. For live chat access, assign a role with live-chat:join and conversations:view. For ticketing, assign tickets:manage.
  5. Select widget access — choose which agents/widgets the key can see data from.
  6. Optionally add an email — if someone later needs dashboard access, they can register with this email to activate the seat.
  7. Click Create Seat & Key. Copy the key immediately.
2

CRM Key for existing user

If a team member already has a dashboard account and you want to give them CRM API access:

  1. Navigate to Settings → API Keys → CRM Integration.
  2. Click Create Key.
  3. Select the team member from the dropdown and give the key a name.
  4. Copy the key immediately — it is shown only once.

The key inherits the user's existing role and widget access. If the user has live-chat permissions, the key automatically works for live chat too.

The raw key is shown only once on creation. Store it securely in your CRM's credential vault. If you lose it, revoke the old key and create a new one.
API-Only seats can be activated later. If you assigned an email to an API-only seat, the person can register with that email to gain full dashboard access. Their role, widget access, and CRM keys are preserved — no reconfiguration needed.

Live Chat Integration

The live chat integration uses WebSocket (Socket.IO) for real-time bidirectional communication. Connect your CRM to receive messages, typing indicators, and conversation events the moment they happen — and reply to visitors instantly.

Required permissions: The user seat must have live-chat:view or conversations:view to connect. To send messages and manage conversations, live-chat:join is required.

WebSocket Connection

Connect to the /crm-chat namespace with your CRM API key:

WebSocket Endpointtext
wss://api.nextcap.ai/crm-chat

Quick Start — Live Chat

import { io } from "socket.io-client";
const CRM_KEY = "nextcap_ck_YOUR_KEY";
// Connect to the CRM chat WebSocket
const socket = io("wss://api.nextcap.ai/crm-chat", {
auth: { apiKey: CRM_KEY },
transports: ["websocket"],
});
socket.on("connect", () => {
console.log("Connected to live chat");
});
// Join a conversation room
socket.emit("crm:join", { conversationId: "CONV_ID" }, (res) => {
console.log("Joined:", res);
});
// Listen for new messages
socket.on("message:new", (msg) => {
console.log(`[${msg.role}] ${msg.content}`);
});
// Send a message as the agent
socket.emit("crm:message", {
conversationId: "CONV_ID",
content: "Hi, how can I help you?",
});
// Listen for typing indicators
socket.on("typing", (data) => {
console.log(`${data.role} is typing...`);
});
// Listen for conversation lifecycle events
socket.on("agent:assigned", (data) => {
console.log("Agent assigned:", data.agentUserId);
});
socket.on("conversation:resolved", (data) => {
console.log("Conversation resolved:", data.conversationId);
});

Client → Server Events

Events your CRM sends to Next Cap:

POSTcrm:joinCRM Key

Join a conversation room to start receiving real-time events for it.

conversationIdUUIDrequired

The conversation to join. Must belong to your organization and be within your widget access.

socket.emit("crm:join", { conversationId: "CONV_ID" }, (res) => {
if (res.success) console.log("Joined conversation");
});
POSTcrm:messageCRM Key

Send a message to a conversation as the authenticated agent. Requires live-chat:join permission.

conversationIdUUIDrequired

Target conversation ID.

contentstringrequired

Message text content.

socket.emit("crm:message", {
conversationId: "CONV_ID",
content: "Hi, how can I help you today?",
}, (res) => {
if (res.success) console.log("Sent:", res.messageId);
});
POSTcrm:typingCRM Key

Send a typing indicator. Requires live-chat:join permission.

conversationIdUUIDrequired

Conversation to show typing in.

POSTcrm:leaveCRM Key

Leave a conversation room. Stops receiving events for it.

conversationIdUUIDrequired

Conversation to leave.

Server → Client Events

Events your CRM receives from Next Cap for conversations you've joined:

Real-time Events
message:newevent

A new message was sent in the conversation. Payload: id, conversationId, role (user/agent/assistant/system), content, createdAt, attachments.

typingevent

Someone is typing. Payload: conversationId, role (agent/visitor), identifier.

typing:contentevent

Visitor's in-progress text (sneak peek). Payload: conversationId, visitorId, content.

agent:assignedevent

An agent was assigned to the conversation. Payload: conversationId, agentUserId, timestamp.

conversation:resolvedevent

The conversation was resolved. Payload: conversationId, timestamp.

ticket:createdevent

A ticket was created from this conversation. Payload: conversationId, ticketNumber, subject, timestamp.

feedback:receivedevent

Visitor submitted a satisfaction rating. Payload: conversationId, satisfactionRating, feedbackData.

Example event payload

message:newjson
{
"id": "msg_abc123",
"conversationId": "conv_xyz789",
"role": "user",
"content": "I need help with my order",
"visitorId": "visitor_456",
"createdAt": "2026-04-07T14:30:00.000Z",
"attachments": null
}

Conversation REST Endpoints

Use these REST endpoints alongside the WebSocket for initial data loading, listing conversations, and managing the handoff queue:

# List live chat conversations
curl -H "X-Api-Key: nextcap_ck_YOUR_KEY" \
"https://api.nextcap.ai/api/v1/external/crm/conversations?status=handed_off&per_page=20"
# Get a conversation with messages
curl -H "X-Api-Key: nextcap_ck_YOUR_KEY" \
"https://api.nextcap.ai/api/v1/external/crm/conversations/CONV_ID"
# Get the handoff queue
curl -H "X-Api-Key: nextcap_ck_YOUR_KEY" \
"https://api.nextcap.ai/api/v1/external/crm/conversations/queue"
# Assign an agent to a conversation
curl -X POST -H "X-Api-Key: nextcap_ck_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"agent_id": "USER_UUID"}' \
"https://api.nextcap.ai/api/v1/external/crm/conversations/CONV_ID/assign"
# Resolve a conversation
curl -X POST -H "X-Api-Key: nextcap_ck_YOUR_KEY" \
"https://api.nextcap.ai/api/v1/external/crm/conversations/CONV_ID/resolve"
GET/external/crm/conversationsCRM Key

List conversations visible to the authenticated user (widget-scoped). Users with conversations:view-own only see their assigned conversations.

statusstring

Filter: active, handed_off, ticketed, resolved, archived.

agent_idUUID

Filter by widget (agent) ID.

assigned_agent_idUUID

Filter by assigned human agent.

visitor_idstring

Filter by visitor ID.

created_afterISO 8601

Filter conversations created after this date.

created_beforeISO 8601

Filter conversations created before this date.

sort_bystring

Sort field: createdAt, updatedAt.

sort_orderstring

asc or desc.

pagenumber

Page number (default: 1).

per_pagenumber

Items per page (default: 20, max: 100).

GET/external/crm/conversations/:idCRM Key

Get a single conversation with its full message history. Includes presigned URLs for attachments (24h validity).

GET/external/crm/conversations/:id/messagesCRM Key

Paginated messages for a conversation.

pagenumber

Page number (default: 1).

per_pagenumber

Items per page (default: 50, max: 100).

GET/external/crm/conversations/queueCRM Key

Get the handoff queue — conversations waiting for an agent to pick up. Requires live-chat:join permission.

POST/external/crm/conversations/:id/assignCRM Key

Assign an agent to a conversation. Triggers agent:assigned WebSocket event.

agent_idUUIDrequired

User ID of the agent to assign.

POST/external/crm/conversations/:id/resolveCRM Key

Resolve a conversation. Triggers conversation:resolved WebSocket event.

Typical Integration Flow

  1. Connect WebSocket — establish a persistent connection to /crm-chat with your API key.
  2. Fetch the queue — call GET /external/crm/conversations/queue to see conversations waiting for an agent.
  3. Join conversations — emit crm:join for each conversation you want to monitor.
  4. Assign agent — call POST /external/crm/conversations/:id/assign to pick up a conversation.
  5. Chat — send messages via crm:message, receive visitor messages via message:new.
  6. Resolve — call POST /external/crm/conversations/:id/resolve when done.

Ticketing Integration

For ticketing, the recommended approach is webhooks + REST API. Webhooks push events to your server the moment something happens (ticket created, status changed, reply received), so your CRM stays in sync without polling. Then use the REST API to take action — reply, assign, close, or update tickets.

Required permissions: The user seat must have ticketing permissions (e.g., tickets:view, tickets:manage) to access ticket endpoints.

Quick Start — Ticketing

# List tickets visible to your CRM user
curl -H "X-Api-Key: nextcap_ck_YOUR_KEY" \
"https://api.nextcap.ai/api/v1/external/crm/tickets?status=open&per_page=20"
# Reply to a ticket
curl -X POST -H "X-Api-Key: nextcap_ck_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "We are looking into this issue."}' \
"https://api.nextcap.ai/api/v1/external/crm/tickets/TICKET_ID/reply"

Ticket Endpoints

GET/external/crm/ticketsCRM Key

List tickets visible to the authenticated user (widget-scoped). Each ticket includes has_attachments and attachment_count so you can filter which tickets to open without a round-trip.

statusstring

Comma-separated: new, open, in_progress, waiting_customer, waiting_internal, resolved, closed, reopened.

prioritystring

Comma-separated: low, normal, high, urgent.

department_idUUID

Filter by department.

assigned_agent_idUUID

Filter by assigned agent.

tagsstring

Comma-separated tag filter.

categorystring

Filter by category.

sourcestring

Comma-separated: handoff, widget, dashboard, email, api, import, telegram.

searchstring

Full-text search on subject and description.

created_afterISO 8601

Filter tickets created after this date.

created_beforeISO 8601

Filter tickets created before this date.

sort_bystring

Sort field: createdAt, updatedAt, priority, ticketNumber.

sort_orderstring

asc or desc.

GET/external/crm/tickets/:idCRM Key

Get a single ticket. Returns 403 if you don't have widget access. Response always includes an attachments[] array with 24h presigned URLs.

GET/external/crm/tickets/:id/eventsCRM Key

Get the ticket timeline. Users with tickets:manage see internal notes.

Internal notes (added by team members) are only visible to users with the tickets:manage permission. All other users see only public events.

Ticket Actions

PATCH/external/crm/tickets/:idCRM Key

Update ticket fields. Validates status transitions.

statusstring

New status (must be valid transition).

prioritystring

low, normal, high, urgent.

categorystring

Free-form category.

tagsstring[]

Replace the tags array.

department_idUUID

Transfer to a department.

assigned_agent_idUUID

Assign to an agent.

subjectstring

Update the subject line.

descriptionstring

Update the description.

custom_fieldsobject

Update org-defined custom fields.

metadataobject

Replace the free-form metadata object. Useful for backfilling fields like panel_source/product_source when the originating panel is only known after creation. Max 10KB.

POST/external/crm/tickets/:id/replyCRM Key

Reply to a ticket as the authenticated user. Sends email to the customer and fires ticket.reply webhook.

contentstringrequired

Reply text content.

attachment_idsUUID[]

Attachment IDs from POST /external/crm/attachments (max 20). Files uploaded this way appear to the visitor as part of the reply.

POST/external/crm/tickets/:id/assignCRM Key

Assign a ticket to a team member.

agent_idUUIDrequired

User ID of the agent to assign.

POST/external/crm/tickets/:id/transferCRM Key

Transfer a ticket to a different department.

department_idUUIDrequired

Target department ID.

unassignboolean

Unassign the current agent (default: true).

POST/external/crm/tickets/:id/closeCRM Key

Close a ticket.

POST/external/crm/tickets/:id/reopenCRM Key

Reopen a resolved or closed ticket.

POST/external/crm/tickets/:id/notesCRM Key

Add a note to the ticket timeline.

contentstringrequired

Note text content.

is_internalboolean

Internal note, hidden from customers (default: true).

Attachments

Two flows are available depending on your needs. Direct upload is simplest — stream the file to our API in one request. Presigned PUT is useful when you want the file to go straight to storage without passing through our server. After either flow you get an attachment_id, which you then pass as attachment_ids[] on ticket create / reply / customer-reply.

POST/external/crm/attachmentsCRM Key

Direct upload. Send multipart/form-data with a file field. Max 50MB (or your plan's limit).

Files uploaded here sit in a staging area for 24h. Reference the returned id when creating a ticket or reply to link the file permanently.
POST/external/crm/attachments/presignCRM Key

Get a presigned PUT URL. PUT the file bytes directly to the returned upload_url, then call /complete.

filenamestringrequired

Original filename.

content_typestringrequired

MIME type (must match the PUT Content-Type header exactly).

sizeintegerrequired

File size in bytes.

POST/external/crm/attachments/:id/completeCRM Key

Confirm a presigned upload finished. The server verifies the file exists and matches the declared size.

GET/external/crm/attachments/:idCRM Key

Get upload metadata plus a fresh 24h download URL.

GET/external/crm/attachments/:id/downloadCRM Key

Stable download endpoint. 302-redirects to a fresh 24h download URL on every request. Use this if you want to cache attachment references without re-fetching the ticket every 24h.

DELETE/external/crm/attachments/:idCRM Key

Delete an uploaded file that hasn't been linked to a ticket yet.

Webhook Events

Use these webhook events to keep your CRM in sync with Next Cap. Register webhooks via the POST /external/webhooks endpoint.

Ticket Events
ticket.createdevent

A new ticket was created.

ticket.updatedevent

A ticket was modified (any field change).

ticket.status_changedevent

Ticket status changed (e.g., open → resolved).

ticket.assignedevent

A ticket was assigned or reassigned.

ticket.department_changedevent

A ticket was transferred to a different department.

ticket.reopenedevent

A ticket was reopened.

ticket.replyevent

An agent replied to a ticket (includes content and actor).

ticket.customer_replyevent

A customer replied to a ticket via widget or email.

Conversation Events
conversation.createdevent

A new conversation was started.

conversation.completedevent

A conversation was completed by the AI.

conversation.handed_offevent

A conversation was escalated to a human agent.

Ticket Status Transitions

Ticket status follows a strict state machine. Invalid transitions return 400 Bad Request.

Valid transitionstext
new → open, in_progress, closed
open → in_progress, waiting_customer, waiting_internal, resolved, closed
in_progress → waiting_customer, waiting_internal, resolved, closed
waiting_customer → open, in_progress, resolved, closed
waiting_internal → open, in_progress, resolved, closed
resolved → closed, reopened
closed → reopened
reopened → open, in_progress, waiting_customer, waiting_internal, resolved, closed

Permissions Reference

The CRM key inherits the user's role permissions. Here's what each feature requires:

Live Chat Permissions
WebSocket connectlive-chat:view or conversations:view

Connect to the /crm-chat WebSocket.

crm:joinconversations:view or conversations:view-own

Join a conversation room.

crm:messagelive-chat:join

Send messages to conversations.

crm:typinglive-chat:join

Send typing indicators.

GET /conversationsconversations:view or conversations:view-own

List conversations (view-own restricts to assigned only).

GET /conversations/:idconversations:view or conversations:view-own

Get conversation details.

GET /conversations/queuelive-chat:join

View the handoff queue.

POST /conversations/:id/assignlive-chat:join

Assign an agent.

POST /conversations/:id/resolvelive-chat:join

Resolve a conversation.

Ticketing Permissions
GET /ticketstickets:view

List tickets (widget scoping applies).

GET /tickets/:idtickets:view

View a single ticket.

GET /tickets/:id/eventstickets:view

View ticket timeline.

PATCH /tickets/:idtickets:manage

Update ticket fields.

POST /tickets/:id/replytickets:reply

Reply to a ticket.

POST /tickets/:id/assigntickets:assign

Assign tickets.

POST /tickets/:id/transfertickets:manage

Transfer departments.

POST /tickets/:id/closetickets:close

Close tickets.

POST /tickets/:id/reopentickets:manage

Reopen tickets.

POST /tickets/:id/notestickets:manage

Add notes.

Need to list or look up the employees in your organization (not visitors or tickets)? See the CRM Team Directory page — list, fetch by ID, and lookup by email. Owner / admin only.