Developers

API changelog

Every change to the API, webhooks, MCP server and developer documentation, dated as it ships. Newest first. Machine-readable feed: changelog.xml (Atom). Live availability: status.flexonthejob.com.

Additive onlyNew fields, endpoints, events and tool parameters may appear at any time. Nothing existing is renamed, removed or changes type inside /api/v1.
12 months' noticeA breaking change ships as a new prefix (/api/v2); the old one keeps working for at least a year after a dated notice here.
Generated from codeThe OpenAPI document and llms-full.txt are built from the running server, so they never lag the contract.

Version 1.0.0-beta.61: Assistant location lists lead with the most valuable items Fixed

  • GET /health now reports "version": "1.0.0-beta.61".
  • In-app AI Assistant: a location's stock list (up to 25 items, totals over all) now leads with the highest stock value, or the most units when the user's role can't see value, and says in plain words how many items it doesn't list. Previously it listed the first 25 by name, so a "biggest items" summary could miss one, and an answer could mention "more items beyond what's shown" with no list shown (found in a third-party re-check of beta.60).
  • No change to /api/v1, MCP or webhook payloads.

Version 1.0.0-beta.60: Assistant no longer repeats lookup notes Fixed

  • GET /health now reports "version": "1.0.0-beta.60".
  • In-app AI Assistant: in a long conversation it could copy the format of its own earlier lookup notes instead of calling a tool, showing an invented figure followed by raw lookup output (found in a third-party re-check of beta.59). Lookup notes are no longer stored in the assistant's own words, and a reply that contains one is never shown: the assistant is asked to look the data up and answer again.
  • No change to /api/v1, MCP or webhook payloads.

Version 1.0.0-beta.59: Assistant stock by location and better item search Added

  • GET /health now reports "version": "1.0.0-beta.59".
  • The in-app AI Assistant can read stock by location (on hand per location, and what is stocked at a location and the locations inside it) and an item's unit cost. Stock value totals also need ViewReports.
  • The Assistant's item search matches every word in any order, handles plurals, also searches barcodes and categories, and ranks exact name, part number and barcode matches first.
  • No change to /api/v1, MCP or webhook payloads.

Version 1.0.0-beta.58: AI Assistant in the mobile app Added

  • GET /health now reports "version": "1.0.0-beta.58".
  • The Flex mobile app can use the AI Assistant module. Turns still run on the hosted service against live data and the organization's monthly allowance; the app is a chat window over it.
  • No change to /api/v1, MCP or webhook payloads. The app's endpoints are first-party and not part of the public API.

Version 1.0.0-beta.57: delete draft invoices and photos, replay-safe webhooks, MCP parity Added

  • GET /health now reports "version": "1.0.0-beta.57".
  • DELETE /api/v1/invoices/{id} deletes a draft invoice (a draft never needs voiding). It needs ManageInvoices and an Idempotency-Key. Returns 204; 404 not in your organization; 409 when the invoice is not a draft, is consolidated or has recorded payments. The job's down payment and any applied store credit are returned, and the new invoice.deleted webhook event fires.
  • Job photos: GET /api/v1/jobs/{id}/photos lists them, newest first (also as CSV); GET /api/v1/jobs/{id}/photos/{photoId}/content downloads the image; DELETE /api/v1/jobs/{id}/photos/{photoId} deletes one (ManageJobs, 204). Only Photo entries can be deleted, never signatures, notes or status changes. The web app gained a delete button on each photo.
  • Webhooks are replay-safe. Every delivery attempt also carries X-Flex-Timestamp (unix seconds), X-Flex-Signature-V2: t=<ts>,v1=<hex> (HMAC-SHA256 of <ts>.<raw body> with the subscription secret), X-Flex-Delivery (the same on every retry, so you can de-duplicate) and X-Flex-Event. Retries are re-signed with a fresh timestamp. Reject a t more than 5 minutes from your clock. The body-only X-Flex-Signature: sha256= header is unchanged, so existing receivers keep working. The TypeScript and Python SDKs gain a timestamped verifier.
  • MCP: new tools for job photos (list, get, upload, delete), deleting draft invoices, creating, updating and deleting roles, and updating customers. Every /api/v1 operation except webhook management now has an MCP tool (139 tools).
  • AI Assistant: it remembers the conversation, and a request to change something shows an Approve / Decline card with a preview instead of a question in text. Purchase orders can be drafted for any item with a chosen quantity and vendor.
  • Additive only: nothing existing in /api/v1, MCP or webhook payloads changed.

Version 1.0.0-beta.56: attach photos to jobs Added

  • GET /health now reports "version": "1.0.0-beta.56".
  • POST /api/v1/jobs/{id}/photos attaches a photo to a job. Send multipart/form-data with the image in a part named file and an optional caption (up to 2000 characters), e.g. curl -F "[email protected]" -F "caption=Rust on the bottom panel". JPEG and PNG only, detected from the file's bytes; up to 10 MB. It needs ManageJobs (the same permission as adding a job update in the app) and an Idempotency-Key; a retry with the same key and the same file replays the first response even if your client picks a new multipart boundary. Returns 201 with the new timeline entry (type Photo); 400 no file or caption too long, 404 job not in your organization, 413 over 10 MB, 415 not a JPEG or PNG.
  • The photo is the same kind of entry the field app creates from the phone camera: a thumbnail on the job's page in the web app, synced to every device, and included under attachments/ in Export All Data. The web app gained the same thing as an Add Photo button in Job Updates.
  • Timeline entries (updates on GET /api/v1/jobs/{id}) have a new attachmentName field: the image's file name on Photo and Signature entries, null otherwise.
  • AI Assistant: an organization on a fixed monthly usage limit, such as the Copperline demo company shared with reviewers ($0.25 a month), no longer sees top-up options in Settings → AI Assistant, and the chat says the limit resets at the start of next month. Top-ups are refused for such an organization.
  • Additive only: nothing existing in /api/v1, MCP or webhook payloads changed.

Version 1.0.0-beta.55: delete roles over the API; sandbox fixes Added Fixed

  • GET /health now reports "version": "1.0.0-beta.55".
  • DELETE /api/v1/roles/{id} deletes a custom role, like Roles → Delete in the app. It needs ManageRoles (the same permission as POST and PATCH /roles) and an Idempotency-Key, and returns 200 with the role id and a confirmation. A role still held by any account is refused with 409; remove it from them first, or repeat with ?unassignUsers=true to remove it from those accounts in the same call. Either way, retry with a new Idempotency-Key: the 409 is replayed for the old one. Built-in roles cannot be deleted; a role outside your organization is 404.
  • Fixed (sandbox): webhook events on sandbox.flexonthejob.com are now recorded as Skipped in Settings → Webhooks → Delivery Log, with the reason that delivery is switched off there, not "Pending, 0 attempts". This matches how the sandbox records automation runs. Production and trial accounts deliver as before; no API field changed.
  • Fixed (sandbox): the sandbox AI Assistant's demo script now matches questions by whole words with weighted topics, so "draft a purchase order" gets a purchase-order answer, not the open-invoices one. It also recognises estimate, transfer and schedule questions (and says the Assistant does not handle estimates or stock transfers yet, rather than listing them as things it can show), every reply carries the same "scripted example, not a live AI answer" label, and when nothing in the script fits it says so, lists what it can show, and points to a trial account for real answers.
  • Additive only: nothing existing in /api/v1, MCP or webhook payloads changed.

Version 1.0.0-beta.54 Changed Fixed

  • GET /health now reports "version": "1.0.0-beta.54". This entry was added after the release: beta.54 went out on 2026-09-29 without one, and every change it carried is listed here.
  • Sandbox: automations are switched off, and the log says so. On sandbox.flexonthejob.com you can still create and edit automation rules, and a matching event still evaluates the rule, but its action is not carried out. The run shows up in Settings → Automations → Run Log as Skipped with the reason, not stuck at Pending, and both pages show a "switched off in the demo sandbox" notice. Trial and production accounts are unchanged: active automations run.
  • Sandbox: the AI Assistant answers from a labelled demo script. The sandbox assistant does not call an AI model or read live data. It replies from a small fixed script, and every reply says so. It is limited to 20 answers per user per hour. To use the real assistant against your own data, use a trial account.
  • Sandbox: scheduled export is not available. Settings → Export All Data → Schedule exports now says it isn't available in the demo sandbox instead of showing a delivery form. Full export (the one-off zip download) is unchanged.
  • Fixed: the Export All Data card in Settings said photos and signatures were left out, which contradicted the export's README and manifest. It now says they are included as files under attachments/, which is what the zip contains. The manifest reports how many were included; an organization with no photos shows 0.
  • Calendar reminder tasks: a dated to-do on the scheduling calendar that is not a job (no customer, items or workflow). You can assign it and mark it complete in the web, back-office and field apps. It needs the Scheduling module. Export All Data now includes a ReminderTasks table, described in the data dictionary. GET /api/v1/schedule is unchanged and still returns jobs only.
  • New reports: average revenue per job, estimate-to-job conversion rate, and maintenance and installation customer breakdowns. Invoices can add line items from the item catalog, and the Jobs page has a "New Job" button next to "New Estimate".
  • The sandbox server's description in /openapi/v1.json now lists what does not run there, including automation actions (logged as skipped) and the live AI assistant.
  • No API change. /api/v1, MCP tools and webhook payloads are the same as in beta.53.

List labor rates; jobs return their custom charges Added

  • GET /api/v1/labor-rates (and /{id}) plus the MCP list_labor_rates tool — list a job's labor rates so an agent can pick a laborRateId when adding a labor line. Paged and CSV-capable, gated on ViewJobs.
  • A job now returns its individual custom charges with their ids (customCharges[] on the job detail, over both the REST API and MCP get_job), so an agent can discover a charge and then edit or remove it. Internal cost fields remain omitted from the read surface.

Sign in with Google (single sign-on) Added

  • You can now sign in to the Flex web app with Google. It links to your existing Flex account by verified email — SSO never creates accounts — and administrators can use it too, always with two-factor.
  • SAML / enterprise OIDC is on the roadmap. No API change.

Admin sandbox access for reviewers and auditors Changed

  • Reviewers and auditors can now request temporary admin access to the sandbox — a sandbox admin login and an admin-scoped API key — to test (not just read about) the admin-gated features: required two-factor, API-key expiry, IP allow-lists, the activity log, full export, and user and role writes.
  • Available privately on request via [email protected] or [email protected]. The public read-only and write keys are unchanged. No API change.

Zapier and Make connectors (private beta) Added

  • Flex apps for Zapier and Make, built on our webhooks and write API: four instant triggers (job.completed, invoice.paid, inventory.low_stock, customer.created), Create Customer and Create Job actions, and a Find Customer search.
  • They are in private beta — public listing in the Zapier and Make catalogs is in progress; early access is available on request via [email protected]. No API change: both use the existing public /api/v1 endpoints and webhooks with an flx_live_ key.

Manage webhooks over the API + connection test Added

  • GET/POST/DELETE /api/v1/webhooks (and GET /api/v1/webhooks/events) let an integration register and remove webhook subscriptions programmatically — the "REST Hooks" surface Zapier and Make use for instant triggers. POST returns the HMAC signing secret once.
  • GET /api/v1/me returns the key's organization and permissions, for validating a connection. Both are additive to /api/v1; the OpenAPI doc, /docs and llms.txt describe them.

AI Assistant: top-up controls Changed

  • Manage AI Assistant top-ups under Settings → AI Assistant: set a monthly cap on automatic top-ups, and (with "require approval" on) approve or dismiss each top-up request in-app before any charge.
  • Adds a per-turn usage record for accounting and a safeguard against charging a top-up twice.

AI Assistant (optional module) Added

  • A new opt-in module: an in-app assistant that answers questions about your business in plain language and does busy-work for you — find a customer's open invoices, see what's low on stock, draft a purchase order, create an invoice from a job. No API keys to manage; we run the AI for you.
  • It acts with your own data and permissions and confirms before making any change. Includes $35 of usage a month with optional top-ups. Turn it on under Modules (an admin acknowledges the data-processing terms); manage usage under Settings → AI Assistant.

Automations: no-code "when this, do that" rules Added

  • Set up rules that run themselves — for example, when a job is completed, create its invoice; when an item runs low, draft a purchase order to its preferred vendor. Start from a template or build your own under Settings → Automations.
  • Each rule watches for an event, checks optional conditions, and runs an action: email a notification, create an invoice from a job, draft a purchase order, assign a job, set a job status, mark an invoice sent, or queue an "on my way" text. A run log shows what fired, and a rule that keeps failing is paused automatically.

Web app now shares the API's editing engine Changed

  • The web app's job line editing (materials, labor, custom charges) and draft-invoice editing now run through the same Flex.Core services as /api/v1 and the MCP tools, so pricing, counter-sales stock, the billed-line freeze and validation are identical no matter which one you use. No API change — the wire contract and every endpoint are unchanged.
  • One behaviour is now consistent everywhere: in counter-sales mode, lowering a line's quantity below what has already been physically delivered is rejected with a clear message (use returns instead) rather than silently pinned to the delivered amount — the rule the API already enforced.

Edit draft invoices over the API Added

  • PATCH /api/v1/invoices/{id} edits a draft invoice's header — invoiceNumber, invoiceDate, dueDate, taxRate, terms, notes, paymentInstructions (partial update; only a draft; a finalized invoice is immutable). Totals aren't set by hand — they come from the job.
  • POST /api/v1/invoices/{id}/refresh-from-job recomputes a draft's total from its job's current unbilled amount (the app's "Update draft from job"), so after you change the job's lines the invoice follows. New MCP tools update_invoice and refresh_invoice_from_job (126 tools). Together with job line editing, a job and its invoice can now be built and adjusted end to end over the API.

Edit job line items over the API Added

  • Add, edit and remove a job's material, labor and custom-charge lines: POST/PATCH/DELETE /api/v1/jobs/{id}/items/{itemId}, …/labor/{laborId} and …/charges/{chargeId}. Every write returns the updated job (same shape as GET /api/v1/jobs/{id}).
  • Same rules as the app: a cancelled job is read-only, lines on a finalized invoice change only through returns, material quantities move only while Planned (or in counter-sales), tier pricing and counter-sales stock are handled server-side. Nine new MCP tools (add_job_item, update_job_item, remove_job_item, and the same for _labor and _charge) — 124 tools total. Invoice editing follows.

Edit jobs over the API Added

  • PATCH /api/v1/jobs/{id} edits a draft or estimate job's core fields — jobType, description, notes, scheduledDate, customerId, customerAddressId. Partial update (only sent fields change); the job is repriced afterward; status changes keep their own endpoint. New MCP tool update_job (115 tools).
  • Same rules as the app: only editable-status jobs, org-scoped customer/address/status. Line items, labor and invoice editing follow.

Full export now includes photos Added

  • Settings → Export All Data (and scheduled exports) now bundle your photos and signatures as files under attachments/<SyncId>.<ext>, alongside the JSON/CSV tables and data dictionary. A full export is now genuinely complete.
  • No API change.

Five more agent recipes Added

  • The MCP server now ships 13 ready-to-run recipes (prompts). New: fulfill_ready_sales_orders, stock_valuation_report, end_of_day_summary, onboard_customer and update_costs_from_vendor.
  • Each starts with get_org_capabilities, says which permission to grant if one is missing, and confirms before any write. They use only existing tools — no API change.

Webhook events for purchasing and sales Added

  • Six new outbound events: purchase_order.created, purchase_order.status_changed, sales_order.created, sales_order.status_changed, vendor.created and vendor.updated. Status-change events carry data.previousStatus.
  • They fire on every write path — API, web app and synced devices — and are signed exactly like the existing events (X-Flex-Signature, HMAC-SHA256 of the body). Existing subscriptions are unaffected; new events are only delivered to subscriptions that select them.
  • No breaking change; additive only.

Fixed: scheduled data-export and other automated emails Fixed

  • A scheduled Export all data now emails its download link. The export zip was built and stored, but the email was never sent because the send path expected a signed-in web request; server-run (background) sends now resolve the organization directly.
  • The same fix covers other emails our servers send outside a page action (for example an admin-account change taking effect). Emails sent from a normal page action were unaffected.
  • No API change.

Sign-in and browser hardening Changed

  • Browser sign-in now locks an account for five minutes after five wrong passwords, matching the installed apps, and repeated attempts from one address are rate-limited. The sign-in page returns one generic message for a wrong password, an unknown email, a locked account or a deactivated account, so it can't be used to enumerate accounts.
  • Signing out of the web app now invalidates the session on our servers, not only in the browser.
  • Session and anti-forgery cookies are marked Secure, and every response carries X-Content-Type-Options, Referrer-Policy, X-Frame-Options and a Content-Security-Policy (frame-ancestors 'self'; object-src 'none'; base-uri 'self').
  • GET /api/v1/users now returns each user's email only to keys that hold ManageUsers; the response shape is unchanged (the field is null otherwise). No other API change.

Sandbox data: Copperline Heating & Air Changed

  • The sandbox's demo organization is now Copperline Heating & Air, a fictional HVAC company with technicians, customers, jobs, invoices and a few hundred stocked parts. It is reset from its master copy every night at 08:00 UTC, as before.
  • The public read-only and write keys are unchanged: same strings, same permissions, same 30 requests/minute. Record ids you saved from the old demo data no longer exist; list endpoints show the new ones.
  • Records you create in the sandbox get ids from 1,000,000,001 up, so they never clash with the demo data, and they are gone after the next reset.
  • No API change.

Version 1.0.0-beta.34 Changed

  • GET /health now reports "version": "1.0.0-beta.34". It covers every change dated 2026-09-23 and 2026-09-24 below. From now on the version moves with every production release that changes behaviour; build still names the exact commit that is running.
  • The sandbox's /openapi/v1.json is the same document as production's except for the order of servers: each host lists itself first, so "Try it" in the API reference calls the host you are reading. Paths, schemas and info.version are identical, so the files' hashes differ even though the API does not.
  • No API change.

Security hardening: device sync, sign-in and two-factor Changed

  • Device sync is bound to the device's organization. Sync, billing links and email sending from a desktop, phone or office app need that device's own token for the organization being synced, and an active person of that organization. Once a device's token is replaced, the old one is refused. Signing in on a new install adds its own registration and never takes over another install's. An install registered to one organization no longer syncs for a person from another organization; register it again for that organization.
  • Sign-in gives one answer for an unknown email, a missing password and a wrong password, including while the account is locked or deactivated. Only someone who got the password right is told that the account is locked or deactivated. A lockout no longer erases offline sign-in on the desktop, phone or office apps.
  • The organization's "Require two-factor" setting is changed only in the web app, and our server ignores it from any other app. The web app now also needs your password to turn two-factor on, and a current code (or, to turn it off or reset it, a recovery code) to turn it off, reset it or make new recovery codes, as the desktop app already did. In the desktop, phone and office apps, a code that was already used is answered "wait for the next code" and is not counted as a wrong code.
  • Changing your password in the desktop app needs a current code if you use two-factor. The server only takes the change from a device of your organization, and limits how often it can be tried.
  • Offline: the desktop no longer lets in a two-factor user whose offline sign-in was earned without a code. The phone and office apps now also check with our server after every sync, so a password-only offline sign-in stops working there as soon as a sync completes.
  • No API change: API keys, the MCP server and webhooks work exactly as before.

Kit and transfer template deletes reach synced devices Changed

  • Deleting a kit, or a transfer template (on its own or together with its kit), in the web app now removes it from synced desktops, phones and office apps too. A kit takes all its lines and their custom fields and calculations with it; a template takes its waypoints with it. Before, these deletes stayed on other devices.
  • No API change.

Two-factor sign-in in the installed apps Changed

  • The desktop, phone and office apps now ask for the authenticator code (or a recovery code) when someone who uses two-factor signs in, and when a new device is registered by signing in. Our server holds the keys and checks the code; no key is kept on any device, and nothing is issued to the device until the code is right. Five wrong codes lock sign-in for five minutes.
  • Desktop: turning two-factor on or off, resetting the authenticator, new recovery codes and an admin's reset from the user list all go through our server, so they apply on every device. Turning it off, resetting it and making new recovery codes need a current code. "Remember this machine" stays a web-app feature.
  • The phone and office apps check with our server at every sign-in when they can reach it, so two-factor turned on since someone last signed in on that device is asked for right away. Offline, they let in only people who signed in online on that device within the last 30 days.
  • In an organization that requires two-factor, someone without an authenticator is sent to the web app to set one up before an installed app lets them in.
  • Signing out of the desktop app now deletes the credential it uses for user and role administration, which is stored encrypted and issued only once sign-in (including the code) is complete. If the device is online, our server also refuses that credential from then on.
  • No API change: API keys, the MCP server and webhooks work exactly as before.

Category writes and an item's categories Added

  • POST /api/v1/categories { "name": "Hardwood", "description": "…", "parentCategoryId": 1 } (201), PATCH /categories/{id} (only the fields you send; parentCategoryId: 0 moves it to the top level and every item's inherited parent categories are recalculated) and DELETE /categories/{id} — the Item Categories page's New, Edit, drag-and-drop and Delete. Names must be unique among categories with the same parent (409); a parent that is the category itself or one of its descendants, the built-in "No Category" or a Template Builder category answers 422; a negative parentCategoryId is a 422 too (only 0 means top level); Template Builder categories can't be changed or deleted (409; delete the Template Builder instead); a category with child categories can't be deleted (409). Deleting moves items whose primary category it was to "No Category" and returns { id, reassignedItemCount, removedAssignmentCount }.
  • GET /api/v1/items/{id}/categories — primaryCategoryId, the hand-assigned assignedCategoryIds, and every category the item is in (with their parents and Template Builder categories). POST /items/{id}/categories/{categoryId} and DELETE …/categories/{categoryId} assign or unassign one by hand, like the checkboxes on the item's page; parent categories follow, and both return the item's categories. The primary categoryId is unchanged — set it with PATCH /items/{id}.
  • Reads need ViewInventory, writes ManageInventory and an Idempotency-Key. The built-in "No Category" is never listed, changed or assignable by hand (404 / 422).
  • MCP: create_category, update_category, delete_category, get_item_categories, assign_item_category, remove_item_category. 114 tools now.

Reorder points across the organization Added

  • GET /api/v1/reorder-points — every reorder point of an active item (Reorder → Thresholds), paged, CSV too; filter by itemId, locationId (0 = only the all-locations ones), isActive and updatedSince. Needs ViewInventory.
  • DELETE /api/v1/reorder-points/{id} — remove one; 200 with the reorder point as it was, 404 if it isn't in your organization. Needs ManageInventory and an Idempotency-Key. The item drops off GET /items/low-stock for it at once, and synced desktops and phones now remove it too (deletes on the Thresholds page used to stay on other devices). To keep the rule but stop it, send "isActive": false to PUT /items/{id}/reorder-points.
  • MCP: list_reorder_points, delete_reorder_point. 108 tools.

Fixes: waypoint skip, deleted key creators, transfer quantity errors Changed

  • POST /api/v1/transfers/{id}/waypoints/{waypointId}/skip and the MCP Skip step now accept only a Pending waypoint. An Arrived waypoint has already had its lines dropped off, so it answers 409: leave it with …/depart. Skipping it used to redirect those lines, and completing the transfer then moved the same units a second time.
  • An API key whose creator's account has been deleted now stops working, the same as when the creator is deactivated. Deleting a user also revokes the keys they created.
  • On POST /transfers/{id}/lines/{lineId}/pick, …/unpick and PATCH /transfers/{id}/lines/{lineId}, a quantity outside 1–1000 answers 400, with the problem listed in errors.quantity. This was always the behaviour; the documentation listed a 422 that could not be returned.
  • When a custom-field value on an item flows down to its units, those units' updatedAt now moves, so GET /items/{id}/units?updatedSince= and incremental exports pick the change up.

Organization Admin removal requests Added

  • Removing an Organization Admin — demoting them to Manager or User, or deactivating them — now needs their approval or 14 days' notice they can contest. PATCH /api/v1/users/{id}, POST /users/{id}/deactivate and MCP update_user answer 403 for an Organization Admin and point to the requests below.
  • GET /api/v1/users/removal-requests (pending first; includeClosed; CSV too). POST /users/{id}/removal-requests { "action": "Demote", "newRole": "Manager", "reason": "…" } or { "action": "Deactivate" } — 201 with effectiveAt and daysRemaining; for the key creator's own account it steps down at once (200, applied: true).
  • POST /users/{id}/removal-requests/{requestId}/approve and /contest (only the admin named in the request; contest only before effectiveAt), /cancel (the requester). A pending request applies on its own once the notice ends. The organization's last active Organization Admin can never be removed (409). All need ManageUsers and an Idempotency-Key.
  • MCP: list_admin_removal_requests, request_admin_removal, approve_admin_removal, contest_admin_removal, cancel_admin_removal. 106 tools now.
  • Sandbox: user writes (POST/PATCH /users, deactivate, reactivate, removal requests) now work there for a key that holds ManageUsers; the public keys still don't.

Categories, stock units, crew order; custom fields in item.updated Added

  • GET /api/v1/categories (search, parentCategoryId — 0 for top level, updatedSince; CSV too), GET /categories/{id}, GET /categories/{id}/properties (own and inherited). POST /categories/{id}/properties, PATCH …/{propertyId}, DELETE …/{propertyId} — the Manage Properties page's rules: names unique regardless of case, Cost and Unit Price reserved, defaults and options checked against the type. Reads need ViewInventory, writes ManageInventory.
  • GET /api/v1/items/{id}/units (locationId, updatedSince; CSV too) and GET …/units/{unitId} — each unit of stock with its status (Available, InTransit, AtJobSite, InBundle), location and property values. PUT …/units/{unitId}/properties/{name} { "value": "…" } sets one unit's value and recomputes that unit's calculated values; calculated values, Cost and Unit Price answer 409.
  • PUT /api/v1/schedule-groups/{id}/members { "userIds": [ … ], "leadIds": [ … ] } replaces a crew in one call; POST /schedule-groups/reorder { "parentGroupId": 3, "ids": [ 8, 5, 9 ] } sets the display order. Both all or nothing (422 otherwise).
  • item.updated now also fires when one of an item's custom fields is added, removed, renamed, retyped or given a new value or options (changedFields properties) and when a custom-field value changes on one of its stock units (unitProperties). New additive fields changedProperties and changedUnitProperties name them. One event per item per save.
  • MCP: list_categories, get_category_properties, add_category_property, update_category_property, delete_category_property, list_item_units, set_item_unit_property, reorder_schedule_groups, set_schedule_group_members. 101 tools now.

Sales-order edits, line picking and returns Added

  • PATCH /api/v1/sales-orders/{id} — order number, customer PO, payment terms, requested delivery date, notes, taxRate, shippingCost; only the fields you send, totals and draft invoices follow. Lines: POST …/lines, PATCH …/lines/{lineId} (unitPrice, unitCost), DELETE …/lines/{lineId} (409 while the line is on a transfer).
  • PUT …/lines/{lineId}/picked-quantity { "quantityPicked": 2 } — raise to claim more stock onto the pick transfer, lower to unpick and release units. POST …/lines/{lineId}/pick, POST …/lines/pick-all and POST …/scan { "barcode": "…" } are the Fulfill page's counters (order lines only; stock doesn't move). POST …/transfers creates another transfer for what is still uncovered.
  • GET …/returnable-items and POST …/returns { "reason": "Wrong size", "stockUnitIds": [ 881 ] } (or "all": true, or a transferId) — restocked at the source or a return location, lines, totals and invoices reduced, overpayment becomes store credit, all in one transaction. Need ManageReturns as well.
  • All writes need ManageSalesOrders and an Idempotency-Key. The Details and Fulfill pages now call the same code.
  • MCP: update_sales_order, add_sales_order_lines, update_sales_order_line, remove_sales_order_line, pick_sales_order_lines, create_sales_order_transfer, get_sales_order_returnable_items, return_sales_order_items. 92 tools now.

Transfer routes: submit, per-line picking, scanning, waypoints Added

  • POST /api/v1/transfers/{id}/submit — Draft to Pending; over the approval threshold it waits for approval, as a new transfer does.
  • While a transfer is Pending and not awaiting approval: POST …/lines/{lineId}/pick and …/unpick ({ "quantity": 2 }, or no body for all), PATCH …/lines/{lineId} { "quantity": 1 } (reduce only; the dropped units are released) and POST …/scan { "barcode": "…" }.
  • In transit: POST …/waypoints/{waypointId}/arrive (stock moves to the stop), /deliver, /depart and /skip ({ "destinationType": "Waypoint" | "Destination" | "Location", "destinationId": 7 } for lines still assigned to the stop). POST …/lines/{lineId}/redirect sends one line to another stop, the destination or another location.
  • GET /transfers/{id} gains currentWaypointId, waypoints and each line's destinationWaypointId. Standalone transfers only; ManageTransfers; unknown line or waypoint → 404.
  • MCP: submit_transfer, pick_transfer_line, set_transfer_line_quantity, scan_transfer_barcode, advance_transfer_waypoint, redirect_transfer_line; get_transfer returns waypoints. 84 tools now.

Data dictionary descriptions Docs

  • Every column of the 30 most-used tables in the data dictionary — items, templates, properties, categories, locations, customers, jobs and their lines, invoices and payments, purchase and sales orders, transfers, vendors, settings, crews and users — now has a plain-language description, with units, currency, UTC versus local dates and value formats. 17 more tables are covered too.
  • The same text is in the data-dictionary.json inside every export zip. Tables and columns are unchanged.

Reorder points, low stock and item vendors; finalize invoices from MCP Added

  • GET /api/v1/items/low-stock — every item below one of its reorder points (Reorder → Thresholds), with onHand, minimumStock, suggestedOrderQuantity, cost and the vendor to order from; filter by vendorId (0 = no known vendor) or locationId; CSV too. Computed on request: it raises no alert and fires no webhook.
  • GET /items/{id}/reorder-points and PUT /items/{id}/reorder-points { "locationId": 3, "minimumStock": 10, "reorderQuantity": 24 } — create or update the reorder point for one location, or all locations together (201 / 200). Reads need ViewInventory, the PUT ManageInventory.
  • Items gain preferredVendorId, preferredVendorName and vendorPartNumber (also as CSV columns); set them with PATCH /items/{id} (0 / "" clears; needs ManageTemplates, as on Item Templates → Edit). item.updated lists them in changedFields.
  • MCP: list_low_stock, get_reorder_points, set_reorder_point, and mark_invoice_sent (the same checks as POST /invoices/{id}/mark-sent); update_item takes the vendor fields. 78 tools now. The reorder_below_minimum prompt starts from reorder points and preferred vendors; invoice_completed_jobs and customer_balance_followup can finalize drafts after you confirm.

Custom fields: calculations recompute, deletes sync Changed

  • Every custom-field value write — PATCH /items/{id}/properties, PUT …/{propertyId}/value, POST …/{propertyId}/revert, MCP set_item_property, set_item_properties, revert_item_property, and the app's editors — now recomputes the calculated properties that read the changed value, on the item and on the stock units it flowed to, in the same transaction. The returned property list shows the new values.
  • Deleting a property (DELETE /items/{id}/properties/{propertyId}, MCP delete_item_property, or in the app) now also removes it, and its calculations, from synced desktop installs and devices on their next sync.

Fixes: fulfilment retries, approvals, export links; MCP user lookup Added

  • MCP: list_users (active accounts, or all with includeInactive) and get_user, both ManageUsers with no module needed — where update_user gets its ids. 74 tools now.
  • POST /sales-orders/{id}/pick with lines is all or nothing: a failure part-way rolls every line back, so retrying the 500 with the same Idempotency-Key never picks twice.
  • POST /sales-orders/{id}/cancel: when returning stock or cancelling transfers fails, everything is rolled back and the answer is a 500 you can retry with the same key (it was a stored 409).
  • POST /sales-orders/{id}/complete-pick no longer counts a line whose stock row was deleted after the transfer was made (the app's scan dialog doesn't show it either). /deliver now documents that lines marked Failed stay where they are and end Returned.
  • POST /transfers/{id}/approve refuses with 409 when one of the transfer's units is already on another open or moving transfer, as starting a transfer does. POST /transfers no longer leaves a half-created transfer holding stock when it fails part-way.
  • Scheduled export: changing the settings while an export is running cancels that run's delivery, so an address you just removed never receives a link; re-saving a schedule that is already off keeps the links a Run now sent.

Item custom fields over the API and MCP Added

  • GET /api/v1/items/{id}/properties — every property of a catalog item: id, name, value, dataType (Text, Number, Date, Boolean, Dropdown, MultiSelect), options, and whether it is calculated, item-level, customized or a pricing field.
  • Values: PATCH /items/{id}/properties { "values": { "Color": "Red" } } (several by name, all or nothing), PUT /items/{id}/properties/{propertyId}/value { "value": "5" }, POST …/{propertyId}/revert (back to the template default). Values are checked by type — invariant decimal numbers, true/false, yyyy-MM-dd dates, dropdown values from the options — and copied to the item's stock units the way the Items page does; every write returns the full list and propagated.
  • Definitions: POST /items/{id}/properties { "name": "Warranty years", "dataType": "Number" } adds an item-level property (201); PATCH /items/{id}/properties/{propertyId} renames it, changes its type or replaces its dropdown options; DELETE /items/{id}/properties/{propertyId}, refused while a calculation uses it. Cost and Unit Price are changed with PATCH /items/{id}; calculated properties are read-only.
  • Values and item-level properties need ManageInventory; definition changes and deleting a template property need ManageTemplates — the same split as the Items and Item Templates pages, which now call the same service.
  • MCP: get_item_properties, set_item_property, set_item_properties, revert_item_property, add_item_property, update_item_property, delete_item_property. 72 tools now.

Schedule-group and customer-location writes Added

  • GET /api/v1/schedule-groups/{id}, POST /schedule-groups (201), PATCH /schedule-groups/{id} (only the fields you send; a parent that would create a cycle → 422), DELETE /schedule-groups/{id} (soft delete; 409 while it has child groups). Members one at a time: PUT /schedule-groups/{id}/members/{userId} with an optional { "isLead": true } (active users only) and DELETE …/members/{userId}. Reads need ViewJobs, writes ManageJobs; all need the Scheduling module.
  • GET /api/v1/customers/{id}/locations/{locationId}, POST /customers/{id}/locations, PATCH …/{locationId} (optional If-Match → 412 when stale) and DELETE …/{locationId} (deactivates; 409 while a job uses the address). Same rules as the Customers → Addresses page: name, address, city, state and postal code are required, and marking one address as mailing unmarks the others. Reads need ViewCustomers, writes ManageCustomers.
  • MCP: list_schedule_groups, create_schedule_group, update_schedule_group, delete_schedule_group, set_schedule_group_member, remove_schedule_group_member, list_customer_locations, create_customer_location, update_customer_location, delete_customer_location. 65 tools now.

Users and roles over the API and MCP Added

  • GET /api/v1/users/{id}, POST /users { "email": "[email protected]", "firstName": "Sam", "lastName": "Carter", "password": "…", "role": "Manager" } (201, or 202 when the account exists on the server but hasn't reached this host yet), PATCH /users/{id} (name, role, isActive, the full customRoleIds set), POST /users/{id}/deactivate and /reactivate. Need ManageUsers.
  • GET /api/v1/roles (built-in and custom roles with rank and permissions; CSV too), GET /roles/{id}; POST /roles and PATCH /roles/{id} create and edit custom roles and need ManageRoles.
  • Guard rails, checked before anything is written: the key's creator must be an Organization Admin, cannot change their own account, must outrank the account they change and can never assign System Admin; whatever a call grants — a role, a reactivation, a custom role's permissions — must be held by both the key and its creator, otherwise 403 names what is missing. Password resets and access-request approvals stay in the app.
  • GET /users keeps its Scheduling-module gate; the new user endpoints are not module-gated.
  • MCP: list_roles, create_user, update_user. 55 tools now.

Sales-order fulfilment over the API and MCP Added

  • POST /api/v1/sales-orders/{id}/pick — Pick All with no body, or { "lines": [ { "transferItemId": 5120, "quantity": 2 } ] } for given units (every line checked before anything is picked). POST …/complete-pick — every line fully picked; the transfer goes in transit and the order's status is recalculated (Packed once every line is picked and covered).
  • POST …/ship { "carrier": "UPS", "trackingNumber": "1Z999AA10123456784" } or { "withoutTracking": true }; POST …/deliver — stock moves to the delivery location and is consumed there when it is a job site or consuming location; POST …/complete; POST …/cancel { "reason": "…" } — delivered units come back to stock and live transfers are cancelled.
  • Each step takes an optional transferId; omitted, the order's only transfer in the state that step needs is used (409 if there is none, 422 if there are several). Every step answers with the order's recalculated status and fulfillmentStatus. Need ManageSalesOrders and an Idempotency-Key.
  • The app's scan dialog, Ship dialog, Mark as Delivered and Cancel page now call the same service. Returns and line edits stay in the app.
  • MCP: advance_sales_order with steps Pick, CompletePicking, Ship, ShipWithoutTracking, Deliver, Complete, Cancel. 52 tools now.

Standalone transfers over the API and MCP Added

  • POST /api/v1/transfers { "sourceLocationId": 1, "destinationLocationId": 4, "notes": "Restock van 2", "lines": [ { "itemId": 12, "quantity": 3 } ] } — moves whole units between two locations like Transfers → Create. The server picks the units at the source (not in transit, not in a bundle, not on another open transfer); nothing is written unless every line can be filled. 201 with a Location header. Over the organization's approval threshold the transfer waits for /approve or /reject.
  • POST /transfers/{id}/pick → /start → /complete, or /fail — the Transfers page's Pick All, Mark as In Transit, Complete and Mark as Failed buttons, allowed only in the states where the page shows them. /complete takes optional { "failedLines": [ { "lineId": 911, "reason": "Damaged in transit" } ] }; those lines stay where they are.
  • POST /transfers/{id}/cancel now also cancels a standalone transfer while it is Draft, or Pending and not awaiting approval. Sales-order transfers move with the sales-order steps.
  • All need ManageTransfers and an Idempotency-Key; state problems answer 409, a bad field 422. The web pages now call the same service.
  • MCP: create_transfer, pick_transfer, start_transfer, complete_transfer, fail_transfer; cancel_transfer covers standalone transfers too. 51 tools now.

MCP prompts: eight agent recipes Added

  • The MCP server now advertises the prompts capability. prompts/list returns reorder_below_minimum, receive_delivery, cycle_count, restock_vans, invoice_completed_jobs, morning_dispatch_summary, customer_balance_followup and review_pending_transfers; prompts/get returns one user message with the plan.
  • Each starts with get_org_capabilities, names the permissions and module it needs, uses only existing tools, and shows the user what it will change and waits before any write. Arguments are optional strings; a bad number or date is refused rather than ignored.
  • No new tools or endpoints. Details and REST equivalents: developers → recipes.

Data dictionary Docs

  • Every organization export zip now includes data-dictionary.json: each table's description, organization scoping and primary key, and each column's database-independent type, nullability, maximum length, foreign-key target, enum values and description. README.txt in the zip lists it.
  • The same dictionary is published at /data-dictionary (and /data-dictionary.json), generated from the model the export uses, so it lists exactly the tables and columns in the zip and what is left out.

Scheduled export Added

  • Settings → Export All Data → Schedule exports: the full organization zip, daily or weekly at a chosen UTC hour, to an SFTP server (password or private key; host key pinned after the first run), an S3-compatible bucket (AWS or a custom https endpoint) or an emailed download link that works for 7 days. "Run now" tests a destination; the card shows the last run's result.
  • Destination secrets are encrypted and never shown again; SFTP hosts and bucket endpoints must resolve to public addresses. Organization admins only (ManageOrganizations).
  • No API change. Schedules are not run in the sandbox.

API key expiry and IP allow-lists Added

  • A key can now have an expiry (never, 30/60/90/180 days, a year, or a date — valid through the end of that UTC day) and an allow-list of up to 50 IPv4/IPv6 addresses or CIDR ranges, set on Settings → Integrations → API Keys when it is created or later with the new Edit button. An expired key can be extended.
  • Both are checked on every request to /api/v1 and /mcp. An expired key gets 401 with a problem detail giving the expiry time, and its WWW-Authenticate header carries error="invalid_token" and error_description="The API key has expired"; a call from outside the allow-list gets 403 naming the caller's address (never the list). Unknown or revoked keys answer as before.
  • Refused calls are logged against the key with a new authFailure field (key_expired, ip_not_allowed) in GET /api/v1/activity, list_api_activity and the activity CSV — added as the last column, so existing columns keep their order.

API and agent activity log Added

  • GET /api/v1/activity — the organization's own API and MCP request log: key id, name and prefix, method, path, status, duration, IP and time, and for MCP calls the JSON-RPC method and the tool name. Oldest first, so passing nextCursor back as after follows new rows; filters since, until, apiKeyId, statusClass (1–5, e.g. 4 for 4xx) and search (path, MCP method or tool); limit up to 500. Also CSV. Rows are kept 90 days. Needs ManageOrganizations.
  • MCP: list_api_activity (newest first). 46 tools now.
  • Settings → Integrations → API activity shows the same log with filters and a CSV download.
  • For MCP, only the method and tool name are recorded — never arguments, results or the key.

Required two-factor and full data export Added

  • Settings → Security: an organization admin can require two-factor sign-in for everyone. People without an authenticator app are asked to set one up before they can continue, and can't turn it off. The admin needs it on their own account first.
  • Settings → Export All Data: one zip with every table of the organization as JSON and as CSV, plus a manifest (row counts, how each table was scoped, what was left out). Passwords, sign-in and API credentials, payment tokens and webhook secrets are never included.
  • No API change: API keys, the MCP server and device tokens are not affected by the two-factor requirement.

Batch stock operations Added

  • POST /api/v1/items/stock/batch { "operations": [ { "action": "Adjust", "itemId": 12, "locationId": 3, "delta": 10 }, { "action": "Move", "itemId": 12, "fromLocationId": 3, "toLocationId": 4, "quantity": 2 } ] } — 1 to 100 operations, at most 1000 units, applied in order (a later removal can use units an earlier one added). Each follows the rules of /stock/adjust or /stock/move.
  • All or nothing: every operation is checked before anything is written, and the writes share one transaction. A failure answers with the single-call status, a detail starting operations[i]: and a failedIndex extension; nothing changes. One Idempotency-Key covers the batch.
  • MCP: apply_stock_batch. 45 tools now.

Purchase-order creation and vendors Added

  • POST /api/v1/purchase-orders { "vendorId": 7, "receivingLocationId": 1, "lines": [ { "itemId": 12, "quantityOrdered": 24, "unitPrice": 3.10 } ] } — a Draft order with the same rules and PO numbering as the New Purchase Order page; the order and its lines are written together or not at all. POST /purchase-orders/{id}/lines adds a line later.
  • POST /purchase-orders/{id}/mark-ordered, /mark-shipped, /mark-delivered and /cancel — the page's status buttons, with its messages when a step doesn't fit (409). With /receive, an order now goes from draft to stock entirely over the API — including in the sandbox.
  • GET /api/v1/vendors (also CSV), GET /vendors/{id}, POST /vendors, PATCH /vendors/{id}. Vendor writes need ManageVendors; the public sandbox write key now holds it.
  • MCP: create_purchase_order, add_purchase_order_line, advance_purchase_order, list_vendors, create_vendor, update_vendor. The web pages now call the same service, so the app, the API and agents create orders identically.

Public sandbox write key Added

  • A second public key for sandbox.flexonthejob.com, flx_live_PPZ35t5mlrEPePjZl3yDzQeB7Gweq9GU, holds the read permissions plus ManageInventory, ManageTemplates, ManageCustomers, ManageJobs, ManageInvoices, ManageTransfers, sales orders and purchase orders, at 30 requests/minute. Every published write, REST or MCP, can be tried without an account.
  • The demo organization is reset from its master copy every night at 08:00 UTC. No user, role, API-key or webhook management; email and webhook delivery stay off.

Two-factor sign-in Added

  • Authenticator-app two-factor authentication (TOTP — Microsoft Authenticator, Google Authenticator, 1Password, Authy and the like) with 10 single-use recovery codes. Each user turns it on from Account → Settings by scanning a QR code and entering a code; they can regenerate recovery codes, reset the authenticator or turn it off there. "Remember this machine" skips the code on that browser for 14 days.
  • Optional and per user; there is no organization-wide requirement yet. An administrator can reset it for someone who lost their phone from the user list.
  • Covers browser sign-in to the web app and the desktop app. No API change: API keys, the MCP server, webhooks and device (phone/office app) tokens work exactly as before.

Sales orders over the API and MCP Added

  • GET /api/v1/sales-orders (filter by status, customerId, updatedSince; also CSV) and GET /api/v1/sales-orders/{id} — the order with its totals, payment and fulfilment status, and its lines with ordered, picked and shipped quantities.
  • POST /api/v1/sales-orders { "customerId": 42, "lines": [ { "itemId": 5, "quantity": 10, "unitPrice": 125.00 } ] } — creates the order exactly like the app's New Sales Order page and item picker: Pending, the next SO number, the organization's default tax rate (0 for a tax-exempt customer) and payment terms when omitted, lines for the same item merged, totals computed. An unknown customer, an address that is not the customer's or an unknown item answers 422 naming the field, and nothing is written. 201 with the order and a Location header.
  • POST /api/v1/sales-orders/{id}/fulfill — the Begin Fulfillment button: creates the pick transfer from the order's source locations (else the warehouse) and returns the resulting status and transferId. The status is Picking when the transfer covers every line and stays Pending when stock does not; 409 for a complete or cancelled order, when every line is already covered by a transfer (a repeat call only tops up what is still missing, so a retry cannot reserve stock twice) or when no stock is available. This is the only fulfilment step over the API: picking (scanning), packing, shipping and delivery are done in the app.
  • MCP: list_sales_orders, get_sales_order, create_sales_order, fulfill_sales_order. 38 tools now.
  • Reads need ViewSalesOrders, writes need ManageSalesOrders and an Idempotency-Key. Same service methods as the web app. Editing, cancelling and shipping sales orders stay in the app.

Purchase orders and receiving over the API and MCP Added

  • GET /api/v1/purchase-orders (filter by status, vendorId, updatedSince; also CSV) and GET /api/v1/purchase-orders/{id} — the order with its lines: ordered, shipped, received and remaining quantities, unit price, shipment. Deleted orders are never returned.
  • POST /api/v1/purchase-orders/{id}/receive { "lines": [ { "lineId": 1207, "quantity": 4, "locationId": 11 } ] } — receives stock exactly like the app's receiving panel: only a Delivered or PartialReceived order can be received, every line is checked before anything is written, a quantity above the line's remaining is refused, locationId defaults to the order's receiving location, and each unit becomes a stock item with the order's unit price as its cost (plus shipping cost when the line is on a costed shipment). The order's status is recomputed from its shipments, so it reads Received only once every unit is received and every shipment is delivered. Answers 200 with the updated order; 409 when the order's status does not allow receiving; 422 naming the line field when a line cannot be received.
  • MCP: list_purchase_orders, get_purchase_order, receive_purchase_order. 34 tools now.
  • Reads need ViewPurchaseOrders, receiving needs ManagePurchaseOrders and an Idempotency-Key. Same service method as the web app. Creating and editing purchase orders stays in the app.

Job-site transfers over the API and MCP Added

  • POST /api/v1/jobs/{id}/transfer — no body. Creates the job-site transfer exactly like the Job Details "Create Transfer" button: stock is picked for every planned material line, the transfer is Pending with the job's site as its destination, the job's lines become Reserved and the job history records it. Same rules as the button: the organization must not be in counter-sales mode and the job must be Estimated, Scheduled or InProgress. Answers 201 with the transfer and its lines; 409 otherwise, or when the job has no job site, nothing needs transferring or no stock is available.
  • GET /api/v1/transfers (filter by jobId, status, approvalStatus, updatedSince; also CSV — ?approvalStatus=Pending&status=Pending lists the transfers awaiting approval) and GET /api/v1/transfers/{id} (lines, approval state, rejection reason).
  • POST /api/v1/transfers/{id}/cancel — cancels a job-site transfer and releases its stock; the job's lines go back to Planned. POST /api/v1/transfers/{id}/approve and /reject { "reason": "…" } — for transfers awaiting approval that are still Pending (standalone transfers over your approval threshold; job-site transfers never need it).
  • Webhooks: transfer.created (sent once the transfer's lines are saved) and transfer.cancelled (including a rejected approval), same payload as transfer.completed. completedAt is present only when the transfer is completed.
  • MCP: list_transfers, get_transfer, create_job_transfer, cancel_transfer, approve_transfer, reject_transfer. 31 tools now.
  • Reads and writes need ManageTransfers — the app's Transfers pages require it for reads too. Writes require an Idempotency-Key. Same service method as the web app.

Build id on /health Added

  • GET /health now answers { "status": "ok", "version": "1.0.0-beta.33", "build": "a1b2c3d", "time": "2026-09-23T14:02:11.4821345+00:00" }. build is the short git commit the running binary was published from (git describe --always --dirty; a -dirty suffix means it was published with uncommitted changes), dev for a build published without one. time is ISO-8601 with an explicit +00:00 offset and fractional seconds. Existing fields are unchanged.
  • Why: deploy verification. version comes from version.txt and does not change between deploys of the same version, so build is what tells you which commit is live. Anonymous, no authentication, not part of the OpenAPI document; polled by the status page.

Create and update catalog items Added

  • POST /api/v1/items — { "name": "1/2\" PEX Coupling", "categoryId": 4, "unitPrice": 1.25, "cost": 0.40 }. Same as adding an item in the app: the organization's default "No Category" when categoryId is omitted, Cost and Unit Price properties created (0 when not supplied, stored to 2 decimals) and the category's properties copied onto the item. An active item with the same name is refused (409); an unknown category is 422. Creates no stock — follow up with POST /items/{id}/stock/adjust. Answers 201 with the item exactly as GET /items/{id} returns it.
  • PATCH /api/v1/items/{id} — { "unitPrice": 1.35 }. Only the fields you send change; "description": "" clears the description. Same rules as the Items page: renaming also renames every inventory unit, a new unitPrice / cost is written to the item and its future stock only (existing units keep their pricing), and a new categoryId copies that category's properties onto the item. Send the item's ETag as If-Match to get 412 instead of overwriting a concurrent change. Fires item.updated with changedFields.
  • MCP: create_item and update_item, same rules. 25 tools now.
  • Both writes require an Idempotency-Key and ManageInventory — the same permission the Items page uses to create items and change name, price, cost and category. The one exception mirrors the app too: description is edited only on the Item Templates page, so sending it (including "" to clear) also requires ManageTemplates; otherwise 403 and nothing changes. Same service method as the web app.

Publish an estimate: finalize endpoint and publishImmediately Added

  • POST /api/v1/jobs/{id}/finalize — no body. The Publish Estimate button: only a Draft can be published, it needs at least one charge (labor, material or custom) and every material line must have a unit price; anything else is 409 with the app's own message. The job lands on Estimated, on Scheduled when a date was already set while drafting, or on AwaitingOptionSelection for a multi-option estimate, and a history entry records the change. Returns the job id and confirmation message.
  • "publishImmediately": true on POST /api/v1/jobs — the Create & Publish button in one call. When the new job cannot be published yet it is still created as a Draft (201) and the job's new warning field says why — never a 4xx for a job that was created. warning is always null on GET.
  • MCP: finalize_estimate, and create_job takes publishImmediately. 23 tools now.
  • Requires an Idempotency-Key and ManageJobs. Same service method as the web app.

CSV on every list endpoint Added

  • GET /api/v1/customers, /customers/{id}/locations, /jobs, /invoices, /items, /locations, /schedule, /schedule-groups and /users answer in CSV when you send Accept: text/csv or append ?format=csv. Same filters, updatedSince and limit as JSON. GET /modules and single-resource GETs stay JSON only.
  • Paging is identical; because a CSV body has no envelope, the next page's cursor comes back in the X-Next-Cursor header and as Link: <next page URL>; rel="next". Both are absent on the last page. The response is an attachment named <resource>.csv.
  • Columns are the JSON property names in declaration order — a nested object is flattened to parent.child, a list of scalars is ;-joined, a list of objects is compact JSON in the cell — and values are written exactly as in the JSON (invariant decimals, ISO-8601 UTC dates, enum names, true/false, empty for null). UTF-8 with BOM, CRLF rows, RFC 4180 quoting; a text cell starting with =, +, -, @, TAB or CR is prefixed with an apostrophe so a spreadsheet will not run it as a formula.
  • A list's ETag is per representation (the JSON and CSV tags of the same page differ) and list responses carry Vary: Accept. Errors are always application/problem+json, whatever you asked for. The OpenAPI document and llms.txt advertise the CSV representation and its headers; a scheduled nightly export recipe (bash and PowerShell) is on the developers page.

Create a standalone invoice, record a payment Added

  • POST /api/v1/invoices — an invoice that is not tied to a job: { "customerId": 12, "lines": [{ "description": "Service call", "itemType": "Labor", "quantity": 1, "unitPrice": 150 }] }. Every omitted field defaults exactly as the New Invoice page does (next INV-SA-… number, today, the organization's default payment terms, tax rate and payment instructions; a tax-exempt customer is taxed at 0). paymentTerms takes DueOnReceipt, Net15, Net30, Net45, Net60, COD or Prepaid and sets the due date when none is given. Like the page, the invoice is created already finalized (status Sent) so a payment can follow at once. Billing a job is still POST /invoices/from-job.
  • POST /api/v1/invoices/{id}/payments — { "amount": 150, "paymentMethod": "Check", "referenceNumber": "1042" }, optionally applyStoreCredit with storeCreditToApply. Same rules as the Record Payment dialog: a payment needs a method, the total may not exceed the balance due (422 — extra money belongs in store credit), and drafts or consolidated members are refused (409). Returns the invoice with its new status, amountPaid and balanceDue; invoice.paid fires when it reaches Paid.
  • MCP: create_invoice and record_payment, same rules and defaults. 22 tools now.
  • Both writes require an Idempotency-Key and ManageInvoices. Same service methods as the web app.

Adjust and move stock, list locations, cancel a job from MCP Added

  • POST /api/v1/items/{id}/stock/adjust — { "locationId": 3, "delta": -2, "reason": "Damaged in van" }. A positive delta creates that many units at the location exactly like "Add inventory" on the Items page; a negative one writes off the oldest available units the way "Delete" on the Inventory page does (soft delete — restorable in the app, recorded in each unit's change history with the reason). Removing more than is on hand answers 409 and changes nothing; units in transit on a transfer are never touched.
  • POST /api/v1/items/{id}/stock/move — { "fromLocationId": 1, "toLocationId": 4, "quantity": 3 }, the same thing as "Quick move": the oldest available units are re-pointed, nothing is created or destroyed. Job sites are refused at either end (422) — stock reaches a job site through a job or a transfer.
  • GET /api/v1/locations and GET /api/v1/locations/{id} — stock locations (warehouses, shelves, vehicles) so a client can pick ids for the calls above; search, parentLocationId, updatedSince, includeJobSites=true, includeInactive=true. Needs ViewInventory.
  • MCP: adjust_stock, move_stock, list_locations and cancel_job (same guards as the Cancel Job button — a job with payments or sent/paid invoices cannot be cancelled; an uncollected down payment can become store credit with issueStoreCredit when the key holds ManageStoreCredit). 20 tools now.
  • Both stock writes require an Idempotency-Key and ManageInventory, and return the item's updated per-location stock so no follow-up read is needed. Same service methods as the web app.

Delete a customer, deactivate an item, void an invoice Added

  • DELETE /api/v1/customers/{id} — the same soft delete as the Customers page: the customer leaves every list and lookup but keeps its history and can be restored in the app. Refused with 409 while active jobs, unpaid invoices or active sales orders reference it; the problem detail names which.
  • DELETE /api/v1/items/{id} — deactivates an item (soft delete, inventory rows untouched, restorable from Deactivated Items). A parent group that still contains active items answers 409; add ?cascadeChildren=true to deactivate the group and its items together.
  • POST /api/v1/invoices/{id}/void — voids a finalized invoice with no recorded payments (optional { "reason": "…" } body); applied store credit returns to the customer and the job's payment status is recalculated. Drafts, consolidated invoices and paid invoices answer 409 with the reason.
  • All three require an Idempotency-Key and need ManageCustomers, ManageInventory and ManageInvoices respectively. Same rules, same messages as the web app — the endpoints call the service methods the pages use.

Rate-limit headers, richer OpenAPI metadata, status page Added

  • Every /api/v1 and /mcp response now carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the key's one-minute window resets), so clients can pace themselves instead of discovering the limit by hitting it. 429 + Retry-After is unchanged.
  • /openapi/v1.json gains info.contact, info.termsOfService, info.license and a servers[] list (production and sandbox, with the host that served the document first).
  • GET /health on the app and sandbox hosts: anonymous liveness with the running version.
  • Public status page at status.flexonthejob.com — web app, API, MCP, sandbox, sync and website, checked every minute, with incident history.
  • This page. /changelog on either host now lands here; the standard Data Processing Addendum is published at /dpa.

Public sandbox and webhooks in the spec Docs

  • Sandbox at https://sandbox.flexonthejob.com with a published read-only key — every GET and every read MCP tool works without an account; writes return 403. Details on the developer page.
  • Outbound webhooks are now part of the OpenAPI document as x-webhooks (OpenAPI 3.1 webhooks shape): one post per event with the signed envelope and a component schema per payload. The same catalogue is in llms.txt and llms-full.txt.
  • Sandbox demo organization scrubbed of real-person data.

Four change-driven webhook events Added

  • invoice.sent, transfer.completed, inventory.low_stock, item.updated. Additive — existing subscriptions are unchanged until you tick the new events. Twelve events in total.
  • Public developer documentation, security page and AI/roadmap page. /api, /docs, /openapi.json and /developers on either host redirect to the right place. robots.txt and security.txt published on both hosts.

MCP server and webhooks Added

  • Streamable-HTTP MCP server at /mcp (16 tools, 2 resources) under the same API keys, permissions and rate limits as /api/v1.
  • Webhooks: job.created, job.status_changed, job.scheduled, job.assigned, job.completed, invoice.created, invoice.paid, customer.created. X-Flex-Signature HMAC-SHA256 over the raw body; retry with backoff; delivery log in Settings.

API v1 initial release Added

  • /modules, /customers (read, create, update, locations), /items, /invoices (read, from-job, mark-sent), /jobs (read, create, status, assignees, reschedule, cancel), /schedule, /schedule-groups, /users.
  • API keys with per-key permissions and rate limits; cursor paging; updatedSince on every list endpoint; ETags; Idempotency-Key required on writes; RFC 7807 errors.
  • /docs (Scalar), /openapi/v1.json, /llms.txt, /llms-full.txt.

Planned work is on the roadmap. Questions about a change: [email protected].