Quick Start
A single, end-to-end walkthrough of the whole ixblix integration built on the official
@ixblix/sdk-js package. Follow it top to bottom to go from an empty project to a
live, end-to-end encrypted overflow conversation.
An operator backend (your CRM/help desk) that registers companies, hands customers a secure deeplink, and exchanges end-to-end encrypted messages, media and rich messages with them — while receiving real-time events over signed webhooks.
Integration flow
- Install & configure the SDK — one client for integrator calls, one for company calls.
- Register your integrator platform — prove you own your hostname via a callback challenge.
- Register a company — keyless onboarding that returns payment instructions.
- Activate the company — after payment you receive the company
X-API-Key. - Configure the webhook endpoint — store the HMAC secret to verify events.
- Register the operator encryption key — your RSA public key for E2EE.
- Create a conversation — get a deeplink to share with the customer.
- Exchange encrypted messages — text, media, rich content, reactions, presence.
Prerequisites
- Node.js 18+ (the SDK uses the built-in
node:cryptomodule). - An integrator account with a publicly reachable
callbackUrl(HTTPS). - A webhook endpoint to receive events (can be the same host).
Step 1 — Install the SDK
npm install @ixblix/sdk-js Step 2 — Create the API clients
Integrator-scoped calls (registering companies, listing plans) use HTTP Basic Auth, while every
company-scoped call uses the X-API-Key header. Create two clients from the same base URL.
import { IxblixClient } from '@ixblix/sdk-js';
const BASE_URL = 'https://api.ixblix.app';
// Integrator client — company onboarding & catalog.
export const integrator = new IxblixClient({
baseUrl: BASE_URL,
integratorId: process.env.IXBLIX_INTEGRATOR_ID,
integratorAccessToken: process.env.IXBLIX_INTEGRATOR_TOKEN,
});
// Company client — conversations, messages, media, keys.
// The API key is obtained after activation (Step 5).
export const company = new IxblixClient({
baseUrl: BASE_URL,
apiKey: process.env.IXBLIX_API_KEY,
}); Step 3 — Register your integrator platform
/integrators/register HTTP Basic
Registration is a three-way handshake: ixblix POSTs a challenge to your
callbackUrl, your endpoint must echo the original registration payload back, and then
ixblix POSTs the credentials which your endpoint must echo with a 200.
await integrator.registerIntegrator({
name: 'My CRM Platform',
hostname: 'mycrm.example.com',
callbackUrl: 'https://mycrm.example.com/ixblix/callback',
});
// The callback handler on your side must answer the challenge,
// then acknowledge the credentials payload.
// POST { type: "challenge" } -> respond with { name, hostname, callbackUrl }
// POST { integratorId, accessToken } -> respond 200 Step 4 — Register a company
/companies/register HTTP Basic Companies are created keyless. You receive the company record plus payment instructions to redirect the customer to the hosted checkout.
const { company: created, payment } = await integrator.registerCompany({
name: 'Acme Support',
planId: 'plan_starter',
email: 'billing@acme.example',
confirmationWebhookUrl: 'https://mycrm.example.com/ixblix/company-activated',
});
console.log(payment.transactionId, payment.checkoutUrl); Step 5 — Activate the company
/companies/{id}/activate/{transactionId} HTTP Basic
Once the payment is confirmed, activate the company to obtain its X-API-Key. Store it
in your secret manager — it grants full access to every company resource.
const { apiKey } = await integrator.activateCompany(
created.id,
payment.transactionId,
);
// Persist the key and build the company-scoped client.
const company = new IxblixClient({ baseUrl: BASE_URL, apiKey }); Step 6 — Configure the webhook endpoint
/companies/webhook X-API-Key
Set (or rotate) the URL where ixblix delivers events. Every URL change — or
rotateSecret: true — generates a fresh HMAC secret that is returned once.
Store it; you need it to verify signatures.
const { webhookUrl, webhookSecret } = await company.updateWebhook({
webhookUrl: 'https://mycrm.example.com/ixblix/webhook',
});
// webhookSecret looks like: whsec_xxxxxxxx...
// Store it in your secret manager. It is never returned again. Step 7 — Generate and register the operator key
/companies/encryption-key X-API-Key Hold a persistent RSA keypair. The private key never leaves your process; only the public key is registered. New conversations snapshot the active key, and rotating it keeps the previous key for reading old conversations.
import {
IxblixClient,
loadOrCreateOperatorKey,
} from '@ixblix/sdk-js';
// Load an existing keypair or create one on first run (stored with 0600 perms).
const operatorKey = loadOrCreateOperatorKey('./.ixblix-keys');
await company.registerEncryptionKey({
keyId: operatorKey.keyId,
publicKey: operatorKey.publicKeySpki,
});
// Build a client that can encrypt outgoing messages.
const companyWithKey = new IxblixClient({
baseUrl: BASE_URL,
apiKey: process.env.IXBLIX_API_KEY,
operatorKey,
}); Step 8 — Receive events
ixblix pushes every event to your endpoint with an HMAC-SHA256 signature in the
X-Ixblix-Signature header. Always verify it against the raw body before acting. Use the
raw-body middleware below so the exact bytes are available for verification.
import express from 'express';
import { verifyWebhook, WEBHOOK_SIGNATURE_HEADER } from '@ixblix/sdk-js';
const app = express();
const webhookSecret = process.env.IXBLIX_WEBHOOK_SECRET!;
// Keep the raw body so the signature can be verified byte-for-byte.
app.post(
'/ixblix/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.header(WEBHOOK_SIGNATURE_HEADER);
const body = JSON.parse(req.body.toString('utf8'));
let event;
try {
({ event } = verifyWebhook(body, webhookSecret, signature));
} catch (err) {
return res.status(401).send('invalid signature');
}
switch (event.event) {
case 'CUSTOMER_JOINED':
return void onCustomerJoined(event);
case 'MESSAGE_RECEIVED':
return void onMessageReceived(event);
case 'MESSAGE_DELIVERED':
case 'MESSAGE_READ':
return void onMessageRead(event);
case 'CONVERSATION_CLOSED':
return void onConversationClosed(event);
case 'BALANCE_LOW':
return void onBalanceLow(event);
default:
return void res.status(200).send('ok');
}
},
); | Event | When it fires |
|---|---|
COMPANY_ACTIVATED | A company finished onboarding and received its API key. |
CUSTOMER_JOINED | The customer opened the deeplink and registered their public key. |
MESSAGE_RECEIVED | The customer sent a message (text or media). |
MESSAGE_DELIVERED | The customer's device received a message you sent. |
MESSAGE_READ | The customer read a message you sent. |
TYPING / STOPPED_TYPING / RECORDING / CHAT_CLOSED | Presence reported by the customer's chat window. |
CONVERSATION_CLOSED | The conversation was closed. |
BALANCE_LOW | The company pre-paid balance dropped below the threshold. |
Step 9 — Create a conversation
/conversations X-API-Key
A conversation represents the overflow thread for one contact. Attach the operator identity so the
customer's client can show who is handling the chat, then share the returned deeplink.
const { conversation, contact, deeplink } = await companyWithKey.createConversation({
contact: {
externalId: 'whatsapp_5511999999999',
name: 'Jane Doe',
metadata: { phone: '+55 11 99999-9999', ticket: 'T-1234' },
},
operator: {
uuid: 'agent-42',
name: 'Maria Silva',
image: 'https://cdn.example.com/avatars/maria.png',
// or: gravatarHash: md5('maria@example.com')
},
});
// Send the deeplink back on the original channel (WhatsApp, Instagram, ...). Step 10 — Wait for the customer to join
/conversations/{conversationId}/keys X-API-Key
The customer's browser generates a keypair when the deeplink opens and registers the public key
with ixblix. Prefer reacting to the CUSTOMER_JOINED webhook; only poll if you must.
sendEncryptedMessage fetches the keys internally — you just need to know the customer
has joined before calling it.
async function waitForCustomer(conversationId: string) {
for (;;) {
const keys = await companyWithKey.getConversationKeys(conversationId);
if (keys.keyStatus === 'ACTIVE' && keys.customerPublicKey) {
return; // customer joined — safe to call sendEncryptedMessage
}
await new Promise((r) => setTimeout(r, 2000));
}
}
await waitForCustomer(conversation.id); Step 11 — Send an encrypted message
/messages/company X-API-Key
Use sendEncryptedMessage — a single convenience method that fetches the conversation
keys, encrypts the content (and/or media and/or attachments) with one AES key, and sends the
message. The SDK wraps the AES key both to the customer and to your own public key so you can
read back what you sent.
// Text message with operator identity embedded in the encrypted attachments.
await companyWithKey.sendEncryptedMessage(conversation.id, {
content: 'Hello Jane! How can we help you today?',
operatorUuid: '550e8400-e29b-41d4-a716-446655440000',
operatorIdentity: {
uuid: '550e8400-e29b-41d4-a716-446655440000',
name: 'Maria Silva',
gravatarHash: md5('maria@example.com'),
},
}); Step 12 — Receive encrypted messages
/messages/{conversationId} X-API-Key
Messages are always stored as ciphertext. Decrypt them with your private key. Prefer handling
MESSAGE_RECEIVED webhooks instead of polling, then fetch the thread and decrypt.
import { decryptEnvelope } from '@ixblix/sdk-js';
const messages = await companyWithKey.listMessages(conversation.id);
for (const message of messages) {
if (!message.contentEncrypted) continue; // system/legacy plaintext
const plaintext = decryptEnvelope(message, operatorKey.privateKey);
console.log(message.senderType, message.sentAt, plaintext);
} Step 13 — Send media and rich messages
/media/company X-API-Key sendEncryptedMessage handles text, media and rich attachments through the same call.
Pass content for text, file for media, or both for a media message with
caption. Add attachments for buttons, vCard, location or link previews — everything
shares a single AES key with distinct IVs.
import { readFile } from 'node:fs/promises';
// 1. Plain media (image/audio/document).
const bytes = await readFile('./invoice.pdf');
await companyWithKey.sendEncryptedMessage(conversation.id, {
file: { data: bytes, fileName: 'invoice.pdf', mimeType: 'application/pdf' },
operatorUuid: '550e8400-e29b-41d4-a716-446655440000',
});
// 2. Rich text message (quick replies, buttons, ...).
await companyWithKey.sendEncryptedMessage(conversation.id, {
content: 'Your ticket is ready.',
operatorUuid: '550e8400-e29b-41d4-a716-446655440000',
attachments: {
buttons: [{ type: 'reply', label: 'Open ticket', value: 'open' }],
},
});
// 3. Media + caption + rich attachments in one call.
await companyWithKey.sendEncryptedMessage(conversation.id, {
content: 'Here is your invoice.',
file: { data: bytes, fileName: 'invoice.pdf', mimeType: 'application/pdf' },
operatorUuid: '550e8400-e29b-41d4-a716-446655440000',
attachments: {
buttons: [{ type: 'reply', label: 'Open ticket', value: 'open' }],
},
}); Step 14 — Presence, delivery/read receipts and reactions
// Tell the customer you are typing / recording.
await companyWithKey.reportCompanyPresence(conversation.id, 'typing');
await companyWithKey.reportCompanyPresence(conversation.id, 'stopped');
// Mark a customer message as delivered as soon as your client receives it.
await companyWithKey.markMessageDeliveredByCompany(conversation.id, messageId);
// Mark it as read once the operator actually sees it (window focused and the
// message inside the viewport). The customer sees the receipt live.
await companyWithKey.markMessageReadByCompany(conversation.id, messageId);
// React with an emoji (encrypted to the customer, sent as contentType "react").
await companyWithKey.reactToMessage(conversation.id, messageId, '👍', '550e8400-e29b-41d4-a716-446655440000');
// Pass an empty string to clear your reaction:
// await companyWithKey.reactToMessage(conversation.id, messageId, '', '550e8400-e29b-41d4-a716-446655440000'); Step 15 — Close and erase a conversation
Closing ends the thread. Erasing removes the encrypted content to satisfy LGPD/data-retention rules. Both are customer-token or company-driven depending on the endpoint — see the API reference.
// Report a customer-side close/presence using the conversation token.
// (Company side uses the conversation id with its API key.)
// Erasing encrypted content is a company action:
// POST /api/conversations/{token}/erase Complete example
Putting the whole flow together in one file:
import {
IxblixClient,
loadOrCreateOperatorKey,
decryptEnvelope,
} from '@ixblix/sdk-js';
import { createHash } from 'crypto';
const md5 = (s: string) => createHash('md5').update(s).digest('hex');
const BASE_URL = 'https://api.ixblix.app';
async function main() {
// 1. Clients
const integrator = new IxblixClient({
baseUrl: BASE_URL,
integratorId: process.env.IXBLIX_INTEGRATOR_ID,
integratorAccessToken: process.env.IXBLIX_INTEGRATOR_TOKEN,
});
// 2. Register + activate a company
const { company: created, payment } = await integrator.registerCompany({
name: 'Acme Support',
planId: 'plan_starter',
});
const { apiKey } = await integrator.activateCompany(created.id, payment.transactionId);
// 3. Operator keypair + company client
const operatorKey = loadOrCreateOperatorKey('./.ixblix-keys');
const company = new IxblixClient({ baseUrl: BASE_URL, apiKey, operatorKey });
await company.registerEncryptionKey({
keyId: operatorKey.keyId,
publicKey: operatorKey.publicKeySpki,
});
await company.updateWebhook({ webhookUrl: 'https://mycrm.example.com/ixblix/webhook' });
// 4. Open a conversation and share the deeplink
const { conversation, deeplink } = await company.createConversation({
contact: { externalId: 'whatsapp_5511999999999', name: 'Jane Doe' },
operator: { uuid: '550e8400-e29b-41d4-a716-446655440000', name: 'Maria Silva' },
});
console.log('Share this link:', deeplink);
// 5. Wait for the customer, then talk
let keys = await company.getConversationKeys(conversation.id);
while (keys.keyStatus !== 'ACTIVE' || !keys.customerPublicKey) {
await new Promise((r) => setTimeout(r, 2000));
keys = await company.getConversationKeys(conversation.id);
}
// sendEncryptedMessage fetches keys, encrypts and sends in one call.
await company.sendEncryptedMessage(conversation.id, {
content: 'Hello! How can we help?',
operatorUuid: '550e8400-e29b-41d4-a716-446655440000',
operatorIdentity: {
uuid: '550e8400-e29b-41d4-a716-446655440000',
name: 'Maria Silva',
gravatarHash: md5('maria@example.com'),
},
});
// 6. Read the reply
const messages = await company.listMessages(conversation.id);
for (const message of messages) {
if (message.contentEncrypted) {
console.log(decryptEnvelope(message, operatorKey.privateKey));
}
}
}
main().catch(console.error); Troubleshooting
| Symptom | Cause & fix |
|---|---|
401 UNAUTHORIZED | Missing/invalid X-API-Key or integrator credentials. |
403 FORBIDDEN | Company exists but is not yet activated. |
409 KEYS_NOT_ACTIVE | You tried to encrypt before the customer joined. Wait for CUSTOMER_JOINED. |
402 INSUFFICIENT_BALANCE | Top up with purchaseCredits and retry. |
| Webhook signature fails | Verify against the raw body bytes, not a re-serialized object. |
Next steps
- JavaScript SDK reference — every method and crypto helper.
- REST API reference — every exposed endpoint.
- End-to-End Encryption — how the envelope works.
- Webhooks — event payloads and verification.