# Mav API documentation

Base URL: `https://smartrate.link/api/v3`

## Overview

Mav works your leads. Your own systems know which of them go on to quote and buy. The Mav API closes that loop, so Mav’s records match yours and it stops following up with people who have already converted.

It’s REST over HTTPS with JSON in and out. Every endpoint lives under `https://smartrate.link/api/v3`.

1. **List conversions** once, at setup, and keep the `id` of each one you’ll record.
2. **Find the contact** by phone number when someone quotes or buys on your side.
3. **Record the conversion** against that contact.

## Authentication

Send your account’s API key as a bearer token on every request. Your Mav team can give you your key. Keep it on your server, never in a browser or app: it can read your contacts.

The examples read it from a `MAV_API_KEY` environment variable.

```http
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
```

A missing or wrong key gets **403 Forbidden**.

## Contacts

The people Mav works for you. Find one by phone number to get the `id` the other calls need.

### Find a contact

`GET https://smartrate.link/api/v3/contacts/find`

Looks up a contact by phone number. Send it in any common US format; Mav normalizes it to E.164 (`+13125550142`) before matching. A phone number matches at most one contact in your account.

#### Parameters

- `phone` (string, query, required): The contact’s phone number, like `+13125550142` or `(312) 555-0142`. URL-encode it: a bare `+` in a query string reads as a space.

#### Request

```sh
curl -G https://smartrate.link/api/v3/contacts/find \
  -H "Authorization: Bearer $MAV_API_KEY" \
  --data-urlencode "phone=+13125550142"
```

#### Response: 200 OK

```json
{
  "contact": {
    "id": "nct_7Hq2xKpR9vLm3N",
    "first_name": "Jordan",
    "last_name": "Rivera",
    "email": "jordan.rivera@example.com",
    "phone": "+13125550142",
    "opted_out": false,
    "created_at": "2026-10-06T16:42:18Z",
    "additional_info": {
      "current_carrier": "Example Mutual",
      "vehicles": "2"
    },
    "referral": {}
  }
}
```

#### Response: 404 Not Found

```json
{
  "errors": [
    "Contact not found."
  ]
}
```

### Get a contact

`GET https://smartrate.link/api/v3/contacts/{id}`

Returns one contact by its `id`, like the `contact.id` from Find a contact or a webhook.

#### Parameters

- `id` (string, path, required): The contact’s `id`, like `nct_7Hq2xKpR9vLm3N`.

#### Request

```sh
curl https://smartrate.link/api/v3/contacts/nct_7Hq2xKpR9vLm3N \
  -H "Authorization: Bearer $MAV_API_KEY"
```

#### Response: 200 OK

```json
{
  "contact": {
    "id": "nct_7Hq2xKpR9vLm3N",
    "first_name": "Jordan",
    "last_name": "Rivera",
    "email": "jordan.rivera@example.com",
    "phone": "+13125550142",
    "opted_out": false,
    "created_at": "2026-10-06T16:42:18Z",
    "additional_info": {
      "current_carrier": "Example Mutual",
      "vehicles": "2"
    },
    "referral": {}
  }
}
```

## Agents

Coming soon.

## Conversions

The outcomes you count: Quoted, Sold and any custom conversions you add. Record one when a contact reaches it.

### List conversions

`GET https://smartrate.link/api/v3/conversions`

Returns every conversion your account tracks. Each account starts with Quoted and Sold, and any custom conversions you’ve added are listed too. Conversion IDs never change, so look them up once and keep them in your integration’s settings.

#### Request

```sh
curl https://smartrate.link/api/v3/conversions \
  -H "Authorization: Bearer $MAV_API_KEY"
```

#### Response: 200 OK

```json
{
  "conversions": [
    {
      "id": "cnv_4TzW8bQe2YcJ6s",
      "title": "Quoted",
      "value": 0,
      "created_at": "2026-06-02T14:11:09Z"
    },
    {
      "id": "cnv_9pLx3VnR7aKd2M",
      "title": "Sold",
      "value": 0,
      "created_at": "2026-06-02T14:11:09Z"
    }
  ]
}
```

### Record a conversion

`POST https://smartrate.link/api/v3/conversions/{conversion_id}/events`

Records that the contact reached this conversion. Mav adds it to the contact’s timeline and marks them in the app. By default it also ends Mav’s open conversations with the contact, so Mav stops following up with someone who has already converted.

#### Parameters

- `conversion_id` (string, path, required): The conversion’s `id`, from List conversions.
- `conversion_contact.contact_id` (string, required): The contact’s `id`, from Find a contact.
- `conversion_contact.converted_at` (ISO 8601 timestamp): When the conversion happened. Defaults to when Mav receives the request.
- `conversion_contact.custom_value` (integer, cents): What it was worth, in cents: 184000 is $1,840.00. Zero or more. Defaults to the conversion’s own value.

#### Request

```sh
curl -X POST https://smartrate.link/api/v3/conversions/cnv_9pLx3VnR7aKd2M/events \
  -H "Authorization: Bearer $MAV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "conversion_contact": {
      "contact_id": "nct_7Hq2xKpR9vLm3N",
      "converted_at": "2026-10-08T17:30:00Z",
      "custom_value": 184000
    }
  }'
```

#### Response: 201 Created

```json
{
  "conversion_contact": {
    "id": "6705b2c1e4f0a2c3d4e5f601",
    "contact_id": "nct_7Hq2xKpR9vLm3N",
    "converted_at": "2026-10-08T17:30:00Z",
    "value": 184000,
    "conversion_id": "cnv_9pLx3VnR7aKd2M"
  }
}
```

> Safe to retry. Recording the same conversion for the same contact again returns the original record with 200 OK, unchanged, so nothing is ever counted twice.

## Webhooks

Mav can tell your systems when something happens to a contact, like a reply, a qualified lead, a call or an opt-out. Add a webhook to an agent, and Mav posts each event you pick to your URL as it happens.

In the Mav app, open the agent and click **Add lead box** under Outbound Notifications. Pick **Webhook**, enter your URL and choose the events. Leave every event unchecked to get them all.

### Payload

Each event is one `POST` with a JSON body. `activity` says what happened, and `contact` says who it happened to.

```json
{
  "activity": {
    "type": "outcome",
    "label": "qualified",
    "play_id": null,
    "next_engagement_id": "neg_3Fv8kQz1WpLc6R",
    "note": "Jordan Rivera qualified to talk to a rep.",
    "properties": {},
    "created_at": "2026-10-08T16:55:02Z",
    "updated_at": "2026-10-08T16:55:02Z"
  },
  "contact": {
    "id": "nct_7Hq2xKpR9vLm3N",
    "first_name": "Jordan",
    "last_name": "Rivera",
    "email": "jordan.rivera@example.com",
    "phone": "+13125550142",
    "opted_out": false,
    "created_at": "2026-10-06T16:42:18Z",
    "additional_info": {
      "current_carrier": "Example Mutual",
      "vehicles": "2"
    },
    "referral": {}
  }
}
```

### Fields

- `activity.type` (string, required): `event`, `outcome` or `conversion`.
- `activity.label` (string, required): Which event fired, like `qualified` or `opted_out`. See Events below.
- `activity.next_engagement_id` (string, required): The conversation the event belongs to. Every event in one conversation shares it.
- `activity.play_id` (null): Always `null` on the current Mav app.
- `activity.note` (string): A short summary of what happened, written for people.
- `activity.properties` (object): Extra details some events carry, like the conversion’s title on `custom_conversion`. Often empty.
- `activity.created_at` (ISO 8601 timestamp, required): When it happened.
- `activity.updated_at` (ISO 8601 timestamp, required): When Mav last changed it.
- `contact` (object, required): The contact, with the same fields Find a contact returns. Fields you sent that Mav doesn’t use land in `contact.additional_info` under their own names.

### Authentication

When you add the webhook, pick **Bearer** and enter a token. Mav sends it as a bearer token on every delivery, so your endpoint can check the request came from Mav.

```http
POST /your/webhook/path
Authorization: Bearer YOUR_WEBHOOK_TOKEN
Content-Type: application/json
```

### Testing

Once the webhook is saved, click **Send test** on it under Outbound Notifications. Mav posts one sample event to your URL right away, with your bearer token, and shows whether it went through or why it failed. Managers and admins see the button.

The sample uses a made-up contact named Test Contact. `activity.label` is the first event you picked, or `started` if you get them all, and `activity.properties.test` is `true` so your code can skip it. Nothing is saved to your account. That means `activity.next_engagement_id`, `activity.created_at`, `activity.updated_at` and `contact.created_at` are `null` in the sample. A failed test isn’t retried.

### Delivery

- **Reply fast.** Mav waits 3 seconds to connect and 3 more for your answer. Reply `200` first, then do the work.
- **Any 2xx counts.** Every other status is a failure. Mav doesn’t follow redirects, so a `3xx` fails too.
- **Failures retry.** Mav retries a failed delivery up to 18 times over about four days. If the last try fails, Mav turns the webhook off and emails your account’s admins and managers.
- **Expect repeats.** A retry can deliver an event twice, and payloads carry no event ID. Dedupe on `contact.id`, `activity.label` and `activity.created_at`.
- **Use HTTPS.** Payloads carry your contacts’ personal details.

### Events

Every event you can pick, grouped by `activity.type`.

- `event`: `started`, `interested`, `phone_call_scheduled`, `phone_call_started`, `phone_call_connected`, `phone_call_rep_attempted`, `phone_call_transferred`, `phone_call_transfer_failed`, `phone_call_completed`, `phone_call_missed`, `phone_call_received`
- `outcome`: `dormant`, `opted_out`, `filtered`, `undeliverable`, `qualified`, `not_interested`, `unqualified`
- `conversion`: `quoted`, `sold`, `link_clicked`, `custom_conversion`

## Full example

The whole flow in Node.js 18 or later: look up Sold once, then record a sale whenever a policy binds on your side.

```js
const MAV = "https://smartrate.link/api/v3";
const headers = {
  Authorization: `Bearer ${process.env.MAV_API_KEY}`,
  "Content-Type": "application/json",
};

// Once, at setup: find the conversion you want to record.
const { conversions } = await fetch(`${MAV}/conversions`, { headers }).then((r) => r.json());
const sold = conversions.find((c) => c.title === "Sold");

// Each time a policy binds in your system:
async function recordSale(phone, premiumInCents) {
  const found = await fetch(`${MAV}/contacts/find?phone=${encodeURIComponent(phone)}`, { headers });
  if (found.status === 404) return; // Not a contact Mav has worked.

  const { contact } = await found.json();

  await fetch(`${MAV}/conversions/${sold.id}/events`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      conversion_contact: {
        contact_id: contact.id,
        converted_at: new Date().toISOString(),
        custom_value: premiumInCents,
      },
    }),
  });
}
```

## Errors

Mav answers with a standard HTTP status and, in most cases, an `errors` list saying what went wrong.

- **400 Bad Request**: A required parameter is missing, or the body isn’t valid JSON. Bad JSON answers with an `error` string instead of an `errors` list. Example: `{"errors":["param is missing or the value is empty or invalid: phone"]}`
- **403 Forbidden**: The API key is missing or wrong, or your account isn’t on the current Mav app yet (`Account is not enabled for Next.`). Example: `{"errors":["API key is not authorized."]}`
- **404 Not Found**: The contact or conversion isn’t in your account. Lookups by ID answer with a `message` instead of an `errors` list. Example: `{"errors":["Contact not found."]}`
- **422 Unprocessable Content**: The conversion couldn’t be saved, like a negative `custom_value`. Example: `{"errors":["Custom value must be greater than or equal to 0"]}`

## Good to know

- **Money is in cents.** `value` and `custom_value` are whole numbers of cents: 184000 is $1,840.00.
- **Timestamps are ISO 8601,** like `2026-10-08T17:30:00Z`, both ways.
- **IDs are strings.** Contact IDs start with `nct_` and conversion IDs with `cnv_`. Store them as given.
- **A key sees one account.** Contacts and conversions from any other account answer 404.
- **Retries are safe.** Recording a conversion twice returns the first record instead of making a second.
