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
- 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. - Connected apps also shows the brokerage’s API address. It is the brokerage’s own site followed by
/api/v1, for examplehttps://homes.example.com/api/v1. - 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/jsonwith a body. - Field names are lowercase with underscores, such as
contact_idandcreated_at. - Ids are numbers. Times are ISO 8601 in UTC, such as
2026-10-20T15:01:02.000Z; dates on their own look like2026-11-16. - A field with no value is
null, never left out. - Creating something answers
201with the new record. Deleting answers204with no body. - To change a record, send
PATCHwith 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_errorand 502send_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.
statusactive (the default), inactive for teammates who were turned off, or all.include_inactive1 is the same as status=all.limit, offsetSee 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.
qText to look for in the name, email or phone.emailAn exact email address.phoneA phone number in any format.stageA stage key.sourceWhere they came from, such as zillow.tagsOne or more tags, separated by commas (any of them).agent_idAn agent’s id, or none for contacts with no agent.idsContact ids, separated by commas.updated_since, created_since, active_sinceA date or date-time.sortcreated (the default), updated, last_active, name, id or stage.orderdesc (the default) or asc.limit, offsetSee 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.
nameTheir full name (or first_name and last_name).emailAn email address. A contact needs an email or a phone number.phoneA phone number in any US format.stageA stage key. New by default.sourceWhere they came from.tagsA list of tags.agent_id or agent_emailWho they belong to. Without one, the brokerage’s assignment rules decide.custom_fieldsYour 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).
titleWhat happened, such as Viewed the pricing report. Required.detailMore about it.sourceYour app’s name. The key’s name by default.urlA 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.
sourceWhere the lead came from, such as Open house sign-in.messageWhat they said.contactThe person: the same fields as POST /contacts. Required.propertyThe 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_idOnly 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_idRequired.bodyThe note. Required.pinnedtrue to keep it at the top.privatetrue 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_idOnly this contact’s tasks.agent_idOnly this agent’s tasks.donetrue 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_idRequired.titleWhat to do. Required.kindcall, text, email, follow_up (the default) or showing.due_atA date-time, or a date (due at 5pm).remind_atWhen to remind the agent.agent_id or agent_emailWho does it. The contact’s agent by default.donetrue 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_idOnly 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_idRequired.bodyThe text. Required, up to 1,600 characters.directionoutbound (the default) or inbound.to, fromPhone numbers, when they differ from the contact’s and the brokerage’s.agent_idThe agent it was from or for. The contact’s agent by default.external_idYour app’s own id for the message.sendtrue 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_idOnly 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_idRequired.subjectThe subject line.bodyThe email. Required.directionoutbound (the default) or inbound.to, fromAddresses, when they differ from the contact’s and the brokerage’s.agent_idThe agent it was from or for.external_idYour app’s own id for the message.sendtrue 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_idOnly 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_idRequired.directionoutbound (the default) or inbound.outcomeHow it went, such as Talked or Left voicemail.duration_secondsHow long it was.noteWhat 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_idOnly this contact’s deals.pipeline_idOnly this pipeline’s.statusopen, won or lost.external_idYour app’s id for the deal.updated_sinceA 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.
nameUsually the address. Required.contact_idRequired for a new deal.pipeline_id or pipelineThe pipeline’s id, name or kind (buyer, seller). Buyers by default.stageA stage key, name or position. The first stage by default.statusopen (the default), won or lost.price, commission_rate, commission_amountNumbers.close_dateThe expected closing date.property_address, listing_idThe home.external_idYour app’s id for the deal.lost_reasonWhy it was lost.detailsAnything else, as an object. Merged with what is there.agent_id or agent_emailThe 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_idOnly this contact’s appointments.agent_idOnly this agent’s.from, toOnly those starting at or after from and before to (dates or date-times).external_idYour app’s id for the appointment.orderasc (the default) or desc.limit, offsetSee 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_idRequired for a new appointment.titleWhat it is, such as Showing: 3317 Meadowlark Dr. Required.starts_atA date-time. Required.ends_atA date-time, not before starts_at.locationWhere.agent_id or agent_emailThe agent going. The contact’s agent by default.notesAnything else.external_idYour 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, milesHomes within miles (1 by default, up to 25) of a point.boundssouth,west,north,east.polygonlng,lat;lng,lat;… (three or more points).city, postal_codeA place.addressOne home by its street address, any status.statusactive, pending, sold, or several separated by commas. All three by default.monthsHow far back sold homes go: 24 by default, up to 60.home_typedetached, 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_minNumbers.limitUp 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.
eventOne of the events below. Required.urlAn https address. Required.secretYour 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.createdA contact was added (through the API, a lead source, the site or the team).contact.updatedA contact’s details changed, or a known contact came in again.contact.stage_changedA contact moved to another stage.contact.taggedA contact got a tag they didn’t have.contact.deletedA contact was removed. data holds only their id.note.createdA note was added.note.updatedA note was changed.note.deletedA note was removed.task.createdA task was added.task.updatedA task was changed or done.task.deletedA task was removed.text.createdA text was sent, received or logged.email.createdAn email was sent, received or logged.call.createdA call was logged.deal.createdA deal was added.deal.updatedA deal changed (stage, price, dates, status).deal.deletedA deal was removed.appointment.createdAn appointment was booked.appointment.updatedAn appointment changed (time, place, agent).appointment.deletedAn 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
- Take
t(when we signed it, in seconds since 1970) andv1from the header. - Join
t, a full stop and the raw request body exactly as it arrived:1792508462.{"id":"evt_…",…}. - Compute the HMAC-SHA256 of that with your secret, written as lowercase hex.
- Compare it with
v1using 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.