Developers

API and MCP server docs

Connect kjobdone to the AI assistants and tools your office already uses. One API key works for the MCP server and the REST API, and webhooks tell your other systems when a job changes.

MCP endpoint
https://api.kjobdone.com/mcp
REST base URL
https://api.kjobdone.com
Auth header
X-API-Key: od_live_…

Overview

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:

HTTP headers
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:

MCP endpoint
https://api.kjobdone.com/mcp
Try asking: “Create a job for Maria Lopez, (602) 555-0142, 2417 E Camelback Rd, Phoenix AZ 85004. Her AC is not cooling and the gate code is 4411.” The assistant fills in the fields and files the job in the metro for that ZIP code. Later: “Move Maria Lopez's job to Thursday, 9 to 11am.” The assistant finds the job, reads it and saves just the new appointment.

Connect a client

bash
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

ToolWhat it does
create_jobCreates dataCreates 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 dataChanges an existing job, exactly as a dispatcher saving the job form would. Only the fields sent change; see the table below.
list_jobsRead onlySearches 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 onlyReads 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 onlyYour enabled service areas, with the ZIP code each one is centred on and which is the key default.
list_companiesRead onlyYour enabled companies a job can be filed under, marking the key default.
check_connectionRead onlyConfirms 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.

FieldWhat it changes
expectedVersionThe version from your last read of the job. The edit is refused if someone changed the job since.
customerName, phone, emailThe customer on the job.
address, suite, city, state, zipThe service address. Changing it clears the map location until a new one is picked.
descriptionThe short description technicians see, up to 50 characters.
notesReplaces the job notes, so the assistant includes what should be kept.
jobDateYYYY-MM-DD.
statusAny job status. CLOSED and PENDING_CLOSE are refused until field work is finished.
appointmentDate, appointmentStartTime, appointmentEndTimeReschedules the appointment: a YYYY-MM-DD day with HH:MM 24-hour times.
companyId, metroIdMoves 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 POST to /mcp and is answered in the response body as JSON. There are no sessions, so no Mcp-Session-Id, and GET returns 405.
  • Protocol versions 2025-11-25, 2025-06-18 and 2025-03-26.
  • Notifications get 202 Accepted. Batches are accepted for clients on 2025-03-26.

You can also call it by hand, which helps when checking a key:

bash · initialize
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" }
    }
  }'
bash · tools/call
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.

bash
curl https://api.kjobdone.com/v1/integrations/ping \
  -H "X-API-Key: $KJOBDONE_API_KEY"
200 OK
{
  "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.

bash
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."
  }'
201 Created
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

FieldRequiredDescription
customerNameup to 100YesThe customer's full name.
phoneup to 25YesAny common format. Used to match an existing customer.
emailup to 70NoAlso used to match an existing customer.
addressup to 50YesStreet address of the job.
suiteup to 10NoApartment, suite or unit.
cityup to 30YesCity.
stateup to 30YesUS state, as an abbreviation or full name.
zipup to 10YesPicks the metro the job is filed in.
descriptionup to 2,000NoWhat the job is. Technicians see the first 50 characters; the full text goes into the notes.
notesup to 5,000NoAnything else the dispatcher should know.
jobDateNoYYYY-MM-DD. Defaults to today in the job's timezone.
timezoneNoIANA name such as America/Phoenix. Defaults to your account timezone.
companyIdNoCompany ID (com_…). Defaults to the key's default company.
metroIdNoMetro ID (mtr_…). Defaults to the metro for the ZIP code, then the key's default metro.
  • Where the job goes. An explicit metroId wins. 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 send metroId or 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).

bash
curl "https://api.kjobdone.com/v1/integrations/jobs?search=Lopez&status=SCHEDULED&limit=10" \
  -H "X-API-Key: $KJOBDONE_API_KEY"
200 OK
{
  "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.

bash
curl https://api.kjobdone.com/v1/integrations/jobs/WxNjwF \
  -H "X-API-Key: $KJOBDONE_API_KEY"
200 OK
{
  "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:

bash
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"
  }'
FieldWhat it changes
expectedVersionThe version from your last read of the job. The edit is refused if someone changed the job since.
customerName, phone, emailThe customer on the job.
address, suite, city, state, zipThe service address. Changing it clears the map location until a new one is picked.
descriptionThe short description technicians see, up to 50 characters.
notesReplaces the job notes, so the assistant includes what should be kept.
jobDateYYYY-MM-DD.
statusAny job status. CLOSED and PENDING_CLOSE are refused until field work is finished.
appointmentDate, appointmentStartTime, appointmentEndTimeReschedules the appointment: a YYYY-MM-DD day with HH:MM 24-hour times.
companyId, metroIdMoves 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 with 409 instead 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.

EventSent when
job.createdA job was created, by a dispatcher, the API, MCP or a booking page.
job.assignedA technician was assigned or the job moved to another technician.
job.status_changedThe job status changed. Carries previousStatus.
job.closedThe job was closed. Sent alongside job.status_changed.
job.canceledThe job was canceled. Sent alongside job.status_changed.
document.sentA customer link to an estimate or invoice was sent. Carries data.document instead of data.job.
document.approvedA customer approved and signed an estimate online. Carries signedBy and signedAt.
document.declinedA customer declined an estimate online. Carries declineReason when they gave one.
pingSent by the Send test button, whatever the endpoint subscribes to.
Example delivery
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.

verify-webhook.js
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 id and X-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.

StatusMeaning
400A field is missing, invalid or unknown to an update, or no metro matches the ZIP code. The message says which.
401The API key is missing, wrong or revoked, or the person who created it was disabled.
404No job with that ID or job number in your account.
409The job changed since the expectedVersion you sent. Read it again and check the latest details before retrying.
405GET or DELETE on the MCP endpoint. It is stateless and only takes POST.
5xxSomething 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.

Contact us