A JSON REST API over the same data the app and the dashboard use. Book work from your own website, push jobs in from another system, or pull yesterday’s numbers into a spreadsheet — and get told when something changes rather than polling for it.
Base URL
https://fsm.foundrcode.com/api/v1
Every request needs a bearer token and an Accept: application/json header. Sign in to get one:
curl -X POST https://fsm.foundrcode.com/api/v1/auth/token \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"…","device_name":"My integration"}'
Then send it on everything else:
curl https://fsm.foundrcode.com/api/v1/jobs?status=scheduled \
-H 'Accept: application/json' \
-H 'Authorization: Bearer lum_…'
Two kinds of token
Signing in issues a token that acts as that person, so every permission the dashboard enforces still applies. An integration key, created in Settings, belongs to the company instead and is limited by its scopes alone.
Scopes
A key only does what you tick. Read and write are separate for every resource, so a key that files jobs cannot read invoices.
One company per key
A token reaches exactly one company. There is no company parameter anywhere — the token decides, so no request can be made to cross into another.
Rate limits
Sixty requests a minute per key by default. Over that you get 429 with a Retry-After header saying how long to wait.
Available scopes
jobs:read
jobs:write
customers:read
customers:write
invoices:read
invoices:write
technicians:read
contracts:read
contracts:write
parts:read
reports:read
dashboard:read
Every request carries a bearer token. Tokens are issued to a person by signing in, or created as integration keys in Settings — those belong to the company rather than to anybody, and can do only what their scopes allow.
/api/v1/auth/token
Exchange an email and password for a token.
Body
emailpassworddevice_nameReturns token, token_id, scopes, user, company
Rate limited. Five failures from one address and it stops answering for five minutes.
/api/v1/me
any valid token
Who this token belongs to, and what it may do.
Returns user, company, scopes
/api/v1/auth/revoke
any valid token
Revoke the token making the request.
Returns revoked
An account may run several businesses. A token always reaches exactly one — switching mints a new token and revokes the old one, so a key that leaks still reaches a single business.
/api/v1/businesses
a token issued to a person
The businesses on this account, with is_current marking the one this token is for.
Returns data[]: id, name, timezone, currency, brand_color, accent_color, is_current
An integration key belongs to a company rather than a person, so it gets 403 here — there is no account behind it to enumerate.
/api/v1/businesses/{company}/switch
a token issued to a person
Move this device to another business on the same account.
Returns The same body as signing in. Store it the same way.
A company that does not exist and one that is not yours answer identically, so ids cannot be enumerated.
The work itself. Creating a job stamps the company from the token — a customer_id belonging to somebody else produces a job nobody can read rather than a cross-tenant link.
/api/v1/jobs
jobs:read
List jobs.
Query
statustechnician_idfromtoReturns data[] of jobs with customer, site and technician
/api/v1/jobs/{job}
jobs:read
One job in full.
/api/v1/jobs
jobs:write
Book a job.
Body
customer_idtitlesite_idservice_idscheduled_startscheduled_endassigned_technician_idpriorityCounts against the plan’s monthly job allowance. Past it, this answers 403 plan_limit_reached.
/api/v1/jobs/{job}
jobs:write
Update a job, including moving it through its statuses.
Body
statuscompletion_notes/api/v1/jobs/{job}/signature
jobs:write
Attach the customer’s signature, closing the job out.
Body
datasigner_namesigner_titlelatlngsigned_atclient_uuidSend this before marking the job completed, so the record is never a completion with no evidence behind it.
Customers and the sites they own. A customer’s portal token is never returned — it is a bearer credential for a page anyone holding it can open.
/api/v1/customers
customers:read
List customers.
Query
search/api/v1/customers/{customer}
customers:read
One customer, with their sites.
/api/v1/customers
customers:write
Create a customer.
Body
nametypeemailphoneCounts against the plan’s customer limit.
Money owed and money taken. Nothing here exposes a part’s cost price — what you paid a supplier is not what your customer is shown.
/api/v1/invoices
invoices:read
List invoices.
Query
status/api/v1/invoices/{invoice}
invoices:read
One invoice, with its lines.
/api/v1/invoices/{invoice}/payments
invoices:write
Record a payment against an invoice.
Body
amountmethodThe supporting records: recurring work, stock, and who is on the team.
/api/v1/contracts
contracts:read
Recurring job schedules.
Query
active/api/v1/contracts/{contract}
contracts:read
One contract and its next visit.
/api/v1/contracts/{contract}
contracts:write
Pause or resume a contract.
Body
is_active/api/v1/parts
parts:read
Stock levels.
Query
low_stock/api/v1/parts/{part}
parts:read
One part.
/api/v1/technicians
technicians:read
The team, with their current status.
Aggregates, for a dashboard of your own.
/api/v1/dashboard
dashboard:read
Today at a glance: booked, done, unassigned, outstanding.
/api/v1/reports/jobs
reports:read
Jobs by status and by technician over a period.
Query
fromto/api/v1/reports/revenue
reports:read
Invoiced and collected over a period.
Query
fromto/api/v1/reports/technicians
reports:read
Per-technician completion and hours.
Where to send a push notification. Used by the mobile app; you are unlikely to need these directly.
/api/v1/devices
a token issued to a person
Register this device for notifications.
Body
tokenplatformdevice_name/api/v1/devices
a token issued to a person
Stop notifications to this device.
Body
tokenRather than asking every minute whether anything changed, give us a URL and we will tell you. Add endpoints in Settings, choose the events you care about, and each one gets its own signing secret.
| Event | Fires when |
|---|---|
job.created |
A job was booked, by anyone — a dispatcher, the API, or a recurring contract. |
job.status_changed |
A job moved. The payload carries both the old status and the new one. |
job.completed |
A technician finished, with the completion notes and the time on site. |
invoice.created |
An invoice was raised against a job. |
invoice.paid |
An invoice was settled in full. Fires once, not once per payment. |
customer.created |
A new customer record, however it arrived. |
Every request carries an X-Lumina-Signature header: an HMAC-SHA256 of the raw body, keyed with your endpoint’s secret. Compare it against the raw body before you parse it — a re-encoded payload will not match, and an unverified webhook URL is one anybody who finds it can post to.
// Verify before you trust anything in the body.
$expected = hash_hmac('sha256', $rawBody, $yourEndpointSecret);
if (! hash_equals($expected, $request->header('X-Lumina-Signature'))) {
abort(401);
}
Retries
A 5xx or a 429 is retried with a widening gap. A 4xx is not — your endpoint has said the request is wrong, and sending it again will not change that.
Dead endpoints
After twenty consecutive failures an endpoint switches itself off, and we tell you. Nobody wants a queue quietly filling for a server that was decommissioned last month.
Order and duplicates
Delivery is not ordered and, on a retry, not exactly once. Key on the record id and treat a repeat as a no-op.
Failures answer with JSON carrying an error key. Two are worth reading twice: a 404 covers both "no such record" and "belongs to another company", deliberately, and a 402 means the account itself has lapsed rather than anything about your request.
| 401 | invalid_token |
The token is wrong, revoked, or belongs to a deactivated user. |
| 402 | subscription_required |
The company’s subscription has lapsed. Nothing is readable until it is settled. |
| 403 | insufficient_scope |
A valid token without the scope this route needs. |
| 403 | plan_limit_reached |
The company’s plan does not allow another of these this month. |
| 404 | — |
No such record, or one belonging to another company. The two are deliberately indistinguishable. |
| 422 | — |
Validation failed. The body names each field and what was wrong with it. |
| 429 | — |
Rate limited. Retry-After says how long to wait. |