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.

What you will build

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

  1. Install & configure the SDK — one client for integrator calls, one for company calls.
  2. Register your integrator platform — prove you own your hostname via a callback challenge.
  3. Register a company — keyless onboarding that returns payment instructions.
  4. Activate the company — after payment you receive the company X-API-Key.
  5. Configure the webhook endpoint — store the HMAC secret to verify events.
  6. Register the operator encryption key — your RSA public key for E2EE.
  7. Create a conversation — get a deeplink to share with the customer.
  8. Exchange encrypted messages — text, media, rich content, reactions, presence.

Prerequisites

  • Node.js 18+ (the SDK uses the built-in node:crypto module).
  • 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

Terminal
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.

client.ts
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

POST /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.

register-integrator.ts
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

POST /companies/register HTTP Basic

Companies are created keyless. You receive the company record plus payment instructions to redirect the customer to the hosted checkout.

register-company.ts
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

POST /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.

activate-company.ts
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

PUT /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.

update-webhook.ts
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

PUT /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.

operator-key.ts
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.

webhook.ts
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');
    }
  },
);
EventWhen it fires
COMPANY_ACTIVATEDA company finished onboarding and received its API key.
CUSTOMER_JOINEDThe customer opened the deeplink and registered their public key.
MESSAGE_RECEIVEDThe customer sent a message (text or media).
MESSAGE_DELIVEREDThe customer's device received a message you sent.
MESSAGE_READThe customer read a message you sent.
TYPING / STOPPED_TYPING / RECORDING / CHAT_CLOSEDPresence reported by the customer's chat window.
CONVERSATION_CLOSEDThe conversation was closed.
BALANCE_LOWThe company pre-paid balance dropped below the threshold.

Step 9 — Create a conversation

POST /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.

create-conversation.ts
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

GET /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.

wait-for-customer.ts
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

POST /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.

send-message.ts
// 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

GET /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.

receive-messages.ts
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

POST /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.

media-and-rich.ts
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

presence-read-react.ts
// 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.

lifecycle.ts
// 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:

full-flow.ts
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

SymptomCause & fix
401 UNAUTHORIZEDMissing/invalid X-API-Key or integrator credentials.
403 FORBIDDENCompany exists but is not yet activated.
409 KEYS_NOT_ACTIVEYou tried to encrypt before the customer joined. Wait for CUSTOMER_JOINED.
402 INSUFFICIENT_BALANCETop up with purchaseCredits and retry.
Webhook signature failsVerify against the raw body bytes, not a re-serialized object.

Next steps