Ownware
Home›Jobora›API
Jobora · API

Jobora API, as shipped in the download

Jobora API

Jobs, the schedule, quotes and invoices, over HTTP and over MCP. Everything below runs against your installation — there is no Jobora cloud, no relay and no telemetry.

Authentication

Mint a key in Settings → API & webhooks. It is shown once and stored as a SHA-256 hash.

Authorization: Bearer apk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Key scope — read or full

Every key carries a scope, chosen when you mint it.

ScopeWhat it can do
readGET, HEAD and OPTIONS. Any other method is refused with 403 at the door, before a route runs.
fullEverything the key's user role allows.

The refusal is enforced once, in Api::requireKey(), rather than in each route — the route that forgets is exactly the one that would leak. On the MCP endpoint the same scope hides write tools from tools/list and refuses them by name on tools/call.

A key never exceeds its user's role: a key minted by a member cannot do what that member could not do in the browser, and a viewer's key is refused every write with 403 — on /api/* as well as on the agent endpoint (from 1.0.4; before, the REST writes checked only the key's scope).

curl -H "Authorization: Bearer apk_xxxx" https://jobora.example.com/api/ping
# {"ok":true,"user":"Sam","app":"jobora","version":"1.0.5"}

Endpoints

The machine-readable spec is at /api/openapi.json.

MethodPathWhat it does
GET/api/pingVerify a key
GET/api/customersCustomers on file
GET/api/jobsJobs, newest 200. `?status=scheduled\in_progress\on_hold\completed\cancelled`
POST/api/jobsRaise a job (customer_id, title required)
GET/api/jobs/{id}One job with its visits, costs, margin, sign-off state and invoice
POST/api/jobs/{id}/statusMove it along. Refused 409 when the move is not legal
GET/api/visitsBooked visits in a window (?from=&to=)
POST/api/visitsBook a visit. Refused 409 when that technician is already out
GET/api/quotesQuotes or invoices (`?kind=quote\invoice`)
GET/api/quotes/{id}One document with lines and totals
POST/api/quotes/{id}/acceptAccept a quote — this creates the job

Money and time, so a caller is never surprised

  • Money is integer cents, always, and Jobora computes it. You send quantities and unit prices; subtotal, discount, tax and total come back computed. A caller cannot post a total.
  • margin_cents and margin_pct are null when nothing was quoted. That means unknown, not broke even. A job with £600 of cost and no quote is not a £600 loss — nobody priced it here.
  • Schedule times are local wall-clock minutes ("09:00"), not timestamps. A field day is planned in wall-clock, and storing an instant would move every future booking by an hour at a daylight-saving change.

Booking a visit is refused the same way everywhere

curl -X POST https://jobora.example.com/api/visits \
  -H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
  -d '{"job_id":12,"tech":"Dan Whitlow","visit_date":"2026-08-24","start":"09:00","end":"11:00"}'
# 409 {"error":"conflict","detail":"Dan Whitlow is already booked 09:00–11:00 that day."}

That sentence comes from Schedule::conflict() — the same function the drag-and-drop board, the booking form and the MCP tool call. There is one rule, and four doors that ask it.

Connect it to an assistant

Mint the key in the app first: Settings → API keys. Choose Read only when the assistant should answer questions but never change anything — the endpoint then lists only the read tools and refuses the rest by name, so a careless prompt cannot write. Full access behaves as before.

Every client needs the same three facts, and nothing in the handshake is vendor-specific:

The addresshttps://jobora.example.com/mcp
The keyheader Authorization: Bearer apk_xxxx
The transportMCP over streamable HTTP, stateless

Claude — one command, or the same URL and header as a custom connector in the desktop and web apps:

claude mcp add --transport http jobora https://jobora.example.com/mcp \
  --header "Authorization: Bearer apk_xxxx"

ChatGPT and the OpenAI API — one entry in the Responses API's tools array (in ChatGPT itself, the same URL and key go in as a connector):

{
  "type": "mcp",
  "server_label": "jobora",
  "server_url": "https://jobora.example.com/mcp",
  "authorization": "apk_xxxx",
  "require_approval": "never"
}

Own Your AI reads a list of servers in this shape:

{
  "mcpServers": [
    { "id": "jobora", "name": "Jobora", "url": "https://jobora.example.com/mcp",
      "token": "apk_xxxx", "enabled": true }
  ]
}

**Every other client spells the same three facts differently — copy the shape from its own documentation, not from here.** VS Code is the clearest example of why: its configuration reference (read 6 September 2026) puts servers in .vscode/mcp.json under a "servers" object — *"an object that maps server names to their configurations"* — not an mcpServers array. Pasted as-is, the block above will not load there. The id, the URL and the token are what travel; the JSON around them belongs to whichever client you are configuring.

A local model, n8n, or your own code — n8n's MCP Client node takes the URL and the same Authorization: Bearer header; a model running on your own machine reaches it through any MCP client, so nothing leaves your network at all. Writing it yourself is one POST of JSON-RPC 2.0:

curl -X POST https://jobora.example.com/mcp \
  -H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Any client that speaks MCP's streamable-HTTP transport works, including ones that do not exist yet: Jobora implements the protocol, not an integration with a particular vendor.

Tools

ToolWrites?What it does
list_jobsnoJobs with customer, status, quoted, cost and margin
job_detailnoOne job: visits, costs, whether it is signed off, its invoice
schedule_daynoWho is where on a date, in start-time order
margin_reportnoQuoted against actual across completed jobs; unquoted jobs counted separately
create_jobyesRaise a job for a customer
book_visityesPut a job on the schedule; refused on a clash
advance_jobyesMove a job along its life cycle; a completed job is never reopened
accept_quoteyesAccept a quote, which converts it into a job — once

There is deliberately no sign-off tool, no delete tool and no record-payment tool. A customer signature is a person at a door agreeing the work is done; a machine cannot hold the pen, and a forged one is worse than a missing one. Deleting a job would take its sign-off and photo evidence with it. An agent can plan your week and tell you which jobs lost money — it cannot sign for work or erase the record of it.

Webhooks

Add them in Settings → API & webhooks. Each delivery carries X-Jobora-Signature: sha256=<hmac> over the raw body, using the secret shown when you created it.

EventFires when
job.createda job is raised, from any door
job.scheduled, job.in_progress, job.on_hold, job.completed, job.cancelleda job moves to that status, from any door
job.signed_offa customer signs
quote.accepteda quote converts into a job, from any door

These are exactly the events the settings page offers (from 1.0.4; before, it offered another product's events and none of these could be ticked).

Errors

Standard HTTP, with a body: {"error":"conflict","detail":"…"}.

CodeMeans
401missing or unknown key
403the key is read-only and this is a write, or the role is not allowed
404no such record
409the product refused: a clash, an illegal status move, an already-converted quote
422the input is wrong (bad date, missing field)

Limits, honestly

  • No pagination beyond the caps. Lists return the newest 200 (500 for MCP list_jobs). A field business does not have a million jobs, and a cursor nobody needs is a cursor nobody tests.
  • No rate limiting on the API. It is your server; the login has a rate limit, the API does not.
  • No webhook retries. A delivery is attempted once and the result is recorded. If your endpoint was down, re-read the record — do not expect Jobora to replay it.

← Back to Jobora · Manual · Quickstart · Test run

Affiliate program
Recommend tools people own — earn 35% on every sale. 90-day tracking, instant delivery, payouts by Lemon Squeezy.
Become an affiliate →