Overview
Everything here works with an API key from your account and is scoped to that account. There are three ways to connect:
- Let an AI assistant book and update jobsAdd kjobdone as an MCP server in Claude, Cursor, VS Code or Codex, then ask it to create jobs from an email, a call note or a spreadsheet, look jobs up, or reschedule them.
- Send jobs in from any systemPost a website form, call centre lead or Zapier step to one endpoint and the job arrives ready to dispatch.
- Hear about every job changeGet a signed webhook when a job is created, assigned, changes status, closes or is canceled.
Authentication
An account admin creates API keys on the Integrations page in kjobdone. Each key starts with od_live_, is shown once when it is created, and can be revoked at any time. Create one key per connected system or assistant, so you can revoke one without breaking the others.
Send the key with every request, in either header:
X-API-Key: od_live_your_key
# or
Authorization: Bearer od_live_your_key- Each key has a default company and, optionally, a default metro, used when a job does not name one.
- Jobs created or edited with a key are recorded against the admin who created it. If that person is disabled or locked, the key stops working too.
- A key can also read and edit every job in the account, over MCP or the REST API. Treat each key as you would that admin's sign-in, and revoke it if it leaks.
- Keep keys on a server or in your assistant's configuration. The API does not accept keys from browser code.
MCP server
kjobdone runs a remote Model Context Protocol server, so an AI assistant can find, create and update jobs for you in plain language. Point any MCP client that supports the Streamable HTTP transport at:
https://api.kjobdone.com/mcpConnect a client
claude mcp add --transport http kjobdone https://api.kjobdone.com/mcp \
--header "X-API-Key: od_live_your_key"Run once in your terminal. Add --scope user to use it in every project.
Replace od_live_your_key with your key. Assistants that only support sign-in with OAuth cannot connect yet.
Tools
| Tool | What it does |
|---|---|
create_jobCreates data | Creates a job ready for dispatch, with the same fields as the REST endpoint. Invalid arguments come back as a readable error so the assistant can fix them and try again. |
update_jobChanges data | Changes an existing job, exactly as a dispatcher saving the job form would. Only the fields sent change; see the table below. |
list_jobsRead only | Searches your jobs, newest job date first, 20 a page (up to 50). Filter by date range, status, company, metro or free text such as a customer name, job number or address. With no dates it covers 30 days either side of today. |
get_jobRead only | Reads one job by its ID or short job number: customer, address, notes, appointment window, company, metro, assigned technician and the job's version. |
list_metrosRead only | Your enabled service areas, with the ZIP code each one is centred on and which is the key default. |
list_companiesRead only | Your enabled companies a job can be filed under, marking the key default. |
check_connectionRead only | Confirms the key works and names the account and key it belongs to. |
Every create_job call creates a new job. create_job and update_job are marked as write tools so clients can ask you before running them.
update_job takes the jobId (the job ID or job number) and only the fields to change. Nothing can be blanked: a field left out stays as it is.
| Field | What it changes |
|---|---|
expectedVersion | The version from your last read of the job. The edit is refused if someone changed the job since. |
customerName, phone, email | The customer on the job. |
address, suite, city, state, zip | The service address. Changing it clears the map location until a new one is picked. |
description | The short description technicians see, up to 50 characters. |
notes | Replaces the job notes, so the assistant includes what should be kept. |
jobDate | YYYY-MM-DD. |
status | Any job status. CLOSED and PENDING_CLOSE are refused until field work is finished. |
appointmentDate, appointmentStartTime, appointmentEndTime | Reschedules the appointment: a YYYY-MM-DD day with HH:MM 24-hour times. |
companyId, metroId | Moves the job to another company or metro from list_companies or list_metros. |
- An edit is logged on the job just like a dispatcher's save. A status change also sends the matching webhooks and can notify the assigned technician.
- Assigning a technician is not available over MCP yet, and the assistant cannot read or change customer records or technicians outside a job.
Protocol details
- Streamable HTTP transport, stateless: every JSON-RPC message is a
POSTto/mcpand is answered in the response body as JSON. There are no sessions, so noMcp-Session-Id, andGETreturns 405. - Protocol versions
2025-11-25,2025-06-18and2025-03-26. - Notifications get
202 Accepted. Batches are accepted for clients on2025-03-26.
You can also call it by hand, which helps when checking a key:
curl https://api.kjobdone.com/mcp \
-H "X-API-Key: $KJOBDONE_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": { "name": "my-script", "version": "1.0.0" }
}
}'curl https://api.kjobdone.com/mcp \
-H "X-API-Key: $KJOBDONE_API_KEY" \
-H "Content-Type: application/json" \
-H "MCP-Protocol-Version: 2025-06-18" \
-d '{
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {
"name": "create_job",
"arguments": {
"customerName": "Maria Lopez",
"phone": "(602) 555-0142",
"address": "2417 E Camelback Rd",
"city": "Phoenix", "state": "AZ", "zip": "85004",
"description": "AC not cooling"
}
}
}'REST API
Plain HTTPS and JSON, for Zapier, Make, form tools and your own code. Requests and responses use application/json. The REST API can do everything the MCP tools can: create, search, read and update jobs.
Check a key
GEThttps://api.kjobdone.com/v1/integrations/ping
Returns the account and key name. Use it as the connection test in Zapier.
curl https://api.kjobdone.com/v1/integrations/ping \
-H "X-API-Key: $KJOBDONE_API_KEY"{
"account": "Sunbelt Home Services",
"key": "Website form"
}Create a job
POSThttps://api.kjobdone.com/v1/integrations/jobs
Creates a job exactly as a dispatcher saving the job form would. The job is linked to the existing customer with the same phone number or email, but that customer's saved details are never changed by the request.
curl -X POST https://api.kjobdone.com/v1/integrations/jobs \
-H "X-API-Key: $KJOBDONE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customerName": "Maria Lopez",
"phone": "(602) 555-0142",
"email": "[email protected]",
"address": "2417 E Camelback Rd",
"suite": "Apt 4",
"city": "Phoenix",
"state": "AZ",
"zip": "85004",
"description": "AC not cooling, unit is about 8 years old",
"notes": "Gate code 4411. Call before arriving."
}'HTTP/1.1 201 Created
{
"id": "job_01m4xs7027en98w84qr5q4snb2",
"jobNumber": "WxNjwF",
"status": "ACCEPTED",
"jobDate": "2026-10-14",
"timezone": "America/Phoenix",
"company": "Sunbelt Home Services",
"metro": "Phoenix"
}Fields
| Field | Required | Description |
|---|---|---|
customerNameup to 100 | Yes | The customer's full name. |
phoneup to 25 | Yes | Any common format. Used to match an existing customer. |
emailup to 70 | No | Also used to match an existing customer. |
addressup to 50 | Yes | Street address of the job. |
suiteup to 10 | No | Apartment, suite or unit. |
cityup to 30 | Yes | City. |
stateup to 30 | Yes | US state, as an abbreviation or full name. |
zipup to 10 | Yes | Picks the metro the job is filed in. |
descriptionup to 2,000 | No | What the job is. Technicians see the first 50 characters; the full text goes into the notes. |
notesup to 5,000 | No | Anything else the dispatcher should know. |
jobDate | No | YYYY-MM-DD. Defaults to today in the job's timezone. |
timezone | No | IANA name such as America/Phoenix. Defaults to your account timezone. |
companyId | No | Company ID (com_…). Defaults to the key's default company. |
metroId | No | Metro ID (mtr_…). Defaults to the metro for the ZIP code, then the key's default metro. |
- Where the job goes. An explicit
metroIdwins. Otherwise the job goes to the enabled metro centred on that exact ZIP code, then the key's default metro, then your only metro if you have one. If none applies the request is refused rather than guessed, so sendmetroIdor set a default metro on the key. - Blank strings count as not sent, and fields the endpoint does not know are ignored, so a Zap can map whatever it has.
- Addresses are US only.
Search jobs
GEThttps://api.kjobdone.com/v1/integrations/jobs
Returns your jobs newest job date first, a page at a time. Every query parameter is optional: dateFrom and dateTo (YYYY-MM-DD, 30 days either side of today when left out), status, search (job number, customer name, address, city, ZIP, notes, company, technician or metro), companyId, metroId, page and limit (20 by default, up to 50).
curl "https://api.kjobdone.com/v1/integrations/jobs?search=Lopez&status=SCHEDULED&limit=10" \
-H "X-API-Key: $KJOBDONE_API_KEY"{
"jobs": [
{
"id": "job_01m4xs7027en98w84qr5q4snb2",
"jobNumber": "WxNjwF",
"jobDate": "2026-10-16",
"status": "SCHEDULED",
"customerName": "Maria Lopez",
"phone": "(602) 555-0142",
"address": "2417 E Camelback Rd Apt 4 Phoenix AZ 85004",
"description": "AC not cooling, unit is about 8 years",
"company": "Sunbelt Home Services",
"metro": "Phoenix",
"technician": "Luis Ortega"
}
],
"page": 1,
"limit": 10,
"total": 1,
"totalPages": 1
}Read a job
GEThttps://api.kjobdone.com/v1/integrations/jobs/:jobId
Takes the job ID (job_…) or the short job number. The version is what you send as expectedVersion when updating the job.
curl https://api.kjobdone.com/v1/integrations/jobs/WxNjwF \
-H "X-API-Key: $KJOBDONE_API_KEY"{
"id": "job_01m4xs7027en98w84qr5q4snb2",
"jobNumber": "WxNjwF",
"version": 4,
"status": "SCHEDULED",
"jobDate": "2026-10-16",
"timezone": "America/Phoenix",
"appointmentDate": "2026-10-16",
"appointmentStartTime": "09:00",
"appointmentEndTime": "11:00",
"customerName": "Maria Lopez",
"phone": "(602) 555-0142",
"email": "[email protected]",
"address": "2417 E Camelback Rd",
"suite": "Apt 4",
"city": "Phoenix",
"state": "AZ",
"zip": "85004",
"description": "AC not cooling, unit is about 8 years",
"notes": "AC not cooling, unit is about 8 years old\n\nGate code 4411. Call before arriving.",
"company": { "id": "com_…", "name": "Sunbelt Home Services" },
"metro": { "id": "mtr_…", "name": "Phoenix" },
"technician": "Luis Ortega",
"createdAt": "2026-10-14T16:02:11.000Z"
}Update a job
PATCHhttps://api.kjobdone.com/v1/integrations/jobs/:jobId
Changes only the fields you send, exactly as a dispatcher saving the job form would, and returns the job as saved in the same shape as reading it. This example moves the appointment:
curl -X PATCH https://api.kjobdone.com/v1/integrations/jobs/WxNjwF \
-H "X-API-Key: $KJOBDONE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"expectedVersion": 4,
"appointmentDate": "2026-10-17",
"appointmentStartTime": "13:00",
"appointmentEndTime": "15:00"
}'| Field | What it changes |
|---|---|
expectedVersion | The version from your last read of the job. The edit is refused if someone changed the job since. |
customerName, phone, email | The customer on the job. |
address, suite, city, state, zip | The service address. Changing it clears the map location until a new one is picked. |
description | The short description technicians see, up to 50 characters. |
notes | Replaces the job notes, so the assistant includes what should be kept. |
jobDate | YYYY-MM-DD. |
status | Any job status. CLOSED and PENDING_CLOSE are refused until field work is finished. |
appointmentDate, appointmentStartTime, appointmentEndTime | Reschedules the appointment: a YYYY-MM-DD day with HH:MM 24-hour times. |
companyId, metroId | Moves the job to another company or metro from list_companies or list_metros. |
- Unlike creating a job, an update refuses fields it does not know with a
400, so a misspelt field is never reported as saved. Nothing can be blanked: a field left out stays as it is. - Send
expectedVersion. If someone changed the job since you read it, the update is refused with409instead of overwriting their work. - The edit is logged on the job. A status change also sends the matching webhooks and can notify the assigned technician. Assigning a technician is not available through the API yet.
Webhooks
Add a webhook on the Integrations page, pick the events it receives, and copy its signing secret, shown once. kjobdone then sends a POST to your URL whenever one of those events happens.
| Event | Sent when |
|---|---|
job.created | A job was created, by a dispatcher, the API, MCP or a booking page. |
job.assigned | A technician was assigned or the job moved to another technician. |
job.status_changed | The job status changed. Carries previousStatus. |
job.closed | The job was closed. Sent alongside job.status_changed. |
job.canceled | The job was canceled. Sent alongside job.status_changed. |
document.sent | A customer link to an estimate or invoice was sent. Carries data.document instead of data.job. |
document.approved | A customer approved and signed an estimate online. Carries signedBy and signedAt. |
document.declined | A customer declined an estimate online. Carries declineReason when they gave one. |
ping | Sent by the Send test button, whatever the endpoint subscribes to. |
POST /your/endpoint
Content-Type: application/json
User-Agent: OwnDispatch-Webhooks/1.0
X-OwnDispatch-Event: job.assigned
X-OwnDispatch-Delivery: 2b0c6d1e-8f5a-4f7e-9d3b-1a2c3d4e5f60
X-OwnDispatch-Signature: t=1792000000,v1=5f8c…
{
"id": "2b0c6d1e-8f5a-4f7e-9d3b-1a2c3d4e5f60",
"event": "job.assigned",
"createdAt": "2026-10-14T16:02:11.000Z",
"data": {
"job": {
"id": "job_01m4xs7027en98w84qr5q4snb2",
"jobNumber": "WxNjwF",
"status": "ACCEPTED",
"jobDate": "2026-10-14",
"timezone": "America/Phoenix",
"appointmentFrom": "2026-10-14T17:00:00.000Z",
"appointmentTo": "2026-10-14T19:00:00.000Z",
"occupation": "HVAC",
"description": "AC not cooling, unit is about 8 years old",
"customer": { "name": "Maria Lopez", "phone": "(602) 555-0142", "email": "" },
"address": {
"street": "2417 E Camelback Rd", "suite": "Apt 4",
"city": "Phoenix", "state": "AZ", "zip": "85004", "country": "US"
},
"company": { "id": "com_…", "name": "Sunbelt Home Services" },
"metro": { "id": "mtr_…", "name": "Phoenix" },
"technician": { "id": "thn_…", "name": "Luis Ortega" }
}
}
}Verify the signature
X-OwnDispatch-Signature is t=<unix seconds>,v1=<hex>, where the hex value is an HMAC-SHA256 of <t>.<raw body> with your signing secret. Check it before trusting a delivery, and reject old timestamps to stop replays.
import { createHmac, timingSafeEqual } from "node:crypto"
// rawBody is the request body exactly as received, before JSON parsing.
export function verifyKjobdoneWebhook(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((part) => part.split("=")),
)
const timestamp = Number(parts.t)
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false // replayed
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex")
const received = Buffer.from(parts.v1 ?? "", "hex")
return received.length === 32 &&
timingSafeEqual(received, Buffer.from(expected, "hex"))
}Delivery and retries
- Answer with any 2xx status within 10 seconds. Anything else is retried twice, after 2 and then 10 seconds.
- A retry keeps the same
idandX-OwnDispatch-Delivery, so you can drop one you already handled. - An endpoint that fails 20 deliveries in a row is switched off until someone turns it back on.
- URLs must be public. Private, loopback and link-local addresses are refused.
Errors
REST errors return a JSON body with statusCode, error and a message that says what to fix. MCP tool failures, like a missing field, an unknown job number or a job someone else changed in the meantime, come back inside a normal tool result with isError: true so the assistant can read them and correct its call.
| Status | Meaning |
|---|---|
| 400 | A field is missing, invalid or unknown to an update, or no metro matches the ZIP code. The message says which. |
| 401 | The API key is missing, wrong or revoked, or the person who created it was disabled. |
| 404 | No job with that ID or job number in your account. |
| 409 | The job changed since the expectedVersion you sent. Read it again and check the latest details before retrying. |
| 405 | GET or DELETE on the MCP endpoint. It is stateless and only takes POST. |
| 5xx | Something went wrong on our side. Retry with backoff; contact us if it persists. |
Stuck on an integration, or need a tool the MCP server does not have yet? Tell us what you are connecting.