WorkGuru Public API V2

Modified on Tue, 29 Sep at 3:22 PM

Welcome to the WorkGuru Public API — a curated, versioned REST API for external integrations, partners, and AI tools. It is decoupled from WorkGuru's internal endpoints, so the contracts here stay stable as the product evolves.

  • Base URL: https://api.workguru.io/api/v2 (Australia / New Zealand) or https://ukapi.workguru.io/api/v2 (United Kingdom).
  • Format: JSON request and response bodies.
  • Lists are always paginated (see the Overview below).
  • Current version: v2.

Create a NEW API key for this API. An existing WorkGuru API key will not work here. Keys created before this API existed carry no scopes, and a key with no scopes is refused on every /api/v2 data resource with 403 No scopes granted (only /oauth/token and /ping sit outside the scope model). Create a key under Settings -> API Keys and grant it the scopes your integration needs.

Do not add scopes to a key an existing integration already uses. Scopes confine a key to /api/v2, so the moment one is granted that key stops working on WorkGuru's older service routes. One key per integration.

Connecting and Authentication

Every endpoint requires a JWT Bearer token (this documentation page, the token endpoints and GET /api/v2/ping are the only anonymous routes). You authenticate with an API key + secret, created under Settings → API Keys in WorkGuru. The owning tenant is resolved from the key, so there is nothing tenant-specific to configure — you only ever see and change data in your own WorkGuru tenant.


There are two ways to get a token from the same API key; both return the same Bearer JWT.

Use the standard OAuth2 client-credentials grant, with your API key as client_id and your secret as client_secret:

POST /api/v2/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id={API_KEY}&client_secret={SECRET}

The response is { "access_token": "…", "token_type": "Bearer", "expires_in": 86400 } (the token is valid for 24 hours), plus a scope field when the key is scoped — see below.

To try endpoints in this portal: click Authorize (top of the page), choose OAuth2, then enter your API key in the API Key field and your secret in the API Secret field, and press Authorize. This portal fetches a token from the endpoint above and attaches it to every Test Request for you — no copy-paste. (Credentials Location can be left on either setting; both work.)

Existing integrations — direct key exchange

If you already integrate with WorkGuru, the original direct key-exchange endpoint (JSON { apiKey, secret } → { accessToken, expireInSeconds, … }) is unchanged and fully supported — your existing integration keeps working with no changes. New integrations should prefer the OAuth2 flow above.

Sending the token

Add the header Authorization: Bearer {accessToken} to every request. If you already have a token (e.g. from your own code) and want to paste it here, click Authorize, choose Bearer, and enter the full value including the word — Bearer {accessToken}.

Scopes

A scope limits what an API key can do through this API, on top of the WorkGuru permissions the key already holds — a request must satisfy both. Scopes are granted per key under Settings → API Keys → Scopes.

There is one scope per resource and operation:

  • workguru.{resource}.read — the GET endpoints for that resource, e.g. workguru.quotes.read.
  • workguru.{resource}.write — everything else on it (POST, PUT, PATCH, DELETE and actions), e.g. workguru.quotes.write.
  • workguru.read — a coarse scope granting read on every resource, including margins, unit costs and supplier pricing. Prefer the per-resource read scopes.

{resource} is the path segment: quotes, purchase-orders, stock-levels, and so on. There is deliberately no coarse write scope, so adding a resource to this API never widens an existing key's write access.


A key with no scopes granted cannot reach any data on this API — every /api/v2 data resource refuses it with 403 No scopes granted, however its token was obtained. Only /oauth/token and /ping, which carry no tenant data, sit outside the scope model. Scopes are how a key reaches this API, not a restriction layered on top of it, so a new key needs the scopes your integration uses before it will do anything here. This is also why an existing WorkGuru API key does not work: it predates scopes and therefore has none.


To narrow a token to less than the key holds, send a space-delimited scope parameter to the token endpoint:

grant_type=client_credentials&client_id={API_KEY}&client_secret={SECRET}&scope=workguru.quotes.read workguru.clients.read

The response echoes what the token actually carries in its scope field. Requesting a scope the key has not been granted returns 400 with {"error": "invalid_scope"} rather than quietly issuing a broader token — so what you asked for is always what you hold. Omit scope and the token carries everything the key holds; name a subset to take a narrower token for a particular job without changing the key.

A request the token's scopes do not cover returns 403 with application/problem+json, naming the scope that would have allowed it in requiredScope (and every scope that would, in acceptedScopes).

Core Concepts

WorkGuru models a job-based business — quoting work, delivering it, and billing for it. The core entities and how they relate:

  • Clients & Suppliers — the companies you sell to and buy from. Contacts are the people attached to a client or supplier (reach them via GET /api/v2/contacts?clientId=…).
  • Leads — prospective work in your sales pipeline, each with a category, stage, and owner.
  • Quotes — priced proposals to a client, built from task lines (labour) and product lines (materials). A quote is issued, then accepted or declined. Accepting a quote creates downstream work — either a Project or a Stock Sale (the accept response tells you which was created).
  • Projects — the delivery of work. They carry the same task/product lines and accumulate real costs from Timesheets (labour staff log against task lines) and Purchase Orders (materials bought from suppliers). A project's lines are two different kinds of thing and their ids say which: a task line is tl_… and a product line is prl_…. To log time, send a task line's tl_… id as taskId on POST /api/v2/timesheets. Send an id back unchanged when updating a line.
  • Production Jobs — a specialised project for manufacturing, adding output lines. They share the Project shape and are update-only (born from converting a quote or from production).
  • Purchase Orders — orders placed with a supplier and received into a warehouse.
  • Stock Sales — selling stock directly to a client, outside of project work.
  • Invoices — billing a client for a project or a stock sale. An invoice's lines carry a type of product, task or purchase, and only product lines can be added through the API: a task line records which project task it bills and a purchase line which purchase order, and that link has no field on the contract because it is established when the line is raised from the project or purchase order itself. Existing task and purchase lines can be updated (send the line's id back) or removed (leave them out), and their links are preserved. Note that id and type are BOTH part of a line's identity — the three collections have independent ids, so a lines array can contain a product line and a task line sharing one id value.
  • Credit Notes — a client credit note credits a client; a supplier credit note records a credit from a supplier and is raised against a purchase order.
  • Assets — anything you keep a service history against (equipment, vehicles, tools, sites, and so on). Attach custom data to them and schedule work against them.

Typical lifecycle: Lead → Quote → (accept) → Project or Stock Sale → Invoice, with Projects drawing costs from Timesheets and Purchase Orders along the way.

Custom Fields

Most entities support tenant-defined custom fields. Read the field definitions for a type, then read or set the values on a specific record:

  • GET /api/v2/custom-fields?entityType=client — the fields defined for that entity type (id, label, data type, whether required).
  • GET /api/v2/custom-field-values?entityType=client&entityId={id} — the values set on one record.
  • PUT /api/v2/custom-field-values?entityType=client&entityId={id} — set values, sending [{ "fieldId": "…", "value": "…" }]. It is a partial upsert: fields you send are updated, others are left untouched, and an empty string clears a field.

entityType accepts: client, supplier, lead, asset, quote, project, production-job, purchase-order, stock-sale, timesheet.

Reference data (looking up ids)

Writes reference other records by id — projectManagerId on a project, warehouseId on a purchase order, a tax rate on an invoice line. These read-only endpoints are how you find those ids:

EndpointUse it for
GET /api/v2/usersprojectManagerId, ownerId, timesheet userId — any field naming a staff member
GET /api/v2/warehouseswarehouseId, required on purchase orders. isDefault flags the tenant's default
GET /api/v2/tax-ratestax on invoice, quote, purchase order and credit note lines

They page and filter like every other list. Three things worth knowing:

  • Inactive records are returned. A user who has left, or a retired warehouse, still appears, so an id on an older record always resolves to a name. Filter on isActive=true when you're choosing something for a new write.
  • modifiedSince is not supported on the lookups, and asking for it returns 400 rather than quietly ignoring it. These are tenant configuration, not transactional records — the backing queries don't track changes on them, so a delta filter would silently hand you everything. Page the full list instead; they are small and change rarely. A few of the configuration lists are served in a fixed order and refuse sort for the same reason, naming the order they do use.
  • Tax rates are directional. Check canApplyToRevenue / canApplyToExpenses before using one — a revenue-only rate on a purchase order is rejected. rate is a percentage, so 10 means 10%.

Each has its own scope (workguru.users.read, workguru.warehouses.read, workguru.tax-rates.read), so an integration can be granted exactly the lookups it needs. They are read-only: all three are tenant configuration managed in the WorkGuru app.

API Overview

Every resource follows the same shape:
ActionVerb & Path
List (paginated)GET /api/v2/{resource}
Get oneGET /api/v2/{resource}/{id}
CreatePOST /api/v2/{resource}
UpdatePUT /api/v2/{resource}/{id}
DeleteDELETE /api/v2/{resource}/{id}
  • Delete returns 204 No Content on success, or 409 when the record still has dependents (e.g. a client with quotes/invoices, or a purchase order that's been received). Not every resource is deletable: assets, clients, client and supplier credit notes, invoices, leads, projects, purchase orders, quotes, stock sales, suppliers and timesheets have a delete; the rest (contacts, payments, production jobs, project tasks, stock adjustments, stock usage, custom-field values and the lookups) do not. Timesheets also support bulk create: POST /api/v2/timesheets/bulk with a JSON array of entries returns { "created": N }.
  • Not every resource has every verb, and where one is missing it is because the operation does not exist in WorkGuru rather than because it hasn't been exposed yet. Stock adjustments are the clearest case: they have list, get, create and revert, but no update and no delete. An adjustment writes its stock movements the moment you create it, so there is nothing to edit — and undoing one is a real operation that has to check the stock it moved is still there, not a row deletion.
  • Some records have to be reopened before they can be changed. A project or production job whose status is Completed answers 409 to a PUT, naming the endpoint to use: POST /api/v2/projects/{id}/incomplete and POST /api/v2/production-jobs/{id}/uncomplete. These are not a status write dressed up — reopening a project raises the events and audit entry the app raises, and reopening a production job reverses the stock its completion booked in. That second one can legitimately fail: if the stock produced has since been sold or used, the reversal is refused with 400 naming the product, because there is nothing left to reverse. Reopen first, edit second.
  • Actions that aren't CRUD live at POST /api/v2/{resource}/{id}/{action} — approve, accept, decline, revert, incomplete, uncomplete and so on. They exist wherever the operation does more than set a field, which is most of the time in a system that moves stock and money. Prefer them over writing the equivalent status through a PUT: the endpoints run the side effects, and several status fields are refused outright on a write for exactly that reason.
  • sentToAccounting filters invoices, purchase orders, payments and stock adjustments on whether they have been pushed to your accounting system — false for what is still outstanding, true for what has gone, omit it for everything. It is the same spelling on all four, so one sync can ask them the same question. "Gone" includes documents a user marked Skip send to accounting: those are not outstanding, but were never sent, and carry a sentToAccounting date of 1900-01-01.
  • modifiedSince does not report deletions. A deleted record simply stops appearing, so a delta sync learns about new and changed records only. To catch deletions, periodically page the full list and compare ids.
  • Creates are not idempotent — retry with care. If a create times out or you lose the response, the record may well have been written. Retrying makes a second one. This matters most on POST /api/v2/timesheets/bulk: a timesheet has no natural uniqueness (the same person legitimately books the same length against the same task twice in a day), so nothing can tell your retry from a real duplicate, and a retried 500-row batch creates 500 more time entries — which feed labour cost, project actuals and WIP. Before retrying a create, read back the affected range and check. The same applies to invoices, purchase orders and credit notes, where two retries mean two numbered financial documents.
  • Pagination — pageNumber (1-based) and pageSize (default 10, max 100); responses include totalCount and totalPages.
  • Sorting — sort takes a field name; prefix with - for descending (e.g. -date). The sortable fields differ per resource, so an unrecognised one returns 400 listing the fields that resource does accept — the quickest way to discover them. -createdAtUtc (newest first) works on every transactional resource. Two exceptions: stock-levels is a computed current position rather than a stored record and so has no creation time, and a few of the configuration lookups are served in a fixed order and refuse sort outright, naming the order they use.
  • Errors — the body is RFC 7807: title, status, detail and a correlationId to quote if you need to ask us about one. A validation failure (a malformed body, a bad id, a missing required field) adds an errors object naming each failing field. Error bodies carry Content-Type: application/problem+json.
  • Unknown routes — a path under /api/v2 that matches no endpoint answers a bodiless 404, and an unsupported version such as /api/v3/... answers with the versioning library's own error body. Neither is problem+json; treat them as "wrong URL".
  • Authorization failures — 401 when no valid token was presented, 403 when the token is valid but the key or user lacks the permission the endpoint requires. Both carry the same problem body. No endpoint under /api ever answers with a redirect, so a 3xx from this API means something is wrong; report it.
  • Endpoints — browse the full list in the sidebar.

Was this article helpful?

That’s Great!

Thank you for your feedback

Sorry! We couldn't be helpful

Thank you for your feedback

Let us know how can we improve this article!

Select at least one of the reasons
CAPTCHA verification is required.

Feedback sent

We appreciate your effort and will try to fix the article