> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trikon.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Example: Voice CRM with columns

> Build a CRM whose pipeline is columns, with a Like column, powered by Trikon Voice calls and webhooks.

This example is a **small CRM you host**. Each contact is a card. Columns are pipeline stages. A **Like** column (or a Like control on every row) marks the people your team wants to follow up. Trikon Voice is the calling layer — you do not rebuild telephony.

Trikon already has an in-app [Leads](/user-guide/leads) screen. This page is for when you want **your own** UI (or to embed voice inside an existing CRM).

## What you will ship

* A board: columns such as **New**, **Calling**, **Interested**, **Follow-up**, **Not interested**, **Booked**.
* A **Like** column (heart or star). Liked rows pin to the top of a column or to a **Favourites** view.
* **Call** on a card → `POST /api/outbound-call` or `POST /api/developer/leads/{id}/call`.
* Webhooks move the card when the AI reports `intent` (`interested`, `follow_up`, `not_interested`, `appointment_scheduled`, …).

## Data model (your database)

Keep Trikon IDs so webhooks can find the row.

```sql theme={null}
CREATE TABLE crm_contacts (
  id            UUID PRIMARY KEY,
  name          TEXT NOT NULL,
  phone         TEXT NOT NULL,          -- E.164, e.g. +919845012345
  column_key    TEXT NOT NULL DEFAULT 'new',
  liked         BOOLEAN NOT NULL DEFAULT FALSE,
  liked_at      TIMESTAMPTZ,
  trikon_lead_id TEXT,
  last_call_uuid TEXT,
  last_intent   TEXT,
  last_summary  TEXT,
  created_at    TIMESTAMPTZ DEFAULT now()
);
```

`column_key` is **your** pipeline, not Trikon’s call status. Map AI intent → column in the webhook handler.

`liked` is the Like column: a boolean you store yourself. The Voice API does not persist likes; that is application state.

## 1. Create the contact in Trikon (optional but useful)

When someone is added in your CRM, mirror them into Trikon **Leads** so Call Logs stay aligned:

```http theme={null}
POST /api/developer/leads
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{
  "enterprise": "acmeclinic",
  "agentId": "8f2c1d34-5b6a-4c7e-9f10-2a3b4c5d6e7f",
  "name": "Asha",
  "phone": "+919845012345",
  "notes": "Inbound website form",
  "source": "custom_crm"
}
```

Save `lead.id` from the `201` body as `trikon_lead_id`. You can skip this and only use outbound-call if you do not need the Trikon Leads UI.

## 2. Call from a card

**Option A — call by number** (no lead row required):

```bash theme={null}
curl -X POST https://voice.trikon.tech/api/outbound-call \
  -H "Authorization: Bearer $TRIKON_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+919845012345",
    "agentId": "8f2c1d34-5b6a-4c7e-9f10-2a3b4c5d6e7f",
    "enterprise": "acmeclinic",
    "from": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "name": "Asha",
    "metadata": { "crm_contact_id": "YOUR_ROW_UUID" }
  }'
```

Put `crm_contact_id` in `metadata` if you want your webhook to key off a field you control. Store returned `callUuid` on the row and set `column_key` to `calling`.

**Option B — call an existing Trikon lead:**

```http theme={null}
POST /api/developer/leads/{trikon_lead_id}/call
Authorization: Bearer YOUR_API_KEY
```

Request details: [Make a call](/developers/make-a-call).

## 3. Like column

This is entirely in your app:

```ts theme={null}
// PATCH /contacts/:id/like
await db.contacts.update(id, {
  liked: !current.liked,
  liked_at: current.liked ? null : new Date(),
});
```

UI ideas that match a “column feature”:

* A dedicated **Liked** column on the board that lists every `liked = true` contact, still showing their pipeline column as a badge.
* A Like control as the **first column** of a table view (star / heart), sortable so liked contacts float up.
* Filter chips: All | Liked | Unliked.

Liked does not change how you call. Teams typically Call liked contacts first from a “Favourites” queue.

## 4. Move columns from webhooks

Point **Settings → Developer API** at `https://your-crm.example/webhooks/trikon`. Acknowledge with `200` in under 3 seconds, then update the row asynchronously.

```ts theme={null}
const COLUMN_BY_INTENT: Record<string, string> = {
  interested: "interested",
  follow_up: "follow_up",
  followup: "follow_up",
  not_interested: "not_interested",
  appointment_scheduled: "booked",
  voicemail: "follow_up",
};

async function onTrikonWebhook(body: {
  event: string;
  callUuid: string;
  to: string;
  name?: string;
  intent?: string;
  summary?: string;
  callStatus?: string;
}) {
  if (body.event === "call.no_answer" || body.event === "call.failed") {
    await moveByPhone(body.to, "follow_up");
    return { ok: true };
  }

  if (body.event === "analysis.completed" || body.event === "call.completed") {
    const column =
      (body.intent && COLUMN_BY_INTENT[body.intent.toLowerCase()]) || "follow_up";
    await db.contacts.updateMany(
      { phone: body.to },
      {
        column_key: column,
        last_call_uuid: body.callUuid,
        last_intent: body.intent ?? null,
        last_summary: body.summary ?? null,
      }
    );
  }

  return { ok: true };
}
```

Full payload: [Webhooks](/developers/webhooks). Prefer `analysis.completed` when you need `intent` and `summary`.

<Note>
  One workspace has **one** webhook URL. If Zapier or n8n already occupies it, put a tiny fan-out service in front that POSTs to both your CRM and the automation tool.
</Note>

## 5. Suggested UI

| Region             | Behaviour                                                                       |
| ------------------ | ------------------------------------------------------------------------------- |
| Board              | Drag cards between columns (updates `column_key` only — does not place a call). |
| Card               | Name, phone, last summary, Like, **Call**.                                      |
| Like column / star | Toggles `liked`.                                                                |
| Call               | Disables the button until webhook returns, shows “Calling…”.                    |

## Agent script for this CRM

Give the outbound agent instructions so `intent` is stable enough to map to columns, for example:

```markdown theme={null}
After the conversation, the outcome should be clear:
- Interested in a demo or purchase
- Wants a follow-up on a specific date
- Not interested
- Appointment booked (date and time)

Do not be vague. Confirm the next step out loud before hanging up.
```

## Checklist

<Steps>
  <Step title="Board + Like">
    Columns and `liked` work with fake data, no API yet.
  </Step>

  <Step title="Create + call">
    New contact hits `/api/developer/leads` and Call hits outbound-call. You hear the agent.
  </Step>

  <Step title="Webhook">
    After a real call, the card moves and `last_summary` appears.
  </Step>

  <Step title="Liked queue">
    Filter to liked contacts and call from that list.
  </Step>
</Steps>
