# ixblix JavaScript SDK Integration Guide

This skill provides complete instructions for integrating with the ixblix platform using the official `@ixblix/sdk-js` package.

## Overview

ixblix is an independent commercial messaging platform that enables secure, white-labeled conversations with end-to-end encryption. This guide covers the complete integration flow using the TypeScript SDK.

**API Base URL:** `https://api.ixblix.app`

## Installation

```bash
npm install @ixblix/sdk-js
```

## Integration Flow

### Step 1: Register as an Integrator

Register your application (CRM, help desk, ticket system) to receive API credentials:

```typescript
import { IxblixClient } from "@ixblix/sdk-js";

const integratorClient = new IxblixClient({
  baseUrl: "https://api.ixblix.app",
});

await integratorClient.registerIntegrator({
  name: "My CRM Platform",
  hostname: "mycrm.example.com",
  callbackUrl: "https://mycrm.example.com/ixblix/callback",
  // Optional: subscription identifier when required by the assigned payment provider
  // subscriptionId: '{paymentProvider}:{id}',
});

// ixblix will POST a challenge to your callbackUrl, then POST the credentials.
// Your callback must first echo the original registration payload, then echo
// the credentials payload.
```

**Request fields:**

| Field            | Required | Description                                                                                                                                                                                                                           |
| ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`           | Yes      | Display name of your integrator.                                                                                                                                                                                                      |
| `hostname`       | Yes      | Unique hostname for this integrator. Must be unique across all integrators.                                                                                                                                                           |
| `callbackUrl`    | Yes      | URL where ixblix sends the registration challenge and, after it succeeds, the integrator credentials.                                                                                                                                 |
| `force`          | No       | If true, allows replacing an existing verified hostname registration.                                                                                                                                                                 |
| `subscriptionId` | No       | Subscription identifier in the format `{paymentProvider}:{id}`. The `id` portion is opaque to the API and interpreted by the payment provider internally. Required when the assigned payment provider needs a subscription reference. |

**Callback Handler:**

```typescript
import express from "express";
const app = express();
app.use(express.json());

const registrationPayload = {
  name: "My CRM Platform",
  hostname: "mycrm.example.com",
  callbackUrl: "https://mycrm.example.com/ixblix/callback",
};

app.post("/ixblix/callback", async (req, res) => {
  // Phase 2: answer the challenge with the original registration payload
  if (req.body?.challenge) {
    return res.status(200).json(registrationPayload);
  }

  // Phase 3: store credentials and echo them back
  if (req.body?.event === "INTEGRATOR_CREDENTIALS") {
    const { integratorId, accessToken } = req.body;
    await saveIntegratorCredentials({ integratorId, accessToken });
    return res.status(200).json(req.body);
  }

  res.status(200).json({ received: true });
});
```

### Step 2: Create an Authenticated Client

```typescript
import { IxblixClient, loadOrCreateOperatorKey } from "@ixblix/sdk-js";

const integratorClient = new IxblixClient({
  baseUrl: "https://api.ixblix.app",
  integratorId: "int_a1b2c3d4-...",
  integratorAccessToken: "ixblix_integrator_token_xxxx",
});

// Verify credentials
const profile = await integratorClient.getProfile();
console.log("Registered as:", profile.name);
```

### Step 3: List Available Plans

```typescript
const plans = await integratorClient.listPlans();
console.log("Available plans:", plans);
```

### Step 4: Register a Company

```typescript
const { company, payment } = await integratorClient.registerCompany({
  name: "Acme Corporation",
  planId: plans[0].id,
});

// Redirect the company owner to payment.checkoutUrl to complete payment
console.log("Checkout URL:", payment.checkoutUrl);
```

### Step 5: Activate the Company

After payment confirmation:

```typescript
const { apiKey } = await integratorClient.activateCompany(
  company.id,
  payment.transactionId,
);

// Create company-scoped client
const companyClient = new IxblixClient({
  baseUrl: "https://api.ixblix.app",
  apiKey,
  operatorKey: operatorKey, // from Step 7
});
```

**Important:** The `apiKey` is shown only once. Store it securely.

### Step 6: Configure Webhook URL

```typescript
const { webhookSecret } = await companyClient.updateWebhook({
  webhookUrl: "https://mycrm.example.com/ixblix/webhooks",
});

// Store webhookSecret for signature verification
```

### Step 7: Generate and Register Operator Keypair

```typescript
import {
  generateOperatorKeyPair,
  loadOrCreateOperatorKey,
} from "@ixblix/sdk-js";

// Option A: Generate new keypair
const { publicKey, privateKey } = await generateOperatorKeyPair();

// Option B: Load or create (persists to disk)
const operatorKey = await loadOrCreateOperatorKey({
  path: "./keys/operator.json",
});

// Register public key
await companyClient.registerEncryptionKey({
  keyId: operatorKey.keyId,
  publicKey: operatorKey.publicKeySpki,
});
```

### Step 8: Create a Conversation

```typescript
const { conversation, deeplink } = await companyClient.createConversation({
  contact: {
    externalId: "crm-contact-123",
    name: "Jane Doe",
    metadata: { ticketId: "TK-456" },
  },
});

// Send the deeplink to your customer via WhatsApp, SMS, etc.
console.log("Share this link:", deeplink);
```

### Step 9: Handle Customer Join

When the customer opens the deeplink, you receive a `CUSTOMER_JOINED` webhook:

```typescript
import { verifyWebhook, WEBHOOK_SIGNATURE_HEADER } from "@ixblix/sdk-js";

app.post("/ixblix/webhooks", async (req, res) => {
  // Verify signature
  const signature = req.headers[WEBHOOK_SIGNATURE_HEADER] as string;
  const parsed = verifyWebhook(req.body, webhookSecret, signature);
  const event = parsed.event;

  if (event.event === "CUSTOMER_JOINED") {
    // Store customer's public key for this conversation
    await saveCustomerKey(event.conversationId, event.customerPublicKey);

    // Send welcome message using sendEncryptedMessage (recommended)
    await companyClient.sendEncryptedMessage(event.conversationId, {
      content: "Hello! How can we help you today?",
    });
  }

  res.status(200).send("OK");
});
```

### Step 10: Send an Encrypted Message

```typescript
// High-level: encrypt and send in one call (recommended)
await companyClient.sendEncryptedMessage(conversationId, {
  content: "Hello from Acme Corp!",
});

// Or with operator identity and attachments
await companyClient.sendEncryptedMessage(conversationId, {
  content: "Hello from Acme Corp!",
  operatorUuid: "550e8400-e29b-41d4-a716-446655440000",
  operatorIdentity: {
    uuid: "agent-42",
    name: "Maria Silva",
    gravatarHash: "abc123...",
  },
  // Optional: push notification preview (encrypted to customer's public key)
  pushPreview: {
    text: "Hello from Acme Corp!",
    senderName: "Maria Silva",
  },
});

// Low-level: manual encryption + send (see encryptMessagePayload below)
const keys = await companyClient.getConversationKeys(conversationId);
const payload = encryptMessagePayload(
  { content: "Hello from Acme Corp!" },
  keys.customerPublicKey!,
  operatorKey.keyId,
  operatorKey.publicKeySpki,
);
const envelope = {
  content: payload.content,
  iv: payload.contentIv,
  authTag: payload.contentAuthTag,
  encryptedKey: payload.encryptedKey,
  selfEncryptedKey: payload.selfEncryptedKey,
  keyId: payload.keyId,
  attachments: payload.attachments,
};
await companyClient.sendCompanyMessage(conversationId, envelope);
```

### Step 11: Receive and Decrypt Messages

```typescript
import { decryptEnvelope } from "@ixblix/sdk-js";

if (event.event === "MESSAGE_RECEIVED") {
  // Fetch the full message to get the encrypted content and envelope
  const messages = await companyClient.listMessages(event.conversationId);
  const message = messages.find((m) => m.id === event.messageId);
  if (message) {
    const plaintext = decryptEnvelope(message, operatorKey.privateKey);
    console.log("Customer said:", plaintext);
  }
}
```

### Step 12: Delivery and Read Receipts

Every message moves through three acknowledgement states: `sent`, `delivered`
and `read`. `read` implies `delivered`.

```typescript
// Operator side: mark as delivered as soon as your client receives the message.
await companyClient.markMessageDeliveredByCompany(conversationId, messageId);

// Operator side: mark as read once the operator actually sees it. Only report
// a read when the window is focused AND the message is in the viewport.
await companyClient.markMessageReadByCompany(conversationId, messageId);

// Customer side: MESSAGE_DELIVERED fires when the customer's device receives
// your message; MESSAGE_READ fires when the customer actually sees it.
if (event.event === "MESSAGE_DELIVERED") {
  console.log("Message delivered:", event.messageId, "at", event.deliveredAt);
}
if (event.event === "MESSAGE_READ") {
  console.log("Message read:", event.messageId, "at", event.readAt);
}
```

## Rich Messages

Send interactive buttons using the high-level `sendEncryptedMessage` method:

```typescript
// High-level: encrypt and send rich messages in one call (recommended)
await companyClient.sendEncryptedMessage(conversationId, {
  content: "Please confirm your order:",
  attachments: {
    buttons: [
      { type: "reply", label: "Confirm" },
      { type: "reply", label: "Cancel" },
      {
        type: "url",
        label: "View details",
        url: "https://acme.example.com/order/123",
      },
    ],
    vcard: {
      name: "Support",
      phone: "+5511999998888",
      organization: "Acme Corp",
    },
  },
  operatorIdentity: {
    uuid: "agent-42",
    name: "Maria Silva",
    gravatarHash: "abc123...",
  },
});

// Low-level: manual encryption + send
const keys = await companyClient.getConversationKeys(conversationId);
const payload = encryptMessagePayload(
  {
    content: "Please confirm your order:",
    attachments: {
      buttons: [
        { type: "reply", label: "Confirm" },
        { type: "reply", label: "Cancel" },
      ],
    },
  },
  keys.customerPublicKey!,
  operatorKey.keyId,
  operatorKey.publicKeySpki,
);
const envelope = {
  content: payload.content,
  iv: payload.contentIv,
  authTag: payload.contentAuthTag,
  encryptedKey: payload.encryptedKey,
  selfEncryptedKey: payload.selfEncryptedKey,
  keyId: payload.keyId,
  attachments: payload.attachments,
};
await companyClient.sendCompanyMessage(conversationId, envelope);
```

**Rendering rules:**

- \u2264 3 buttons: rendered inline
- \> 3 buttons: rendered in modal
- Only `reply` buttons send messages back to operator

## Media Attachments

Send encrypted media using the unified `sendEncryptedMessage` method:

```typescript
import fs from "node:fs";

const fileBuffer = fs.readFileSync("./invoice.pdf");

// High-level: encrypt and send media in one call (recommended)
// All parts (caption, file, attachments) share a single AES key with distinct IVs
const message = await companyClient.sendEncryptedMessage(conversationId, {
  content: "Here is your invoice", // optional caption
  file: {
    data: fileBuffer,
    fileName: "invoice.pdf",
    mimeType: "application/pdf",
  },
  operatorIdentity: {
    uuid: "agent-42",
    name: "Maria Silva",
    gravatarHash: "abc123...",
  },
});

console.log("Media message sent:", message.id);

// Low-level: manual encryption + send via sendCompanyMessage with file parameter
// See encryptMediaWithAttachments and sendCompanyMessage documentation.
```

**Download and decrypt received media:**

```typescript
import { decryptMediaEnvelope } from "@ixblix/sdk-js";

const { data, media } = await companyClient.downloadCompanyMedia(
  message.mediaId!,
);

const decrypted = decryptMediaEnvelope(data, message, operatorKey.privateKey);

fs.writeFileSync("./invoice_decrypted.pdf", decrypted);
```

## Presence & Typing Indicators

```typescript
// Report operator typing
await companyClient.reportCompanyPresence(conversationId, "typing");

// Set operator identity
await companyClient.updateOperator(conversationId, {
  name: "Sarah from Support",
  image: "https://acme.example.com/avatars/sarah.png",
});

// Handle customer presence webhooks
if (event.event === "TYPING") {
  console.log("Customer is typing...");
}
if (event.event === "RECORDING") {
  console.log("Customer is recording audio...");
}
if (event.event === "CHAT_CLOSED") {
  console.log("Customer closed the chat window");
}
```

## Key Transfer (Cross-Device)

Allow customers to transfer encryption keys between devices:

```typescript
import crypto from "node:crypto";

// Source device: Create transfer
const pin = Math.floor(100000 + Math.random() * 900000).toString();
const salt = crypto.randomBytes(16);
const kek = crypto.pbkdf2Sync(pin, salt, 200_000, 32, "sha256");
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv("aes-256-gcm", kek, iv);
const keypairJson = JSON.stringify({ publicKey, privateKey });
const ciphertext = Buffer.concat([
  cipher.update(keypairJson, "utf8"),
  cipher.final(),
]);
const authTag = cipher.getAuthTag();

const response = await fetch("https://api.ixblix.app/api/key-transfer", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    ciphertext: ciphertext.toString("base64"),
    iv: iv.toString("base64"),
    salt: salt.toString("base64"),
    kdf: { alg: "PBKDF2-SHA256", iterations: 200_000 },
    publicKeySpki: publicKey, // base64 SPKI public key
  }),
});

const { transferId } = await response.json();
// Show PIN and link to user: https://app.ixblix.app/transfer/{transferId}

// Target device: Retrieve and decrypt
const transferResponse = await fetch(
  `https://api.ixblix.app/api/key-transfer/${transferId}`,
);
const {
  ciphertext: encCiphertext,
  salt: transferSalt,
  iv: transferIv,
  kdf,
} = await transferResponse.json();

const transferKek = crypto.pbkdf2Sync(
  pin,
  Buffer.from(transferSalt, "base64"),
  kdf.iterations,
  32,
  "sha256",
);
const decipher = crypto.createDecipheriv(
  "aes-256-gcm",
  transferKek,
  Buffer.from(transferIv, "base64"),
);
decipher.setAuthTag(Buffer.from(encCiphertext, "base64").subarray(-16));
const decrypted = Buffer.concat([
  decipher.update(Buffer.from(encCiphertext, "base64").subarray(0, -16)),
  decipher.final(),
]);
const { publicKey: transferredPubKey, privateKey: transferredPrivKey } =
  JSON.parse(decrypted.toString("utf8"));

// Confirm transfer
await fetch(`https://api.ixblix.app/api/key-transfer/${transferId}/confirm`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ publicKeySpki: transferredPubKey }),
});
```

## Webhook Event Types

All webhook events use a flat JSON structure with an `event` field indicating the type. They are signed with HMAC-SHA256 via the `X-Ixblix-Signature` header. Each delivery also includes:

- `X-Ixblix-Event-Id`: unique delivery ID (use for deduplication).
- `X-Ixblix-Event`: event type.
- `X-Ixblix-Company-Id`: company ID (useful for multi-tenant receivers to look up the correct secret in O(1)).

| Event                 | When                                  | Key Fields                                   |
| --------------------- | ------------------------------------- | -------------------------------------------- |
| `MESSAGE_RECEIVED`    | Contact sends a message               | `conversationId`, `messageId`, `replyToId?`  |
| `MESSAGE_DELIVERED`   | Customer's device receives a message  | `conversationId`, `messageId`, `deliveredAt` |
| `MESSAGE_READ`        | Customer reads a company message      | `conversationId`, `messageId`, `readAt`      |
| `CUSTOMER_JOINED`     | Customer opens chat and registers key | `conversationId`, `customerPublicKey`        |
| `CONVERSATION_CLOSED` | Conversation is closed                | `conversationId`                             |
| `TYPING`              | Customer starts typing                | `conversationId`, `type: "typing"`           |
| `STOPPED_TYPING`      | Customer stops typing                 | `conversationId`, `type: "stopped"`          |
| `RECORDING`           | Customer records audio                | `conversationId`, `type: "recording"`        |
| `CHAT_CLOSED`         | Customer closes chat window           | `conversationId`, `type: "chat_closed"`      |
| `BALANCE_LOW`         | Company credit balance is low         | `companyId`, `balanceCents`                  |
| `COMPANY_ACTIVATED`   | Company activated after payment       | `companyId`, `transactionId`, `apiKey`       |

## Error Handling

```typescript
import { IxblixError } from '@ixblix/sdk-js';

try {
  await companyClient.createConversation({ ... });
} catch (err) {
  if (err instanceof IxblixError) {
    console.error('API Error:', err.code);     // "VALIDATION_ERROR"
    console.error('Message:', err.message);    // "The 'contact.externalId' field..."
    console.error('Status:', err.status);      // 400
    console.error('Details:', err.details);    // [{ path, message }]
  }
}
```

**Retry strategy for 429 and 5xx errors:**

```typescript
async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (err instanceof IxblixError) {
        const isRetryable = err.status === 429 || err.status >= 500;
        if (!isRetryable || attempt === maxRetries) throw err;

        const delay = Math.min(1000 * 2 ** attempt, 10_000);
        const jitter = Math.random() * 1000;
        await new Promise((r) => setTimeout(r, delay + jitter));
      } else {
        throw err;
      }
    }
  }
  throw new Error("Unreachable");
}
```

## Complete Webhook Handler Example

```typescript
import express from "express";
import {
  IxblixClient,
  verifyWebhook,
  decryptEnvelope,
  loadOrCreateOperatorKey,
  WEBHOOK_SIGNATURE_HEADER,
} from "@ixblix/sdk-js";

const app = express();
app.use(express.json());

const API_URL = "https://api.ixblix.app";
const operatorKey = await loadOrCreateOperatorKey({
  path: "./keys/operator.json",
});
const webhookSecret = process.env.WEBHOOK_SECRET!;
const apiKey = process.env.COMPANY_API_KEY!;

const companyClient = new IxblixClient({
  baseUrl: API_URL,
  apiKey,
  operatorKey,
});

app.post("/ixblix/webhooks", async (req, res) => {
  const signature = req.headers[WEBHOOK_SIGNATURE_HEADER] as string;
  let parsed;
  try {
    parsed = verifyWebhook(req.body, webhookSecret, signature);
  } catch {
    return res.status(401).send("Invalid signature");
  }
  const event = parsed.event;

  switch (event.event) {
    case "CUSTOMER_JOINED": {
      await saveCustomerKey(event.conversationId, event.customerPublicKey);

      // Use sendEncryptedMessage for convenience
      await companyClient.sendEncryptedMessage(event.conversationId, {
        content: "Hello! How can we help?",
      });
      break;
    }

    case "MESSAGE_RECEIVED": {
      const keys = await companyClient.getConversationKeys(
        event.conversationId,
      );
      const messages = await companyClient.listMessages(event.conversationId);
      const message = messages.find((m) => m.id === event.messageId);
      if (message && keys.customerPublicKey) {
        const plaintext = decryptEnvelope(message, operatorKey.privateKey);
        console.log(`Customer: ${plaintext}`);
        // Create ticket in your system, route to operator, etc.
      }
      break;
    }

    case "MESSAGE_DELIVERED":
      console.log(
        "Message delivered:",
        event.messageId,
        "at",
        event.deliveredAt,
      );
      break;
    case "MESSAGE_READ":
      console.log("Message read:", event.messageId, "at", event.readAt);
      break;

    case "CONVERSATION_CLOSED":
      await deleteCustomerKey(event.conversationId);
      break;
  }

  res.status(200).send("OK");
});

app.listen(4000, () => console.log("Webhook server running on port 4000"));
```

## SDK Methods Reference

### IxblixClient

```typescript
// Integrator auth
const integratorClient = new IxblixClient({
  baseUrl: "https://api.ixblix.app",
  integratorId: "int_xxxx",
  integratorAccessToken: "ixblix_integrator_token_xxxx",
});

// Company auth
const companyClient = new IxblixClient({
  baseUrl: "https://api.ixblix.app",
  apiKey: "smci_xxxx",
  operatorKey, // OperatorKeyPair from loadOrCreateOperatorKey
});
```

### Integrator Onboarding

```typescript
await integratorClient.registerIntegrator({
  name,
  hostname,
  callbackUrl,
  force?,        // replace existing verified registration
  subscriptionId?,
});
```

### Company Onboarding (integrator auth)

```typescript
await integratorClient.registerCompany({ name, planId });
await integratorClient.activateCompany(companyId, transactionId);
const plans = await integratorClient.listPlans();
const providers = await integratorClient.listPaymentProviders();
```

### Company Configuration (company auth)

```typescript
const profile = await companyClient.getProfile();
const balance = await companyClient.getBalance();
const { webhookSecret } = await companyClient.updateWebhook({ webhookUrl });
await companyClient.registerEncryptionKey({ keyId, publicKey });
const key = await companyClient.getEncryptionKey();
await companyClient.updateCustomization({ brandName, logoUrl });
await companyClient.uploadBranding({ squareIcon, rectangularLogo });
```

### Conversations

```typescript
const { conversation, deeplink } = await companyClient.createConversation({ contact: { externalId, name?, metadata? }, operator? });
const keys = await companyClient.getConversationKeys(conversationId);
await companyClient.updateOperator(conversationId, { uuid?, name?, image?, gravatarHash? });
await companyClient.reportCompanyPresence(conversationId, 'typing' | 'stopped' | 'recording');
```

### Messages

```typescript
// High-level: encrypt and send in one call (recommended)
// Fetches keys, encrypts all parts (content/file/attachments) with single AES key, and sends.
await companyClient.sendEncryptedMessage(conversationId, {
  content?, // plaintext (required for text, optional caption for media)
  file?: { data: Uint8Array, fileName: string, mimeType: string },
  operatorUuid?, replyToId?,
  attachments?: { buttons?, vcard?, location?, linkPreview? },
  operatorIdentity?: { uuid, name, image?, gravatarHash? },
});

// Low-level: send pre-encrypted envelope
// The operator identity is embedded in the encrypted attachments JSON.
// When file is provided, the message is sent as media (multipart/form-data).
// IMPORTANT: `file.data` must be the ENCRYPTED bytes (the backend stores them
// verbatim as ciphertext) and `envelope.iv`/`envelope.authTag` must match them.
// contentIv/contentAuthTag are for the caption (distinct from media iv/authTag).
await companyClient.sendCompanyMessage(conversationId, envelope, contentType?, operatorUuid?, replyToId?, file?, contentIv?, contentAuthTag?);
await companyClient.reactToMessage(conversationId, messageId, emoji, operatorUuid?);
const messages = await companyClient.listMessages(conversationId);
await companyClient.markMessageDeliveredByCompany(conversationId, messageId);
await companyClient.markMessageReadByCompany(conversationId, messageId);
```

### Media

```typescript
// Media is sent via sendEncryptedMessage with the file parameter
// All parts (caption, file, attachments) share a single AES key with distinct IVs
const message = await companyClient.sendEncryptedMessage(conversationId, {
  content: "caption", // optional
  file: { data, fileName, mimeType },
  operatorIdentity?: { uuid, name, image?, gravatarHash? },
  attachments?: { buttons?, vcard?, location?, linkPreview? },
});

const media = await companyClient.getMedia(mediaId);
const { data, media } = await companyClient.downloadCompanyMedia(mediaId);
```

**Storage class.** Media is stored in one of two buckets. `TRANSIENT` (the
default) is erased by the retention job once the conversation's retention
window elapses; `PERMANENT` is kept indefinitely. Set it on the multipart
upload when you need a file to outlive the retention window:

```typescript
// Via the raw HTTP client (multipart/form-data)
const form = new FormData();
form.append("conversationId", conversationId);
form.append("file", encryptedBlob, "encrypted.bin");
form.append("storageClass", "PERMANENT"); // or "TRANSIENT" (default)
// ...plus iv, authTag, encryptedKey, selfEncryptedKey, keyId
```

The chosen class is returned as `storageClass` on the `Media` object. An
explicit erasure request still removes permanent files — `PERMANENT` only
exempts a file from the automatic retention sweep.

### Credits

```typescript
const { purchase, result } = await companyClient.purchaseCredits({
  amountCents,
  provider,
});
const purchases = await companyClient.listCreditPurchases();
```

### Payment Methods

```typescript
const methods = await companyClient.listPaymentMethods();
const options = await companyClient.getPaymentChangeOptions();
const change = await companyClient.startPaymentChange({ planId? });
```

### Crypto Helpers

```typescript
import {
  generateOperatorKeyPair,
  encryptMessagePayload,
  decryptEnvelope,
  decryptAttachments,
  decryptMediaEnvelope,
  loadOrCreateOperatorKey,
} from "@ixblix/sdk-js";

const { publicKey, privateKey, publicKeySpki, keyId } =
  await generateOperatorKeyPair();

// encryptMessagePayload - unified encryption for content + file + attachments
// All parts share a single AES key with distinct IVs
const payload = encryptMessagePayload(
  {
    content?: string,              // plaintext content/caption
    fileBytes?: Uint8Array,        // media file bytes
    attachments?: MessageAttachments, // buttons, vcard, location, operator identity
  },
  recipientPublicKeySpki,
  senderKeyId,
  senderPublicKeySpki,
);
// Returns: { content, contentIv, contentAuthTag, mediaContent?, mediaIv?, mediaAuthTag?, attachments?, encryptedKey, selfEncryptedKey, keyId }
// NOTE: `mediaContent` is the base64 ciphertext of the file. When uploading
// media manually, send the decoded `mediaContent` bytes (not the plaintext)
// together with `mediaIv`/`mediaAuthTag`.

// decryptEnvelope(message, senderPrivateKey)
const plaintext = decryptEnvelope(message, senderPrivateKey);

// decryptAttachments(message, senderPrivateKey)
const attachments = decryptAttachments(message, senderPrivateKey);

// decryptMediaEnvelope(encryptedData, message, senderPrivateKey)
const decryptedMedia = decryptMediaEnvelope(
  encryptedData,
  message,
  senderPrivateKey,
);
const key = await loadOrCreateOperatorKey({ path });
```

### Webhook Helpers

```typescript
import {
  verifyWebhook,
  parseWebhook,
  WEBHOOK_SIGNATURE_HEADER,
} from "@ixblix/sdk-js";

const signature = req.headers[WEBHOOK_SIGNATURE_HEADER];
const parsed = verifyWebhook(req.body, webhookSecret, signature);
const event = parsed.event;
// Or just parse without verification:
const parsed2 = parseWebhook(req.body);
```

## Push Notification Previews

When the customer has push notifications enabled, include a `pushPreview` option in `sendEncryptedMessage()` to show the actual message content in the notification:

```typescript
await companyClient.sendEncryptedMessage(conversationId, {
  content: "Hello, how can I help?",
  operatorIdentity: { uuid: "op-1", name: "Support Agent" },
  pushPreview: {
    text: "Hello, how can I help?",
    senderName: "Support Agent",
    // Optional: base64 thumbnail for media messages
    // thumbnailBase64: "/9j/4AAQ...",
  },
});
```

The SDK encrypts the preview with the customer's RSA public key and includes it in the message `metadata.encryptedPushPayload`. The backend never sees the preview plaintext — it relays the opaque ciphertext to the customer's device, which decrypts it with its private key.

For low-level API usage, encrypt the preview manually with `encryptPushPreview()`:

```typescript
import { encryptPushPreview } from "@ixblix/sdk-js";

const keys = await companyClient.getConversationKeys(conversationId);
const encryptedPushPayload = encryptPushPreview(
  { text: "Hello!", senderName: "Agent" },
  keys.customerPublicKey!,
);

await companyClient.sendCompanyMessage(
  conversationId,
  envelope,
  "text",
  undefined,
  null,
  undefined,
  undefined,
  undefined,
  {
    encryptedPushPayload,
  },
);
```

## Security Best Practices

- Store API keys and webhook secrets in encrypted vaults or environment variables
- Always verify webhook signatures using `verifyWebhook()` before processing
- Never expose operator private keys in client-side code
- Use HTTPS for all API calls and webhook endpoints
- Rotate webhook secrets periodically
- Implement exponential backoff for retries on `429` and `5xx` errors
- Use `loadOrCreateOperatorKey()` to persist keypairs securely

## Interactive API Documentation

Full OpenAPI spec with interactive testing available at: `https://dev.ixblix.app`

## Full Documentation

For complete API reference with all endpoints, request/response examples, and detailed parameter descriptions, see:

- **REST API Reference**: https://ixblix.app/docs/api-reference
- **SDK Reference**: https://ixblix.app/docs/sdk
- **Quick Start Guide**: https://ixblix.app/docs/quickstart
