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

# Connect your CRM

> Use the Sales Ask API to keep appointments in sync with your CRM, and get every recording's results back by webhook.

Use this when your CRM isn't one of our [ready-made integrations](/integrations/overview). Your CRM keeps Sales Ask's calendar up to date, and Sales Ask returns the outcome of every recorded conversation to your CRM.

```mermaid theme={null}
sequenceDiagram
    participant CRM as Your CRM
    participant SA as Sales Ask
    participant Rep as Rep's app
    CRM->>SA: Appointment created, moved or cancelled
    SA->>Rep: Reminder to record
    Rep->>SA: Recording
    SA->>SA: Link recording to the appointment and analyze it
    SA->>CRM: Webhook with notes, extracted fields and your event_id
```

<Info>
  **Before you build**, make sure you have:

  * your [organization API key](/build/api-keys)
  * the custom CRM integration turned on for your organization: email [support@salesask.com](mailto:support@salesask.com), and tell us whether to match recordings by time or by location
  * every rep added to Sales Ask with the same email address they use in your CRM
</Info>

## Send appointments

Call the API from each place your CRM creates, edits, or deletes an appointment, so Sales Ask always has the same schedule as your CRM.

| In your CRM | Call |
| - | - |
| Appointment created | `POST /v1/scheduled-tasks` |
| Appointment rescheduled, reassigned, or edited | `POST` again with the same `event_id`, or `PUT /v1/scheduled-tasks/{event_id}` |
| Appointment cancelled or deleted | `DELETE /v1/scheduled-tasks/{event_id}` |

`POST` creates the appointment, or updates it when Sales Ask already has that `event_id`. You can send the full appointment every time it changes.

### Appointment fields

<ParamField body="event_id" type="string" required>
  Your CRM's ID for the appointment. Send it as a string. You use it to update or delete the appointment, and it comes back in every webhook.
</ParamField>

<ParamField body="user_email" type="string" required>
  Email of the rep who owns the appointment. It must match a member of your Sales Ask organization.
</ParamField>

<ParamField body="start_time" type="string" required>
  Start time in UTC, ending in `Z` or `+00:00`, for example `2026-03-01T14:00:00.000Z`. Other offsets and Unix timestamps are rejected.
</ParamField>

<ParamField body="end_time" type="string">
  End time in UTC.
</ParamField>

<ParamField body="title" type="string">
  Appointment title, shown to the rep.
</ParamField>

<ParamField body="customer" type="object">
  The customer the appointment is with.

  <Expandable title="customer fields">
    <ParamField body="id" type="string">
      Your CRM's customer ID. Strongly recommended: Sales Ask uses it to group every recording for the same customer into one Client, and to tell appointments apart when matching.
    </ParamField>

    <ParamField body="name" type="string">
      Customer name, shown in the rep's reminders and app.
    </ParamField>

    <ParamField body="email" type="string">
      Customer email, saved on the Client.
    </ParamField>

    <ParamField body="location" type="string">
      Appointment address. Required if you match recordings by location.
    </ParamField>
  </Expandable>
</ParamField>

Any other fields, such as phone numbers or notes, are ignored.

### Examples

<Tabs>
  <Tab title="Create">
    ```bash theme={null}
    curl -X POST https://integrations.salesask.com/v1/scheduled-tasks \
      -H "x-api-key: <YOUR_API_KEY>" \
      -H "Content-Type: application/json" \
      -d '{
        "event_id": "APPT-9871",
        "user_email": "rep@yourcompany.com",
        "start_time": "2026-03-01T14:00:00.000Z",
        "end_time": "2026-03-01T15:00:00.000Z",
        "title": "Design consultation",
        "customer": {
          "id": "CUST-123",
          "name": "John Doe",
          "email": "john@example.com",
          "location": "123 Main St, Austin, TX 78701"
        }
      }'
    ```

    Sales Ask returns `201` with the saved appointment, whether it was new or updated:

    ```json theme={null}
    {
      "appointment": {
        "id": "task_abc123",
        "event_id": "APPT-9871",
        "user_email": "rep@yourcompany.com",
        "startTime": "2026-03-01T14:00:00.000Z",
        "endTime": "2026-03-01T15:00:00.000Z",
        "title": "Design consultation",
        "client": {
          "id": "CUST-123",
          "name": "John Doe",
          "email": "john@example.com",
          "location": "123 Main St, Austin, TX 78701"
        },
        "scheduledAt": "2026-03-01T13:45:00.000Z",
        "status": "scheduled",
        "createdAt": "2026-02-28T09:00:00.000Z",
        "updatedAt": "2026-02-28T09:00:00.000Z"
      }
    }
    ```
  </Tab>

  <Tab title="Reschedule">
    Send only what changed. Fields inside `customer` are merged with what Sales Ask already has.

    ```bash theme={null}
    curl -X PUT https://integrations.salesask.com/v1/scheduled-tasks/APPT-9871 \
      -H "x-api-key: <YOUR_API_KEY>" \
      -H "Content-Type: application/json" \
      -d '{
        "start_time": "2026-03-01T16:00:00.000Z",
        "end_time": "2026-03-01T17:00:00.000Z"
      }'
    ```

    A new start time reschedules the rep's reminders.
  </Tab>

  <Tab title="Cancel">
    ```bash theme={null}
    curl -X DELETE https://integrations.salesask.com/v1/scheduled-tasks/APPT-9871 \
      -H "x-api-key: <YOUR_API_KEY>"
    ```

    The appointment leaves the rep's list and its reminders stop. A second delete returns `404`, so retries are safe.
  </Tab>

  <Tab title="Look up">
    One appointment:

    ```bash theme={null}
    curl https://integrations.salesask.com/v1/scheduled-tasks/APPT-9871 \
      -H "x-api-key: <YOUR_API_KEY>"
    ```

    A rep's appointments for a month:

    ```bash theme={null}
    curl "https://integrations.salesask.com/v1/scheduled-tasks?user_email=rep@yourcompany.com&fromDate=2026-03-01&toDate=2026-03-31" \
      -H "x-api-key: <YOUR_API_KEY>"
    ```

    The list returns `appointments`, `nextCursor`, and `hasMore`. Pass `nextCursor` as `startAfter` for the next page. `limit` is 1 to 100 (default 25). `fromDate` and `toDate` filter on `scheduledAt`, the next reminder time, which is up to 15 minutes before the start, so widen the range slightly.
  </Tab>
</Tabs>

### If a request fails

| Status | Cause |
| - | - |
| `400` | A required field is missing (the response names it in `field`), a time isn't UTC, or `user_email` isn't a member of your organization. |
| `401` | The API key is missing or wrong. |
| `404` | No appointment with that `event_id`. |

## How Sales Ask uses your appointments

| | What Sales Ask does | What it needs from you |
| - | - | - |
| **Linking recordings by time** | Links the recording to the rep's appointment whose start is closest to when recording began, within one hour either side. An appointment at 2:00 PM and a recording started at 2:05 PM are linked. | `user_email`, `start_time` |
| **Linking recordings by location** | Links the appointment whose address is within 250 meters of the rep, among appointments within three hours. Falls back to time if none is close enough. | `customer.location` |
| **Reminders to record** | Sends the rep two push notifications with the customer's name: about 15 minutes before (**Mic Check: Ready to Record in 15 mins?**) and at the start time (**Mic Check: Ready to Record?**). | A start time in the future, and the rep signed in to the mobile app with notifications on |
| **A Client for each customer** | When a linked recording is processed, finds or creates a **Client** and adds the recording to it, so every meeting with the same buyer builds one profile. | `customer.id` |

The link between a recording and an appointment is how your `event_id` gets back to you, so keep appointments current when they move. When two appointments are equally close, Sales Ask lets the rep choose. Reps can also pick or change the appointment themselves. The app shows each rep's appointments from the past 30 days and the next 7.

## Get results back

Sales Ask doesn't write into your CRM. It posts each result to a URL you choose, and your CRM saves it.

### Add a webhook

<Steps>
  <Step title="Create it">
    In Sales Ask, go to **Settings → Webhooks**, add a webhook, and enter your endpoint URL. Set **Trigger** to **All Sources**.
  </Step>

  <Step title="Pick events">
    Under **Events**, select the events below. Most integrations select all three.
  </Step>

  <Step title="Choose extracted fields (optional)">
    Under **Custom Payloads**, add what Sales Ask should pull from each conversation, such as budget, design preferences, or reason to buy.
  </Step>

  <Step title="Turn it on">
    Make sure the webhook is **Active**, and save.
  </Step>
</Steps>

| Event | Sent when |
| - | - |
| `recording.processed` | A recording finishes processing. |
| `recording.integration_updated` | A rep links a recording to an appointment, or changes the link, after processing. Same payload as `recording.processed`. |
| `client.created` | A rep creates a new client in Sales Ask, such as a walk-in, and records with them. |

<Warning>
  For a recording with a client the rep created in Sales Ask, you get `client.created` **instead of** `recording.processed`. Subscribe to both to hear about every recording.
</Warning>

### What you receive

```json theme={null}
{
  "event": "recording.processed",
  "data": {
    "id": "rec_abc123",
    "name": "Design consultation with John Doe",
    "notes": "Buyer is relocating for work and wants to move in by August...",
    "actionItems": "Action items from meeting\n- Send lot pricing\nClient action items\n- Share pre-approval letter",
    "meetingUrl": "https://app.salesask.com/meetings/rec_abc123",
    "process": {
      "answers": [
        { "question": "Did the rep ask about timeline?", "answer": "Yes" }
      ],
      "summary": "Rep covered timeline and budget, but didn't book a follow-up."
    },
    "processFollowed": 3,
    "processMissed": 1,
    "processTotal": 4,
    "integration": {
      "type": "custom",
      "entity": {
        "eventId": "APPT-9871",
        "entityId": "CUST-123",
        "entityName": "John Doe",
        "client": {
          "id": "CUST-123",
          "name": "John Doe",
          "email": "john@example.com",
          "location": "123 Main St, Austin, TX 78701"
        },
        "appointmentDate": "2026-03-01T14:00:00.000Z",
        "title": "Design consultation",
        "userEmail": "rep@yourcompany.com"
      }
    },
    "buyerProfile": {
      "budget": { "value": "$450,000 to $500,000", "reason": "Buyer said they're pre-approved up to $500k." },
      "reasonToBuy": { "value": "Relocating for a new job", "reason": "Starts work in Austin in July." }
    }
  }
}
```

<ResponseField name="data.integration.entity.eventId" type="string">
  The `event_id` you sent. Use it to find the appointment in your CRM. If `data.integration` is `null`, the recording wasn't linked to an appointment.
</ResponseField>

<ResponseField name="data.integration.entity.client" type="object">
  The customer you sent with the appointment. For `client.created`, `entity` holds what the rep entered instead, such as name, email, design, and community.
</ResponseField>

<ResponseField name="data.notes, data.actionItems" type="string">
  AI notes and action items, as plain text.
</ResponseField>

<ResponseField name="data.process" type="object">
  Your sales process questions, the rep's answers, and a summary. `processFollowed`, `processMissed`, and `processTotal` count them. `critical_step` holds the answer to your critical step question, if you have one.
</ResponseField>

<ResponseField name="Custom payload groups" type="object">
  Each group you set up under **Custom Payloads** has its own key, such as `buyerProfile` above.
</ResponseField>

<ResponseField name="data.meetingUrl" type="string">
  Link to the recording in Sales Ask. For the transcript and audio, call [`GET /v1/recordings/{id}`](/api-reference/recordings/get-a-recording) and `GET /v1/recordings/{id}/transcript`.
</ResponseField>

### Handling deliveries

* **Verify the sender:** every webhook carries your organization API key in the `x-api-key` header. Reject requests where it doesn't match.
* **Reply within 10 seconds** with any `2xx` status, and do slow work afterwards. Don't redirect.
* **No retries:** a missed delivery isn't sent again. Fetch the recording with `GET /v1/recordings/{id}` to catch up.
* **Automatic shut-off:** a `410` response turns the webhook off at once, and repeated `404`s over three days do too. Turn it back on in **Settings → Webhooks**.

## Go-live checklist

<Steps>
  <Step title="Send a test appointment">
    Create an appointment for a rep about 20 minutes from now. Check that it shows in their app and the reminder arrives.
  </Step>

  <Step title="Record against it">
    Have the rep record a short conversation at the appointment time.
  </Step>

  <Step title="Check the webhook">
    When processing finishes, confirm your endpoint got `recording.processed` with your `event_id` in `data.integration.entity.eventId`.
  </Step>

  <Step title="Clean up">
    Delete the test appointment with `DELETE /v1/scheduled-tasks/{event_id}`.
  </Step>
</Steps>

<Tip>
  To test your endpoint without recording, click the test button on the webhook in **Settings → Webhooks**. You can also call `POST /v1/webhooks/test` with `url` and a processed `recording` object from `GET /v1/recordings/{id}`. `GET /v1/webhooks` lists your organization-wide webhooks, but not team webhooks or their events.
</Tip>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Recordings aren't linking to appointments">
    Check that the rep's Sales Ask email matches the `user_email` you sent, that the recording started within an hour of the appointment, and that you send `customer.id`. If you match by location, check `customer.location` is a full street address.
  </Accordion>

  <Accordion title="The webhook never arrives">
    Check the webhook is **Active** and subscribed to the event, and that **Trigger** is **All Sources**. If the rep created the client in Sales Ask, you get `client.created`, not `recording.processed`.
  </Accordion>

  <Accordion title="Reps don't get reminders">
    The rep must be signed in to the mobile app with notifications on, and the appointment must start in the future when you send it.
  </Accordion>

  <Accordion title="Still stuck?">
    Email [support@salesask.com](mailto:support@salesask.com) with your organization name, the request you sent (without your API key), the response, and the `event_id` or recording `id`.
  </Accordion>
</AccordionGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.