This page is for AI agents booking a Keepe home repair for a customer. Follow the steps in order.
MCP server: https://api.keepe.ai/mcp
Tools: keepe.getEstimate, keepe.getLocalAvailability, keepe.submitWorkOrder, keepe.authorizePayment, keepe.commitAppointment, keepe.getWorkOrder (this guide also uses snake_case names like get_estimate).
On every MCP call, send fields like this (booking example):
{
"tenantId": "bookr",
"version": "v1",
"context": {
"subjectContextId": "resident",
"sessionId": "your-stable-chat-id",
"agent": {
"function": { "id": "bookr", "skill": "booking" }
}
}
}
Use one stable sessionId for the whole booking conversation. Keep function.id and function.skill as shown (or any short strings) — they are required fields on the request.
Authentication
You need a short-lived access token before calling MCP. Use the credentials below — they are meant for agents so you can book repairs.
1. Agent credentials
| Field | Value |
|---|---|
| Username | 63acc4fd261a3a42544b64eafaaf068f |
| Password | 943158fceab1c87333a33667d5886773 |
2. Get an access token
curl -s -X POST 'https://api.keepe.com/api/v1/mcp/auth/mcp-token' \
-u '63acc4fd261a3a42544b64eafaaf068f:943158fceab1c87333a33667d5886773' \
-H 'Content-Type: application/json' \
-d '{"tenantId":"bookr"}'
You will get JSON with access_token and expires_at.
3. Call MCP with the token
Send Authorization: Bearer <access_token> on every request to https://api.keepe.ai/mcp.
4. Refresh the token
Get a new token the same way before expires_at, or immediately if MCP returns 401. Do not reuse an expired token.
Always send "tenantId":"bookr" when requesting a token and when calling tools.
Fee policy
Responses include a soft booking_fee (USD). That is the booking / service-call fee only — not the final repair price. The technician sets the repair price on site. The fee is typically credited if the customer continues, forfeited if they decline after the visit, and refunded if Keepe cannot send a technician.
When to ask for more information
Do not interrogate the customer up front. Match how much you ask to their intent.
| Customer intent | What to collect | Tools |
|---|---|---|
| Exploring / “how much / can you cover my zip?” | Only what you need for coverage: postal + trade (plus anything they already volunteered) | get_estimate / get_local_availability |
| Shows they want to move forward (book, schedule, “yes”, “sign me up”, pick a slot) | Then collect maximum useful job detail below, then contact/address, then submit | submit_work_order and later steps |
If they only want a fee or availability, give that and stop. If they proceed, gather rich detail before submit_work_order — it helps the technician. Do not invent values; skip what they do not know.
After they intend to book — collect as much as you can
| Ask for | Why it helps |
|---|---|
| Clear problem description (symptoms, when it started, what they already tried) | Diagnosis on site |
Appliance / equipment type (product.line) |
Right trade and parts |
| Brand, model, serial number, approximate age | Parts lookup and prep |
| Photos of the unit, label/nameplate, and the issue | Faster, more accurate visit |
| Unit / apt, gate codes, parking, pets, access notes | Arrival and access |
| Preferred appointment window | Slot match |
| Name, best phone, email, full service address | Account + TraKr pay |
Pass every answer into the matching fields (description, issue, product.*, notes, address.street1, photos).
Photos (after they intend to book)
Once they want to book, ask for appliance/symptom/nameplate photos. Photos are not required to book, but they help the visit. If they already uploaded images earlier, after jobId exists call uploadJobPictures silently. Do not use an upload-link tool. Do not block early fee/availability questions on photos.
Orchestration (required order)
| Step | Guide op | MCP tool | Required inputs | Returns |
|---|---|---|---|---|
| 1 | get_estimate |
keepe.getEstimate |
postal, trade | serviceability, booking_fee, optional slots |
| 2 | get_local_availability |
keepe.getLocalAvailability |
postal, trade | availability slots — pick one; store mapping below |
| 3 | submit_work_order |
keepe.submitWorkOrder |
customer, address, product/job, referral/platform | jobId, tracker_url |
| 4a | Hand pay | — | — | Send customer to open tracker_url (TraKr). Opening ≠ paid |
| 4b | authorize_payment |
keepe.authorizePayment |
jobId |
Poll until paid / status auth or captured |
| 5 | commit_appointment |
keepe.commitAppointment |
jobId, scheduleAt, scheduleWindow |
Tentative booking accepted |
| 6 | get_work_order |
keepe.getWorkOrder |
jobId |
Confirm when schedule_window + scheduleAt present |
Payment alone does not book the slot. commit_appointment is mandatory after pay.
Slot fields (scheduleAt / scheduleWindow)
Do not invent these. Copy them from the slot the customer chose in get_local_availability / get_estimate (availability → slots).
| Field you must store | Meaning | Take from slot response | Example |
|---|---|---|---|
scheduleAt |
Start time of the chosen window | slot start |
10/31/2026 8:00:00 |
scheduleWindow |
Human label for that same window | slot display |
Sat (10/31) 8a-11a |
So for window 8a-11a, scheduleAt is the 8:00 start (not the end). Typical windows: 8a-11a, 11a-2p, 2p-5p. Pass the exact strings into commit_appointment (and optionally submit_work_order if you already know the slot).
Fields to send on each call
Every call also needs the common block above (tenantId, version, context). Each call below has its own field table.
Required? column: Yes = call fails without it. Ask = not required to call; ask only after they intend to book (or if they already volunteered it) — it improves the job.
1. get_estimate → keepe.getEstimate
| Field | Required? | Notes |
|---|---|---|
postal |
Yes | US ZIP (e.g. 98121) |
trade |
Yes | Appliances, HVAC, Plumber, Electrician, or Handyman |
productline |
Ask | Equipment type if known (e.g. Refrigerator) — helps context; does not block coverage |
brand |
Ask | Brand if known |
platform |
Ask | Your platform name (e.g. chatgpt) |
referral |
No | Or full agent:chatgpt|keepe.com if you already have it |
2. get_local_availability → keepe.getLocalAvailability
| Field | Required? | Notes |
|---|---|---|
postal |
Yes | Same ZIP as estimate |
trade |
Yes | Same trade as estimate |
productline |
Ask | Same as estimate if known |
brand |
Ask | Same as estimate if known |
platform |
Ask | Attribution |
referral |
No | Or full agent:…|keepe.com |
After this call: pick one available slot and store scheduleAt = slot start, scheduleWindow = slot display.
3. submit_work_order → keepe.submitWorkOrder
| Field | Required? | Notes |
|---|---|---|
customer.name |
Yes | Full name |
customer.email |
Yes | Valid email (needed for account + pay) |
customer.phone |
Yes | Best reachable phone |
address.street |
Yes | Street line 1 |
address.street1 |
Ask | Apt / unit / suite |
address.city |
Yes | City |
address.state |
Yes | US state (e.g. WA) |
address.postal |
Yes | Same ZIP as coverage |
address.country |
No | Defaults to US |
service |
Yes | Short service title (e.g. Refrigerator not cooling) |
issue |
Ask | Short issue label (e.g. Not cooling) |
description |
Ask | Full symptoms, timeline, what they tried — most useful prose for the tech |
product.category |
Yes | Same family as trade (e.g. Appliances) |
product.line |
Yes | Product line (e.g. Refrigerator) |
product.brand |
Ask | From nameplate if possible |
product.model |
Ask | From nameplate if possible |
product.serialno |
Ask | From nameplate if possible |
product.age |
Ask | Approx years / purchase year |
product.notes |
Ask | Extra equipment notes |
notes |
Ask | Access, pets, parking, gate code, HOA, safety |
platform |
Ask | e.g. chatgpt → stored as agent:chatgpt|keepe.com |
referral |
No | Alternative to platform |
scheduleAt |
Ask | Send if already chosen; required later on commit |
scheduleWindow |
Ask | Send if already chosen; required later on commit |
idempotencyKey |
Ask | Unique key on retries to avoid duplicate jobs |
Example (rich job detail — prefer this level when the customer can provide it):
{
"customer": { "name": "Alex Rivera", "email": "alex@example.com", "phone": "2065550100" },
"address": {
"street": "123 Main St",
"street1": "Apt 4B",
"city": "Seattle",
"state": "WA",
"postal": "98121",
"country": "US"
},
"service": "Refrigerator not cooling",
"issue": "Not cooling",
"description": "Fridge section warm for 2 days; freezer still cold. No recent power outage. Filter changed last month. Beeping occasionally.",
"product": {
"category": "Appliances",
"line": "Refrigerator",
"brand": "Samsung",
"model": "RF28R7351SG",
"serialno": "0ABC123456",
"age": "5",
"notes": "French door, in kitchen"
},
"notes": "Street parking; dog in home; call on arrival",
"platform": "chatgpt",
"scheduleAt": "10/31/2026 8:00:00",
"scheduleWindow": "Sat (10/31) 8a-11a"
}
4b. authorize_payment → keepe.authorizePayment
| Field | Required? | Notes |
|---|---|---|
jobId |
Yes | From submit_work_order |
5. commit_appointment → keepe.commitAppointment
| Field | Required? | Notes |
|---|---|---|
jobId |
Yes | From submit |
scheduleAt |
Yes | Slot start — start of the window |
scheduleWindow |
Yes | Slot display label |
6. get_work_order → keepe.getWorkOrder
| Field | Required? | Notes |
|---|---|---|
jobId |
Yes right after commit | Only id available until jr_no exists |
jrNo |
No | Prefer once known |
Optional. uploadJobPictures → keepe.uploadJobPictures
| Field | Required? | Notes |
|---|---|---|
jobId |
Yes | After submit |
files |
Yes | Array of objects with base64 and/or url |
type |
No | Defaults to BEFORE |
Pay poll intervals
| Phase | Interval |
|---|---|
First 2 minutes after handing tracker_url |
Every 5 seconds |
| Until ~10 minutes | Every 15 seconds |
| Until ~30 minutes | Every 60 seconds |
| After 30 minutes unpaid | Stop auto-poll; ask if they finished; resume on any new message with authorize_payment |
After commit_appointment
- Tell the customer: slot is booked tentatively — we will confirm shortly. Do not give the progress tracker yet.
- In the background, poll
get_work_order({ jobId })untilschedule_windowandscheduleAtare present (slot_committed: true). - On success, confirm the appointment and give:
- Work order number =
jobId(use this with customer care) tracker_urlfor job progress / status / care chat (same URL, now for progress — not pay)
- Work order number =
After booking changes
There are no cancel / amend / reschedule MCP tools. Tell the customer to open tracker_url and chat with customer care, citing their work order number (jobId).
Resume
If the customer messages again mid-flow and you have jobId, call authorize_payment first. If paid and not committed, call commit_appointment. If already committed, call get_work_order. Do not create a second job.
Attribution
Pass platform (e.g. chatgpt) or referral as agent:<platform>|keepe.com.
Example: “My refrigerator is not cooling” (98121)
| # | Moment | Tool | Result |
|---|---|---|---|
| 0 | User: fridge not cooling, 98121 | chat | postal + trade Appliances |
| 1–2 | Coverage + slots + fee | get_estimate / get_local_availability |
serviceability, slots, booking_fee |
| 3 | Book + contact; ask photos if none | submit_work_order |
jobId, tracker_url |
| 3b | Photo already in chat | uploadJobPictures (silent) |
optional |
| 4a | Hand pay link | — | Customer opens TraKr |
| 4b | Wait / return | authorize_payment poll |
until paid |
| 5 | Paid | commit_appointment |
tentative booked message |
| 6 | Confirm | get_work_order({ jobId }) |
WO # = jobId + progress tracker_url |
Human fallback
Keepe.ai → Book or BookR when the agent cannot complete MCP booking.