Demo

Developers

The Kept in Touch API

The Kept in Touch API lets your software work with a brokerage’s CRM: read and add contacts, log notes, calls, texts and emails, send texts and emails from the brokerage’s own number and address, hand over leads, book appointments, keep deals in step, look up homes from the brokerage’s MLS feed, and hear about changes as they happen. Questions? Email support@keptintouch.com.

Getting started

  1. The brokerage’s owner signs in, opens Settings, finds Connected apps and makes a key for your app. The key starts with kit_ and is shown once, so store it somewhere safe.
  2. Connected apps also shows the brokerage’s API address. It is the brokerage’s own site followed by /api/v1, for example https://homes.example.com/api/v1.
  3. Check the key works:
curl "https://homes.example.com/api/v1/account" \
  -H "Authorization: Bearer $KIT_API_KEY"

Authentication

Send the key on every request in the Authorization header: Authorization: Bearer kit_…. If your tool only does basic authentication, use the key as the username with an empty password.

A key belongs to one brokerage and sees only that brokerage’s records. It acts as the brokerage’s owner, so treat it like a password: keep it on your server, never in a web page or an app people download. The owner can revoke a key in Connected apps at any time, and it stops working at once. A key also stops working while the brokerage is paused or has the CRM turned off.

Requests and responses

  • Send and receive JSON. Send Content-Type: application/json with a body.
  • Field names are lowercase with underscores, such as contact_id and created_at.
  • Ids are numbers. Times are ISO 8601 in UTC, such as 2026-10-20T15:01:02.000Z; dates on their own look like 2026-11-16.
  • A field with no value is null, never left out.
  • Creating something answers 201 with the new record. Deleting answers 204 with no body.
  • To change a record, send PATCH with only the fields that change.

Pagination

Every list answers the same way:

{
  "data": [
    "…"
  ],
  "total": 132,
  "limit": 25,
  "offset": 50
}

data is this page, total is how many there are in all, and limit and offset are the page you asked for. Ask with ?limit= (25 by default, up to 100) and ?offset= (0 by default). There are more pages while offset + limit is less than total.

Errors

Anything other than a 2xx answer has the same body:

{
  "error": {
    "code": "invalid_request",
    "message": "stage must be one of: new, nurturing, showing, active, closed, lost",
    "field": "stage"
  }
}

code is for your code to check, message is for a person to read, and field names the field that was wrong when there is one.

  • 400 invalid_request: a field is missing or wrong. invalid_json: the body isn’t JSON.
  • 401 unauthorized: no key, or a key that doesn’t exist or was revoked.
  • 403 forbidden: the brokerage is paused or doesn’t have the CRM turned on.
  • 404 not_found: there is no such record in this brokerage, or no such endpoint.
  • 409 conflict: for example, another contact already has that email.
  • 422: a text or email couldn’t be sent; the code says why (see Texts and Emails).
  • 429 rate_limited: slow down (see Rate limits).
  • 500 server_error and 502 send_failed: something went wrong on our side or the carrier’s. Try again in a moment.

Rate limits

Each key can make 600 requests a minute. Every answer carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers; past the limit the answer is 429 until the minute is up.

Account

Who the key belongs to. A quick way to check a key works.

GET /api/v1/account

The brokerage the key belongs to, the key’s name, and the owner it acts as.

Request

curl "https://homes.example.com/api/v1/account" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "brokerage": {
    "id": 7,
    "name": "Peak Realty",
    "slug": "peak",
    "domain": "homes.peakrealty.com",
    "site_url": "https://homes.peakrealty.com"
  },
  "key": {
    "id": 3,
    "name": "Zapier",
    "prefix": "kit_9fQ2xA",
    "created_at": "2026-10-18T10:00:00.000Z"
  },
  "owner": {
    "id": 1,
    "name": "Pat Peak",
    "email": "pat@peakrealty.com"
  }
}

Agents

The people on the brokerage’s team: the owner and the agents. Use an agent’s id wherever a request takes agent_id.

GET /api/v1/agents

The active team, by name.

  • status active (the default), inactive for teammates who were turned off, or all.
  • include_inactive 1 is the same as status=all.
  • limit, offset See Pagination.

Request

curl "https://homes.example.com/api/v1/agents?limit=25&offset=0" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "data": [
    {
      "id": 12,
      "name": "Andy Agent",
      "first_name": "Andy",
      "last_name": "Agent",
      "email": "andy@peakrealty.com",
      "phone": "+19705550101",
      "role": "agent",
      "active": true,
      "created_at": "2026-03-02T16:20:00.000Z"
    }
  ],
  "total": 1,
  "limit": 25,
  "offset": 0
}

GET /api/v1/agents/12

One teammate.

Request

curl "https://homes.example.com/api/v1/agents/12" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "id": 12,
  "name": "Andy Agent",
  "first_name": "Andy",
  "last_name": "Agent",
  "email": "andy@peakrealty.com",
  "phone": "+19705550101",
  "role": "agent",
  "active": true,
  "created_at": "2026-03-02T16:20:00.000Z"
}

Stages

Where a contact is with the brokerage. A contact’s stage is one of these keys.

GET /api/v1/stages

All six stages, in order.

Request

curl "https://homes.example.com/api/v1/stages" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "data": [
    {
      "key": "new",
      "name": "New",
      "position": 1
    },
    {
      "key": "nurturing",
      "name": "Nurturing",
      "position": 2
    },
    {
      "key": "showing",
      "name": "Showing",
      "position": 3
    },
    {
      "key": "active",
      "name": "Active",
      "position": 4
    },
    {
      "key": "closed",
      "name": "Closed",
      "position": 5
    },
    {
      "key": "lost",
      "name": "Lost",
      "position": 6
    }
  ],
  "total": 6,
  "limit": 6,
  "offset": 0
}

Contacts

The brokerage’s leads and clients. A new contact goes through the same lead intake as a lead from the website: they are assigned to an agent by the brokerage’s rules, land in the team inbox and start any action plans that apply. If the email (or, without one, the phone number) already belongs to a contact, that contact is updated instead of a second one being made.

GET /api/v1/contacts

Contacts, newest first.

  • q Text to look for in the name, email or phone.
  • email An exact email address.
  • phone A phone number in any format.
  • stage A stage key.
  • source Where they came from, such as zillow.
  • tags One or more tags, separated by commas (any of them).
  • agent_id An agent’s id, or none for contacts with no agent.
  • ids Contact ids, separated by commas.
  • updated_since, created_since, active_since A date or date-time.
  • sort created (the default), updated, last_active, name, id or stage.
  • order desc (the default) or asc.
  • limit, offset See Pagination.

Request

curl "https://homes.example.com/api/v1/contacts?stage=showing&agent_id=12&limit=25" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "data": [
    {
      "id": 4821,
      "name": "Ava Buyer",
      "first_name": "Ava",
      "last_name": "Buyer",
      "email": "ava@example.com",
      "phone": "+19705550100",
      "stage": "showing",
      "source": "zillow",
      "tags": [
        "Buyer",
        "Pre-approved"
      ],
      "agent_id": 12,
      "agent": {
        "id": 12,
        "name": "Andy Agent",
        "email": "andy@peakrealty.com"
      },
      "lead_score": 72,
      "intent": "buy",
      "subscribed": true,
      "addresses": [
        {
          "kind": "home",
          "street": "2845 W 13th St",
          "city": "Greeley",
          "state": "CO",
          "postal_code": "80634",
          "beds": 4,
          "baths": 2,
          "living_area": 2392,
          "year_built": 1971,
          "estimate": 445000,
          "list_price": null
        }
      ],
      "custom_fields": {
        "lender": "First Western",
        "move_by": "2027-03-01",
        "must_haves": [
          "shop",
          "big yard"
        ]
      },
      "first_contacted_at": "2026-10-14T15:03:11.000Z",
      "last_touched_at": "2026-10-19T21:40:00.000Z",
      "last_active_at": "2026-10-20T13:12:45.000Z",
      "next_touch_at": "2026-10-22T15:00:00.000Z",
      "created_at": "2026-10-14T15:01:02.000Z",
      "updated_at": "2026-10-20T13:12:45.000Z"
    }
  ],
  "total": 1,
  "limit": 25,
  "offset": 0
}

POST /api/v1/contacts

Add a contact, or update the one with the same email or phone. The answer is 201 for a new contact and 200 for one that was already there.

  • name Their full name (or first_name and last_name).
  • email An email address. A contact needs an email or a phone number.
  • phone A phone number in any US format.
  • stage A stage key. New by default.
  • source Where they came from.
  • tags A list of tags.
  • agent_id or agent_email Who they belong to. Without one, the brokerage’s assignment rules decide.
  • custom_fields Your own fields, as an object (see Custom fields). Merged with what the contact already has.

Request

curl -X POST "https://homes.example.com/api/v1/contacts" \
  -H "Authorization: Bearer $KIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"first_name":"Ava","last_name":"Buyer","email":"ava@example.com","phone":"(970) 555-0100","source":"Zillow","tags":["Buyer"],"stage":"showing"}'

Response: 201

{
  "id": 4821,
  "name": "Ava Buyer",
  "first_name": "Ava",
  "last_name": "Buyer",
  "email": "ava@example.com",
  "phone": "+19705550100",
  "stage": "showing",
  "source": "zillow",
  "tags": [
    "Buyer",
    "Pre-approved"
  ],
  "agent_id": 12,
  "agent": {
    "id": 12,
    "name": "Andy Agent",
    "email": "andy@peakrealty.com"
  },
  "lead_score": 72,
  "intent": "buy",
  "subscribed": true,
  "addresses": [
    {
      "kind": "home",
      "street": "2845 W 13th St",
      "city": "Greeley",
      "state": "CO",
      "postal_code": "80634",
      "beds": 4,
      "baths": 2,
      "living_area": 2392,
      "year_built": 1971,
      "estimate": 445000,
      "list_price": null
    }
  ],
  "custom_fields": {
    "lender": "First Western",
    "move_by": "2027-03-01",
    "must_haves": [
      "shop",
      "big yard"
    ]
  },
  "first_contacted_at": "2026-10-14T15:03:11.000Z",
  "last_touched_at": "2026-10-19T21:40:00.000Z",
  "last_active_at": "2026-10-20T13:12:45.000Z",
  "next_touch_at": "2026-10-22T15:00:00.000Z",
  "created_at": "2026-10-14T15:01:02.000Z",
  "updated_at": "2026-10-20T13:12:45.000Z"
}

GET /api/v1/contacts/4821

One contact.

Request

curl "https://homes.example.com/api/v1/contacts/4821" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "id": 4821,
  "name": "Ava Buyer",
  "first_name": "Ava",
  "last_name": "Buyer",
  "email": "ava@example.com",
  "phone": "+19705550100",
  "stage": "showing",
  "source": "zillow",
  "tags": [
    "Buyer",
    "Pre-approved"
  ],
  "agent_id": 12,
  "agent": {
    "id": 12,
    "name": "Andy Agent",
    "email": "andy@peakrealty.com"
  },
  "lead_score": 72,
  "intent": "buy",
  "subscribed": true,
  "addresses": [
    {
      "kind": "home",
      "street": "2845 W 13th St",
      "city": "Greeley",
      "state": "CO",
      "postal_code": "80634",
      "beds": 4,
      "baths": 2,
      "living_area": 2392,
      "year_built": 1971,
      "estimate": 445000,
      "list_price": null
    }
  ],
  "custom_fields": {
    "lender": "First Western",
    "move_by": "2027-03-01",
    "must_haves": [
      "shop",
      "big yard"
    ]
  },
  "first_contacted_at": "2026-10-14T15:03:11.000Z",
  "last_touched_at": "2026-10-19T21:40:00.000Z",
  "last_active_at": "2026-10-20T13:12:45.000Z",
  "next_touch_at": "2026-10-22T15:00:00.000Z",
  "created_at": "2026-10-14T15:01:02.000Z",
  "updated_at": "2026-10-20T13:12:45.000Z"
}

PATCH /api/v1/contacts/4821

Change a contact. Send only what changes. tags replaces the whole list. agent_id: null leaves them with no agent. custom_fields is merged with the contact’s: the keys you send are set, a key set to null is removed, and the rest stay.

Request

curl -X PATCH "https://homes.example.com/api/v1/contacts/4821" \
  -H "Authorization: Bearer $KIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"stage":"active","tags":["Buyer","Pre-approved"],"custom_fields":{"move_by":null,"timeline":"3-6 months"}}'

Response: 200

{
  "id": 4821,
  "name": "Ava Buyer",
  "first_name": "Ava",
  "last_name": "Buyer",
  "email": "ava@example.com",
  "phone": "+19705550100",
  "stage": "active",
  "source": "zillow",
  "tags": [
    "Buyer",
    "Pre-approved"
  ],
  "agent_id": 12,
  "agent": {
    "id": 12,
    "name": "Andy Agent",
    "email": "andy@peakrealty.com"
  },
  "lead_score": 72,
  "intent": "buy",
  "subscribed": true,
  "addresses": [
    {
      "kind": "home",
      "street": "2845 W 13th St",
      "city": "Greeley",
      "state": "CO",
      "postal_code": "80634",
      "beds": 4,
      "baths": 2,
      "living_area": 2392,
      "year_built": 1971,
      "estimate": 445000,
      "list_price": null
    }
  ],
  "custom_fields": {
    "lender": "First Western",
    "must_haves": [
      "shop",
      "big yard"
    ],
    "timeline": "3-6 months"
  },
  "first_contacted_at": "2026-10-14T15:03:11.000Z",
  "last_touched_at": "2026-10-19T21:40:00.000Z",
  "last_active_at": "2026-10-20T13:12:45.000Z",
  "next_touch_at": "2026-10-22T15:00:00.000Z",
  "created_at": "2026-10-14T15:01:02.000Z",
  "updated_at": "2026-10-20T13:12:45.000Z"
}

DELETE /api/v1/contacts/4821

Remove a contact. Webhooks for contact.deleted are sent first.

Request

curl -X DELETE "https://homes.example.com/api/v1/contacts/4821" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 204 (no body)

Custom fields

Anything about a contact the other fields don’t hold, in custom_fields on the contact: an object of your own keys. A value is text (up to 1,000 characters), a number, true or false, or a list of those. A contact can have up to 50 keys, each name up to 64 characters, and 16 KB in all; past that the answer is 400 with field custom_fields. There is nothing to set up first: send a key and it exists.

GET /api/v1/custom-fields

The keys the brokerage’s contacts use, most used first: each name, the kind of value it usually holds (string, number, boolean or list) and how many contacts have it.

Request

curl "https://homes.example.com/api/v1/custom-fields?limit=25" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "data": [
    {
      "name": "lender",
      "type": "string",
      "count": 212
    },
    {
      "name": "must_haves",
      "type": "list",
      "count": 87
    },
    {
      "name": "move_by",
      "type": "string",
      "count": 40
    }
  ],
  "total": 3,
  "limit": 25,
  "offset": 0
}

Activity

A contact’s timeline: texts, emails, calls, notes, tasks, appointments, tours, site visits and more, newest first.

GET /api/v1/contacts/4821/activity

The 300 most recent things on the contact’s timeline, a page at a time.

Request

curl "https://homes.example.com/api/v1/contacts/4821/activity?limit=25" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "data": [
    {
      "id": "msg-77120",
      "kind": "text",
      "title": "Text sent",
      "detail": "Hi Ava, it’s Andy with Peak Realty. Does Saturday at 10 work for showings?",
      "link": null,
      "occurred_at": "2026-10-20T14:10:00.000Z"
    },
    {
      "id": "note-9311",
      "kind": "note",
      "title": "Note by Nadia",
      "detail": "Wants a big yard and a shop. Pre-approved to $550k.",
      "link": null,
      "occurred_at": "2026-10-20T14:02:00.000Z"
    }
  ],
  "total": 2,
  "limit": 25,
  "offset": 0
}

POST /api/v1/contacts/4821/activity

Put something the contact did in your app on their timeline (it does not create a lead).

  • title What happened, such as Viewed the pricing report. Required.
  • detail More about it.
  • source Your app’s name. The key’s name by default.
  • url A link to it.

Request

curl -X POST "https://homes.example.com/api/v1/contacts/4821/activity" \
  -H "Authorization: Bearer $KIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Viewed the pricing report","url":"https://app.example.com/reports/88"}'

Response: 201

{
  "id": "ev-55102",
  "kind": "activity",
  "title": "Viewed the pricing report",
  "detail": null,
  "link": "https://app.example.com/reports/88",
  "occurred_at": "2026-10-20T16:00:00.000Z"
}

Leads

For lead sources: send each inquiry here. It goes through lead intake exactly like a lead from the brokerage’s own site, with the message and the home it was about on the contact’s timeline.

POST /api/v1/leads

A lead from a source. 201 when it made a new contact, 200 when the contact was already there.

  • source Where the lead came from, such as Open house sign-in.
  • message What they said.
  • contact The person: the same fields as POST /contacts. Required.
  • property The home they asked about: address (or street, city, state, postal_code), price and url.

Request

curl -X POST "https://homes.example.com/api/v1/leads" \
  -H "Authorization: Bearer $KIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"source":"Open house sign-in","message":"Loved the kitchen. Is the seller flexible?","contact":{"name":"Cara Visitor","email":"cara@example.com","phone":"970-555-0122"},"property":{"address":"12 Elm St, Greeley, CO 80634","price":425000}}'

Response: 201

{
  "created": true,
  "contact": {
    "id": 4822,
    "name": "Cara Visitor",
    "first_name": "Cara",
    "last_name": "Visitor",
    "email": "cara@example.com",
    "phone": "+19705550122",
    "stage": "new",
    "source": "open house sign-in",
    "tags": [],
    "agent_id": 12,
    "agent": {
      "id": 12,
      "name": "Andy Agent",
      "email": "andy@peakrealty.com"
    },
    "lead_score": 72,
    "intent": "buy",
    "subscribed": true,
    "addresses": [],
    "custom_fields": {},
    "first_contacted_at": "2026-10-14T15:03:11.000Z",
    "last_touched_at": "2026-10-19T21:40:00.000Z",
    "last_active_at": "2026-10-20T13:12:45.000Z",
    "next_touch_at": "2026-10-22T15:00:00.000Z",
    "created_at": "2026-10-14T15:01:02.000Z",
    "updated_at": "2026-10-20T13:12:45.000Z"
  }
}

Notes

Notes on a contact, shown on their timeline. Notes made through the API carry your key’s name as the author. private is kept and shown back to you, so your app can mark its own notes; it does not yet hide a note from anyone in the CRM.

GET /api/v1/notes

Notes, newest first.

  • contact_id Only this contact’s notes.

Request

curl "https://homes.example.com/api/v1/notes?contact_id=4821" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "data": [
    {
      "id": 9311,
      "contact_id": 4821,
      "body": "Wants a big yard and a shop. Pre-approved to $550k.",
      "pinned": true,
      "private": false,
      "author": "Nadia",
      "created_at": "2026-10-20T14:02:00.000Z",
      "updated_at": "2026-10-20T14:02:00.000Z"
    }
  ],
  "total": 1,
  "limit": 25,
  "offset": 0
}

POST /api/v1/notes

Add a note.

  • contact_id Required.
  • body The note. Required.
  • pinned true to keep it at the top.
  • private true or false (false by default).

Request

curl -X POST "https://homes.example.com/api/v1/notes" \
  -H "Authorization: Bearer $KIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contact_id":4821,"body":"Wants a big yard and a shop. Pre-approved to $550k.","pinned":true}'

Response: 201

{
  "id": 9311,
  "contact_id": 4821,
  "body": "Wants a big yard and a shop. Pre-approved to $550k.",
  "pinned": true,
  "private": false,
  "author": "Nadia",
  "created_at": "2026-10-20T14:02:00.000Z",
  "updated_at": "2026-10-20T14:02:00.000Z"
}

GET /api/v1/notes/9311

One note.

Request

curl "https://homes.example.com/api/v1/notes/9311" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "id": 9311,
  "contact_id": 4821,
  "body": "Wants a big yard and a shop. Pre-approved to $550k.",
  "pinned": true,
  "private": false,
  "author": "Nadia",
  "created_at": "2026-10-20T14:02:00.000Z",
  "updated_at": "2026-10-20T14:02:00.000Z"
}

PATCH /api/v1/notes/9311

Change a note’s body, or whether it is pinned or private.

Request

curl -X PATCH "https://homes.example.com/api/v1/notes/9311" \
  -H "Authorization: Bearer $KIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"pinned":false}'

Response: 200

{
  "id": 9311,
  "contact_id": 4821,
  "body": "Wants a big yard and a shop. Pre-approved to $550k.",
  "pinned": false,
  "private": false,
  "author": "Nadia",
  "created_at": "2026-10-20T14:02:00.000Z",
  "updated_at": "2026-10-20T14:02:00.000Z"
}

DELETE /api/v1/notes/9311

Remove a note.

Request

curl -X DELETE "https://homes.example.com/api/v1/notes/9311" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 204 (no body)

Tasks

To-dos for the team about a contact. They show in the agent’s day and on the contact.

GET /api/v1/tasks

Tasks, newest first.

  • contact_id Only this contact’s tasks.
  • agent_id Only this agent’s tasks.
  • done true or false.

Request

curl "https://homes.example.com/api/v1/tasks?agent_id=12&done=false" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "data": [
    {
      "id": 2207,
      "contact_id": 4821,
      "title": "Send comps for 2845 W 13th St",
      "kind": "email",
      "due_at": "2026-10-22T17:00:00.000Z",
      "remind_at": null,
      "done": false,
      "done_at": null,
      "agent_id": 12,
      "agent": {
        "id": 12,
        "name": "Andy Agent",
        "email": "andy@peakrealty.com"
      },
      "created_at": "2026-10-20T14:05:00.000Z",
      "updated_at": "2026-10-20T14:05:00.000Z"
    }
  ],
  "total": 1,
  "limit": 25,
  "offset": 0
}

POST /api/v1/tasks

Add a task.

  • contact_id Required.
  • title What to do. Required.
  • kind call, text, email, follow_up (the default) or showing.
  • due_at A date-time, or a date (due at 5pm).
  • remind_at When to remind the agent.
  • agent_id or agent_email Who does it. The contact’s agent by default.
  • done true if it is already done.

Request

curl -X POST "https://homes.example.com/api/v1/tasks" \
  -H "Authorization: Bearer $KIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contact_id":4821,"title":"Send comps for 2845 W 13th St","kind":"email","due_at":"2026-10-22"}'

Response: 201

{
  "id": 2207,
  "contact_id": 4821,
  "title": "Send comps for 2845 W 13th St",
  "kind": "email",
  "due_at": "2026-10-22T17:00:00.000Z",
  "remind_at": null,
  "done": false,
  "done_at": null,
  "agent_id": 12,
  "agent": {
    "id": 12,
    "name": "Andy Agent",
    "email": "andy@peakrealty.com"
  },
  "created_at": "2026-10-20T14:05:00.000Z",
  "updated_at": "2026-10-20T14:05:00.000Z"
}

GET /api/v1/tasks/2207

One task.

Request

curl "https://homes.example.com/api/v1/tasks/2207" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "id": 2207,
  "contact_id": 4821,
  "title": "Send comps for 2845 W 13th St",
  "kind": "email",
  "due_at": "2026-10-22T17:00:00.000Z",
  "remind_at": null,
  "done": false,
  "done_at": null,
  "agent_id": 12,
  "agent": {
    "id": 12,
    "name": "Andy Agent",
    "email": "andy@peakrealty.com"
  },
  "created_at": "2026-10-20T14:05:00.000Z",
  "updated_at": "2026-10-20T14:05:00.000Z"
}

PATCH /api/v1/tasks/2207

Change a task, or mark it done with done: true.

Request

curl -X PATCH "https://homes.example.com/api/v1/tasks/2207" \
  -H "Authorization: Bearer $KIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"done":true}'

Response: 200

{
  "id": 2207,
  "contact_id": 4821,
  "title": "Send comps for 2845 W 13th St",
  "kind": "email",
  "due_at": "2026-10-22T17:00:00.000Z",
  "remind_at": null,
  "done": true,
  "done_at": "2026-10-21T18:00:00.000Z",
  "agent_id": 12,
  "agent": {
    "id": 12,
    "name": "Andy Agent",
    "email": "andy@peakrealty.com"
  },
  "created_at": "2026-10-20T14:05:00.000Z",
  "updated_at": "2026-10-20T14:05:00.000Z"
}

DELETE /api/v1/tasks/2207

Remove a task.

Request

curl -X DELETE "https://homes.example.com/api/v1/tasks/2207" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 204 (no body)

Texts

Text messages with a contact. agent_name is the name of the agent the text was from or for, when there is one. By default POST /texts records a text your app sent or received on its own number. With send: true, the text goes out from the brokerage’s own texting number instead, and the same rules apply as when an agent sends one: the contact must have agreed to texts and not replied STOP, texts only go out between 8am and 9pm, and the brokerage’s usage wallet pays for it.

GET /api/v1/texts

Texts, newest first.

  • contact_id Only this contact’s texts.

Request

curl "https://homes.example.com/api/v1/texts?contact_id=4821" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "data": [
    {
      "id": 77120,
      "contact_id": 4821,
      "agent_id": 12,
      "agent_name": "Andy Agent",
      "direction": "outbound",
      "body": "Hi Ava, it’s Andy with Peak Realty. Does Saturday at 10 work for showings?",
      "to": "+19705550100",
      "from": "+19705559999",
      "external_id": null,
      "status": "sent",
      "error": null,
      "created_at": "2026-10-20T14:10:00.000Z"
    }
  ],
  "total": 1,
  "limit": 25,
  "offset": 0
}

POST /api/v1/texts

Record a text, or send one through the brokerage. An inbound text also lands in the team inbox and pauses the contact’s action plans.

  • contact_id Required.
  • body The text. Required, up to 1,600 characters.
  • direction outbound (the default) or inbound.
  • to, from Phone numbers, when they differ from the contact’s and the brokerage’s.
  • agent_id The agent it was from or for. The contact’s agent by default.
  • external_id Your app’s own id for the message.
  • send true to send it from the brokerage’s number.

When a text can’t be sent the answer is 422 and error.code says why: not_set_up, no_number or off (texting isn’t ready at the brokerage), no_phone, no_consent, opted_out, quiet_hours or wallet_empty. A 502 with send_failed means the phone company refused it; try again later.

Request

curl -X POST "https://homes.example.com/api/v1/texts" \
  -H "Authorization: Bearer $KIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contact_id":4821,"body":"Hi Ava, it’s Andy with Peak Realty. Does Saturday at 10 work for showings?","send":true}'

Response: 201

{
  "id": 77120,
  "contact_id": 4821,
  "agent_id": 12,
  "agent_name": "Andy Agent",
  "direction": "outbound",
  "body": "Hi Ava, it’s Andy with Peak Realty. Does Saturday at 10 work for showings?",
  "to": "+19705550100",
  "from": "+19705559999",
  "external_id": null,
  "status": "sent",
  "error": null,
  "created_at": "2026-10-20T14:10:00.000Z"
}

GET /api/v1/texts/77120

One text.

Request

curl "https://homes.example.com/api/v1/texts/77120" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "id": 77120,
  "contact_id": 4821,
  "agent_id": 12,
  "agent_name": "Andy Agent",
  "direction": "outbound",
  "body": "Hi Ava, it’s Andy with Peak Realty. Does Saturday at 10 work for showings?",
  "to": "+19705550100",
  "from": "+19705559999",
  "external_id": null,
  "status": "sent",
  "error": null,
  "created_at": "2026-10-20T14:10:00.000Z"
}

Emails

Emails with a contact. They work like texts: recorded by default, sent from the brokerage’s own email with send: true (the contact must have an email address and not have turned emails off).

GET /api/v1/emails

Emails, newest first.

  • contact_id Only this contact’s emails.

Request

curl "https://homes.example.com/api/v1/emails?contact_id=4821" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "data": [
    {
      "id": 77131,
      "contact_id": 4821,
      "agent_id": 12,
      "agent_name": "Andy Agent",
      "direction": "outbound",
      "subject": "Three homes for you",
      "body": "Hi Ava, here are three homes that fit what you told me…",
      "to": "ava@example.com",
      "from": "andy@peakrealty.com",
      "external_id": null,
      "status": "sent",
      "error": null,
      "created_at": "2026-10-20T14:15:00.000Z"
    }
  ],
  "total": 1,
  "limit": 25,
  "offset": 0
}

POST /api/v1/emails

Record an email, or send one through the brokerage.

  • contact_id Required.
  • subject The subject line.
  • body The email. Required.
  • direction outbound (the default) or inbound.
  • to, from Addresses, when they differ from the contact’s and the brokerage’s.
  • agent_id The agent it was from or for.
  • external_id Your app’s own id for the message.
  • send true to send it from the brokerage.

When an email can’t be sent the answer is 422 with error.code no_email or email_off.

Request

curl -X POST "https://homes.example.com/api/v1/emails" \
  -H "Authorization: Bearer $KIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contact_id":4821,"subject":"Three homes for you","body":"Hi Ava, here are three homes that fit what you told me…","send":true}'

Response: 201

{
  "id": 77131,
  "contact_id": 4821,
  "agent_id": 12,
  "agent_name": "Andy Agent",
  "direction": "outbound",
  "subject": "Three homes for you",
  "body": "Hi Ava, here are three homes that fit what you told me…",
  "to": "ava@example.com",
  "from": "andy@peakrealty.com",
  "external_id": null,
  "status": "sent",
  "error": null,
  "created_at": "2026-10-20T14:15:00.000Z"
}

GET /api/v1/emails/77131

One email.

Request

curl "https://homes.example.com/api/v1/emails/77131" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "id": 77131,
  "contact_id": 4821,
  "agent_id": 12,
  "agent_name": "Andy Agent",
  "direction": "outbound",
  "subject": "Three homes for you",
  "body": "Hi Ava, here are three homes that fit what you told me…",
  "to": "ava@example.com",
  "from": "andy@peakrealty.com",
  "external_id": null,
  "status": "sent",
  "error": null,
  "created_at": "2026-10-20T14:15:00.000Z"
}

Calls

Calls your app made or took. A logged call counts as the brokerage reaching the contact.

GET /api/v1/calls

Logged calls, newest first.

  • contact_id Only this contact’s calls.

Request

curl "https://homes.example.com/api/v1/calls?contact_id=4821" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "data": [
    {
      "id": 9320,
      "contact_id": 4821,
      "direction": "outbound",
      "outcome": "Talked",
      "duration_seconds": 312,
      "note": "Set showings for Saturday.",
      "logged_by": "Nadia",
      "created_at": "2026-10-20T15:00:00.000Z"
    }
  ],
  "total": 1,
  "limit": 25,
  "offset": 0
}

POST /api/v1/calls

Log a call.

  • contact_id Required.
  • direction outbound (the default) or inbound.
  • outcome How it went, such as Talked or Left voicemail.
  • duration_seconds How long it was.
  • note What was said.

Request

curl -X POST "https://homes.example.com/api/v1/calls" \
  -H "Authorization: Bearer $KIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contact_id":4821,"outcome":"Talked","duration_seconds":312,"note":"Set showings for Saturday."}'

Response: 201

{
  "id": 9320,
  "contact_id": 4821,
  "direction": "outbound",
  "outcome": "Talked",
  "duration_seconds": 312,
  "note": "Set showings for Saturday.",
  "logged_by": "Nadia",
  "created_at": "2026-10-20T15:00:00.000Z"
}

GET /api/v1/calls/9320

One call.

Request

curl "https://homes.example.com/api/v1/calls/9320" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "id": 9320,
  "contact_id": 4821,
  "direction": "outbound",
  "outcome": "Talked",
  "duration_seconds": 312,
  "note": "Set showings for Saturday.",
  "logged_by": "Nadia",
  "created_at": "2026-10-20T15:00:00.000Z"
}

Pipelines and deals

A deal is a contact’s transaction, moving through the stages of a pipeline. Every brokerage starts with a Buyers and a Sellers pipeline.

GET /api/v1/pipelines

The brokerage’s pipelines and their stages.

Request

curl "https://homes.example.com/api/v1/pipelines" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "data": [
    {
      "id": 3,
      "name": "Buyers",
      "kind": "buyer",
      "position": 0,
      "built_in": true,
      "stages": [
        {
          "key": "consult",
          "name": "Consultation",
          "position": 1,
          "probability": 10
        },
        {
          "key": "showing",
          "name": "Showing homes",
          "position": 2,
          "probability": 25
        },
        {
          "key": "offer",
          "name": "Offer made",
          "position": 3,
          "probability": 50
        },
        {
          "key": "under_contract",
          "name": "Under contract",
          "position": 4,
          "probability": 80
        },
        {
          "key": "closing",
          "name": "Closing",
          "position": 5,
          "probability": 95
        }
      ]
    },
    {
      "id": 4,
      "name": "Sellers",
      "kind": "seller",
      "position": 1,
      "built_in": true,
      "stages": [
        {
          "key": "listing_appt",
          "name": "Listing appointment",
          "position": 1,
          "probability": 20
        },
        {
          "key": "listed",
          "name": "Listed",
          "position": 2,
          "probability": 40
        },
        {
          "key": "under_contract",
          "name": "Under contract",
          "position": 3,
          "probability": 80
        },
        {
          "key": "closing",
          "name": "Closing",
          "position": 4,
          "probability": 95
        }
      ]
    }
  ],
  "total": 2,
  "limit": 2,
  "offset": 0
}

GET /api/v1/deals

Deals, most recently changed first.

  • contact_id Only this contact’s deals.
  • pipeline_id Only this pipeline’s.
  • status open, won or lost.
  • external_id Your app’s id for the deal.
  • updated_since A date or date-time.

Request

curl "https://homes.example.com/api/v1/deals?status=open&pipeline_id=3" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "data": [
    {
      "id": 518,
      "name": "3317 Meadowlark Dr",
      "status": "open",
      "pipeline_id": 3,
      "pipeline": "Buyers",
      "stage": "offer",
      "stage_name": "Offer made",
      "contact_id": 4821,
      "contact": {
        "id": 4821,
        "name": "Ava Buyer",
        "email": "ava@example.com"
      },
      "agent_id": 12,
      "agent": {
        "id": 12,
        "name": "Andy Agent",
        "email": "andy@peakrealty.com"
      },
      "price": 541000,
      "commission_rate": 2.5,
      "commission_amount": null,
      "close_date": "2026-11-16",
      "property_address": "3317 Meadowlark Dr, Loveland, CO 80538",
      "listing_id": null,
      "external_id": "file-2291",
      "lost_reason": null,
      "details": {
        "inspection_deadline": "2026-10-30"
      },
      "won_at": null,
      "lost_at": null,
      "stage_changed_at": "2026-10-20T15:30:00.000Z",
      "created_at": "2026-10-20T15:30:00.000Z",
      "updated_at": "2026-10-20T15:30:00.000Z"
    }
  ],
  "total": 1,
  "limit": 25,
  "offset": 0
}

POST /api/v1/deals

Add a deal. If you send an external_id the brokerage already has, that deal is updated instead and the answer is 200, so your app can send the whole deal every time it changes.

  • name Usually the address. Required.
  • contact_id Required for a new deal.
  • pipeline_id or pipeline The pipeline’s id, name or kind (buyer, seller). Buyers by default.
  • stage A stage key, name or position. The first stage by default.
  • status open (the default), won or lost.
  • price, commission_rate, commission_amount Numbers.
  • close_date The expected closing date.
  • property_address, listing_id The home.
  • external_id Your app’s id for the deal.
  • lost_reason Why it was lost.
  • details Anything else, as an object. Merged with what is there.
  • agent_id or agent_email The agent. The contact’s agent by default.

Request

curl -X POST "https://homes.example.com/api/v1/deals" \
  -H "Authorization: Bearer $KIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"3317 Meadowlark Dr","contact_id":4821,"pipeline":"buyer","stage":"offer","price":541000,"close_date":"2026-11-16","property_address":"3317 Meadowlark Dr, Loveland, CO 80538","external_id":"file-2291","details":{"inspection_deadline":"2026-10-30"}}'

Response: 201

{
  "id": 518,
  "name": "3317 Meadowlark Dr",
  "status": "open",
  "pipeline_id": 3,
  "pipeline": "Buyers",
  "stage": "offer",
  "stage_name": "Offer made",
  "contact_id": 4821,
  "contact": {
    "id": 4821,
    "name": "Ava Buyer",
    "email": "ava@example.com"
  },
  "agent_id": 12,
  "agent": {
    "id": 12,
    "name": "Andy Agent",
    "email": "andy@peakrealty.com"
  },
  "price": 541000,
  "commission_rate": 2.5,
  "commission_amount": null,
  "close_date": "2026-11-16",
  "property_address": "3317 Meadowlark Dr, Loveland, CO 80538",
  "listing_id": null,
  "external_id": "file-2291",
  "lost_reason": null,
  "details": {
    "inspection_deadline": "2026-10-30"
  },
  "won_at": null,
  "lost_at": null,
  "stage_changed_at": "2026-10-20T15:30:00.000Z",
  "created_at": "2026-10-20T15:30:00.000Z",
  "updated_at": "2026-10-20T15:30:00.000Z"
}

GET /api/v1/deals/518

One deal.

Request

curl "https://homes.example.com/api/v1/deals/518" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "id": 518,
  "name": "3317 Meadowlark Dr",
  "status": "open",
  "pipeline_id": 3,
  "pipeline": "Buyers",
  "stage": "offer",
  "stage_name": "Offer made",
  "contact_id": 4821,
  "contact": {
    "id": 4821,
    "name": "Ava Buyer",
    "email": "ava@example.com"
  },
  "agent_id": 12,
  "agent": {
    "id": 12,
    "name": "Andy Agent",
    "email": "andy@peakrealty.com"
  },
  "price": 541000,
  "commission_rate": 2.5,
  "commission_amount": null,
  "close_date": "2026-11-16",
  "property_address": "3317 Meadowlark Dr, Loveland, CO 80538",
  "listing_id": null,
  "external_id": "file-2291",
  "lost_reason": null,
  "details": {
    "inspection_deadline": "2026-10-30"
  },
  "won_at": null,
  "lost_at": null,
  "stage_changed_at": "2026-10-20T15:30:00.000Z",
  "created_at": "2026-10-20T15:30:00.000Z",
  "updated_at": "2026-10-20T15:30:00.000Z"
}

PATCH /api/v1/deals/518

Change a deal. Send only what changes.

Request

curl -X PATCH "https://homes.example.com/api/v1/deals/518" \
  -H "Authorization: Bearer $KIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"stage":"under_contract"}'

Response: 200

{
  "id": 518,
  "name": "3317 Meadowlark Dr",
  "status": "open",
  "pipeline_id": 3,
  "pipeline": "Buyers",
  "stage": "under_contract",
  "stage_name": "Under contract",
  "contact_id": 4821,
  "contact": {
    "id": 4821,
    "name": "Ava Buyer",
    "email": "ava@example.com"
  },
  "agent_id": 12,
  "agent": {
    "id": 12,
    "name": "Andy Agent",
    "email": "andy@peakrealty.com"
  },
  "price": 541000,
  "commission_rate": 2.5,
  "commission_amount": null,
  "close_date": "2026-11-16",
  "property_address": "3317 Meadowlark Dr, Loveland, CO 80538",
  "listing_id": null,
  "external_id": "file-2291",
  "lost_reason": null,
  "details": {
    "inspection_deadline": "2026-10-30"
  },
  "won_at": null,
  "lost_at": null,
  "stage_changed_at": "2026-10-20T15:30:00.000Z",
  "created_at": "2026-10-20T15:30:00.000Z",
  "updated_at": "2026-10-20T15:30:00.000Z"
}

DELETE /api/v1/deals/518

Remove a deal.

Request

curl -X DELETE "https://homes.example.com/api/v1/deals/518" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 204 (no body)

Appointments

Showings, consultations and other meetings with a contact at a set time. Each one shows on the contact’s timeline when it is booked.

GET /api/v1/appointments

Appointments by when they start, earliest first.

  • contact_id Only this contact’s appointments.
  • agent_id Only this agent’s.
  • from, to Only those starting at or after from and before to (dates or date-times).
  • external_id Your app’s id for the appointment.
  • order asc (the default) or desc.
  • limit, offset See Pagination.

Request

curl "https://homes.example.com/api/v1/appointments?agent_id=12&from=2026-10-24T00:00:00-06:00&to=2026-10-25T00:00:00-06:00" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "data": [
    {
      "id": 3304,
      "contact_id": 4821,
      "title": "Showing: 3317 Meadowlark Dr",
      "starts_at": "2026-10-24T16:00:00.000Z",
      "ends_at": "2026-10-24T16:45:00.000Z",
      "location": "3317 Meadowlark Dr, Loveland, CO 80538",
      "notes": "Lockbox on the side door. Ava is bringing her dad.",
      "agent_id": 12,
      "agent": {
        "id": 12,
        "name": "Andy Agent",
        "email": "andy@peakrealty.com"
      },
      "external_id": "nadia-bk-7731",
      "made_by": "Nadia",
      "created_at": "2026-10-20T16:10:00.000Z",
      "updated_at": "2026-10-20T16:10:00.000Z"
    }
  ],
  "total": 1,
  "limit": 25,
  "offset": 0
}

POST /api/v1/appointments

Book an appointment. If you send an external_id the brokerage already has, that appointment is updated instead and the answer is 200, so your app can send it again whenever it changes.

  • contact_id Required for a new appointment.
  • title What it is, such as Showing: 3317 Meadowlark Dr. Required.
  • starts_at A date-time. Required.
  • ends_at A date-time, not before starts_at.
  • location Where.
  • agent_id or agent_email The agent going. The contact’s agent by default.
  • notes Anything else.
  • external_id Your app’s id for the appointment.

Request

curl -X POST "https://homes.example.com/api/v1/appointments" \
  -H "Authorization: Bearer $KIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contact_id":4821,"title":"Showing: 3317 Meadowlark Dr","starts_at":"2026-10-24T10:00:00-06:00","ends_at":"2026-10-24T10:45:00-06:00","location":"3317 Meadowlark Dr, Loveland, CO 80538","agent_email":"andy@peakrealty.com","notes":"Lockbox on the side door. Ava is bringing her dad.","external_id":"nadia-bk-7731"}'

Response: 201

{
  "id": 3304,
  "contact_id": 4821,
  "title": "Showing: 3317 Meadowlark Dr",
  "starts_at": "2026-10-24T16:00:00.000Z",
  "ends_at": "2026-10-24T16:45:00.000Z",
  "location": "3317 Meadowlark Dr, Loveland, CO 80538",
  "notes": "Lockbox on the side door. Ava is bringing her dad.",
  "agent_id": 12,
  "agent": {
    "id": 12,
    "name": "Andy Agent",
    "email": "andy@peakrealty.com"
  },
  "external_id": "nadia-bk-7731",
  "made_by": "Nadia",
  "created_at": "2026-10-20T16:10:00.000Z",
  "updated_at": "2026-10-20T16:10:00.000Z"
}

GET /api/v1/appointments/3304

One appointment.

Request

curl "https://homes.example.com/api/v1/appointments/3304" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "id": 3304,
  "contact_id": 4821,
  "title": "Showing: 3317 Meadowlark Dr",
  "starts_at": "2026-10-24T16:00:00.000Z",
  "ends_at": "2026-10-24T16:45:00.000Z",
  "location": "3317 Meadowlark Dr, Loveland, CO 80538",
  "notes": "Lockbox on the side door. Ava is bringing her dad.",
  "agent_id": 12,
  "agent": {
    "id": 12,
    "name": "Andy Agent",
    "email": "andy@peakrealty.com"
  },
  "external_id": "nadia-bk-7731",
  "made_by": "Nadia",
  "created_at": "2026-10-20T16:10:00.000Z",
  "updated_at": "2026-10-20T16:10:00.000Z"
}

PATCH /api/v1/appointments/3304

Change an appointment. Send only what changes; null clears ends_at, location or notes.

Request

curl -X PATCH "https://homes.example.com/api/v1/appointments/3304" \
  -H "Authorization: Bearer $KIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"starts_at":"2026-10-24T11:00:00-06:00","ends_at":"2026-10-24T11:45:00-06:00"}'

Response: 200

{
  "id": 3304,
  "contact_id": 4821,
  "title": "Showing: 3317 Meadowlark Dr",
  "starts_at": "2026-10-24T17:00:00.000Z",
  "ends_at": "2026-10-24T17:45:00.000Z",
  "location": "3317 Meadowlark Dr, Loveland, CO 80538",
  "notes": "Lockbox on the side door. Ava is bringing her dad.",
  "agent_id": 12,
  "agent": {
    "id": 12,
    "name": "Andy Agent",
    "email": "andy@peakrealty.com"
  },
  "external_id": "nadia-bk-7731",
  "made_by": "Nadia",
  "created_at": "2026-10-20T16:10:00.000Z",
  "updated_at": "2026-10-21T09:30:00.000Z"
}

DELETE /api/v1/appointments/3304

Remove an appointment, such as one that was cancelled.

Request

curl -X DELETE "https://homes.example.com/api/v1/appointments/3304" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 204 (no body)

Listings

Homes from the brokerage’s MLS feed: active and pending homes, and sold homes where the brokerage has sold data. Show the attribution and disclaimer that come back alongside the homes wherever you show the data.

GET /api/v1/listings

Homes in an area, newest first. Give one area: a point and a radius, a box, a drawn shape, a city or a postal code, or one address.

  • lat, lng, miles Homes within miles (1 by default, up to 25) of a point.
  • bounds south,west,north,east.
  • polygon lng,lat;lng,lat;… (three or more points).
  • city, postal_code A place.
  • address One home by its street address, any status.
  • status active, pending, sold, or several separated by commas. All three by default.
  • months How far back sold homes go: 24 by default, up to 60.
  • home_type detached, attached, land, commercial or other.
  • beds_min, beds_max, baths_min, baths_max, sqft_min, sqft_max, price_min, price_max, year_min, year_max, garage_min Numbers.
  • limit Up to 1,000 homes (500 by default).

Request

curl "https://homes.example.com/api/v1/listings?lat=40.4176&lng=-104.7395&miles=1&status=active,pending&beds_min=3" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "data": [
    {
      "id": "IR1023344",
      "status": "active",
      "mls_status": "Active",
      "feed": "ires",
      "mls": "IRES",
      "address": "2901 W 12th St",
      "street": "2901 W 12th St",
      "unit": null,
      "city": "Greeley",
      "state": "CO",
      "postal_code": "80634",
      "county": "Weld",
      "subdivision": "Westmoor",
      "property_type": "Residential",
      "property_subtype": "Single Family Residence",
      "home_type": "detached",
      "beds": 4,
      "baths": 2,
      "living_area": 2300,
      "lot_size": 8712,
      "lot_acres": 0.2,
      "year_built": 1971,
      "garage_spaces": 2,
      "list_price": 450000,
      "original_list_price": 465000,
      "sold_price": null,
      "list_date": "2026-10-01",
      "closed_date": null,
      "days_on_market": 19,
      "lat": 40.418,
      "lng": -104.74,
      "photo": "https://photos.example.com/IR1023344/1.jpg",
      "url": "https://homes.example.com/homes-for-sale/2901-w-12th-st/",
      "updated_at": "2026-10-20T06:00:00.000Z"
    }
  ],
  "total": 1,
  "limit": 500,
  "offset": 0,
  "attribution": {
    "feed": "ires",
    "name": "IRES MLS",
    "sample": false,
    "courtesy": "Listing courtesy of Peak Realty",
    "disclaimer": "Information is deemed reliable but not guaranteed.",
    "updated_at": "2026-10-20T06:00:00.000Z"
  }
}

Webhooks

Ask to be told when something happens instead of checking. See Receiving webhooks below for what arrives and how to check it.

GET /api/v1/webhooks

The brokerage’s webhooks.

Request

curl "https://homes.example.com/api/v1/webhooks" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "data": [
    {
      "id": 41,
      "event": "contact.created",
      "url": "https://app.example.com/hooks/kit",
      "status": "active",
      "failures": 0,
      "made_by": "Zapier",
      "last_delivered_at": "2026-10-20T15:01:02.000Z",
      "last_error": null,
      "created_at": "2026-10-18T10:00:00.000Z",
      "updated_at": "2026-10-18T10:00:00.000Z"
    }
  ],
  "total": 1,
  "limit": 1,
  "offset": 0
}

POST /api/v1/webhooks

Subscribe a URL to one event. Without a secret, one is made for you and shown in this answer only, so keep it. Subscribing the same URL to the same event again turns it back on and answers 200; send a secret with it to sign its deliveries with that secret from then on.

  • event One of the events below. Required.
  • url An https address. Required.
  • secret Your own signing secret, at least 16 characters.

Request

curl -X POST "https://homes.example.com/api/v1/webhooks" \
  -H "Authorization: Bearer $KIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event":"contact.created","url":"https://app.example.com/hooks/kit"}'

Response: 201

{
  "id": 41,
  "event": "contact.created",
  "url": "https://app.example.com/hooks/kit",
  "status": "active",
  "failures": 0,
  "made_by": "Zapier",
  "last_delivered_at": null,
  "last_error": null,
  "created_at": "2026-10-18T10:00:00.000Z",
  "updated_at": "2026-10-18T10:00:00.000Z",
  "secret": "whsec_Zx4p0Qv8bL2nT7yR1cW9eK3sJ6mA5dHf"
}

GET /api/v1/webhooks/41

One webhook.

Request

curl "https://homes.example.com/api/v1/webhooks/41" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 200

{
  "id": 41,
  "event": "contact.created",
  "url": "https://app.example.com/hooks/kit",
  "status": "active",
  "failures": 0,
  "made_by": "Zapier",
  "last_delivered_at": "2026-10-20T15:01:02.000Z",
  "last_error": null,
  "created_at": "2026-10-18T10:00:00.000Z",
  "updated_at": "2026-10-18T10:00:00.000Z"
}

PATCH /api/v1/webhooks/41

Change the URL, or pause and resume it with status paused or active. Resuming clears its failures.

Request

curl -X PATCH "https://homes.example.com/api/v1/webhooks/41" \
  -H "Authorization: Bearer $KIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"paused"}'

Response: 200

{
  "id": 41,
  "event": "contact.created",
  "url": "https://app.example.com/hooks/kit",
  "status": "paused",
  "failures": 0,
  "made_by": "Zapier",
  "last_delivered_at": "2026-10-20T15:01:02.000Z",
  "last_error": null,
  "created_at": "2026-10-18T10:00:00.000Z",
  "updated_at": "2026-10-18T10:00:00.000Z"
}

DELETE /api/v1/webhooks/41

Stop it.

Request

curl -X DELETE "https://homes.example.com/api/v1/webhooks/41" \
  -H "Authorization: Bearer $KIT_API_KEY"

Response: 204 (no body)

Receiving webhooks

When an event you subscribed to happens, we send a POST to your URL with a JSON body:

{
  "id": "evt_5f0c2b7e9a1d4c3b8e6f2a9d1c7b4e30",
  "type": "contact.created",
  "created_at": "2026-10-20T15:01:02.000Z",
  "data": {
    "id": 4821,
    "name": "Ava Buyer",
    "first_name": "Ava",
    "last_name": "Buyer",
    "email": "ava@example.com",
    "phone": "+19705550100",
    "stage": "showing",
    "source": "zillow",
    "tags": [
      "Buyer",
      "Pre-approved"
    ],
    "agent_id": 12,
    "agent": {
      "id": 12,
      "name": "Andy Agent",
      "email": "andy@peakrealty.com"
    },
    "lead_score": 72,
    "intent": "buy",
    "subscribed": true,
    "addresses": [
      {
        "kind": "home",
        "street": "2845 W 13th St",
        "city": "Greeley",
        "state": "CO",
        "postal_code": "80634",
        "beds": 4,
        "baths": 2,
        "living_area": 2392,
        "year_built": 1971,
        "estimate": 445000,
        "list_price": null
      }
    ],
    "custom_fields": {
      "lender": "First Western",
      "move_by": "2027-03-01",
      "must_haves": [
        "shop",
        "big yard"
      ]
    },
    "first_contacted_at": "2026-10-14T15:03:11.000Z",
    "last_touched_at": "2026-10-19T21:40:00.000Z",
    "last_active_at": "2026-10-20T13:12:45.000Z",
    "next_touch_at": "2026-10-22T15:00:00.000Z",
    "created_at": "2026-10-14T15:01:02.000Z",
    "updated_at": "2026-10-20T13:12:45.000Z"
  }
}

id is unique to this event, so you can ignore one you have already handled. data is the record as the API shows it at the moment we send; for a .deleted event it is only { "id": … }. The request also carries a KIT-Event header with the event’s name.

Events

  • contact.created A contact was added (through the API, a lead source, the site or the team).
  • contact.updated A contact’s details changed, or a known contact came in again.
  • contact.stage_changed A contact moved to another stage.
  • contact.tagged A contact got a tag they didn’t have.
  • contact.deleted A contact was removed. data holds only their id.
  • note.created A note was added.
  • note.updated A note was changed.
  • note.deleted A note was removed.
  • task.created A task was added.
  • task.updated A task was changed or done.
  • task.deleted A task was removed.
  • text.created A text was sent, received or logged.
  • email.created An email was sent, received or logged.
  • call.created A call was logged.
  • deal.created A deal was added.
  • deal.updated A deal changed (stage, price, dates, status).
  • deal.deleted A deal was removed.
  • appointment.created An appointment was booked.
  • appointment.updated An appointment changed (time, place, agent).
  • appointment.deleted An appointment was removed.

Answering

Answer with any 2xx status within 10 seconds; do slow work after you answer. Anything else counts as a failure. A webhook that fails 50 times in a row is paused; fix your endpoint, then resume it with PATCH /webhooks/{id} and {"status": "active"}.

Checking the signature

Every delivery is signed with the webhook’s secret, so you can be sure it came from us and wasn’t changed on the way. The KIT-Signature header looks like this:

KIT-Signature: t=1792508462,v1=6f1c0d…e93a
  1. Take t (when we signed it, in seconds since 1970) and v1 from the header.
  2. Join t, a full stop and the raw request body exactly as it arrived: 1792508462.{"id":"evt_…",…}.
  3. Compute the HMAC-SHA256 of that with your secret, written as lowercase hex.
  4. Compare it with v1 using a constant-time comparison, and turn away anything older than five minutes.

In Node.js:

import crypto from 'node:crypto';

// rawBody: the request body exactly as it arrived (a string, before JSON.parse).
export function verifyKitSignature(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(String(header || '').split(',').map((p) => p.trim().split('=')));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || !/^[0-9a-f]{64}$/.test(parts.v1 || '')) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(parts.v1, 'hex'));
}

In Python:

import hashlib, hmac, time

def verify_kit_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
    try:
        t = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > tolerance:
        return False
    signed = f"{t}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Check the signature against the raw body before you parse it: parsing and re-serializing JSON changes the bytes.