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

# Quickstart Guide: Building a Custom API CRM Integration

> Learn how to build a custom API CRM integration with Siro. Sync appointments, automatically link recordings to CRM records, and pull extracted conversation data back to your system.

## Overview

### What you'll build

A bidirectional integration enabling:

* Automatic recording-to-opportunity matching
* CRM context in Siro (customer names, deal amounts, outcomes)
* Entity extraction data flowing back to your CRM

### Time estimate

4-8 hours for a developer familiar with REST APIs

### Prerequisites

* API development experience
* Admin access to your Siro workspace
* Your system exposes appointment/opportunity data via API or database

***

### Step 1: Get Your Siro API Credentials

### Generate your organization token

1. Log into your Siro workspace as an admin
2. Navigate to **Person Icon** → **API Tokens**
3. Click **Generate New API Token**
4. Store this securely — use it as the **Bearer** value below (this is your **organization** integration token, not a user OAuth access token)

### API Base URL

Most API calls use:

```
https://functions.siro.ai/api-externalApi/v1
```

**Authentication:** Send the organization token as a Bearer token:

```bash theme={null}
curl --request GET \
  --url 'https://functions.siro.ai/api-externalApi/v1/core/mobile-events?pageSize=100' \
  --header 'Authorization: Bearer <organization-api-token>'
```

**Documentation:** Full API reference at [docs.siro.ai](https://docs.siro.ai)

***

## Step 2: Understand the Basics of the Data Model

Siro's integration data model maps to standard CRM concepts.

### CRM entity IDs

* **Siro-internal id** (`id`): The UUID Siro stores for a synced CRM row (account, engagement, opportunity, external user, etc.). Use this when an API accepts our primary key—for example when [linking a recording to CRM entities](/api-references/put-recording-crm-link)—or when reading ids from webhooks and detail payloads.
* **CRM-native id** (`externalId`): The identifier in *your* CRM (for example a Salesforce Id). When you target entities by `externalId`, also send **`integrationConnectionId`** so Siro knows which CRM connection that id belongs to.

### People: Siro Users vs External Users

* **Siro User**: Someone who can sign into your Siro workspace (web or mobile). This maps to a **`SiroUser`** in our database. Examples: OAuth app **`owner`**, recording **`userId`** on recording detail, webhook **`siroUserId`**.
* **External User** (same concept as **CRM User**): The synced representation of a rep or roster row from your CRM—the API resource named **`User`**. Sync payloads include **`externalId`**, **`email`**, and **`name`**; Siro maps External Users to **Siro Users** for attribution (automatic match or **Settings → Integrations → User Mapping**).

| Your System                   | In Siro / API          | Purpose                                                                        |
| ----------------------------- | ---------------------- | ------------------------------------------------------------------------------ |
| Sales rep / roster row in CRM | External User (`User`) | CRM-side identity; maps to a **Siro User** for workspace login and attribution |
| Customer / Account            | `Account`              | Customer information                                                           |
| Opportunity / Deal / Lead     | `Opportunity`          | Sales outcomes with dollar values                                              |
| Appointment / Meeting         | `Engagement`           | Scheduled interactions that get recorded                                       |
| Recording                     | `Recordings`           | Siro recording                                                                 |

### Key Relationship

The critical link is: **Recording ↔ Engagement ↔ Opportunity AND/OR Account**

When you sync an Engagement to Siro with appointment details (time, location, rep), Siro can automatically match recordings to that engagement and surface the linked Opportunity context.

***

## Step 3: Sync Your Data to Siro

#### 3.1 Link External Users to Siro Users

User linking happens automatically. When you sync engagements or opportunities, include each rep’s CRM identifiers on **`User`** payloads (`externalId`, `email`, and `name`). Siro maps **External Users** to **Siro Users** by email or name match. Manual override is available in **Settings → Integrations → User Mapping**.

### 3.2 Sync Appointments (Engagements)

**This is the core of the integration.** Syncing engagements enables appointment lists for reps, automatic recording linking, and CRM context for AI features.

**Endpoint:** `PUT /v1/integrations/sync/engagements` • [Full docs](https://docs.siro.ai/api-references/sync-an-engagement)

**Example request:**

```bash theme={null}
curl --request PUT \
  --url 'https://functions.siro.ai/api-externalApi/v1/integrations/sync/engagements' \
  --header 'Authorization: Bearer <organization-api-token>' \
  --header 'Content-Type: application/json' \
  --data '@-' <<EOF
{
  "externalId": "appt-456",
  "startTime": "2026-02-15T14:00:00Z",
  "endTime": "2026-02-15T15:00:00Z",
  "subject": "Smith Kitchen Remodel Consultation",
  "engagementType": {
    "activityType": "MEETING",
    "name": "meeting"
  },
  "engagementUsers": [
    {
      "externalId": "rep-123-from-your-crm",
      "email": "rep-123@kitchenremodel.com",
      "name": "John Sales"
    }
  ],
  "account": {
    "externalId": "account-789"
  },
  "opportunity": {
    "externalId": "opp-101"
  }
}
EOF
```

**Payload notes:**

* `activityType`: Valid values are MEETING, CALL, APPOINTMENT, EMAIL, TEXT, EVENT
* Only MEETING, APPOINTMENT, and EVENT appear in the Appointment List feature

### 3.3 Sync Opportunities

Adds deal context (amount, disposition, customer name) to recordings.

**Endpoint:** `PUT /v1/integrations/sync/opportunities` • [Full docs](https://docs.siro.ai/api-references/sync-an-opportunity)

**Example request:**

```bash theme={null}
curl --request PUT \
  --url 'https://functions.siro.ai/api-externalApi/v1/integrations/sync/opportunities' \
  --header 'Authorization: Bearer <organization-api-token>' \
  --header 'Content-Type: application/json' \
  --data '@-' <<EOF
{
  "externalId": "opp-101",
  "name": "Smith Kitchen Remodel",
  "amount": 45000,
  "closedAt": "2026-03-01T00:00:00Z",
  "disposition": "WON",
  "account": {
    "externalId": "account-789"
  },
  "opportunityUsers": [
    {
      "externalId": "rep-123-from-your-crm",
      "email": "rep-123@kitchenremodel.com",
      "name": "John Sales"
    }
  ]
}
EOF
```

### 3.4 Sync Accounts

Adds customer context (name, address, contact info) to recordings. Account syncing happens automatically when you include the nested `account` object with an `externalId` in engagement or opportunity payloads. See the engagement example in 3.2 for the basic structure, or add optional fields like `emailAddresses`, `phoneNumbers`, and `addresses` as needed.

***

## Step 4: Pull Data from Siro Back to Your System

Once recordings are created and linked, retrieve AI-generated insights. Siro supports two ways to receive this data:

1. **Real-time webhooks (recommended)** — subscribe to events on your Siro workspace and receive HTTP callbacks the moment a recording is processed and linked to a CRM engagement. Covered in sections 4.1–4.3 below.
2. **Polling** — periodically query the engagement endpoint to check for linked recordings, then pull enriched data from separate endpoints. See [Alternative: Polling Flow](#alternative-polling-flow).

**Note:** Most integrations use your **organization API token**. To call **Get Recording Details** (section 4.3) or **Get Entity Extractions** (section 4.5), you also need **OAuth** setup below. If webhooks plus engagement polling are enough for your use case, you can skip OAuth.

### OAuth Setup (Required for Recording Details & Entity Extractions)

#### 1. Create an OAuth App

**Endpoint:** `POST /v1/core/oauth/apps` • [Full docs](https://docs.siro.ai/api-references/create-an-oauth-app)

Provide `appName`, `owner` (**Siro User** id), and `organizationId`. Store the returned `clientID` and `clientSecret` securely.

#### 2. Generate OAuth Access Tokens

**Endpoint:** `POST /v1/core/oauth/apps/{clientId}/access-token` • [Full docs](https://docs.siro.ai/api-references/create-an-oauth-access-token)

Provide `clientSecret`, `userId` (**Siro User** id), and `scope: "read"`. Use the returned `accessToken` for entity extractions. Tokens expire after 16 hours.

### 4.1 Subscribe to Webhook Events

Siro emits two event types that together signal a recording is fully ready for downstream processing:

| Event type                        | Fires when                                                                            |
| --------------------------------- | ------------------------------------------------------------------------------------- |
| `integrations.recordingProcessed` | Siro finishes processing the recording (transcript, summary, entity extraction, etc.) |
| `integrations.recordingLinked`    | Siro recording is definitively linked to a CRM record                                 |

These events fire independently and may arrive in either order — the recording may be linked before processing finishes, or vice versa.

**How to subscribe:**

Webhook subscriptions are managed from the **Webhooks** page in the Siro dashboard.

1. As an org admin, open the **Organization Admin Area** from the top-right menu and select **Webhooks**. You'll see the Endpoints list where all your configured webhooks are shown.

<Frame>
  <img src="https://mintcdn.com/siro-508e7b35/b8gkrq6umOt5NVjQ/images/webhooks-endpoints-list.png?fit=max&auto=format&n=b8gkrq6umOt5NVjQ&q=85&s=b9ee5ff0f9c6d3f952dfee75c50203a1" alt="Webhooks endpoints list" width="2980" height="1210" data-path="images/webhooks-endpoints-list.png" />
</Frame>

2. Click **+ Add Endpoint**. Fill in your **Endpoint URL** (the HTTPS URL where you want to receive events), an optional **Description**, and select the events to subscribe to. Check both **integrations.recordingLinked** and **integrations.recordingProcessed**.

<Frame>
  <img src="https://mintcdn.com/siro-508e7b35/b8gkrq6umOt5NVjQ/images/webhooks-new-endpoint.png?fit=max&auto=format&n=b8gkrq6umOt5NVjQ&q=85&s=078d4ac3549f7c80ec55868f133511dc" alt="Add a new webhook endpoint" width="2980" height="1516" data-path="images/webhooks-new-endpoint.png" />
</Frame>

3. After saving, you'll see the endpoint detail page with your **Signing Secret**. Copy this secret — you'll use it to verify that incoming webhook payloads are authentic.

<Frame>
  <img src="https://mintcdn.com/siro-508e7b35/b8gkrq6umOt5NVjQ/images/webhooks-endpoint-detail.png?fit=max&auto=format&n=b8gkrq6umOt5NVjQ&q=85&s=f6c43c7c759aeac12cb79e22a5816a64" alt="Webhook endpoint detail with signing secret" width="2978" height="1518" data-path="images/webhooks-endpoint-detail.png" />
</Frame>

<Tip>
  You can test your endpoint before going live using the **Testing** tab on the
  endpoint detail page, or by using [Webhook.site](https://webhook.site) to
  inspect payloads without setting up a server.
</Tip>

**Verifying webhook signatures**

Every webhook delivery includes three headers used for verification:

| Header           | Description                                  |
| ---------------- | -------------------------------------------- |
| `svix-id`        | Unique message identifier                    |
| `svix-timestamp` | Timestamp in seconds since epoch             |
| `svix-signature` | Base64-encoded signature(s), e.g. `v1,<sig>` |

The recommended approach is to use the official Svix library, which handles signature construction and timestamp tolerance for you.

<CodeGroup>
  ```js Node.js theme={null}
  import { Webhook } from "svix"; // npm install svix

  const secret = "whsec_YOUR_SIGNING_SECRET";

  export default async function handler(req, res) {
    const payload = (await buffer(req)).toString(); // raw body
    const headers = req.headers;

    const wh = new Webhook(secret);
    try {
      wh.verify(payload, headers);
    } catch (err) {
      return res.status(400).json({});
    }

    // Signature valid — process the event
    res.json({});
  }
  ```

  ```python Python theme={null}
  from svix.webhooks import Webhook, WebhookVerificationError  # pip install svix

  secret = "whsec_YOUR_SIGNING_SECRET"

  def webhook_handler(request):
      payload = request.body
      headers = request.headers

      wh = Webhook(secret)
      try:
          msg = wh.verify(payload, headers)
      except WebhookVerificationError:
          return HttpResponse(status=400)

      # Signature valid — process the event
      return HttpResponse(status=200)
  ```

  ```go Go theme={null}
  import svix "github.com/svix/svix-webhooks/go" // go get github.com/svix/svix-webhooks/go

  secret := "whsec_YOUR_SIGNING_SECRET"

  wh, err := svix.NewWebhook(secret)
  err = wh.Verify(payload, headers) // returns nil on success
  ```
</CodeGroup>

<Warning>
  You **must** use the raw request body when verifying. Parsing the JSON first
  and re-serializing it will break the signature.
</Warning>

Subscribe to **both** event types. Every delivery carries the current state of both flags regardless of which event triggered it, which is what makes the filtering rule in 4.2 work without any correlation logic on your side.

### 4.2 Handle Webhook Payloads

Both event types share the same payload shape:

```json theme={null}
{
  "recordingId": "rec-abc-def",
  "siroUserId": "siro-user-123",
  "recordingProcessed": true,
  "recordingLinked": true,
  "eventType": "integrations.recordingProcessed",
  "crm": {
    "integrationConnectionId": "conn-123",
    "users": [{ "id": "user-uuid", "externalId": "crm-user-123" }],
    "engagement": { "id": "eng-uuid", "externalId": "appt-456" },
    "opportunity": { "id": "opp-uuid", "externalId": "opp-101" },
    "account": { "id": "acct-uuid", "externalId": "account-789" }
  }
}
```

Key fields:

* `recordingProcessed` / `recordingLinked` — current boolean state of each condition. Every payload reports both, regardless of which event was emitted.
* `siroUserId` — **Siro User** id for the rep associated with the recording.
* `crm.users[]` — linked **External Users**; `id` is Siro-internal for that user row, `externalId` is the id from your CRM (when set).
* `crm.engagement` / `crm.opportunity` / `crm.account` — each includes Siro-internal `id` plus CRM-native `externalId` where applicable.
* `crm.engagement.externalId` — the appointment ID you originally synced (from 3.2).
* `crm.opportunity.externalId` — the opportunity ID you originally synced (from 3.3).

**Filtering rule:** act only when both `recordingProcessed` and `recordingLinked` are `true`.

Because every delivery reports current state for both conditions, the handler for whichever event fires last will naturally observe `true` / `true` — no correlation or state machine is needed.

**Example handler:**

```ts theme={null}
async function onSiroWebhook(payload) {
  if (!payload.recordingProcessed || !payload.recordingLinked) {
    // Not ready yet — the other event will arrive.
    return;
  }

  const { recordingId, crm } = payload;
  const engagementExternalId = crm?.engagement?.externalId;
  const opportunityExternalId = crm?.opportunity?.externalId;

  // Fetch enriched data — see 4.3
  const recording = await fetchRecordingDetails(recordingId);

  await yourCrm.attach({
    engagementExternalId,
    opportunityExternalId,
    summary: recording.summary,
    extractions: recording.entityExtractions,
  });
}
```

**Idempotency:** because the "both true" condition can be observed twice (once per event), dedupe by `recordingId` if your downstream side-effects aren't idempotent.

### 4.3 Get Recording Details (Summary + Entity Extractions)

**Endpoint:** `GET /v1/core/recordings/{recordingId}`

**Base URL:** `https://api.siro.ai`

**Note:** This base URL is different from the main external API (`https://functions.siro.ai/api-externalApi/v1`). It matches the base used by the Entity Extractions endpoint (4.5).

**Authentication:** Use OAuth access token (header: `x-siro-auth-token`) — see the OAuth Setup section above.

**Query parameters:**

| Param                   | Type    | Purpose                                                     |
| ----------------------- | ------- | ----------------------------------------------------------- |
| `showSummary`           | boolean | Include the concatenated LLM summary in the response        |
| `showEntityExtractions` | boolean | Include per-definition entity extractions with CRM mappings |

Both default to `false`.

**Example request:**

```bash theme={null}
curl --request GET \
  --url 'https://api.siro.ai/v1/core/recordings/rec-abc-def?showSummary=true&showEntityExtractions=true' \
  --header 'x-siro-auth-token: YOUR_OAUTH_ACCESS_TOKEN'
```

**Response (relevant fields):** (`userId` is the **Siro User** id; see [People: Siro Users vs External Users](#people-siro-users-vs-external-users) in Step 2.)

```json theme={null}
{
  "data": {
    "id": "rec-abc-def",
    "organizationId": "org-789",
    "userId": "user-123",

    "summary": "Overview:\nThe rep walked the customer through financing options.\n\nNext Steps:\nSend a written proposal by Friday.",

    "entityExtractions": [
      {
        "id": "extraction_001",
        "recordingId": "rec-abc-def",
        "siroEntityDefinitionId": "def-uuid",
        "siroEntityDefinitionName": "Budget",
        "status": "SUCCESS",
        "extraction": [
          {
            "name": "Budget",
            "value": "$40,000 - $50,000",
            "mappings": [
              { "crmModelName": "Opportunity", "crmFieldName": "Budget__c" }
            ]
          }
        ]
      }
    ]
  }
}
```

**Use case:** in a single round-trip, pull both the conversation summary and any extracted custom fields ready to write back to your CRM.

***

## Alternative: Polling Flow

If webhooks aren't an option for your environment, use the engagement endpoint to poll for linked recordings and pull enriched data from the endpoints below. This flow uses an org API token for everything except entity extractions (4.5), which still requires OAuth.

### 4.4 Get Linked Recording for an Engagement

**Endpoint:** `GET /v1/integrations/engagements/{id}` • [Full docs](https://docs.siro.ai/api-references/get-an-engagement)

**Response includes:**

```json theme={null}
{
  "id": "eng-123",
  "externalId": "appt-456",
  "recordingId": "rec-abc-def",
  "engagementType": "appointment",
  "opportunityId": "opp-101",
  "accountId": "account-789"
}
```

Use the `recordingId` to fetch detailed data.

### 4.5 Get Entity Extractions (CRM Autofill Data)

**Endpoint:** `GET /v1/core/entities/extractions/{recordingId}` • [Full docs](https://docs.siro.ai/api-references/get-extractions)

**Base URL:** `https://api.siro.ai`

**Note:** This base URL is different from other API endpoints.

**Authentication:** Use OAuth access token (header: `x-siro-auth-token`)

**Response example:**

```json theme={null}
{
  "data": [
    {
      "id": "extraction_001",
      "recordingId": "rec-abc-def",
      "extraction": [
        {
          "name": "Budget",
          "value": "$40,000 - $50,000"
        },
        {
          "name": "Timeline",
          "value": "Spring 2026"
        },
        {
          "name": "Decision Maker",
          "value": "Both homeowners present, wife is primary"
        },
        {
          "name": "Objections",
          "value": "Concerned about project timeline due to summer vacation plans"
        }
      ],
      "createdAt": "2026-01-28T00:00:00Z",
      "updatedAt": "2026-01-28T00:00:00Z"
    }
  ]
}
```

**Use case:** Write extracted fields back to your CRM to auto-populate data from conversations.

### 4.6 Get Recording Summaries

**Endpoint:** `GET /v1/core/recordings/{recordingId}/summaries`

**Response:**

```json theme={null}
{
  "data": [
    {
      "id": "summary-001",
      "name": "AI Summary",
      "content": "Summary text here..."
    }
  ]
}
```

**Use case:** Write summaries as CRM notes to create an activity trail.

***

## Auto Start/Stop via Deep Linking (Optional)

Start Siro recordings directly from your mobile app:

**URL format:**

```
siro://record?appointmentId={external-id}&title={customer-name}&opportunityId={opp-external-id}
```

**Example:**

```
siro://record?appointmentId=appt-456&title=Smith Kitchen Remodel&opportunityId=opp-101
```

Siro will use the `appointmentId` to link the recording to the engagement and `opportunityId` to link the associated opportunity immediately.

***

## Next Steps

* **Test it:** Sync a test appointment, record a conversation at that time, and verify CRM context appears
* **Automate syncs:** Use real-time webhooks (recommended) or batch jobs every 10 minutes
* **Custom fields:** Work with your CSM to configure custom entity extraction fields
* **Troubleshoot:** If recordings aren't linking, check Settings → Users to verify email mappings
* **Train your team:** Show reps how recordings link to CRM records

***

## Getting Help

**Technical questions:** Email your Customer Success Manager or reach out to [customersuccess@siro.ai](mailto:customersuccess@siro.ai)

**API issues:** Include your request details (method, endpoint, payload) and organization ID when reaching out.
