Genie logo API Documentation
22 sections

Genie API Reference Guide

Application
Genie (Smartbookgenie)
Backend
GenieBack — Node.js / NestJS 11 (TypeScript, Express adapter, TypeORM + MySQL)
Frontend
GenieFront — React / Next.js 15 (App Router)
Document version
1 September 2026

Table of Contents

1. Application Overview

Genie is a practice-management platform for accounting firms. A firm's staff use it to plan and track client work, log time against that work, bill it, close each client's books every month, and report on all of it.

The product is organised into the following functional areas, which this document mirrors:

Area What it covers
Authentication & Session Sign-in (password, two-factor, Google), CSRF, the shared company/period selection carried across every screen, notifications
Tasks & Time The core work-management area: tasks, task boards, calendar, projects, recurring work, templates, timesheets, invoicing, reports, and per-firm configuration
Monthly Close The month-end close workflow per client company: section-by-section review (Balance Sheet, Income Statement, Bank & Card, AP, AR, Payroll, Uncharacterized, Cash Flow, Other), GL validation, review assignments, close status, QuickBooks close dates
Client Success Client (company) master information, contacts, service teams, service roles, tags
Report Center Firm-level financial report definitions, the report writer, report packages and report previews/exports
Admin Companies, company groups, reporting groups, users and their access matrices, activity logs, QuickBooks sync status
Feedback Center In-product suggestions, support tickets and issue threads with attachments
Comments Center Comment threads attached to tasks and to invoices
Integrations QuickBooks Online (accounting data and invoice posting), Google Drive/Sheets (exports), Google Cloud Storage (attachments), SMTP (notifications)

A number of route spellings, field names and response envelopes are retained for compatibility with earlier clients of this API. Where that affects how a request must be formed, this document states it at the endpoint concerned.

↑ Back to Table of Contents


2. API Overview

  • Style: JSON over HTTP. The API is not REST-shaped: most operations are a POST to a verb-like path (/api/task/save, /api/invoicing/preview/delete) carrying the payload in the body, including many read operations. Path parameters are used sparingly.
  • Transport: application/json for almost all endpoints. A small number accept multipart/form-data (file uploads) or return binary or base64 content (exports).
  • Authentication: server-side session cookie. There is no bearer token or JWT scheme and no public API key.
  • Versioning: the API is unversioned. There is no /v1 prefix and no version header.
  • Path prefixes: most endpoints are served under /api/...; the authentication and OAuth routes are served at the root (/login, /csrf, /google/callback).
  • Machine-readable specification: none is published. This document is the specification.

↑ Back to Table of Contents


3. Base URL and Environments

The backend listens on the port given by PORT (default 4000).

Environment Backend base URL Notes
Local development http://localhost:4000 The web application runs on http://localhost:3000, which is also the default allowed browser origin
Deployed Configured through the deployment environment (APP_URL / BACKEND_ORIGIN) Runs behind a TLS-terminating proxy; proxy headers are trusted so that secure session cookies are issued correctly

No production hostname is fixed in the application. Clients must substitute the base URL for their own environment; every path in this document is relative to it. The example requests use https://api.example.com as a placeholder.

3.1 Calling through the web application

The Genie web application proxies the API rather than calling it cross-origin. A browser client should call the application path, which is same-origin and therefore carries the session cookie without any cross-origin configuration.

Application path Backend path
/api/csrf /csrf
/api/login, /api/login/check, /api/login/select-2fa /login, /login/check, /login/select-2fa
/api/login/verify/otp, /api/login/verify/googleotp, /api/login/verify/resend same paths without /api
/api/password/email, /api/password/reset /password/email, /password/reset
/api/logout /api/logout
/google/redirect, /google/callback, /google/drive/callback same (browser-visited, proxied straight through)
/api/google/signup /google/signup
/api/google/:path* /api/google/:path*
/api/logs/*, /api/check/session, /api/user-based/notification[/all] identical
/api/company/*, /api/company-group/*, /api/reporting-group/*, /api/firm/list, /api/firm/company/list identical
/api/task[/*], /api/tat/*, /api/invoicing/*, /api/send/* identical
/api/mc/*, /api/other/*, /api/open-comment/*, /api/thread/*, /api/comment/* identical
/api/cs/*, /api/user/*, /api/utilities/*, /api/subscription/*, /api/subscribe/* identical
/api/reports/*, /api/feedbackcenter/*, /api/support/*, /api/new/*, /api/issue/*, /api/ajax_upload/*, /api/qbo/*, /api/get/* identical

Note that /api/google/signup maps to the backend's /google/signup (no /api segment), while the Google Drive routes map to /api/google/.... A proxy configuration must therefore declare the sign-up rule ahead of the general /api/google/* rule.

3.2 Cross-origin requests

A single browser origin is allowed, configured as FRONTEND_ORIGIN, and credentials are permitted on it. A browser client calling the backend directly must therefore be served from that origin and must send credentials with each request (credentials: 'include'). Calling through the application paths in §3.1 avoids cross-origin configuration altogether and is the recommended integration for browser clients.

↑ Back to Table of Contents


4. Authentication and Session

Endpoint-level detail is in 8. Authentication, Session & Core. This is the operational summary.

4.1 Session model

  • Sessions are server-side and are persisted in the database, so any application instance can serve any request once a session exists.
  • Cookie name: genie.sid — httpOnly, SameSite=Lax, Secure in production, lifetime 7 days.
  • The session carries the signed-in user, the authentication state (2fa_pending or authenticated), the two-factor method in use, and the shared company and period selection described in §5.4.
  • There is no refresh-token mechanism. A session ends when the cookie expires, when the user signs out, or when the stored session is removed.

4.2 CSRF protection

State-changing authentication endpoints require a synchronizer CSRF token:

  1. GET /csrf issues a 32-byte hex token, stores it on the session, and returns it in a JavaScript-readable XSRF-TOKEN cookie.
  2. The client replays it on protected requests in the X-XSRF-TOKEN header (a _token body field is also accepted).
  3. A missing or mismatched token returns 403 {"message": "CSRF token mismatch."}.

CSRF protection applies to: POST /login, /password/email, /password/reset, /login/select-2fa, /login/verify/otp, /login/verify/googleotp, /login/verify/resend and POST /google/signup. It does not apply to the application (/api/...) endpoints, which rely on the session cookie and its SameSite=Lax restriction.

4.3 Sign-in flow

GET  /csrf                     → issue CSRF token (sets XSRF-TOKEN cookie)
POST /login                    → email + password  (X-XSRF-TOKEN required)
        │
        ├─ no 2FA required     → session state = 'authenticated'
        ├─ auth_type = '0'     → user must choose a method: POST /login/select-2fa
        ├─ auth_type = '1'     → Google Authenticator: POST /login/verify/googleotp
        └─ auth_type = '3'     → emailed OTP:          POST /login/verify/otp
                                  (resend via POST /login/verify/resend)
        │
GET  /login/check              → reports which branch applies; also generates and sends the OTP
        ↓
   on success: enc_log cookie set (HttpOnly), { redirect } returned
        ↓
GET  /api/check/session        → confirm the session on subsequent page loads
POST /api/logout               → end the session

Sign-in guardrails:

Rule Value
Maximum login attempts before lockout 6
Lockout window 60 seconds
Emailed OTP validity 5 minutes
OTP resend cooldown 30 seconds
Failed-credentials message These credentials do not match our records.

Sign in with Google is an alternative entry point: GET /google/redirect → Google consent → GET /google/callback, which either signs the user in or, for a Google identity that is not yet linked to a Genie account, sets a 60-second google cookie and routes to the account-linking screen served by GET /google/signup and POST /google/signup.

4.4 Authorization model

Authorization is enforced per endpoint rather than by a single global gate. Every authenticated request first resolves the caller from the session — an absent session, or a user who is inactive or deleted, produces 401 — after which each endpoint applies its own scope and permission rules. This document records those rules for every endpoint in the Authorization row of its fact table.

Two mechanisms are used.

a) Named roles — rows in user_roles:

Id Role Id Role
1 Global Master System Administrator 10 Feedback Center Administrator
2 Global User Administrator 11 Monthly Close Manager
3 Global Company Administrator 12 Primary Accountant
4 Global Company Group Administrator 13 Invoice Approver
5 Global Report and Dashboard Administrator 14 Invoice Manager
6 Standard 15 Invoice Approver By Service Line
7 Company Group Company Administrator 16 Timesheet Approver
8 Company Group Administrator 17 Firm Internal Accounting Manager
9 Company Group Report and Dashboard Administrator

Two flags on the user record sit above the role table: global_admin = 1 (system-wide access, ignoring firm scoping) and firm_admin = 1 (full access within the user's own firm).

b) Per-menu access levels — user_company_access and user_group_access rows grant an access level per menu. Throughout this document the levels are referred to by name — ACCESS_NO (1), ACCESS_VIEW (2), ACCESS_STANDARD (3) and ACCESS_ADMIN (4) — and the menus by id: 3 (Monthly Close), 10 (Client Success) and 81 (Tasks & Time):

Level Meaning Menu Id
1 No access Monthly Close 3
2 View Client Success 10
3 Standard (read and write) Tasks & Time 81
4 Admin

↑ Back to Table of Contents


5. Common API Conventions

5.1 Request headers

Header When Value
Content-Type any request with a body application/json (or multipart/form-data for uploads)
Cookie every authenticated request must include genie.sid
X-XSRF-TOKEN the CSRF-protected authentication requests listed in §4.2 the token from GET /csrf

No Authorization header, API key or tenant header is used — the firm and company scope is derived from the session.

5.2 Response envelopes

Three response shapes occur across the API.

a) The error envelope (most common):

{ "error": false, "data": [ ... ], "message": "Saved successfully." }

b) A bare object or array — several list endpoints return the rows directly, with no envelope.

c) A redirect instruction — the authentication endpoints return { "redirect": "/home" } for the client to navigate to, rather than issuing an HTTP redirect. The browser-visited OAuth routes are the exception and do issue real 302 responses.

Important for integrators: a failed business rule usually returns HTTP 200 (or 201) with {"error": true, "message": "..."}, not a 4xx status. Duplicate names, permission refusals applied server-side, "not found" conditions and grid validation failures are all reported this way. Clients must inspect the response body, not only the status code. HTTP error statuses are reserved for request validation (400), a missing or invalid session (401), explicit permission refusals (403) and unhandled failures (500).

5.3 Status codes

Code Meaning in this API
200 OK Successful GET; also successful PUT, PATCH and DELETE
201 Created Every successful POST — the framework default is not overridden, so 201 is returned even for read-style POST requests and for {"error": true} bodies
302 Found The browser-visited OAuth routes only (/google/redirect, /google/callback, /google/drive/callback)
400 Bad Request Request validation failure, or an explicit bad-request condition
401 Unauthorized No session, or the session user is inactive or deleted — {"message": "These credentials do not match our records."}
403 Forbidden CSRF mismatch, or an explicit permission refusal
404 Not Found Explicit not-found condition (uncommon — most "not found" cases use the error envelope)
413 Payload Too Large Body over 20 MB (see §5.7)
500 Internal Server Error Unhandled failure — {"error": true, "msg": "The server failed to complete this request.", "debug": "..."}
503 Service Unavailable GET / and GET /health when the database check fails

5.4 The shared company and period selection

Genie keeps one company selection and one as-on period on the session, shared by every screen. Selecting a company in Report Center also changes it in Client Success, Admin, Tasks & Time and Monthly Close. This is intentional: the selection is a single application-wide value.

Endpoint Purpose
GET /api/company/session Read the current selection (company and period)
POST /api/company/session Change the company. company_id: 0 is a real, persistent value meaning "Global", not "unset"
POST /api/company/session/period Change only the month and year, keeping the company

Most data endpoints fall back to this session selection when the corresponding id is omitted from the request body.

5.5 Filtering, sorting and pagination

There is no single global convention. Endpoints that support these capabilities take them as body fields, and the field names differ per endpoint; each endpoint's Request body table is authoritative. The recurring patterns are:

  • Filtering — a filter object, or discrete fields such as companyId, firmId, userId, status, fromDate / toDate, monthVal / yearVal. Multi-select filters are frequently sent as comma-separated id strings (for example "12,45,78") rather than JSON arrays; both forms are documented per endpoint.
  • Sorting — where supported, sortBy or sortColumn plus sortOrder (asc / desc).
  • Pagination — where supported, offset + limit, or page + perPage. Many list endpoints are unpaginated and return the full result set. A few endpoints accept offset/limit fields that do not affect the result set; these are noted individually.

5.6 Dates, times and periods

Concept Format Notes
Calendar date (wire format) YYYY-MM-DD Some endpoints accept or emit MM/DD/YYYY; noted per endpoint
Date-time (wire format) YYYY-MM-DD HH:mm:ss Server timezone
Monthly Close period monthVal (for example "Jan") + yearVal (for example 2026) Resolved to a month-start date server-side
Timesheet week Week string; weeks run Saturday → Friday "Week Ending" is the Friday
Durations Stored in seconds, presented as hours Rounding rules are documented in the Timesheet section

5.7 Request size limits

Request bodies are accepted up to 20 MB for both JSON and URL-encoded content. The allowance exists for Report Writer and Reports Builder saves, whose per-value conditional-formatting rules can produce very large payloads. Exceeding 20 MB returns 413.

5.8 Numeric precision

QuickBooks Online issues reference ids of up to 18 digits, beyond the range JavaScript can represent exactly as a number. Such values are returned as numbers when they fit that range and as strings when they do not. A client must therefore accept either a number or a numeric string for QuickBooks id fields (class_qbo, customer, item and location ids, and similar).

5.9 Encrypted identifiers

Some route parameters and body ids are exchanged in encrypted form rather than as plain integers — a base64 value that the API decrypts server-side. Endpoints accept either form:

  • a plain numeric id — "123"
  • a numeric id with a suffix — "123_profile"
  • an encrypted base64 id — decrypted server-side (+ and space URL-encoding variants are both tolerated)

Where an endpoint's behaviour differs between the two forms, this document says so. Clients should treat these ids as opaque strings and echo back exactly what the API returned.

5.10 File uploads and downloads

  • Uploads use multipart/form-data. The field name, accepted file types and any size limit are documented per endpoint. Feedback Center attachments are stored in Google Cloud Storage.
  • Downloads take one of three forms, which differ by endpoint:
    1. a direct binary stream (.xlsx and similar) with a Content-Disposition header;
    2. base64 content inside a JSON envelope — several export endpoints return the file this way rather than as a stream, so the client must decode it rather than stream it;
    3. a two-step token flow — POST .../export builds the file and returns a single-use token, then GET .../export/generated/:token downloads it.

Deployment consideration: in the two-step flow the generated file is held in temporary server-side storage associated with the application instance that produced it, so the download request must reach that same instance. On several of these routes the single-use token is the only credential presented. See Deployment & Operational Considerations.

5.11 Error handling and diagnostics

  • Failures that occur outside normal request handling are returned as JSON rather than as plain text: {"error": true, "msg": "The server failed to complete this request.", "debug": "<message>"}.
  • All 5xx responses are logged server-side. Report requests are additionally traced on arrival and completion, and a connection closed before a response was sent is logged explicitly, so that "the request never arrived", "the request is still running" and "the request failed" can be distinguished during support investigations.

↑ Back to Table of Contents


6. How to Read the Endpoint Reference

Each endpoint is documented as:

  • a heading with the HTTP method and full path;
  • a one-line statement of purpose;
  • a fact table — Module, Authentication, Authorization, Content type;
  • Path parameters, Query parameters and Request body tables (omitted where the endpoint has none);
  • an Example request;
  • a Success response, with status code, example body and field descriptions;
  • an Errors table listing the conditions the endpoint actually produces; where no endpoint-specific conditions are documented, the standard responses in §5.3 apply;
  • Notes covering business rules, side effects, related endpoints and compatibility behaviour.

Where a detail is not established by the current implementation, it is marked "Not specified in the current implementation." rather than assumed.

↑ Back to Table of Contents


7. Module Directory

# Section Endpoints
8 Authentication, Session & Core 25
9 Admin 71
10 Client Success 41
11 Tasks & Time 184
12 Monthly Close 83
13 Comments Center 8
14 Report Center 27
15 Feedback Center 20
16 Integrations 17
17 Deployment & Operational Considerations —
18 Appendix A — Complete Endpoint Index 476
19 Appendix B — Environment Variables —
20 Appendix C — Health Checks & Operations —

↑ Back to Table of Contents

8. Authentication, Session & Core

This module is the front door of the Genie backend. It covers backend health checks, the CSRF token lifecycle, credential login and password recovery, both two-factor branches (Google Authenticator and emailed OTP), "Sign in with Google", logout, and the shared server-side session state that every other screen reads when it loads — the signed-in user, their roles and per-company access map, and the selected company + as-on period that drives every data query in the product. It also carries three session-scoped areas that belong to no other feature area: the user notification feed, the shared company/group tree used by the "Select company/group" picker, and two read-only role-map diagnostics. Except where a route is documented as a browser redirect, every endpoint in this module exchanges JSON.

8.1 How authentication works end to end

  1. Mint a CSRF token. The client calls GET /csrf once at boot. The token is stored on the server session and echoed in a JS-readable XSRF-TOKEN cookie; every state-changing auth POST must replay it in the X-XSRF-TOKEN header (or a _token body field).
  2. Submit credentials. POST /login verifies the email and password against the stored password hash, stamps users.last_login, and records the user id and two-factor method on the session. Failures are throttled: 6 attempts per email|ip then a 60-second lockout.
  3. Branch on auth_type. Login always answers redirect: "/login/check". The client then calls GET /login/check, which decides what the user must do next:
    - auth_type = 0 → the user has never chosen a 2FA method. Response is twoFactorType: "select"; they must call POST /login/select-2fa to pick Google Authenticator ('1') or email OTP ('3').
    - auth_type = 1 → Google Authenticator. Response is twoFactorType: "google", plus a QR code and secret when the user is not yet enrolled.
    - auth_type = 2 or 3 → an OTP is generated, stored on users.otp and emailed once per login attempt; response is twoFactorType: "otp".
    - Any other state → already authenticated, redirect: "/home".
  4. Verify. POST /login/verify/googleotp (TOTP) or POST /login/verify/otp (emailed code). The body carries only the code — the user identity is read from the server session, never from the request. Emailed OTPs expire after 5 minutes; POST /login/verify/resend issues a fresh one with a 30-second cooldown.
  5. enc_log + redirect. On success the server writes users.enc_log (an encrypted <userId>_<Y-m-d H:i:s> value), sets it as an HttpOnly enc_log cookie, marks the session authenticated, and returns { "redirect": "/home" }. The cookie is HttpOnly and is not readable by client-side scripts.
  6. Bootstrap the app. The client then calls GET /api/check/session, which returns the user, roles, per-company access map and the selected company/period.

Google sign-in bypasses 2FA entirely. GET /google/redirect → Google consent → GET /google/callback, which either logs an already-linked user straight in (setting enc_log), or — for a brand-new Google identity — sets a 60-second google cookie and redirects to the "One Last Time" linking screen backed by GET /google/signup and POST /google/signup.

8.2 Session and CSRF cookies

Cookie Set by Flags Purpose
genie.sid the session middleware (global) httpOnly, sameSite=lax, secure in production, maxAge 7 days The session id. Sessions are persisted in the MySQL sessions table, so every backend instance reads the same session state and the requests of a single login flow may be served by different instances. See Deployment & Operational Considerations.
XSRF-TOKEN GET /csrf httpOnly: false (JS-readable), sameSite=lax, path=/, secure in production The synchronizer CSRF token. Replayed as X-XSRF-TOKEN.
enc_log the two OTP verify routes, GET /google/callback, POST /google/signup httpOnly, sameSite=lax, path=/, secure in production The login token, also written to users.enc_log. Cleared by POST /api/logout.
google GET /google/callback (new Google identity only) httpOnly, sameSite=lax, path=/, secure in production, maxAge 60 s Holds the Google account id for the 1-minute account-linking window.

Request validation is applied globally: declared types are coerced and unknown body properties are stripped rather than rejected. A validation failure returns HTTP 400 with { "message": [ ...messages ], "error": "Bad Request", "statusCode": 400 }.


8.3 Health & CSRF

GET/

Backend liveness ping used by hosting/uptime checks. Verifies the database connection as well as the process.

Module Authentication, Session & Core > Health & CSRF
Authentication Not required
Authorization No role requirement
Content type application/json

Example request

curl https://api.example.com/

Success response — 200 OK (or 503 Service Unavailable when the database probe fails)

{
  "status": "ok",
  "service": "smartbook-genie-backend",
  "message": "Backend and database are healthy.",
  "checks": {
    "database": { "status": "up", "latencyMs": 7 }
  },
  "timestamp": "2026-09-01T09:14:22.481Z"
}
Field Type Description
status string "ok" or "error"
service string Always "smartbook-genie-backend"
message string Human-readable summary
checks.database.status string "up" or "down"
checks.database.latencyMs number Round-trip of SELECT 1 (only when up)
checks.database.error string Driver error message (only when down)
timestamp string ISO 8601 timestamp

Errors

Status Condition Message/shape
503 SELECT 1 against the database fails Same payload with status: "error" and checks.database.status: "down"

GET/health

Explicit health endpoint. Returns the same payload and behaviour as GET /, for platforms that require a named health path.

Module Authentication, Session & Core > Health & CSRF
Authentication Not required
Authorization No role requirement
Content type application/json

Example request

curl https://api.example.com/health

Success response — 200 OK (or 503)

{
  "status": "ok",
  "service": "smartbook-genie-backend",
  "message": "Backend and database are healthy.",
  "checks": {
    "database": { "status": "up", "latencyMs": 5 }
  },
  "timestamp": "2026-09-01T09:14:31.902Z"
}

Fields are identical to GET /.

Errors

Status Condition Message/shape
503 Database connectivity probe fails status: "error", checks.database.status: "down", checks.database.error

GET/csrf

Issues (or returns the existing) synchronizer CSRF token for this session and echoes it in a JS-readable cookie. Must be called before any CSRF-protected POST.

Module Authentication, Session & Core > Health & CSRF
Authentication Not required — the request creates or reuses the genie.sid session
Authorization No role requirement
Content type application/json

Example request

curl -c cookies.txt https://api.example.com/csrf

Success response — 200 OK

{ "csrfToken": "3f0b8c1de4a95c77b2c9f1a06d8e4471a2c5b93e0f6d18ca7b4e2d905c31af68" }
Field Type Description
csrfToken string 64-char hex token (32 random bytes)

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • Idempotent: a session that already holds a token gets the same value back; a new token is only minted when the session has none.
  • Sets cookie XSRF-TOKEN with httpOnly: false, path=/, sameSite=lax, secure in production — readable by JavaScript so the client can replay it.

8.4 Credential Login & Password Recovery

POST/login

Verifies email and password and establishes the pre-2FA session.

Module Authentication, Session & Core > Credential Login & Password Recovery
Authentication Not required, but a CSRF token (X-XSRF-TOKEN) must be supplied
Authorization No role requirement — this endpoint is reached before a user is authenticated
Content type application/json

Request body

Field Type Required Validation / allowed values Description
email string Yes Valid email address ("The email field must be a valid email address."), must not be empty ("The email field is required.") Account email
password string Yes String, must not be empty ("The password field is required.") Plain-text password, compared against the bcrypt hash

Example request

curl -b cookies.txt -c cookies.txt -X POST https://api.example.com/login \
  -H 'Content-Type: application/json' \
  -H 'X-XSRF-TOKEN: 3f0b8c1de4a95c77b2c9f1a06d8e4471a2c5b93e0f6d18ca7b4e2d905c31af68' \
  -d '{"email":"jane.doe@example.com","password":"S3cret!pass"}'

Success response — 200 OK

{ "requires2fa": true, "redirect": "/login/check" }
Field Type Description
requires2fa boolean true when auth_type is 1, 2, 3 or 0 (an unchosen method still blocks entry)
redirect string Always /login/check — the client must call that endpoint next

Errors

Status Condition Message/shape
400 Request validation failure { "message": ["The email field must be a valid email address."], "error": "Bad Request", "statusCode": 400 }
401 No active, non-deleted user with that email, or wrong password { "message": "These credentials do not match our records." }
403 Missing/mismatched CSRF token { "message": "CSRF token mismatch." }
429 More than 6 failures for the same email\|ip { "message": "Too many login attempts. Please try again in 47 seconds." }

Notes

  • Only users with active = 1 and deleted = 0 can authenticate.
  • Failed attempts are throttled at 6 attempts with a 60-second decay, counted per email|ip; the counter is cleared on a successful credential check. Throttle counters are held per application instance, so multi-instance deployments must preserve instance affinity for the login routes if the limit is to apply exactly as documented. See Deployment & Operational Considerations.
  • Stored hashes carrying a $2y$ prefix are normalised to $2b$ for comparison only — stored values are never rewritten.
  • On success users.last_login is stamped and session.login gets userId, authType, and state (2fa_pending or authenticated). otpGeneratedAt is cleared so a fresh OTP can be issued by GET /login/check.

GET/login/check

The post-credential branch point: tells the client whether the user must select a 2FA method, enrol in Google Authenticator, enter an emailed OTP, or go straight to the app.

Module Authentication, Session & Core > Credential Login & Password Recovery
Authentication Required — a login session established by POST /login
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Example request

curl -b cookies.txt https://api.example.com/login/check

Success response — 200 OK (emailed-OTP branch)

{
  "requires2fa": true,
  "authType": "3",
  "twoFactorType": "otp",
  "redirect": "/login/verify"
}

Google Authenticator branch, first-time enrolment:

{
  "requires2fa": true,
  "authType": "1",
  "twoFactorType": "google",
  "redirect": "/login/verify",
  "enroll": true,
  "google2fa_secret": "JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP",
  "otpauthUrl": "otpauth://totp/jane.doe%40example.com?secret=JBSWY3DP...&issuer=Genie",
  "qr": "data:image/png;base64,iVBORw0KGgoAAAANS..."
}
Field Type Description
requires2fa boolean Whether verification is still outstanding
authType "0" | "1" | "2" | "3" Effective users.auth_type: 0 = not chosen, 1 = Google Authenticator, 2 & 3 = emailed OTP
twoFactorType "select" | "google" | "otp" Present only when requires2fa is true. "select" means the user must first choose a method
redirect string /login/verify while pending, /home once authenticated
enroll boolean Google branch only: true when the user had no google2fa_secret and one was just generated
google2fa_secret string Google branch, un-enrolled only: base32 secret to type into the authenticator app
otpauthUrl string Google branch, un-enrolled only: otpauth:// URL, issuer from APP_NAME (default Genie)
qr string Google branch, un-enrolled only: inline PNG data URL. Absent when QR rendering is unavailable on the server — the client then falls back to showing google2fa_secret

Already-authenticated response:

{ "requires2fa": false, "authType": "3", "redirect": "/home" }

Errors

Status Condition Message/shape
401 No userId on the session, or the user is no longer active/exists { "message": "These credentials do not match our records." }
400 SMTP is not fully configured, or the OTP email could not be sent { "message": "SMTP is not fully configured. Please set MAIL_HOST, MAIL_PORT, MAIL_USERNAME, MAIL_PASSWORD, and MAIL_SENDER_ADDRESS." }

Notes

  • Side effect: for auth_type 2 and 3 this endpoint generates a 4-digit OTP, writes it to users.otp, stamps session.login.otpGeneratedAt, and emails it (subject "OTP For Genie Login"). Delivery is email-only, so auth_type 2 and 3 behave identically.
  • Idempotency: the OTP is issued only once per login attempt. Repeat calls to this route within the same login attempt do not issue a second code; issuance is gated on otpGeneratedAt, not on state.
  • Side effect: for an un-enrolled auth_type = 1 user, a new base32 secret is generated and persisted to users.google2fa_secret.
  • auth_type = 0 is treated as "must choose" — the user cannot reach the app until POST /login/select-2fa sets a real method.

POST/password/email

Requests a password-reset link for an email address.

Module Authentication, Session & Core > Credential Login & Password Recovery
Authentication Not required, but a CSRF token (X-XSRF-TOKEN) must be supplied
Authorization No role requirement — this endpoint is reached before a user is authenticated
Content type application/json

Request body

Field Type Required Validation / allowed values Description
email string Yes String, must not be empty, valid email address Account email to send the reset link to

Example request

curl -b cookies.txt -X POST https://api.example.com/password/email \
  -H 'Content-Type: application/json' \
  -H 'X-XSRF-TOKEN: 3f0b8c1d...' \
  -d '{"email":"jane.doe@example.com"}'

Success response — 201 Created (the framework's default status for POST)

{ "message": "We have emailed your password reset link." }
Field Type Description
message string Always the same text, whether or not the account exists
resetUrl string Non-production only, and only when SHOW_DEV_RESET_LINK=true. The reset link, echoed for local testing

Errors

Status Condition Message/shape
400 Request validation failure { "message": ["email must be an email"], "error": "Bad Request", "statusCode": 400 }
400 SMTP not configured, or the email could not be sent { "message": "Unable to send password reset email. Please verify SMTP credentials and sender configuration." }
403 Missing/mismatched CSRF token { "message": "CSRF token mismatch." }

Notes

  • Account enumeration is prevented — the same message is returned for unknown emails, and no work is done for them.
  • Side effect (known account only): any existing password_resets rows for that email are deleted and a new row is inserted holding a bcrypt hash of a 32-byte random token; the plain token only ever leaves the server inside the emailed link.
  • Link format: {FRONTEND_ORIGIN|FRONTEND_URL|http://localhost:3000}/password/reset/<token>?email=<email>.
  • resetUrl is never returned when NODE_ENV=production, and requires an explicit SHOW_DEV_RESET_LINK=true opt-in otherwise, so a mis-configured non-production deployment still cannot leak reset links.

POST/password/reset

Consumes a reset token, sets the new password, and signs the user straight in.

Module Authentication, Session & Core > Credential Login & Password Recovery
Authentication Not required, but a CSRF token (X-XSRF-TOKEN) must be supplied
Authorization No role requirement — this endpoint is reached before a user is authenticated
Content type application/json

Request body

Field Type Required Validation / allowed values Description
token string Yes String, must not be empty The raw token from the emailed link
email string Yes String, must not be empty, valid email address The account being reset
password string Yes String, must not be empty, minimum length 8 New password
password_confirmation string Yes String, must not be empty; must equal password (checked server-side) Confirmation of the new password

Example request

curl -b cookies.txt -c cookies.txt -X POST https://api.example.com/password/reset \
  -H 'Content-Type: application/json' \
  -H 'X-XSRF-TOKEN: 3f0b8c1d...' \
  -d '{
        "token":"9ad3...c1",
        "email":"jane.doe@example.com",
        "password":"N3wS3cret!",
        "password_confirmation":"N3wS3cret!"
      }'

Success response — 201 Created

{ "message": "Your password has been reset!", "redirect": "/home" }
Field Type Description
message string Confirmation text
redirect string Always /home — the session is already authenticated

Errors

Status Condition Message/shape
400 Request validation failure (including password shorter than 8 characters) { "message": ["password must be longer than or equal to 8 characters"], ... }
400 Empty token, no password_resets row for the email, or token hash mismatch { "message": "This password reset token is invalid." }
400 password and password_confirmation differ { "message": "The password confirmation does not match." }
400 Token valid but the account is inactive/deleted/missing { "message": "These credentials do not match our records." }
403 Missing/mismatched CSRF token { "message": "CSRF token mismatch." }
409 Token older than 60 minutes (or with an unusable created_at) { "message": "This password reset token has expired." }

Notes

  • Tokens are valid for 60 minutes; an expired token also deletes every password_resets row for that email.
  • Side effect: on success the password is re-hashed with bcrypt (cost 10), users.last_login is stamped, all password_resets rows for the email are deleted, and the session is marked authenticated — the user does not go through 2FA on this path.

8.5 Two-Factor Verification

POST/login/select-2fa

For users whose auth_type is still 0, records their chosen second factor and returns everything the verify screen needs to continue.

Module Authentication, Session & Core > Two-Factor Verification
Authentication Required — a mid-login session awaiting two-factor verification, plus a CSRF token (X-XSRF-TOKEN)
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Request body

Field Type Required Validation / allowed values Description
authType string Yes Normalised server-side to a trimmed string, then One of 1, 3 — message "Invalid auth type." '1' = Google Authenticator, '3' = email OTP. Coerced to a string so it binds to the MySQL ENUM correctly

Example request

curl -b cookies.txt -X POST https://api.example.com/login/select-2fa \
  -H 'Content-Type: application/json' \
  -H 'X-XSRF-TOKEN: 3f0b8c1d...' \
  -d '{"authType":"1"}'

Success response — 201 Created (Google Authenticator chosen)

{
  "requires2fa": true,
  "authType": "1",
  "twoFactorType": "google",
  "redirect": "/login/verify",
  "enroll": true,
  "google2fa_secret": "JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP",
  "otpauthUrl": "otpauth://totp/jane.doe%40example.com?secret=JBSWY3DP...&issuer=Genie",
  "qr": "data:image/png;base64,iVBORw0KGgoAAAANS..."
}

Email OTP chosen:

{ "requires2fa": true, "authType": "3", "twoFactorType": "otp", "redirect": "/login/verify" }

Fields are the same as GET /login/check. Note that on the Google branch this endpoint always returns the QR and secret — even for a user who already had a google2fa_secret — so the selection screen can display the QR inline the moment Google Authenticator is picked.

Errors

Status Condition Message/shape
400 authType is not '1' or '3' { "message": "Invalid auth type." }
401 No userId on the session, session is not 2fa_pending, or the user is inactive/deleted { "message": "These credentials do not match our records." }
403 Missing/mismatched CSRF token { "message": "CSRF token mismatch." }
400 SMTP not configured or OTP email send failed (email branch) { "message": "Unable to send OTP email. Please verify SMTP credentials and sender configuration." }

Notes

  • Side effect: persists the choice to users.auth_type — this is permanent, not per-session.
  • Side effect (email branch): immediately generates, stores and emails a fresh OTP and stamps otpGeneratedAt.
  • Side effect (Google branch): generates and persists a google2fa_secret if the user did not already have one.
  • Callable only mid-login. Only '1' and '3' are selectable; '2' exists in the schema but is not offered at login (it behaves the same as '3' — email delivery).

POST/login/verify/googleotp

Verifies a Google Authenticator TOTP code and completes login.

Module Authentication, Session & Core > Two-Factor Verification
Authentication Required — a mid-login session awaiting two-factor verification, plus a CSRF token (X-XSRF-TOKEN)
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Request body

Field Type Required Validation / allowed values Description
otp string Yes String, must not be empty ("Please Enter OTP") The 6-digit TOTP from the authenticator app

Example request

curl -b cookies.txt -c cookies.txt -X POST https://api.example.com/login/verify/googleotp \
  -H 'Content-Type: application/json' \
  -H 'X-XSRF-TOKEN: 3f0b8c1d...' \
  -d '{"otp":"418205"}'

Success response — 201 Created

{ "redirect": "/home" }
Field Type Description
redirect string Always /home

Errors

Status Condition Message/shape
400 Request validation failure { "message": ["Please Enter OTP"], "error": "Bad Request", "statusCode": 400 }
400 User has no google2fa_secret, or auth_type is not 1 { "message": "2FA not configured for this account." }
401 No userId on the session or session is not 2fa_pending { "message": "These credentials do not match our records." }
401 TOTP does not verify { "message": "Invalid OTP" }
403 Missing/mismatched CSRF token { "message": "CSRF token mismatch." }

Notes

  • Identity comes from the server session only — the body carries the code and nothing else, so a caller cannot verify on another user's behalf.
  • TOTP is checked with a window: 1 tolerance (one step either side of the current 30-second window).
  • Side effect: clears users.otp, writes users.enc_log (the encrypted <userId>_<Y-m-d H:i:s> value) and users.enc_log_on, and marks the session authenticated.
  • Cookie: sets enc_log with httpOnly: true, path=/, sameSite=lax, secure in production. The cookie is not readable by client-side scripts.
  • Development environments only: when APP_ENV is set to local or development, a development-only bypass allows a small set of pre-configured test accounts to complete this step without a real authenticator code. It never applies to the emailed-OTP path and is inactive whenever APP_ENV is unset or set to anything else, so it does not exist in a production deployment.

POST/login/verify/otp

Verifies the emailed OTP (for auth_type 2 and 3) and completes login.

Module Authentication, Session & Core > Two-Factor Verification
Authentication Required — a mid-login session awaiting two-factor verification, plus a CSRF token (X-XSRF-TOKEN)
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Request body

Field Type Required Validation / allowed values Description
otp string Yes String, must not be empty ("Please Enter OTP") The 4-digit code from the OTP email

Example request

curl -b cookies.txt -c cookies.txt -X POST https://api.example.com/login/verify/otp \
  -H 'Content-Type: application/json' \
  -H 'X-XSRF-TOKEN: 3f0b8c1d...' \
  -d '{"otp":"7413"}'

Success response — 201 Created

{ "redirect": "/home" }
Field Type Description
redirect string Always /home

Errors

Status Condition Message/shape
400 Request validation failure { "message": ["Please Enter OTP"], "error": "Bad Request", "statusCode": 400 }
400 User missing, or auth_type is not 2/3 { "message": "2FA not configured for this account." }
401 No userId on the session or session is not 2fa_pending { "message": "These credentials do not match our records." }
401 OTP older than 5 minutes, or never issued this attempt { "message": "Your OTP has expired. Please request a new one." }
401 Submitted code does not match users.otp { "message": "Invalid OTP" }
403 Missing/mismatched CSRF token { "message": "CSRF token mismatch." }

Notes

  • Identity comes from the server session only; the body carries just the code.
  • OTP TTL is 5 minutes, measured from session.login.otpGeneratedAt. An expired code is cleared from users.otp as part of the rejection, forcing a resend.
  • Side effect: on success clears users.otp and otpGeneratedAt, writes users.enc_log / enc_log_on, and marks the session authenticated.
  • Cookie: sets enc_log (HttpOnly, path=/, sameSite=lax, secure in production).
  • There is no dev master-code bypass on this route — the emailed path is never bypassed.

POST/login/verify/resend

Issues a fresh emailed OTP and restarts the expiry clock.

Module Authentication, Session & Core > Two-Factor Verification
Authentication Required — a mid-login session awaiting two-factor verification, plus a CSRF token (X-XSRF-TOKEN)
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Example request

curl -b cookies.txt -X POST https://api.example.com/login/verify/resend \
  -H 'X-XSRF-TOKEN: 3f0b8c1d...' \
  -H 'Content-Length: 0'

Success response — 201 Created

{ "message": "A new OTP has been sent." }
Field Type Description
message string Confirmation text

Errors

Status Condition Message/shape
400 User missing, or auth_type is not 2/3 { "message": "2FA not configured for this account." }
400 Called within 30 seconds of the previous OTP generation { "message": "Please wait 18 seconds before requesting another OTP." }
400 SMTP not configured, or the email could not be sent { "message": "Unable to send OTP email. Please verify SMTP credentials and sender configuration." }
401 No userId on the session or session is not 2fa_pending { "message": "These credentials do not match our records." }
403 Missing/mismatched CSRF token { "message": "CSRF token mismatch." }

Notes

  • Takes no request body.
  • Anti-spam cooldown of 30 seconds between generations.
  • Side effect: overwrites users.otp with a new 4-digit code, re-stamps otpGeneratedAt (resetting the 5-minute TTL), and emails the code.
  • Only valid for the emailed-OTP branch — Google Authenticator users have nothing to resend.

8.6 Sign in with Google

GET/google/redirect

Starts the Google OAuth flow by redirecting the browser to Google's consent screen.

Module Authentication, Session & Core > Sign in with Google
Authentication Not required
Authorization No role requirement
Content type No body — HTTP redirect

Example request

GET /google/redirect HTTP/1.1
Host: api.example.com

Success response — 302 Found

HTTP/1.1 302 Found
Location: https://accounts.google.com/o/oauth2/v2/auth?client_id=...&redirect_uri=https%3A%2F%2Fapp.example.com%2Fgoogle%2Fcallback&response_type=code&scope=openid+email+profile&access_type=online&prompt=select_account

Response body: no body — this route only issues a redirect.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • The consent request uses the scopes openid email profile, with access_type=online and prompt=select_account.
  • redirect_uri is derived as ${APP_URL}/google/callback (trailing slash trimmed; APP_URL defaults to http://localhost:3000). client_id comes from GOOGLE_CLIENT_ID.
  • This is a browser navigation endpoint, not an XHR endpoint.

GET/google/callback

Google's OAuth redirect target. Exchanges the authorization code, then logs the user in, auto-links the Google account to a matching Genie account, or sends a brand-new Google identity to the account-linking screen.

Module Authentication, Session & Core > Sign in with Google
Authentication Not required
Authorization No role requirement
Content type No body — HTTP redirect

Query parameters

Name Type Required Default Description
code string No (treated as empty when absent) '' Google's authorization code. A missing or unusable code redirects to the login page with an error

Example request

GET /google/callback?code=4%2F0AeanS0b... HTTP/1.1
Host: api.example.com

Success response — 302 Found

Existing / auto-linked user:

HTTP/1.1 302 Found
Set-Cookie: enc_log=U0ZkV...; Path=/; HttpOnly; SameSite=Lax
Location: /home

Brand-new Google identity:

HTTP/1.1 302 Found
Set-Cookie: google=104829173650192837465; Path=/; HttpOnly; SameSite=Lax; Max-Age=60
Location: /login/google-link?code=U1ZWa1Zt...

Failure (never a non-2xx status — the error is carried in the redirect):

HTTP/1.1 302 Found
Location: /login?g_error=Login%20failed.%20Please%20try%20again

Response body: no body — this route only issues a redirect.

Errors

Status Condition Message/shape
302 Missing code, token exchange failure, profile fetch failure, or a profile with no id/email Redirect to /login?g_error=Login%20failed.%20Please%20try%20again
302 The matched account has active = 0 Redirect to /login?g_error=This%20user%20is%20inactive.%20Please%20contact%20Administrator.

Notes

  • Resolution order: (1) a user already holding this google_id; (2) a user with the same email — their google_id/google_email are written on the spot (auto-link); (3) otherwise, a new identity that must be linked manually.
  • Google sign-in bypasses 2FA entirely — a matched user's session is marked authenticated immediately.
  • Side effect (matched user): stamps last_login, writes enc_log / enc_log_on, and sets the HttpOnly enc_log cookie.
  • Side effect (new identity): sets a 60-second HttpOnly google cookie holding the Google account id, and passes an encrypted <googleId>_<email> value as the code query parameter of the linking screen.
  • The linking code is transient: it is minted here, consumed by the linking screen, and never persisted. It is encrypted with a padded variant of the cipher so that long Google identifier/email pairs survive the round trip intact.
  • The redirect targets are frontend routes, not backend routes.

GET/google/signup

Validates the linking code from GET /google/callback against the 1-minute google cookie and returns the Google email for the "One Last Time" linking screen to display.

Module Authentication, Session & Core > Sign in with Google
Authentication Not required — but the short-lived google cookie set by GET /google/callback must be present
Authorization No role requirement
Content type application/json

Query parameters

Name Type Required Default Description
code string Yes (empty is rejected) '' The encrypted <googleId>_<email> value handed to the linking screen

Example request

curl -b 'google=104829173650192837465' \
  'https://api.example.com/google/signup?code=U1ZWa1Zt...'

Success response — 200 OK

{ "email": "jane.doe@gmail.com" }
Field Type Description
email string The Google account email, for display on the linking screen

Errors

Status Condition Message/shape
400 code missing or empty { "message": "Invalid Code, Please try again" }
400 google cookie absent or expired (60-second window), or the decoded Google id does not match the cookie { "message": "Link has been Expired" }
400 code cannot be decrypted, or does not decode to exactly <id>_<email> { "message": "Invalid Link" }

Notes

  • The google cookie is read from the Cookie request header and must be sent with the request.
  • Purely a read/validation step; it makes no database changes.

POST/google/signup

Completes account linking: verifies the user's existing Genie email + password, attaches the Google account, and signs them in.

Module Authentication, Session & Core > Sign in with Google
Authentication Not required, but a CSRF token (X-XSRF-TOKEN) must be supplied
Authorization No role requirement — this endpoint is reached before a user is authenticated
Content type application/json

Request body

Field Type Required Validation / allowed values Description
code string Yes String, must not be empty ("Link has been Expired") The encrypted <googleId>_<email> linking code
email string Yes String, must not be empty ("Incorrect email or password") The user's existing Genie account email
password string Yes String, must not be empty ("Incorrect email or password") That account's password

Example request

curl -b cookies.txt -c cookies.txt -X POST https://api.example.com/google/signup \
  -H 'Content-Type: application/json' \
  -H 'X-XSRF-TOKEN: 3f0b8c1d...' \
  -d '{
        "code":"U1ZWa1Zt...",
        "email":"jane.doe@example.com",
        "password":"S3cret!pass"
      }'

Success response — 201 Created

{ "redirect": "/home" }
Field Type Description
redirect string Always /home

Errors

Status Condition Message/shape
400 Request validation failure { "message": ["Incorrect email or password"], "error": "Bad Request", "statusCode": 400 }
400 code cannot be decrypted, or does not decode to exactly <id>_<email> { "message": "Link has been Expired" }
400 No active, non-deleted user with that email, or wrong password { "message": "Incorrect email or password" }
400 The Genie account already has a google_id { "message": "This Email already connected with some other Google accounts" }
403 Missing/mismatched CSRF token { "message": "CSRF token mismatch." }

Notes

  • Depends on GET /google/callback having issued the code; unlike GET /google/signup this endpoint does not re-check the google cookie, only that the code decrypts to two parts.
  • Side effect: writes google_id and google_email onto the user, stamps last_login, and writes enc_log / enc_log_on.
  • Cookie: sets enc_log (HttpOnly, path=/, sameSite=lax, secure in production).
  • Marks the session authenticated immediately — no 2FA on the Google path.
  • Password comparison uses the same $2y$ → $2b$ bcrypt normalisation as POST /login.

8.7 Logout

POST/api/logout

Tears down the authenticated session for browser clients.

Module Authentication, Session & Core > Logout
Authentication Not required — the endpoint clears whatever session the caller presents
Authorization No role requirement
Content type application/json

Example request

curl -b cookies.txt -c cookies.txt -X POST https://api.example.com/api/logout -H 'Content-Length: 0'

Success response — 201 Created

{ "error": false, "message": "Logged out successfully." }
Field Type Description
error boolean Always false
message string Always "Logged out successfully."

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • No CSRF token is required on this route, unlike the other authentication POSTs.
  • Resets session.login to an empty object and clears the enc_log cookie (path=/). It does not destroy the underlying genie.sid session row or clear the XSRF-TOKEN cookie, so the CSRF token minted by GET /csrf survives and can be reused for the next login.
  • users.enc_log in the database is left as-is; only the cookie is removed.

8.8 Session State & Company Selection

GET/api/check/session

The bootstrap call every screen makes at mount. Validates the session and returns the signed-in user, their roles, their per-company menu access map, the selected company and as-on period, and supplementary dashboard data.

Module Authentication, Session & Core > Session State & Company Selection
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session. The response reports the caller's effective roles: global_admin is true when the assigned-role list contains role id 1 (Global Master System Administrator), or a role literally named "Global Master System Administrator", or users.global_admin = 1
Content type application/json

Example request

curl -b cookies.txt https://api.example.com/api/check/session

Success response — 200 OK

{
  "userData": {
    "id": 237,
    "email": "jane.doe@example.com",
    "first_name": "Jane",
    "last_name": "Doe",
    "user_name": "Jane Doe",
    "firm_id": 340,
    "firm_admin": 1,
    "roleArray": [11, 13],
    "roles": [
      { "id": 11, "role_name": "Monthly Close Manager" },
      { "id": 13, "role_name": "Invoice Approver" }
    ]
  },
  "user_id": "U0ZkV1ZtMTNaR1p0",
  "menuArray": [],
  "global_user_profile": "",
  "global_company_logo": "uploads/company/1163.png",
  "selectedCompany": {
    "company_id": 1163,
    "global_company_name": "Acme Manufacturing LLC",
    "global_company_logo": "uploads/company/1163.png",
    "qbo_connection": 1
  },
  "carryOver": {
    "company_id": 1163,
    "asOnDate": "2026-09-01",
    "monthVal": "Sep",
    "yearVal": 2026
  },
  "global_role_name": "Monthly Close Manager,Invoice Approver",
  "global_admin": false,
  "accessData": {
    "1163": [
      { "menu_id": 4, "access_type": 2 },
      { "menu_id": 9, "access_type": 1 }
    ]
  },
  "disableEmail": false,
  "downloadReport": [],
  "utilizationData": {
    "resData": [
      { "name": "Jane Doe", "billable_hours": "31.50", "target": "35.00" }
    ]
  },
  "firmInternalCompany": {
    "id": 1163,
    "name": "Acme Manufacturing LLC",
    "firm_internal_company": 1,
    "qbo_connection": 1
  },
  "sessionData": {
    "login": {
      "userId": 237,
      "state": "authenticated",
      "authType": "3",
      "company": {
        "company_id": 1163,
        "global_company_name": "Acme Manufacturing LLC",
        "global_company_logo": "uploads/company/1163.png",
        "qbo_connection": 1
      },
      "carryOver": {
        "company_id": 1163,
        "asOnDate": "2026-09-01",
        "monthVal": "Sep",
        "yearVal": 2026
      }
    },
    "company": {
      "company_id": 1163,
      "global_company_name": "Acme Manufacturing LLC",
      "global_company_logo": "uploads/company/1163.png",
      "qbo_connection": 1
    }
  }
}
Field Type Description
userData.id / email / first_name / last_name / user_name number / string Identity of the signed-in user
userData.firm_id number The user's firm
userData.firm_admin number 1 when the user is a firm administrator
userData.roleArray number[] Assigned role ids
userData.roles array Assigned roles as { id, role_name }
user_id string Encrypted <userId>_profile token, used for profile deep links
menuArray array Always []
global_user_profile string Always ""
global_company_logo string Logo path of the selected company
selectedCompany object | null { company_id, global_company_name, global_company_logo, qbo_connection }. company_id: 0 is the sticky Global scope, not "unset"
carryOver object | null { company_id, asOnDate, monthVal, yearVal } — the as-on period every data query filters by
global_role_name string Comma-joined assigned role names, or the primary role name when none are assigned
global_admin boolean Whether the user is a Global Master System Administrator
accessData object Map of company_id → array of { menu_id, access_type }. Rows with access_type = 0 (no access) are omitted
disableEmail boolean Always false
downloadReport array Always []
utilizationData.resData array { name, billable_hours, target } rows for the utilization widget
firmInternalCompany object | null { id, name, firm_internal_company, qbo_connection } for the resolved company
sessionData object Raw echo of the server session: { login, company }

Errors

Status Condition Message/shape
401 No userId on the session, session not authenticated, or the user row is missing/deleted { "message": "These credentials do not match our records." }

Notes

  • Side effect: this endpoint validates and seeds the session company. If the session holds no company, the first active company for the firm (preferring the firm-internal one) is selected and written to the session together with a matching carryOver. An already-selected company is resolved without an active = 1 filter, so a selection that points at an inactive company — as Report Center may legitimately hold — is preserved rather than replaced by a default.
  • company_id: 0 (Global) round-trips untouched and is never replaced by a default company.
  • The utilization query is raced against a 10-second timeout; on timeout utilizationData.resData comes back as [] rather than failing the whole session check.

GET/api/company/session

Reads the shared company selection and as-on period out of the session, so a fresh browser or a second tab starts on the same company.

Module Authentication, Session & Core > Session State & Company Selection
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Example request

curl -b cookies.txt https://api.example.com/api/company/session

Success response — 200 OK

{
  "error": false,
  "company": {
    "company_id": 1163,
    "qbo_connection": 1,
    "global_company_name": "Acme Manufacturing LLC",
    "global_company_logo": "uploads/company/1163.png"
  },
  "carryOver": {
    "company_id": 1163,
    "asOnDate": "2026-09-01",
    "monthVal": "Sep",
    "yearVal": 2026
  }
}
Field Type Description
error boolean Always false on success
company object | null { company_id, qbo_connection, global_company_name, global_company_logo }, or null when nothing is selected yet
carryOver.company_id number The company the period belongs to
carryOver.asOnDate string YYYY-MM-01 — the first of the selected month
carryOver.monthVal string Three-letter month label (Jan … Dec)
carryOver.yearVal number Four-digit year

Errors

Status Condition Message/shape
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • carryOver is never null: a session that only ever stored a company id still reads back a usable period, defaulting to the current month and year.
  • The selection is held server-side in the session rather than in browser storage, so it is shared across tabs and browsers.

POST/api/company/session

Changes the company every screen is scoped to. This is the single shared switch point — picking a company on Reports also changes it on Client Success, Admin, Tasks & Time and Monthly Close.

Module Authentication, Session & Core > Session State & Company Selection
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session, but scoping is enforced: a non-global-admin may only select a company whose firm_id matches their own firm. Global admins (users.global_admin = 1, or the Global Master System Administrator role) may select any company
Content type application/json

Request body — the body is loosely typed; no request body schema is applied, so no field-level validation rules are enforced. Keys read by this endpoint:

Field Type Required Validation / allowed values Description
company_id number | numeric string Yes Coerced to a number; must be finite and >= 0. undefined, null, '', NaN and negatives are rejected The company to select. 0 means the sticky "Global" scope, not "unset"
preserve_period boolean (any truthy value) No Coerced to a boolean When truthy, keeps the existing month/year; otherwise the period resets to the current month

Example request

curl -b cookies.txt -X POST https://api.example.com/api/company/session \
  -H 'Content-Type: application/json' \
  -d '{"company_id":1163,"preserve_period":true}'

Success response — 201 Created

{
  "error": false,
  "company": {
    "company_id": 1163,
    "qbo_connection": 1,
    "global_company_name": "Acme Manufacturing LLC",
    "global_company_logo": "uploads/company/1163.png"
  },
  "carryOver": {
    "company_id": 1163,
    "asOnDate": "2026-09-01",
    "monthVal": "Sep",
    "yearVal": 2026
  }
}

Fields are identical to GET /api/company/session. Selecting Global (company_id: 0) returns global_company_name: "Global", global_company_logo: "", qbo_connection: 0.

Errors

Status Condition Message/shape
400 company_id missing, empty, non-numeric or negative { "message": "Company not found." }
400 The id resolves to no company row, or to a company outside the caller's firm { "message": "Company not found." }
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • Side effect: writes both session keys together (company and carryOver) so they can never drift apart, and touches company.updated_at — which drives the "Last used" ordering on the Admin company list.
  • Period semantics: without preserve_period the as-on month/year reset to today; with it, the existing period is kept and only the company id changes.
  • The company lookup applies no active/deleted filter, so an inactive company whose historical reports must remain accessible can still be selected.

POST/api/company/session/period

Changes only the as-on month/year, keeping the selected company. Backs Monthly Close's month/year selects.

Module Authentication, Session & Core > Session State & Company Selection
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Request body — the body is loosely typed; no request body schema is applied, so no field-level validation rules are enforced. Keys read by this endpoint:

Field Type Required Validation / allowed values Description
monthVal string No Coerced to a string; matched case-insensitively against its first three letters (Jan … Dec). Empty or unrecognised falls back to the current stored month, and an unmatched label resolves asOnDate to January Three-letter month label
yearVal number No Coerced to a number; must be > 0, otherwise the current stored year is kept Four-digit year

Example request

curl -b cookies.txt -X POST https://api.example.com/api/company/session/period \
  -H 'Content-Type: application/json' \
  -d '{"monthVal":"Jul","yearVal":2026}'

Success response — 201 Created

{
  "error": false,
  "company": {
    "company_id": 1163,
    "qbo_connection": 1,
    "global_company_name": "Acme Manufacturing LLC",
    "global_company_logo": "uploads/company/1163.png"
  },
  "carryOver": {
    "company_id": 1163,
    "asOnDate": "2026-07-01",
    "monthVal": "Jul",
    "yearVal": 2026
  }
}

Fields are identical to GET /api/company/session.

Errors

Status Condition Message/shape
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • Side effect: rewrites session.carryOver with a recomputed asOnDate (always the 1st of the resolved month) and leaves session.company untouched.
  • Because the period lives in the shared session, changing it on Monthly Close carries it to every other screen — the same way the company does.

8.9 Notifications

GET/api/user-based/notification

Returns the 10 most recent notifications for the signed-in user plus a total count — the header bell feed.

Module Authentication, Session & Core > Notifications
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session. Which notifications a user sees is decided per source server-side — e.g. invoice-rejection rows only reach users holding the Invoice Manager role (id 14)
Content type application/json

Example request

curl -b cookies.txt https://api.example.com/api/user-based/notification

Success response — 200 OK

{
  "notifications": [
    {
      "id": "invoice-approval-9814",
      "date": "08/28/2026",
      "time": "04:12 PM",
      "subtitle": "Approval required for INV-10422 Invoice",
      "title": "Jane Doe raised the Invoice(INV-10422). You Can Approve/Reject the Invoice.",
      "subject": null,
      "notificationsCount": 1,
      "user": "237",
      "enc_id": "U0ZkV1ZtMTNaR1p0",
      "invoiceBatchId": 9814,
      "invoiceNo": "INV-10422",
      "fromInvoice": true,
      "strtotime": 1787932320
    }
  ],
  "notificationsCount": 27
}
Field Type Description
notifications array The 10 newest items, sorted by strtotime descending
notificationsCount number Total number of notifications available (not the length of the array above)

Each notification item is an untyped object whose keys vary by source. Common keys across sources: id, date (MM/DD/YYYY), time, title, subtitle, subject, user, strtotime (unix seconds; the only sort key). Source markers and extras include fromTimesheet + week, fromInvoice + enc_id/invoiceBatchId/invoiceNo, fromComment + attribute/invoiceNo, and fromMonthlyClose.

Errors

Status Condition Message/shape
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • Aggregates four sources — timesheet approvals/rejections, invoice approvals/rejections, comment threads, and Monthly Close — merged in the order timesheet → invoice → comments → monthly close, which is what decides display order whenever strtotime values tie.
  • Read-only; nothing is marked as read or written back.

GET/api/user-based/notification/all

Returns the complete notification list for the signed-in user — the "See All" screen.

Module Authentication, Session & Core > Notifications
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session; per-source visibility rules apply as above
Content type application/json

Example request

curl -b cookies.txt https://api.example.com/api/user-based/notification/all

Success response — 200 OK

[
  {
    "id": "invoice-approval-9814",
    "date": "08/28/2026",
    "time": "04:12 PM",
    "subtitle": "Approval required for INV-10422 Invoice",
    "title": "Jane Doe raised the Invoice(INV-10422). You Can Approve/Reject the Invoice.",
    "subject": null,
    "notificationsCount": 1,
    "user": "237",
    "enc_id": "U0ZkV1ZtMTNaR1p0",
    "invoiceBatchId": 9814,
    "invoiceNo": "INV-10422",
    "fromInvoice": true,
    "strtotime": 1787932320
  },
  {
    "id": "timesheet-approval-237-2026-W34",
    "date": "08/24/2026",
    "time": "09:03 AM",
    "subtitle": "Approval required for John Smith Timesheet",
    "title": "Timesheet approval required for the John Smith for the week 08/15/2026 - 08/21/2026",
    "subject": "",
    "week": "2026-W34",
    "user": "237",
    "fromTimesheet": true,
    "strtotime": 1787814180
  }
]

The response is a bare array (not wrapped in an object), sorted by strtotime descending, with no 10-item cap. Item fields are the same untyped, source-dependent shape described for GET /api/user-based/notification.

Errors

Status Condition Message/shape
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • Same aggregation and ordering as the bell feed; only the pagination differs.
  • Takes no query parameters or request body.

8.10 Company & Group Tree

POST/api/get/web-treeview

Returns the company/group hierarchy behind the shared "Select company/group" picker, plus flat lists of every visible group and company.

Module Authentication, Session & Core > Company & Group Tree
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session. Visibility is role-driven server-side: a Global Company Administrator sees every group and every company with no firm filter; everyone else is scoped to their own firm
Content type application/json

Example request

curl -b cookies.txt -X POST https://api.example.com/api/get/web-treeview -H 'Content-Length: 0'

Success response — 201 Created

{
  "tree": [
    {
      "id": 42,
      "name": "Est's Firm(test) Accounting Group",
      "expandable": true,
      "child": {
        "group": [
          {
            "id": 57,
            "name": "Boston project Group",
            "expandable": true,
            "child": {
              "group": [],
              "company": [{ "id": 1163, "name": "Acme Manufacturing LLC" }]
            }
          }
        ],
        "company": [{ "id": 1201, "name": "Northwind Traders Inc" }]
      }
    }
  ],
  "group": [
    { "id": 42, "name": "Est's Firm(test) Accounting Group" },
    { "id": 57, "name": "Boston project Group" }
  ],
  "company": [
    { "id": 1163, "name": "Acme Manufacturing LLC" },
    { "id": 1201, "name": "Northwind Traders Inc" }
  ],
  "cacheKey": "_TREE_DATA_237_3"
}
Field Type Description
tree array Root group nodes
tree[].id / name number / string Group identity
tree[].expandable boolean Whether the row renders an expand toggle. Decided on the raw child pointers, so a group whose only child pointer is orphaned still shows a toggle over an empty body
tree[].child.group array Nested group nodes, same shape, recursive
tree[].child.company array { id, name } companies attached to this node, in junction-row order
group array Flat list of every visible group as { id, name }
company array Flat list of every visible company as { id, name }
cacheKey string _TREE_DATA_<userId>_3 — a client-side cache key

Errors

Status Condition Message/shape
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • Takes no request body, query or path parameters despite being a POST.
  • The canonical path is api/get/web-treeview. A separate Monthly Close endpoint is available at api/get/treeview; the two are not interchangeable — this route returns the tree behind the shared "Select company/group" picker, while the Monthly Close route returns that module's own tree, built by different rules.
  • Companies attach to every node, root or nested, with no gate — a group with companies but no child groups is still expandable.
  • firm_internal_group / firm_internal_company rows are not hidden from the tree.
  • Soft-deleted companies (deleted = 1) still appear as leaves; only deleted = 2 placeholder rows drop out. Groups apply whichever soft-delete column the schema has.
  • Recursion is capped at a depth of 20 as a defence against malformed parent pointers.

8.11 Role Map Diagnostics

POST/api/utilities/configure/debug/role-map-compare

Read-only dry run that compares what the role-map engine would derive for a set of companies against what the database currently holds. Used to validate Client Success → Configure role propagation.

Module Authentication, Session & Core > Role Map Diagnostics
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session — any authenticated user may call it. It performs no writes
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyIds number[] Yes Array, Numeric (numeric strings are coerced), each item must be an integer. Ids are then de-duplicated and any id <= 0 is dropped Companies to compare

Example request

curl -b cookies.txt -X POST https://api.example.com/api/utilities/configure/debug/role-map-compare \
  -H 'Content-Type: application/json' \
  -d '{"companyIds":[1163,1201]}'

Success response — 201 Created

{
  "error": false,
  "totalCompanies": 2,
  "mismatches": 1,
  "allMatch": false,
  "report": [
    {
      "companyId": 1163,
      "firmId": 340,
      "primaryAccountant": { "computed": 512, "current": 512, "match": true },
      "mcManager": { "computed": 488, "current": 488, "match": true },
      "invoiceApprovers": { "computed": [512, 907], "current": [512], "match": false }
    },
    {
      "companyId": 1201,
      "firmId": 340,
      "primaryAccountant": { "computed": null, "current": null, "match": true },
      "mcManager": { "computed": 488, "current": 488, "match": true },
      "invoiceApprovers": { "computed": [], "current": [], "match": true }
    }
  ]
}
Field Type Description
error boolean false on success, true for the "no usable ids" case
totalCompanies number Count of distinct valid ids received
mismatches number How many companies differ in at least one of the three roles
allMatch boolean mismatches === 0
report[].companyId / firmId number The company and the firm it was grouped under
report[].primaryAccountant object { computed, current, match } — user id or null, from mc_peer_review_primary.primary_accountant
report[].mcManager object { computed, current, match } — user id or null, from client_assignments.team_leader
report[].invoiceApprovers object { computed: number[], current: number[], match } — from assign_user_roles where role_id = 13

Errors

Status Condition Message/shape
200/201 No valid company ids after filtering (not an HTTP error) { "error": true, "message": "companyIds required." }
400 Request validation failure (e.g. companyIds not an array of integers) { "message": ["each value in companyIds must be an integer number"], "error": "Bad Request", "statusCode": 400 }
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • Strictly read-only — SELECT queries only, no derived table is written.
  • Companies are grouped by company.firm_id; a company whose firm_id is null or 0 is silently skipped and will not appear in report.
  • "Computed" values are derived by the role-map engine from service_roles, service_role_users and cs_override_roles, with override-beats-default resolution; "current" values are read straight from the live derived tables.
  • Diagnostic/support endpoint, not part of any end-user screen.

POST/api/utilities/configure/debug/role-map-inputs

Read-only diagnostic dump of the raw inputs the role-map engine reads for a single company — useful when role-map-compare reports a mismatch and you need to see why.

Module Authentication, Session & Core > Role Map Diagnostics
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session — any authenticated user may call it. It performs no writes
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Numeric (numeric strings are coerced), integer. A value of 0 is additionally rejected The company to dump inputs for

Example request

curl -b cookies.txt -X POST https://api.example.com/api/utilities/configure/debug/role-map-inputs \
  -H 'Content-Type: application/json' \
  -d '{"companyId":1163}'

Success response — 201 Created

{
  "error": false,
  "companyId": 1163,
  "companyFirmId": 340,
  "firm": { "id": 88, "user_id": 340, "use_service_role": 1 },
  "roleFirmId": 340,
  "engineCurrentScope": "firm_id = ?",
  "engineRoleCount": 2,
  "engineRoles": [
    {
      "id": 21,
      "role": "Senior Accountant",
      "firm_id": 340,
      "deleted": 0,
      "primary_accountant": 1,
      "mc_manager": 0,
      "invoice_approver": 1
    },
    {
      "id": 22,
      "role": "Close Manager",
      "firm_id": 340,
      "deleted": 0,
      "primary_accountant": 0,
      "mc_manager": 1,
      "invoice_approver": 0
    }
  ],
  "companyServiceRoleUsers": [
    { "service_role_id": 21, "user_id": 512, "deleted": 0 },
    { "service_role_id": 22, "user_id": 488, "deleted": 0 }
  ],
  "companyOverrides": [
    {
      "service_role_id": 22,
      "override_primary_accountant": 0,
      "override_mc_manager": 1,
      "override_invoice_approver": 0,
      "deleted_at": null
    }
  ],
  "involvedRoles": [
    {
      "id": 21,
      "role": "Senior Accountant",
      "firm_id": 340,
      "deleted": 0,
      "primary_accountant": 1,
      "mc_manager": 0,
      "invoice_approver": 1
    },
    {
      "id": 22,
      "role": "Close Manager",
      "firm_id": 340,
      "deleted": 0,
      "primary_accountant": 0,
      "mc_manager": 1,
      "invoice_approver": 0
    }
  ]
}
Field Type Description
error boolean false on success
companyId number Echo of the requested company
companyFirmId number company.firm_id, or 0 when the company does not exist
firm object { id, user_id, use_service_role } from firms (values are null when no firm row matches)
roleFirmId number The firm scope the engine actually uses: 0 when use_service_role = 0, otherwise companyFirmId
engineCurrentScope string The literal SQL predicate applied to service_roles — "firm_id IS NULL" or "firm_id = ?"
engineRoleCount number Number of service_roles rows in that scope
engineRoles array Those rows: id, role, firm_id, deleted, primary_accountant, mc_manager, invoice_approver
companyServiceRoleUsers array service_role_users rows for this company: service_role_id, user_id, deleted
companyOverrides array cs_override_roles rows for this company: service_role_id, the three override_* flags, deleted_at
involvedRoles array The service_roles rows referenced by the two lists above, whatever their firm scope — this is how an out-of-scope role shows up

Errors

Status Condition Message/shape
200/201 companyId resolves to 0 (not an HTTP error) { "error": true, "message": "companyId required." }
400 Request validation failure (e.g. companyId not an integer) { "message": ["companyId must be an integer number"], "error": "Bad Request", "statusCode": 400 }
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • SELECT-only, no writes.
  • A company id that matches no row returns companyFirmId: 0 and a firm block of nulls rather than a 404.
  • Diagnostic/support endpoint, not part of any end-user screen.

↑ Back to Table of Contents


9. Admin

The Admin module is the administrative back office of Genie: it owns companies (create, edit, activate/deactivate/delete, logo, billing details, reporting-group definitions, chart-of-accounts mapping and industry verticals), the company-group hierarchy and the per-group user access matrices, the user directory (invite, roles, module/company access, impersonation, 2FA type, profile picture, and the "User Check" reassignment workflow), reporting groups, the global activity/audit log, and the QuickBooks Online sync-status board.

Every route in this module is served under the /api prefix. The caller is resolved from the session on every request: an established, authenticated session is required, otherwise the request fails with 401 { "message": "These credentials do not match our records." }. All role and scope checks are enforced server-side.

Request validation is applied globally, so request bodies and query strings are validated against the schemas documented below, unknown properties are stripped, and numeric fields are coerced from strings. A failed validation returns 400 { "message": [...], "error": "Bad Request", "statusCode": 400 }.

Many identifiers accept either a plain numeric id or the application's encrypted (base64) id: 123, 123_suffix and encrypted strings are all accepted, and an identifier that cannot be resolved is treated as 0. Responses that carry an id usually also carry an encryptId / encryptedId for use in subsequent calls.

9.1 Company

Company administration: the company pickers used across the app, the Admin > Company list, company creation (single and bulk Excel import), status changes, the company edit screen (details, billing, logo), the per-company reporting-group definitions, the chart-of-accounts to reporting-group mapping, and industry / business-model verticals.

POST/api/company/list

Returns the company dropdown for the signed-in user, scoped by their role and by their access to the requested module.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the backend scopes results by role — a global admin (Global Master System Administrator 1 / Global Company Administrator 3 / Global Company Group Administrator 4) is unscoped, everyone else is narrowed to the companies they hold access to for the requested module
Content type application/json

Request body

Field Type Required Validation / allowed values Description
module number No Integer, coerced from string Menu id to scope company access by. Defaults to 3 (Monthly Close) when omitted; the value 1 is remapped to 81 (Tasks & Time).

Example request

curl -X POST https://api.example.com/api/company/list \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"module": 81}'

Success response — 200 OK

{
  "companies": [
    {
      "id": 412,
      "name": "Northwind Services",
      "encId": "3kan9eKI6bt+Qq1w==",
      "firmId": 27,
      "firmInternalCompany": 0,
      "deleted": 0
    }
  ]
}
Field Type Description
companies[].id number Company id
companies[].name string Company name ('' when null)
companies[].encId string Encrypted company id, accepted by every company_id field in this section
companies[].firmId number Owning firm id (0 when null)
companies[].firmInternalCompany number 1 when this is the firm's internal-accounting company
companies[].deleted number 0 active, 1 inactive, 2 deleted

Errors

Status Condition Message/shape
400 module is not an integer Validation error array
401 No authenticated session, or the user row is missing/deleted { "message": "These credentials do not match our records." }

Notes

  • The module id drives an access-scope lookup, so the same user can see different companies for Tasks & Time than for Monthly Close.
GET/api/firm/list

Returns the "Account" (firm) dropdown shown above the Company dropdown for a Global Master System Administrator, who has no firm of their own to scope by.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Example request

curl https://api.example.com/api/firm/list \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "accounts": [
    { "id": 27, "name": "Acme Accounting Partners" },
    { "id": 41, "name": "Beacon CPA Group" }
  ]
}
Field Type Description
accounts[].id number Firm's user_id (the id POST /api/firm/company/list expects as firmId)
accounts[].name string Firm account name

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Compatibility route: retained for compatibility with earlier clients.
  • Schema-adaptive: returns { "accounts": [] } when neither a firms nor a firm table (or its id/name columns) exists.
  • Non-deleted firms only, ordered by account name ascending.
POST/api/firm/company/list

Returns the Company dropdown for one specific firm — the second step after a Global Master System Administrator picks an Account from GET /api/firm/list.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmId number Yes Integer, coerced from string Firm user_id from GET /api/firm/list

Example request

curl -X POST https://api.example.com/api/firm/company/list \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"firmId": 27}'

Success response — 200 OK

{
  "companies": [
    { "id": 412, "name": "Northwind Services", "encId": "3kan9eKI6bt+Qq1w==", "firmId": 27 }
  ]
}
Field Type Description
companies[].id number Company id
companies[].name string Company name
companies[].encId string Encrypted company id
companies[].firmId number Owning firm id

Errors

Status Condition Message/shape
400 firmId missing or not an integer Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Compatibility route: retained for compatibility with earlier clients.
  • Results are not narrowed by the caller's own company access, because the caller has already chosen the firm to drill into. Ordered by company name ascending.
  • firmId is required and must be an integer.
POST/api/company/view_all

The Admin > Company list: a paged, searchable company table plus every page-level flag and dropdown source the screen needs.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization No hard role gate; four separate role tests shape the response — isGlobalAdmin = roles 1/3/4, canFilterAccounts = roles 1/3, canAddCompany = roles 1/3 and not a firm admin, canSelectAccountForAdd = roles 1/2. Non-global, non-firm-admin users are narrowed to their mapped companies.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
content string No String Free-text search on company name
selectCompany string No String Show mode. show_active (default) or show_all
selcet_company number No Integer Accounts filter: restrict to this firm id. Honoured only for canFilterAccounts users. The field name is spelled exactly as shown and must be sent that way.
offset number No Integer, minimum value 0 Row offset. Default 0
limit number No Integer, minimum value 1 Page size. Default 50, clamped to a maximum of 200

Example request

curl -X POST https://api.example.com/api/company/view_all \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"content": "north", "selectCompany": "show_active", "offset": 0, "limit": 50}'

Success response — 200 OK

{
  "firms": [{ "user_id": 27, "account_name": "Acme Accounting Partners", "plan_type": 2 }],
  "canFilterAccounts": true,
  "canAddCompany": true,
  "canSelectAccountForAdd": true,
  "firmAdmin": false,
  "isGlobalAdmin": true,
  "userPlanTypes": [2, 3],
  "isMultiple": false,
  "defaultPlan": 0,
  "firmPlanType": 0,
  "runningQueue": false,
  "companyData": [
    {
      "id": 412,
      "encryptId": "3kan9eKI6bt+Qq1w==",
      "name": "Northwind Services",
      "total_tasks": 7,
      "firm_internal_company": 0,
      "qbo_connection": 1,
      "deleted": 0,
      "active": 1,
      "isCurrent": true,
      "last_used": "Aug 21, 2026"
    }
  ],
  "currentCompany": {
    "id": 412,
    "encryptId": "3kan9eKI6bt+Qq1w==",
    "name": "Northwind Services",
    "qbo_connection": 1
  },
  "total": 128
}
Field Type Description
firms array Account options; populated only for a global company admin
canFilterAccounts boolean Render the Accounts filter (roles 1/3)
canAddCompany boolean Render the add/import/download toolbar (roles 1/3 and not a firm admin)
canSelectAccountForAdd boolean Render the Accounts select inside the add form (roles 1/2)
firmAdmin boolean Caller is a firm admin
isGlobalAdmin boolean Caller holds role 1
userPlanTypes number[] Hardcoded [2, 3] — 2 = Accounting Tier Plan, 3 = All Modules Plan
isMultiple boolean Mirrors firmAdmin
defaultPlan number Always 0
firmPlanType number Firm's plan type; 0 unless the caller is a firm admin
runningQueue boolean A companyImport queue job is currently running for this user
companyData[] array The page of company rows
companyData[].total_tasks number Open task count (same predicate the inactivate guard uses)
companyData[].deleted number 0 active, 1 inactive, 2 deleted
companyData[].isCurrent boolean Row is the session's currently selected company
companyData[].last_used string updated_at formatted as MMM DD, YYYY (zero-padded day)
currentCompany object | null The session's selected company, or null
total number Total matching rows, ignoring offset/limit

Errors

Status Condition Message/shape
400 Invalid offset / limit / selcet_company Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • show_all includes inactive and soft-deleted companies; show_active does not.
  • When the caller has an empty access scope, the response still carries all page meta but companyData: [], currentCompany: null.
POST/api/company/search

Search variant of the company list. Identical contract and behaviour to POST /api/company/view_all.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization Same as POST /api/company/view_all
Content type application/json

Request body — identical to POST /api/company/view_all.

Example request

curl -X POST https://api.example.com/api/company/search \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"content": "north"}'

Success response — 200 OK. Identical shape to POST /api/company/view_all.

Errors — identical to POST /api/company/view_all.

Notes

  • Compatibility route: retained for compatibility with earlier clients; POST /api/company/view_all is the canonical route. Both routes share one implementation, so the search response carries the full page metadata as well as the matching rows.
POST/api/company/newcompany

Creates a company, seeds its accounting defaults and service roles, attaches it to a company group, and makes it the session's selected company.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization Requires (role 1 Global Master System Administrator OR role 3 Global Company Administrator) AND not a firm admin, or the caller is a firm admin. accUserId is honoured only for roles 1/2.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
company_name string Yes String New company name. Trimmed; must be non-empty and globally unique
plan_type number No Integer userPlanRadio from the "Choose plan type" block — 2 = Accounting Tier Plan, 3 = All Modules Plan
accUserId number No Integer Firm user_id to create the company under (the add form's Accounts select). Honoured only for roles 1/2; otherwise the caller's own firm is used
domain string No String Company domain
group_id number No Integer Company group to attach the new company to

Example request

curl -X POST https://api.example.com/api/company/newcompany \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"company_name": "Northwind Services", "plan_type": 3, "accUserId": 27, "domain": "northwind.example", "group_id": 88}'

Success response — 200 OK

{
  "error": false,
  "id": 412,
  "encryptId": "3kan9eKI6bt+Qq1w==",
  "name": "Northwind Services",
  "redirect": "/admin/company/3kan9eKI6bt%2BQq1w%3D%3D"
}
Field Type Description
error boolean false on success
id number New company id
encryptId string Encrypted company id
name string Stored company name
redirect string Client route for the new company's edit screen

Business-rule failures return 200 with an error envelope instead of an HTTP error:

{ "error": true, "msg": "Company Name already Exist" }

Errors

Status Condition Message/shape
200 company_name blank after trim { "error": true, "msg": "Please provide company name" }
200 A non-deleted company already has that name (global, case-insensitive) { "error": true, "msg": "Company Name already Exist" }
400 company_name missing / wrong type Validation error array
401 No authenticated session, or caller may not add companies { "message": "These credentials do not match our records." }

Notes

  • Company names are unique across the whole install, not per firm.
  • Side effects: seeds the four per-company accounting config tables from accounting_default; attaches the company to the firm's group (or group_id when supplied); seeds service roles; writes session.login.company and session.login.carryOver, so the new company becomes the active one everywhere.
  • The new company's firm_id is set to the selected account (or the caller's own firm when no account is selectable).
GET/api/company/check_company_exists

Live duplicate check for the company-name field in the add form.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Query parameters

Name Type Required Default Description
company_name string Yes — Name to test. String; compared case-insensitively after trimming

Example request

curl -G https://api.example.com/api/company/check_company_exists \
  --data-urlencode 'company_name=Northwind Services' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{ "is_company": "exists" }
Field Type Description
is_company string "exists" when a non-deleted company already has that name, otherwise "add"

Errors

Status Condition Message/shape
400 company_name missing Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • A blank name short-circuits to {"is_company": "add"}.
  • The lookup is global (not firm-scoped) and matches only deleted = 0 rows — unlike POST /api/company/newcompany, which also blocks names held by inactive companies.
POST/api/company/import/template/download

Builds the bulk-import Excel template for a firm and returns a one-shot download link.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session
Content type application/json (the file itself is fetched from the returned link)

Request body

Field Type Required Validation / allowed values Description
plan_type number No Integer Accepted by the request body schema but not used by this endpoint
firm_id number No Integer Firm whose company groups seed the template's dropdown sheet. Falls back to the caller's firm

Example request

curl -X POST https://api.example.com/api/company/import/template/download \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"firm_id": 27}'

Success response — 200 OK

{ "redirect": "/api/company/import/template/generated/1f0c2b1e-8a4a-4c1d-9a71-3ad0f1f9c2ab" }
Field Type Description
redirect string Path to GET /api/company/import/template/generated/:token, which streams the workbook

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Generates a two-sheet .xlsx: Company Info (header row Company Name, Plan Type, Company Domain, Company Group, Industry Verticals, Business Model Verticals, plus one sample row) and Available Data (valid plan types, the firm's company groups, industry verticals, business-model verticals).
  • The generated workbook is held as temporary server-side storage and referenced by a single-use token. The download itself is performed by GET /api/company/import/template/generated/:token, which must be served by the same application instance that produced the workbook; see Deployment & Operational Considerations.
POST/api/company/import/template

Uploads a filled-in import workbook, validates every row, and queues the valid ones for background company creation.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session
Content type multipart/form-data

Request body

Field Type Required Validation / allowed values Description
file file Yes Extension must be .xlsx, .xls or .xlx The filled-in template, sent under the field name file
plan_type number No Integer Accepted by the request body schema but not used by this endpoint
firm_id number No Integer Firm to import the companies into. Falls back to the caller's firm

Example request

curl -X POST https://api.example.com/api/company/import/template \
  -b 'genie.sid=<session-cookie>' \
  -F 'file=@Company Import Template.xlsx' \
  -F 'firm_id=27'

Success response — 200 OK

{
  "error": false,
  "message": "Data are Imported Successfully.",
  "createdCount": 14,
  "errorData": [],
  "runningQueue": true
}
Field Type Description
error boolean false only when at least one row imported and no row failed
message string Toast text. Omitted entirely when errorData is non-empty
createdCount number Rows accepted for import
errorData array Rejected rows with their reasons; drives the error modal
errorExportLink string Present only when errorData is non-empty: link to an .xlsx export of the rejected rows
runningQueue boolean | number true when this call started a queue job, otherwise the current companyImport queue state

Errors

Status Condition Message/shape
200 No file, or an unsupported extension { "error": true, "message": "Please provide Excel sheet format supported file.", "runningQueue": 0 }
200 Workbook has no data rows { "error": true, "message": "Empty Excel Sheet.Please put on some data to import.", "runningQueue": 0 }
200 Header row does not match the template { "error": true, "message": "Invalid Excel File. Columns are not matched with the template.", "runningQueue": 0 }
200 Every row was rejected { "error": true, "message": "No Data to Import.", "createdCount": 0, "errorData": [], "runningQueue": 0 }
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The header must match Company Name | Plan Type | Company Domain | Company Group | Industry Verticals | Business Model Verticals exactly.
  • Valid rows are inserted asynchronously by a queue_manager job; the response returns before the import finishes. Poll GET /api/company/import/queue/status for completion.
  • When the response contains error rows it carries no message, so the client shows only the error modal and no toast.
  • Duplicate detection is scoped to the target firm and treats deleted IN (0, 1) names as taken.
GET/api/company/import/queue/status

Reports whether a bulk company-import job is currently running for the caller.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Example request

curl https://api.example.com/api/company/import/queue/status \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{ "runningQueue": true }
Field Type Description
runningQueue boolean A companyImport queue row is still running for this user

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Intended for polling after POST /api/company/import/template returns runningQueue: true.
GET/api/company/import/template/generated/:token

Streams a previously generated import template workbook and then deletes it.

Module Admin > Company
Authentication Not required — possession of the single-use token is the only credential
Authorization No role requirement; the single-use token is the only credential
Content type Binary .xlsx download, delivered as Company Import Template.xlsx

Path parameters

Name Type Required Description
token string Yes UUID returned in the redirect of POST /api/company/import/template/download (or of an import error export)

Example request

curl -OJ https://api.example.com/api/company/import/template/generated/1f0c2b1e-8a4a-4c1d-9a71-3ad0f1f9c2ab

Success response — 200 OK, binary body, with Content-Disposition: attachment; filename="Company Import Template.xlsx".

Response body: binary workbook — not JSON.

Errors

Status Condition Message/shape
404 Token unknown or already consumed { "error": true, "message": "File not found." }

Notes

  • Single use: the token is invalidated before the download starts and the temporary file is removed once the transfer completes. A second request for the same token returns 404.
  • The download must be served by the same application instance that generated the workbook, so multi-instance deployments must preserve instance affinity for these routes. See Deployment & Operational Considerations.
POST/api/company/redirect

Called on every company-row click: pins the company into the session, touches its "Last used" timestamp, and returns the edit URL plus registration state.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization The company must be inside the caller's access scope; additionally, a non-global-company-admin (not roles 1/3/4) may only target a company belonging to their own firm
Content type application/json

Request body

Field Type Required Validation / allowed values Description
company_id string Yes String; plain or encrypted id Company to select
type number No Integer Ignored by this route; the request body schema is shared with POST /api/company/delete

Example request

curl -X POST https://api.example.com/api/company/redirect \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"company_id": "3kan9eKI6bt+Qq1w=="}'

Success response — 200 OK

{
  "error": false,
  "id": 412,
  "encryptId": "3kan9eKI6bt+Qq1w==",
  "qbo_connection": 1,
  "steps": 5,
  "no_register": 5,
  "company_logo": "company-412-1756700000000.png",
  "companyEditUrl": "/admin/company/3kan9eKI6bt%2BQq1w%3D%3D"
}
Field Type Description
error boolean Always false on success
id / encryptId number / string Selected company
qbo_connection number 1 when QuickBooks is connected
steps / no_register number company.reg_steps — how far registration got; the client uses it to redirect to the registration wizard instead of the edit screen
company_logo string Stored logo file name ('' when none)
companyEditUrl string Client route for the company edit screen

Errors

Status Condition Message/shape
400 company_id does not resolve, or the company does not exist "Company not found."
401 Company outside the caller's scope, or belongs to another firm { "message": "These credentials do not match our records." }

Notes

  • The route is a POST because it mutates the session and updates the company row.
  • Side effects: writes session.login.company and session.login.carryOver (resetting the as-on period) and updates company.updated_at, which is what the list's "Last used" column renders.
  • The firm check is enforced server-side, so a firm admin cannot pin another firm's company into their session by posting a raw company id.
POST/api/company/delete

Reactivates, deactivates, or soft-deletes a company.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization type = 2 (delete) requires role 1 (Global Master System Administrator). type = 0 / 1 have no additional role gate.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
company_id string Yes String; plain or encrypted id Company to act on
type number Yes in practice Optional Integer — but a missing value becomes NaN and is rejected as an invalid status type 0 = make active, 1 = make inactive, 2 = delete

Example request

curl -X POST https://api.example.com/api/company/delete \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"company_id": "3kan9eKI6bt+Qq1w==", "type": 1}'

Success response — 200 OK

{ "error": false }
Field Type Description
error boolean false when the status change was applied

Errors

Status Condition Message/shape
200 type = 1 and the company still has open tasks { "error": true, "msg": "The company was unable to be inactive. This company has 7 open task(s)." }
400 Company not found "Company not found."
400 type is not 0, 1 or 2 "Invalid company status type."
401 type = 2 without role 1 { "message": "These credentials do not match our records." }

Notes

  • The company name is rewritten to carry the state: "<name>", "<name> (inactive)" or "<name> (deleted)", alongside deleted = 0/1/2 and active = 1/0/0.
  • On delete the QuickBooks realm id is prefixed with D_ and qbo_connection is cleared.
  • On every transition the company's company_group_companies memberships are removed, and saved Task List "Company" filters are rewritten to follow the rename.
  • The open-task guard uses the same predicate as the list's total_tasks, so the client count and the server guard can never disagree.
GET/api/company/details/:id

Loads the company edit screen: core details, billing information, the edit permission flag and the QuickBooks reconnection warning.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization Company must be inside the caller's access scope, and (unless the caller holds roles 1/3/4) must belong to their firm. canEdit is false for an inactive/deleted company unless the caller is a firm admin or holds role 1.
Content type application/json

Path parameters

Name Type Required Description
id string Yes Company id, plain or encrypted

Example request

curl https://api.example.com/api/company/details/3kan9eKI6bt%2BQq1w%3D%3D \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "company": {
    "id": 412,
    "encryptId": "3kan9eKI6bt+Qq1w==",
    "name": "Northwind Services",
    "domain": "northwind.example",
    "budget": 1,
    "qbo_connection": 0,
    "logo": "company-412-1756700000000.png",
    "logo_url": "/api/company/logo/3kan9eKI6bt%2BQq1w%3D%3D"
  },
  "billing": {
    "id": 91,
    "company_id": 412,
    "billing_name": "Northwind Services LLC",
    "billing_email": "ap@northwind.example",
    "address": "1 Harbour Way, Portland OR"
  },
  "canEdit": true,
  "warning": "QuickBooks connection not found for Northwind Services. Please click here to reconnect."
}
Field Type Description
company.budget number company.actual_vs_budjet — the Actual vs Budget flag (column name misspelled in the schema)
company.logo_url string Link to GET /api/company/logo/:id, or '' when no logo is stored
billing object Raw user_billing_info row for the company; {} when there is none
canEdit boolean Drives the Edit button; every mutation on this screen re-checks it server-side
warning string Non-empty only when qbo_connection = 0

Errors

Status Condition Message/shape
400 Id does not resolve, or the company row is missing "Company not found."
401 Company outside the caller's scope or another firm's { "message": "These credentials do not match our records." }

Notes

  • Both this endpoint and POST /api/company/save enforce the company scope check server-side, so an authenticated session cannot read or rename a company outside its scope by id.
  • billing is read straight from user_billing_info and its exact columns depend on the deployment's schema.
POST/api/company/save

Saves the company edit form (name, domain, budget flag).

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization Same scope check as GET /api/company/details/:id; additionally rejects the save when canEdit is false
Content type application/json

Request body

Field Type Required Validation / allowed values Description
company_id string Yes String; plain or encrypted id Company to update
company_name string Yes String; must be non-empty after trimming New name
company_domain string No String Written to company.domain
budget number No Integer Written to company.actual_vs_budjet

Example request

curl -X POST https://api.example.com/api/company/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"company_id": "3kan9eKI6bt+Qq1w==", "company_name": "Northwind Services", "company_domain": "northwind.example", "budget": 1}'

Success response — 200 OK

{ "error": false, "id": 412, "name": "Northwind Services" }
Field Type Description
error boolean false on success
id number Company id
name string Stored name

Errors

Status Condition Message/shape
200 Another company in the same firm already uses that name { "error": true, "msg": "Company name already exist." }
400 company_name blank after trim "Company name is required."
400 Company not found "Company not found."
401 Outside scope, or the company is read-only (canEdit = false) { "message": "These credentials do not match our records." }

Notes

  • The duplicate check here is firm-scoped and treats deleted IN (0, 1) as taken — narrower than the global check on POST /api/company/newcompany.
  • Side effects on rename: saved Task List "Company" filters are rewritten, the companyToReplace placeholder in user_logs is substituted, the company is (re-)attached to the firm's first company group with the saver's mapping, and session.login.company.global_company_name is refreshed so the header repaints without a re-login.
POST/api/company/logo/upload

Uploads a company logo image.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization The company must belong to the caller's firm
Content type multipart/form-data

Request body

Field Type Required Validation / allowed values Description
file file Yes Extension must be .png, .jpg, .jpeg, .gif, .webp or .svg; must be non-empty The image, under the field name file
company_id string Yes String; plain or encrypted id Company to attach the logo to

Example request

curl -X POST https://api.example.com/api/company/logo/upload \
  -b 'genie.sid=<session-cookie>' \
  -F 'file=@logo.png' \
  -F 'company_id=3kan9eKI6bt+Qq1w=='

Success response — 200 OK

{ "error": false, "logo_url": "/api/company/logo/3kan9eKI6bt%2BQq1w%3D%3D" }
Field Type Description
error boolean false on success
logo_url string Link to GET /api/company/logo/:id

Errors

Status Condition Message/shape
200 No file, or an empty buffer { "error": true, "message": "Please choose an image file." }
200 Extension not in the allow-list { "error": true, "message": "Unsupported image format." }
401 Company belongs to another firm Rejected by the firm-ownership check

Notes

  • The image is kept as temporary server-side storage under a generated file name of the form company-<id>-<timestamp><ext>, and only that file name is stored in company.logo. Logo storage is local to the application instance that received the upload, so deployments must preserve instance affinity for the logo routes and must expect stored logos not to survive an instance restart. See Deployment & Operational Considerations.
POST/api/company/logo/remove

Removes a company's logo file and clears company.logo.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization The company must belong to the caller's firm
Content type application/json

Request body

Field Type Required Validation / allowed values Description
company_id string Yes String; plain or encrypted id Company whose logo to remove

Example request

curl -X POST https://api.example.com/api/company/logo/remove \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"company_id": "3kan9eKI6bt+Qq1w=="}'

Success response — 200 OK

{ "error": false }

Errors

Status Condition Message/shape
401 Company belongs to another firm Rejected by the firm-ownership check

Notes

  • Deleting the stored image file is best-effort; the company.logo column is cleared regardless.
GET/api/company/logo/:id

Streams a company's logo image.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization The company must belong to the caller's firm
Content type binary download — image/png, image/jpeg, image/gif, image/webp, image/svg+xml, or application/octet-stream by extension

Path parameters

Name Type Required Description
id string Yes Company id, plain or encrypted

Example request

curl https://api.example.com/api/company/logo/3kan9eKI6bt%2BQq1w%3D%3D \
  -b 'genie.sid=<session-cookie>' --output logo.png

Success response — 200 OK, raw image bytes with Content-Type set from the stored file's extension.

Response body: binary image — not JSON.

Errors

Status Condition Message/shape
404 No logo recorded, or the file is missing from server-side storage { "error": true, "message": "Logo not found." }
401 Company belongs to another firm Rejected by the firm-ownership check

Notes

  • This is the URL returned as logo_url by GET /api/company/details/:id and POST /api/company/logo/upload.
POST/api/company/billInfo/save

Upserts the company's billing contact block.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Request body

Field Type Required Validation / allowed values Description
company_id string Yes String; plain or encrypted id Company to bill
billing_name string No String Billing contact name; trimmed
billing_email string No String — no email-format validation Billing email; trimmed
address string No String Billing address; trimmed

Example request

curl -X POST https://api.example.com/api/company/billInfo/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"company_id": "3kan9eKI6bt+Qq1w==", "billing_name": "Northwind Services LLC", "billing_email": "ap@northwind.example", "address": "1 Harbour Way, Portland OR"}'

Success response — 200 OK

{ "error": false }

Errors

Status Condition Message/shape
400 company_id missing Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Writes to user_billing_info, inserting when no row exists for the company and updating otherwise. Every column is written only when present in the deployment's schema.
  • Returns { "error": false } unchanged when the user_billing_info table is absent — the save silently no-ops.
  • Unlike the other company-edit routes, this one performs no company-scope or canEdit check.
GET/api/company/defineGroups/:id

Loads the "Define Reporting Groups" screen for a company: the global groups, the company's custom groups and the Name/Number mode.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization Company must be inside the caller's scope and (unless roles 1/3/4) belong to their firm; canEdit follows the same read-only rule as GET /api/company/details/:id
Content type application/json

Path parameters

Name Type Required Description
id string Yes Company id, plain or encrypted

Example request

curl https://api.example.com/api/company/defineGroups/3kan9eKI6bt%2BQq1w%3D%3D \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "canEdit": true,
  "define_group_type": 1,
  "global_groups": [
    { "id": 1, "report_name": "Revenue", "type": 1, "is_removable": 0 },
    { "id": 2, "report_name": "Cost of Goods Sold", "type": 1, "is_removable": 0 }
  ],
  "custom_groups": [
    { "id": 55, "name": "Owner Draws", "type": 2, "is_removable": 1 }
  ]
}
Field Type Description
canEdit boolean When false, the client hides New Group, the row actions and the Name/Number switch
define_group_type number 1 = map by account Name, 2 = map by account Number. Defaults to 1
global_groups[] array Install-wide reporting groups (global_account_groups), in id order
global_groups[].type number 1 = Income Statement, 2 = Balance Sheet
global_groups[].is_removable number 0 for the nine protected P&L/BS groups
custom_groups[] array The company's own groups (custom_account_group), in id order

Errors

Status Condition Message/shape
400 Company not found "Company not found."
401 Company outside scope or another firm's { "message": "These credentials do not match our records." }

Notes

  • Groups are ordered by id, not by name: id order is the P&L reading order accountants expect (Revenue, COGS, Cost of Service Delivery, Marketing and Sales, G&A, AR, AP, Other Income, Other Expense).
POST/api/company/defineGroups

Creates a new reporting group — either a global one or a company-specific custom one.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization Company scope check plus canEdit; a read-only company rejects the create
Content type application/json

Request body

Field Type Required Validation / allowed values Description
company_id string Yes String; plain or encrypted id Owning company (used for scope, and for custom groups also for storage)
group_type number Yes Integer 1 = create a global group, anything else = create a custom group
type number Yes Integer Statement the group belongs to — 1 = Income Statement, 2 = Balance Sheet
custom_name string Yes String; non-empty after trim Group name

Example request

curl -X POST https://api.example.com/api/company/defineGroups \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"company_id": "3kan9eKI6bt+Qq1w==", "group_type": 2, "type": 1, "custom_name": "Owner Draws"}'

Success response — 200 OK

{ "error": false, "id": 55, "name": "Owner Draws" }
Field Type Description
error boolean false on success
id number New group id
name string Stored name

Errors

Status Condition Message/shape
200 Global group name already used { "error": true, "msg": "Global reporting group name already exist" }
200 Custom group name already used for this company { "error": true, "msg": "Custom reporting group name already exist" }
200 Backing table missing { "error": true }
400 custom_name blank after trim "Group name is required."
401 Outside scope, or company is read-only { "message": "These credentials do not match our records." }

Notes

  • The canEdit gate is enforced server-side, not only by hiding the Add Group button in the client.
  • New global groups are created with is_removable = 1, so they can be deleted again; the nine seeded groups cannot.
POST/api/company/defineGroups/edit

Renames an existing reporting group.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization For a custom group the owning company must be in scope and editable (canEdit). A global group has no company to scope by and therefore no additional check.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number Yes Integer Group id to rename
type string Yes String Group kind. Treated as global when the string contains global (case-insensitive), otherwise custom
name string Yes String; non-empty after trim New name

Example request

curl -X POST https://api.example.com/api/company/defineGroups/edit \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"id": 55, "type": "custom", "name": "Owner Distributions"}'

Success response — 200 OK

{ "error": false, "msg": "Updated successfully." }

Errors

Status Condition Message/shape
200 New global name collides with another global group { "error": true, "msg": "Global reporting group name already exist" }
200 New custom name collides within the same company { "error": true, "msg": "Custom reporting group name already exist" }
400 name blank after trim "Group name is required."
400 Custom group id not found "Group not found."
401 Owning company outside scope, or read-only { "message": "These credentials do not match our records." }

Notes

  • Renaming enforces the same name-uniqueness rule as creation, using the same predicate, so a rename cannot introduce a duplicate name.
  • Authorisation for a custom group is derived from the group's stored company_id, not from anything the caller sends.
DELETE/api/company/defineGroups

Soft-deletes a reporting group, unless it is still referenced by Report Writer.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization For a custom group, the owning company must be in scope and editable. A global group has no additional check.
Content type application/json (request body on a DELETE)

Request body

Field Type Required Validation / allowed values Description
g_id number Yes Integer Group id to delete
type string Yes String Treated as global when the string contains global, otherwise custom

Example request

curl -X DELETE https://api.example.com/api/company/defineGroups \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"g_id": 55, "type": "custom"}'

Success response — 200 OK

{ "error": false }

Errors

Status Condition Message/shape
200 The group is referenced in Report Writer { "error": true, "msg": "Cannot remove this Custom Reporting Group. Because, it was added in Report Writer" } (Global when the group is global)
400 Custom group id not found "Group not found."
401 Owning company outside scope, or read-only { "message": "These credentials do not match our records." }

Notes

  • Soft delete only: sets deleted = 1 (plus active = 0 / deleted_at when those columns exist). Custom groups are additionally constrained by company_id in the UPDATE.
  • The Report Writer usage check runs against report_writer_details and resolves its account/group-type columns defensively across schema variants.
POST/api/company/defineGroups/type/edit

Switches a company between mapping its chart of accounts by account Name and by account Number.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization Company scope check plus canEdit
Content type application/json

Request body

Field Type Required Validation / allowed values Description
company_id string Yes String; plain or encrypted id Company to update
type number Yes Integer; must be 1 or 2 1 = by Name, 2 = by Number

Example request

curl -X POST https://api.example.com/api/company/defineGroups/type/edit \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"company_id": "3kan9eKI6bt+Qq1w==", "type": 2}'

Success response — 200 OK

{ "error": false }

Errors

Status Condition Message/shape
400 type is neither 1 nor 2 "Invalid define group type."
400 Company not found "Company not found."
401 Outside scope, or read-only { "message": "These credentials do not match our records." }

Notes

  • type is validated against the allowed values and the submitted company_id is checked for ownership before the update is applied.
POST/api/company/mapping/get

Loads the chart-of-accounts to reporting-group mapping grid for one statement, in either by-name or by-number mode.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization Company scope check; canEdit is returned as a hint and re-enforced on save
Content type application/json

Request body

Field Type Required Validation / allowed values Description
company_id string Yes String; plain or encrypted id Company
type number Yes Integer Statement: 1 = Income Statement, 2 = Balance Sheet. Defaults to 1 if absent from the parsed body
group_mode number No Integer 1 = map by account name, 2 = map by account-number range. Default 1
changes number No Integer Set to 1 from the "Changes have been made to the Chart of Accounts" banner; narrows the account list to changed accounts

Example request

curl -X POST https://api.example.com/api/company/mapping/get \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"company_id": "3kan9eKI6bt+Qq1w==", "type": 1, "group_mode": 1}'

Success response — 200 OK

{
  "canEdit": true,
  "type": 1,
  "group_mode": 1,
  "global_groups": [{ "id": 1, "report_name": "Revenue", "type": 1 }],
  "custom_groups": [{ "id": 55, "name": "Owner Draws", "type": 1 }],
  "accounts": [
    {
      "id": 9001,
      "qbo_id": "84",
      "account_name": "Consulting Income",
      "account_num": "4000",
      "account_type": "Income",
      "active": 1,
      "deleted": 0,
      "global_group_id": 1,
      "custom_group_ids": [55]
    }
  ],
  "ranges": [
    { "global_group_id": 1, "custom_group_id": 0, "from": "4000", "to": "4999" }
  ]
}
Field Type Description
canEdit boolean Read-only hint for the client
type / group_mode number Echo of the resolved request values
global_groups[] / custom_groups[] array Groups available for this statement, in id order
accounts[] array Chart-of-accounts rows for the statement's account types
accounts[].account_name string Falls back to qbo_id when blank
accounts[].account_num string Falls back to "-" when blank
accounts[].global_group_id number Currently mapped global group, 0 when none
accounts[].custom_group_ids number[] Currently mapped custom groups
ranges[] array By-number mapping ranges (populated in group_mode = 2)

Errors

Status Condition Message/shape
400 Company not found "Company not found."
401 Company outside scope or another firm's { "message": "These credentials do not match our records." }

Notes

  • The account-type filter is fixed per statement: type = 1 covers Income, Other Income, Cost of Goods Sold, Expense, Other Expense; type = 2 covers Accounts Payable, Accounts Receivable, Bank, Credit Card, Equity, Fixed Asset, Long Term Liability, Other Current Liability, Other Current Asset, Other Asset.
  • Existing mappings come from accounting_group_config (by name) and accounting_group_config_by_number (ranges).
POST/api/company/mapping/save

Saves the chart-of-accounts mapping for one statement and one mapping mode.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization Company scope check plus canEdit. Any role that can reach the company may save unless the company is read-only.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
company_id string Yes String; plain or encrypted id Company
type number Yes Integer Statement: 1 = Income Statement, 2 = Balance Sheet. Falls back to 1
group_mode number Yes Integer 1 = by name, 2 = by number range. Falls back to 1
account_group_mode number No Integer For by-number saves: 1 = Global column, 2 = Custom column. Anything other than 2 is treated as 1. Default 1
data unknown No Optional, untyped The mapping payload. Must be an array of row objects; any other value is treated as an empty array

Example request

curl -X POST https://api.example.com/api/company/mapping/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
        "company_id": "3kan9eKI6bt+Qq1w==",
        "type": 1,
        "group_mode": 1,
        "account_group_mode": 1,
        "data": [{ "qbo_id": "84", "global_group_id": 1, "custom_group_ids": [55] }]
      }'

Success response — 200 OK

{ "error": false }

Errors

Status Condition Message/shape
400 Company not found "Company not found."
401 Outside scope, or read-only (canEdit = false) { "message": "These credentials do not match our records." }

Notes

  • data is untyped: its element shape is whatever the mapping screen posts, and it is interpreted server-side according to group_mode.
  • By-number ranges are cleaned up per company, statement and scope (Global or Custom), so saving one scope never affects the ranges saved for the other.
  • Access is governed by the company-scoped canEdit check rather than by a global-admin/firm-admin-only gate, so roles such as Company Group Company Administrator can save mappings for companies they can reach.
  • Returns { "error": false } unchanged when the required by-number columns are missing from the schema.
POST/api/company/industry/get

Loads the Industry Verticals and Business Model Verticals pickers for a company.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization Company scope check; canEdit is returned as a hint and re-enforced on save
Content type application/json

Request body

Field Type Required Validation / allowed values Description
company_id string Yes String; plain or encrypted id Company

Example request

curl -X POST https://api.example.com/api/company/industry/get \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"company_id": "3kan9eKI6bt+Qq1w=="}'

Success response — 200 OK

{
  "canEdit": true,
  "verticals": {
    "selected": [{ "vertical_id": 12, "name": "Professional Services", "vertical_type": 1 }],
    "available": [{ "id": 15, "name": "Retail", "vertical_type": 1 }]
  },
  "models": {
    "selected": [],
    "available": [{ "id": 31, "name": "Subscription", "vertical_type": 2 }]
  }
}
Field Type Description
canEdit boolean Read-only hint; enforced server-side on save
verticals.selected[] array Industry verticals (vertical_type = 1) already mapped to the company; carries vertical_id, name, vertical_type
verticals.available[] array Unselected industry verticals; carries id, name, vertical_type
models.selected[] / models.available[] array Same, for Business Model verticals (vertical_type = 2)

Errors

Status Condition Message/shape
400 Company not found "Company not found."
401 Company outside scope or another firm's { "message": "These credentials do not match our records." }

Notes

  • selected rows come from the vertical_mapping join and therefore use the key vertical_id; available rows come from verticals and use id. Both lists are ordered by name.
  • Returns empty lists rather than erroring when the verticals / vertical_mapping tables are absent.
POST/api/company/industry/save

Replaces a company's selected verticals for one vertical type.

Module Admin > Company
Authentication Required — authenticated session cookie
Authorization Company scope check plus canEdit
Content type application/json

Request body

Field Type Required Validation / allowed values Description
company_id string Yes String; plain or encrypted id Company
vertical_type number Yes Integer, one of 1, 2 1 = Industry Verticals, 2 = Business Model Verticals
verticalArr number[] Yes Array, maximum 500 items, each item must be an integer, minimum value 1 The complete selected set for that type. May be empty to clear the selection

Example request

curl -X POST https://api.example.com/api/company/industry/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"company_id": "3kan9eKI6bt+Qq1w==", "vertical_type": 1, "verticalArr": [12, 15]}'

Success response — 200 OK

{ "error": false }

Errors

Status Condition Message/shape
400 vertical_type not 1 or 2, or verticalArr exceeds 500 entries / contains non-positive integers Validation error array
400 Company not found "Company not found."
401 Outside scope, or read-only { "message": "These credentials do not match our records." }

Notes

  • Ids whose stored vertical_type does not match the submitted vertical_type are silently dropped rather than rejected; the rest of the payload is still applied.
  • The 500-item limit bounds the per-element database work each save performs.
  • Returns { "error": false } unchanged when the vertical_mapping table is absent.

9.2 Company Groups

The company-group hierarchy: the group tree, group creation and editing, the placement preview, per-group user access matrices, and the "Manage Access to Internal Firm Accounting" screen.

Three role predicates recur across this subsection:

  • View — users.global_admin = 1, or one of roles 1 (Global Master System Administrator), 4 (Global Company Group Administrator), 8 (Company Group Administrator). There is no firm-admin bypass.
  • Manage — global_admin, firm_admin, or one of roles 1 / 4 / 8.
  • Delete — global_admin or role 1 only. Firm admins and role 4 do not hold it.

A "role-8 scoped actor" (holds role 8, is not a firm admin and not a global admin) is further restricted: they see only the group trees containing a group they are mapped to, and cannot create or remove user access.

POST/api/company-group/list

Returns the company-group tree, optionally filtered by a search term, plus the tree's action flags.

Module Admin > Company Groups
Authentication Required — authenticated session cookie
Authorization View required. A Global Master System Administrator sees every group; a role-8 scoped actor sees only their mapped trees; everyone else is firm-scoped.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
query string No String Free-text filter applied to the built tree (case-insensitive)

Example request

curl -X POST https://api.example.com/api/company-group/list \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"query": "north"}'

Success response — 200 OK

{
  "canEdit": true,
  "canDelete": true,
  "canDetachCompany": true,
  "tree": [
    {
      "id": 88,
      "encryptId": "Yk9wQmJ4TA==",
      "name": "Northwest Region",
      "type": "group",
      "firm_internal_group": 0,
      "children": [
        { "id": 412, "encryptId": "3kan9eKI6bt+Qq1w==", "name": "Northwind Services", "type": "company", "children": [] }
      ]
    }
  ],
  "totalGroups": 14
}
Field Type Description
canEdit boolean Manage permission — render New Group / Edit
canDelete boolean Delete permission (Global Master System Administrator only)
canDetachCompany boolean Mirrors canDelete; gates the company-leaf "x"
tree[] array Nested nodes with id, encryptId, name, type (group | company), optional firm_internal_group, and children
totalGroups number Group count before the search filter

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }
403 Caller lacks View "You do not have access to company groups."

Notes

  • The tree is not filtered by company_group.firm_id for a global admin, who has no firm of their own; such a caller sees every group.
POST/api/company-group

Creates a company group.

Module Admin > Company Groups
Authentication Required — authenticated session cookie
Authorization Manage required. A global creator (global admin, or role 1 / role 4) creates the group unstamped — no firm id — and its name is checked globally; everyone else creates inside their own firm and is checked within it.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
group_name string No String Group name. Either this or g_name must be present and non-empty
g_name string No String Alternative spelling of group_name, accepted for compatibility with earlier clients; used when group_name is absent
parent_id number No Integer Parent group id. Default 0 (root)

Example request

curl -X POST https://api.example.com/api/company-group \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"group_name": "Northwest Region", "parent_id": 0}'

Success response — 200 OK

{
  "error": false,
  "id": 88,
  "encryptId": "Yk9wQmJ4TA==",
  "message": "Company Group Added Successfully",
  "msg": "Company Group Added Successfully"
}
Field Type Description
error boolean false on success
id / encryptId number / string New group
message / msg string The same success text under both keys; msg is retained for compatibility with earlier clients

Errors

Status Condition Message/shape
200 Name already used in the applicable scope { "error": true, "message": "Company Group Already exist", "msg": "Company Group Already exist" }
400 Both name fields blank after trim "Group name field is required"
400 company_group table missing "Company group table is unavailable."
401 Caller lacks Manage "Not Allowed to add company group"

Notes

  • company_group.manager is NOT NULL with no default, so a new group is written with manager = 0; the Manager select is hidden in the UI.
  • The parent column is resolved defensively across parent / parent_id / parent_group_id.
GET/api/company-group/:id

Loads the company-group detail screen: the group, its parent options, child-group and company checklists, the users with access, and the placement preview tree.

Module Admin > Company Groups
Authentication Required — authenticated session cookie
Authorization View required
Content type application/json

Path parameters

Name Type Required Description
id string Yes Company group id, plain or encrypted

Example request

curl https://api.example.com/api/company-group/Yk9wQmJ4TA%3D%3D \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "canEdit": true,
  "group": {
    "id": 88,
    "encryptId": "Yk9wQmJ4TA==",
    "group_name": "Northwest Region",
    "parent": 0,
    "firm_internal_group": 0,
    "manager": 0
  },
  "firmType": 2,
  "noCompanySelect": 0,
  "isFirmGroup": false,
  "parentOptions": [{ "id": 12, "encryptId": "MTI=", "group_name": "All Regions" }],
  "companyGroupOptions": [{ "id": 91, "group_name": "Portland Cluster", "checked": true }],
  "companyOptions": [],
  "usersWithAccess": [],
  "users": [],
  "viewerUserType": 2,
  "buttonDisable": false,
  "moduleRows": [{ "id": 81, "menu_name": "Tasks & Time" }],
  "placementTree": []
}
Field Type Description
canEdit boolean Manage permission
group object The group itself; manager is the hidden Manager select value (0 = none)
firmType number firms.plan_type — 0 none/global, 1 End Company plan, 2 Firm plan. Picks the default access mode
noCompanySelect number 1 when neither this group nor any child owns a company; gates "New User Access"
isFirmGroup boolean true only for a firm admin viewing their firm's first group; gates the "Manage Access to Internal Firm Accounting" link
parentOptions[] array Groups eligible as parent (firm-internal groups excluded)
companyGroupOptions[] array Child-group checklist with current checked state
companyOptions array Companies that are unassigned or already this group's
usersWithAccess / users array Users currently mapped to the group, and the assignable-user picker
viewerUserType number The viewer's users.user_type, which drives the "( End Company Admin )" vs "( Firm Admin )" badge
buttonDisable boolean true for a role-8 scoped actor: hides "New User Access" and the row trash (row edit stays available)
moduleRows[] array Modules for the access matrix (id, menu_name)
placementTree array Preview tree with the active group marked; nodes carry isParent / isActive

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }
403 Caller lacks View "You do not have access to company groups."

Notes

  • companyOptions lists non-deleted companies that are not attached to any other group. Unchecking a box is what detaches a company — there is no separate remove control on this screen.
  • companyGroupOptions and parentOptions both exclude firm-internal accounting groups, so those groups cannot be nested under another group.
POST/api/company-group/details/save

Saves the company-group detail form: name, parent, child groups, member companies and the Manager select.

Module Admin > Company Groups
Authentication Required — authenticated session cookie
Authorization Manage required
Content type application/json

Request body

Field Type Required Validation / allowed values Description
company_group_id string Yes String; plain or encrypted id Group to update
group_name string Yes String; non-empty after trim New name
parent_id number No Integer New parent. 0 = root. Cannot be the group itself or one of its descendants
companyGroupArr number[] No Array Child group ids to attach
companyArr number[] No Array Company ids to attach
userList number | string No Optional, untyped The hidden Manager select value; persisted to company_group.manager (0 = none)

Example request

curl -X POST https://api.example.com/api/company-group/details/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
        "company_group_id": "Yk9wQmJ4TA==",
        "group_name": "Northwest Region",
        "parent_id": 12,
        "companyGroupArr": [91],
        "companyArr": [412, 413],
        "userList": 0
      }'

Success response — 200 OK

{
  "error": false,
  "message": "Company group details saved successfully.",
  "msg": "Information saved successfully",
  "userAccessUpdated": true,
  "alertMsg": 1
}
Field Type Description
error boolean false on success
message / msg string Success text under both keys
userAccessUpdated boolean Membership changes caused user access to be recomputed
alertMsg number 1 when userAccessUpdated; the UI raises a delayed second toast ("User Access has been updated for all the users …")

Errors

Status Condition Message/shape
200 Name already used in the firm { "error": true, "message": "Company Group Already exist", "msg": "Company Group Already exist" }
400 group_name blank after trim "Group name field is required"
400 Group not found "Company group not found."
400 parent_id equals the group id "Group cannot be its own parent."
400 parent_id is a descendant of the group "Circular hierarchy is not allowed."
401 Caller lacks Manage "Not allowed to edit company groups."

Notes

  • Attaching or detaching companies and child groups fans out into user_company_mapping / user_group_access so members' effective access follows the new membership; that fan-out is what sets userAccessUpdated.
  • userList carries the Manager select value and is the field that maintains company_group.manager from this screen.
POST/api/company-group/placement

Renders a preview of where a group and its selected children/companies would sit in the tree, using unsaved checkbox state.

Module Admin > Company Groups
Authentication Required — authenticated session cookie
Authorization Manage required
Content type application/json

Request body

Field Type Required Validation / allowed values Description
company_group_id string No String; plain or encrypted id Group being placed. Falls back to group_id
group_id string No String; plain or encrypted id Alias for company_group_id
parent_id number No Integer Proposed parent. 0 re-roots the group
companyGroupArr number[] No Array Proposed child group ids
childIdArr number[] No Array Additional proposed child group ids (merged with companyGroupArr)
companyArr number[] No Array Proposed company ids
companyIdArr number[] No Array Additional proposed company ids (merged with companyArr)

Example request

curl -X POST https://api.example.com/api/company-group/placement \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"company_group_id": "Yk9wQmJ4TA==", "parent_id": 12, "companyGroupArr": [91], "companyArr": [412]}'

Success response — 200 OK

{
  "placementTree": [
    {
      "id": 12,
      "encryptId": "MTI=",
      "name": "All Regions",
      "type": "group",
      "isParent": true,
      "children": [
        {
          "id": 88,
          "encryptId": "Yk9wQmJ4TA==",
          "name": "Northwest Region",
          "type": "group",
          "isActive": true,
          "children": []
        }
      ]
    }
  ]
}
Field Type Description
placementTree[] array The active group's subtree wrapped in its ancestor header. Nodes carry id, encryptId, name, type, optional firm_internal_group, isParent, isActive, children

Errors

Status Condition Message/shape
401 Caller lacks Manage "Not allowed to preview company group placement."

Notes

  • Read-only: nothing is persisted.
  • Companies are de-duplicated by id, and a ticked company that is not yet attached to any group is included in the preview.
  • Unlike the list, this preview is always scoped to the caller's own firm.
POST/api/company-group/user-access/load

Loads the per-group user-access editor: the assignable users, the module matrix, and the selected user's current grants.

Module Admin > Company Groups
Authentication Required — authenticated session cookie
Authorization Manage required. The group must belong to the caller's firm; a supplied user_id must be assignable and mutable in this group.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
group_id string Yes String; plain or encrypted id Company group
user_id string No String; plain or encrypted id The user whose current access to load. Omit for the empty "New User Access" state
mode string No One of new, edit Which entry point this call serves. new (default) = the New User Access picker, edit = the Edit Access button. Changes only how splUser is computed

Example request

curl -X POST https://api.example.com/api/company-group/user-access/load \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"group_id": "Yk9wQmJ4TA==", "user_id": "1042", "mode": "edit"}'

Success response — 200 OK

{
  "users": [{ "id": 1042, "name": "Reed, Alex", "email": "alex@acme.example" }],
  "modules": [{ "id": 81, "menu_name": "Tasks & Time" }],
  "reportDashboard": false,
  "checkRole": false,
  "roleName": "",
  "splUser": 0,
  "firmAdmin": 1,
  "endclientUser": 0,
  "userType": 2,
  "companies": [{ "id": 412, "name": "Northwind Services" }],
  "firmType": 2,
  "selectedUserId": 1042,
  "currentAccess": {
    "company_access": 1,
    "accessType": 1,
    "moduleAccessData": { "81": 3 },
    "separateAccessData": {}
  },
  "selected_user_id": 1042,
  "current_access": {
    "company_access": 1,
    "accessType": 1,
    "moduleAccessData": { "81": 3 },
    "separateAccessData": {}
  },
  "check_role": 0,
  "view": null
}
Field Type Description
users[] array Assignable users; users already mapped to the group are filtered out unless they are the selected user
modules[] array Modules for the matrix — the firm's top menus minus Home/Feedback and menu 97
reportDashboard boolean Target holds role 9 (Company Group Report and Dashboard Administrator); menus 4 and 5 lock to Administrator
checkRole boolean Target holds role 1, 7, 8 or 9 — the UI shows a red role warning and hides the Company Access selector
roleName string Comma-space joined role names, only when checkRole
splUser number 1 forces Company Access to Administrator and hides its selector. Computed from the target in mode: "new" and from the viewer in mode: "edit"
firmAdmin number 1 when the viewer is a firm admin
endclientUser number 1 when the target's users.user_type is 1 (End Client User)
userType number Target's users.user_type — 1 End Client User, 2 Firm User, 3 End Company User
companies[] array Companies of the whole group subtree — the set the Separate-access grid is built from
firmType number firms.plan_type
currentAccess object | null company_access (1/2), accessType (1 = group access, 2 = separate/per-company), moduleAccessData {menuId: level}, separateAccessData {companyId: {menuId: level}}
selected_user_id / current_access / check_role — Aliases of the fields above, retained for compatibility with earlier clients
view null Always null

Access levels used throughout the matrices: 1 No Access, 2 View Only, 3 Standard, 4 Administrator.

Errors

Status Condition Message/shape
401 Caller lacks Manage "Not allowed to view user access settings."
401 Group belongs to another firm, or the user is not assignable / not mutable here Rejected by the corresponding ownership or assignability check

Notes

  • This one endpoint serves both the New User Access picker and the Edit Access button; mode is what distinguishes them, and it affects only how splUser is derived.
  • The matrices cover the whole group subtree's companies, not just the group's direct companies.
POST/api/company-group/user-access/save

Grants or updates one user's access within a company group, in either group-wide or per-company mode.

Module Admin > Company Groups
Authentication Required — authenticated session cookie
Authorization Manage required; group must belong to the caller's firm and the target user must be assignable. A role-8 scoped actor may only edit a user already mapped to the group — they cannot create a new grant.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
group_id string Yes String; plain or encrypted id Company group
user_id string Yes String; plain or encrypted id User to grant
accessType number No Integer 1 = group-wide access (default), 2 = separate per-company access. Any value other than 2 is treated as 1
company_access number No Integer Company Access level flag; 2 when explicitly 2, otherwise 1. Falls back to the accessData.companyAccessData payload
moduleAccessData object No Optional, Record<string, number> Group-wide grid, { menuId: level }. Used when accessType = 1
separateAccessData object No Optional, Record<string, Record<string, number>> Per-company grid, { companyId: { menuId: level } }. Used when accessType = 2
accessData object No Optional, untyped Alternative envelope accepted for compatibility with earlier clients; accessData.moduleAccessData and accessData.companyAccessData are read when the top-level fields are absent
firmType number No Integer Firm plan type hint from the client

Example request

curl -X POST https://api.example.com/api/company-group/user-access/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
        "group_id": "Yk9wQmJ4TA==",
        "user_id": "1042",
        "accessType": 1,
        "company_access": 1,
        "moduleAccessData": { "3": 4, "81": 3 }
      }'

Success response — 200 OK

{
  "error": false,
  "message": "User access saved successfully.",
  "msg": "Information saved successfully",
  "user_id": 1042,
  "user_name": "Alex Reed",
  "user_email": "alex@acme.example",
  "removedFromGroup": false
}
Field Type Description
error boolean false on success
message / msg string Success text under both keys
user_id number Target user
user_name string "First Last" (the table renders "Last, First"; both shapes exist so the client never has to guess)
user_email string Target email
removedFromGroup boolean true when every level in the grid was 1 (No Access) and the membership row was therefore removed

Errors

Status Condition Message/shape
400 user_company_mapping has no group-mapping column "User company mapping schema is missing group mapping column."
400 Any other failure during the save "Could not save user access: <underlying message>" (the full error is also logged server-side)
401 Caller lacks Manage "Not allowed to manage access."
401 Role-8 scoped actor granting access to an unmapped user "Not allowed to add user access."

Notes

  • All-No-Access short-circuit: when every level in the grid is 1, the membership row is soft-deleted so the user drops off the group's "Users with access" list — but the explicit all-1 rows are still written to user_group_access. This applies to both access modes.
  • Writes an Admin > Logs activity entry carrying old vs new access; logging failures never break the save.
  • The save covers the whole group subtree's companies, so a child company's grid is persisted along with the group's direct companies.
DELETE/api/company-group/user-access

Revokes a user's access to a company group.

Module Admin > Company Groups
Authentication Required — authenticated session cookie
Authorization Manage required; group must belong to the caller's firm and the target must be assignable and mutable. A role-8 scoped actor is rejected outright.
Content type application/json (request body on a DELETE)

Request body

Field Type Required Validation / allowed values Description
group_id string Yes String; plain or encrypted id Company group
user_id string Yes String; plain or encrypted id User to revoke

Example request

curl -X DELETE https://api.example.com/api/company-group/user-access \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"group_id": "Yk9wQmJ4TA==", "user_id": "1042"}'

Success response — 200 OK

{
  "error": false,
  "user_id": 1042,
  "user_data": "Alex Reed | alex@acme.example",
  "user_name": "Alex Reed",
  "user_email": "alex@acme.example"
}
Field Type Description
error boolean false on success
user_id number Revoked user
user_data string "<name> \| <email>" — the ready-formatted string the client re-inserts into the user picker without a reload
user_name / user_email string The same values split out

Errors

Status Condition Message/shape
400 user_company_mapping has no group-mapping column "User company mapping schema is missing group mapping column."
401 Caller lacks Manage "Not allowed to remove access."
401 Caller is a role-8 scoped actor "Not allowed to remove user access."

Notes

  • Soft delete: mapping and access rows are flagged deleted = 1 rather than removed, so a revoke can be audited and reversed.
  • Writes an Admin > Logs activity entry (event type 3, "Group Access") capturing the access map before the revoke.
DELETE/api/company-group

Deletes a company group and its whole descendant subtree.

Module Admin > Company Groups
Authentication Required — authenticated session cookie
Authorization Delete required — users.global_admin = 1 or role 1 (Global Master System Administrator) only. The group must belong to the caller's firm.
Content type application/json (request body on a DELETE)

Request body

Field Type Required Validation / allowed values Description
group_id string Yes String; plain or encrypted id Group to delete

Example request

curl -X DELETE https://api.example.com/api/company-group \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"group_id": "Yk9wQmJ4TA=="}'

Success response — 200 OK

{ "error": false }

Errors

Status Condition Message/shape
401 Caller lacks Delete "Not allowed to delete groups."
401 Group belongs to another firm Rejected by the firm-ownership check

Notes

  • Cascades: every descendant is collected across both the parent column and the company_group_child join table, then all of them are soft-deleted. The removed root is detached from its parent chain, and child links, company memberships and user access rows pointing at any deleted group are cleaned up.
  • Falls back to a hard DELETE only when the table has no soft-delete column.
POST/api/company-group/firm-accounting-managers

Loads the "Manage Access to Internal Firm Accounting" screen for the caller's firm.

Module Admin > Company Groups
Authentication Required — authenticated session cookie
Authorization Firm admin only (users.firm_admin = 1). When group_id is supplied it must be that firm's first group — the same gate the link itself uses.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
group_id string No String; plain or encrypted id Optional company group. The screen has no id in its path, so the firm is normally derived from the session

Example request

curl -X POST https://api.example.com/api/company-group/firm-accounting-managers \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{}'

Success response — 200 OK

{
  "error": false,
  "firmId": 27,
  "firmInternalCompany": { "id": 511, "name": "Acme Internal Accounting" },
  "firmCompanies": [{ "id": 412, "name": "Northwind Services" }],
  "firmUsers": [{ "id": 1042, "name": "Alex Reed" }],
  "accountingManagers": [{ "manager": 1042 }]
}
Field Type Description
error boolean false on success
firmId number Firm the screen is scoped to (the group's firm_user_id when a group was supplied)
firmInternalCompany object | null The firm's internal-accounting company
firmCompanies[] array Companies under the firm
firmUsers[] array Users under the firm — the manager picker's source
accountingManagers[] array Currently assigned managers as { manager: <userId> }, filtered to users still under the firm

Errors

Status Condition Message/shape
401 Caller is not a firm admin, or the supplied group is not the firm's first group "Not allowed to manage internal firm accounting."

Notes

  • The firm is derived server-side — from the session, or from the encrypted group id — and is never taken from the request body, so a caller cannot read another firm's internal-accounting setup.
POST/api/company-group/save-firm-accounting-managers

Assigns the Firm Internal Accounting Manager role on the firm's internal company and rebuilds the corresponding internal group and access fan-out.

Module Admin > Company Groups
Authentication Required — authenticated session cookie
Authorization Firm admin only; same gate as POST /api/company-group/firm-accounting-managers
Content type application/json

Request body

Field Type Required Validation / allowed values Description
group_id string No String; plain or encrypted id Optional company group used to derive the firm
companyId number Yes Integer The firm's internal-accounting company id. Must be greater than 0
managers array No Array; elements are number or { manager?: number } Users to assign as internal accounting managers. Both the object form and a plain array of ids are accepted

Example request

curl -X POST https://api.example.com/api/company-group/save-firm-accounting-managers \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId": 511, "managers": [{ "manager": 1042 }, 1043]}'

Success response — 200 OK

{
  "error": false,
  "message": "Firm Internal Accounting Managers Created/Updated Successfully.",
  "msg": "Firm Internal Accounting Managers Created/Updated Successfully.",
  "groupId": 903
}
Field Type Description
error boolean false on success
message / msg string Success text under both keys
groupId number The firm-internal group created or reused for the access fan-out

Errors

Status Condition Message/shape
400 companyId is 0 or negative "Firm Internal Company Field is Required."
400 Any other failure during the save "Could not save firm internal accounting managers: <underlying message>" (also logged server-side)
401 Caller is not a firm admin, or the group gate fails "Not allowed to manage internal firm accounting."

Notes

  • The save touches assign_user_roles, company, company_group, company_group_companies, user_company_mapping and user_group_access. Database errors are logged server-side and returned as a 400 carrying a readable message.
  • The role assigned is Firm Internal Accounting Manager (role 17).
DELETE/api/company-group/company

Detaches one company from one company group, without touching the group.

Module Admin > Company Groups
Authentication Required — authenticated session cookie
Authorization Delete required — users.global_admin = 1 or role 1. The group must belong to the caller's firm.
Content type application/json (request body on a DELETE)

Request body

Field Type Required Validation / allowed values Description
group_id string Yes String; plain or encrypted id Company group
company_id string Yes String; plain or encrypted id Company to detach

Example request

curl -X DELETE https://api.example.com/api/company-group/company \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"group_id": "Yk9wQmJ4TA==", "company_id": "3kan9eKI6bt+Qq1w=="}'

Success response — 200 OK

{ "error": false, "msg": "Company removed from the company group." }

Errors

Status Condition Message/shape
400 Either id fails to resolve to a positive number "Invalid company group or company selected."
401 Caller lacks Delete "Not allowed to remove a company from this group."
401 Group belongs to another firm Rejected by the firm-ownership check

Notes

  • This route backs the company-leaf "x" on the group tree. Detaching a company and deleting a child group are separate routes, so the two delete semantics are never selected by a flag on one route.
  • Exactly one company link is removed — the one named by company_id. The group's other company links are left intact.
  • Side effects: soft-deletes the company_group_companies link, clears company.company_group_id when it still points at this group, soft-deletes the user_company_mapping rows this group created for the company, and soft-deletes user_company_access rows for users who no longer reach the company through any other group.

9.3 Users

The user directory and user detail screen: listing and searching users, inviting new ones, activating/deactivating/deleting them, the "User Check" dependency-reassignment workflow, impersonation, per-user roles, two-factor method, company/group access matrices, and the profile picture.

Two role predicates recur:

  • View the user list — users.firm_admin = 1, or any of roles 1, 2, 3, 4, 5, 6, 8.
  • Open another user's details — role 1, role 2, or firm admin. A user may always open their own details.

Every endpoint that names a target user resolves that user within the caller's own scope; a target outside that scope is reported as not found rather than as forbidden, so existence is not disclosed.

GET/api/user/list

Returns the Admin > Users list (active users) together with every page-level flag and dropdown the screen needs.

Module Admin > Users
Authentication Required — authenticated session cookie
Authorization Firm admin, or one of roles 1 (Global Master System Administrator), 2 (Global User Administrator), 3, 4, 5, 6 (Standard), 8. Roles 1/2 see every firm's users; a role 7/8 non-firm-admin sees only their mapped groups' users; everyone else is firm-scoped.
Content type application/json

Example request

curl https://api.example.com/api/user/list \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "userData": [
    {
      "id": 1042,
      "encryptId": "MTA0Mg==",
      "first_name": "Alex",
      "last_name": "Reed",
      "lf_name": "Reed, Alex",
      "email": "alex@acme.example",
      "active": 1,
      "deleted": 0,
      "created_on": "Jun 02, 2022",
      "created_at_display": "Jun 02, 2022",
      "last_login": "Aug 21, 2026",
      "role_name": "Standard",
      "roleByCompany": [{ "companyName": "Northwind Services", "roleName": "Primary Accountant" }]
    }
  ],
  "allRoles": [{ "id": 8, "role_name": "Company Group Administrator" }],
  "firm_admin": 0,
  "firms": [{ "user_id": 27, "account_name": "Acme Accounting Partners", "plan_type": 2 }],
  "canFilterAccounts": true,
  "showAction": 1,
  "parentUser": 27,
  "authUserId": 1001,
  "canImpersonate": true,
  "canOpenDetails": true,
  "firmAdmin": false,
  "firmPlanType": 2
}
Field Type Description
userData[] array User rows. Columns follow the users table plus the derived fields below
userData[].created_on / created_at_display / last_login string Formatted as MMM DD, YYYY with a zero-padded day; "-" when null or unparseable
userData[].lf_name string "Last, First"
userData[].roleByCompany[] array Company-scoped role assignments (companyName, roleName)
allRoles[] array Roles the caller may assign
firm_admin number 1 when the caller is a firm admin
firms[] array Accounts dropdown; populated only for roles 1/2
canFilterAccounts boolean Render the Accounts select (roles 1/2)
showAction number 0 for a role 7/8 admin who is not a firm admin — hides the whole action-icon cluster
parentUser number The parent firm user id, or 0 when the caller is that user. Row actions are hidden for this user
authUserId number The caller's own id (rows for self hide their actions)
canImpersonate boolean Render "Login as this user" (role 1 or firm admin)
canOpenDetails boolean A row click navigates to details (roles 1/2 or firm admin)
firmAdmin boolean Which Add User affordance to render — firm admins get the popover, everyone else the New User modal
firmPlanType number Decides the user_type the firm-admin popover submits

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }
403 Caller holds none of the allowed roles "You do not have access to the user list."

Notes

  • Equivalent to POST /api/user/search with selectUser: "show_active" and no filters; the two share the same response envelope so the client can re-render the list from either.
GET/api/user/module/access

Returns the Permission Exclusion configuration — the module_permission rows grouped by module.

Module Admin > Users
Authentication Required — authenticated session cookie
Authorization Firm admin (users.firm_admin = 1), global admin (users.global_admin = 1), or role 1
Content type application/json

Example request

curl https://api.example.com/api/user/module/access \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "Tasks & Time": [
    {
      "id": 4,
      "module": "Tasks & Time",
      "sub_menu": "Projects",
      "fields": "budget,rate",
      "created_at": "2025-02-11 09:14:00",
      "updated_at": null,
      "deleted_at": null
    }
  ]
}

The response is an object keyed by module name; each value is the array of that module's permission rows (id, module, sub_menu, fields, created_at, updated_at, deleted_at). Rows with a blank module name are dropped. Returns {} when the module_permission table does not exist.

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }
403 Caller is neither firm admin, global admin, nor role 1 "You do not have access to permission exclusions."

Notes

  • The role requirement is enforced server-side; the route is not reachable without an authenticated session that satisfies it.
POST/api/user/global

Switches the session out of any selected company and into "Global" scope.

Module Admin > Users
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session
Content type application/json (no request body)

Example request

curl -X POST https://api.example.com/api/user/global \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "error": false,
  "redirect": "/admin/company",
  "company": {
    "company_id": 0,
    "qbo_connection": 0,
    "global_company_name": "Global",
    "global_company_logo": ""
  }
}
Field Type Description
error boolean Always false
redirect string Client route to navigate to
company object The session's new company block. company_id: 0 is a real, sticky value meaning "Global", not "unset"

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Writes both session.login.company and session.login.carryOver, so the whole shell re-scopes to Global.
POST/api/user/search

Filtered/sorted user list. Returns the same envelope as GET /api/user/list minus allRoles.

Module Admin > Users
Authentication Required — authenticated session cookie
Authorization Same as GET /api/user/list
Content type application/json

Request body

Field Type Required Validation / allowed values Description
content string No String Free-text search
order_by string No String Column to order by
sort_by string No String Sort direction
s_id number No Integer Accounts filter: restrict to this firm
selectUser string No String Show mode. show_active (default) restricts to active = 1 AND deleted = 0; show_all applies no active/deleted predicate, so deactivated and soft-deleted users are included

Example request

curl -X POST https://api.example.com/api/user/search \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"content": "reed", "selectUser": "show_all", "s_id": 27}'

Success response — 200 OK

{
  "userData": [],
  "firms": [],
  "canFilterAccounts": true,
  "showAction": 1,
  "parentUser": 27,
  "authUserId": 1001,
  "canImpersonate": true,
  "canOpenDetails": true,
  "firmAdmin": false,
  "firmPlanType": 2
}

Fields are as documented for GET /api/user/list; allRoles and firm_admin are not included here.

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }
403 Caller holds none of the allowed roles "You do not have access to the user list."

Notes

  • show_all applies no deleted predicate, so soft-deleted users are returned alongside deactivated ones. A show_inactive superset mode is also accepted but is not offered by the standard UI.
  • The full page metadata is returned on every search, so the client can re-render the whole list from a single response.
POST/api/user/add_new

Creates a user, assigns roles, and emails them a create-password link.

Module Admin > Users
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. s_id (create under another firm) is honoured only for roles 1/2; everyone else always creates inside their own firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
fname string Yes String First name
lname string Yes String Last name
email string Yes String; non-empty after trim. No email-format validation Login email. Stored exactly as supplied; it is not lower-cased
phone_no string No String Phone number
user_type number No Integer New User modal's User Type — 1 Standard, 2 Administrator
admin_type number[] No Array, Numeric (numeric strings are coerced) Administrator-type role ids from the modal
s_id number No Integer Owning firm's user_id (the modal's Accounts select). 0/omitted means the caller's own firm. Honoured only for roles 1/2
end_user_type number No Integer Set only by the firm-admin "Add new user" popover, which always assigns role 6 (Standard). Firm plan 2 → 2 ("Firm User"), firm plan 1 → 3 ("Firm End Client User"). Kept separate because user_type already means Standard/Administrator on the modal path

Example request

curl -X POST https://api.example.com/api/user/add_new \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"fname": "Alex", "lname": "Reed", "email": "alex@acme.example", "phone_no": "+1 503 555 0142", "user_type": 1, "admin_type": [8], "s_id": 27}'

Success response — 200 OK

{
  "error": false,
  "msg": "New User Added Successfully",
  "createPasswordLink": "https://app.example.com/create-password/8f2c…",
  "createPasswordToken": "8f2c…"
}
Field Type Description
error boolean false on success
msg string Success text
createPasswordLink string | null The password-setup URL, or null when a token could not be minted
createPasswordToken string | null The raw token behind that link

Errors

Status Condition Message/shape
200 A non-deleted user already has that email { "error": true, "msg": "Email already exist." }
400 email blank after trim "Email is required."
400 fname / lname / email missing or wrong type Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Duplicate detection is a plain equality against non-deleted users; emails are stored exactly as supplied and are not lower-cased first.
  • The create-password email is sent fire-and-forget; a mail failure does not fail the request, and the link is also returned in the response.
  • Writes a user_add audit entry recording user_type, role ids (defaulting to [6]) and email.
POST/api/user/save/active

Applies a status change to a user: activate, deactivate, or delete.

Module Admin > Users
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the target must resolve inside the caller's scope
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id string Yes String; plain or encrypted id Target user
type number Yes Integer; must be 1, 2 or 3 1 = activate, 2 = deactivate, 3 = delete

Example request

curl -X POST https://api.example.com/api/user/save/active \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"id": "1042", "type": 2}'

Success response — 200 OK

{ "error": false }

Errors

Status Condition Message/shape
400 Target not found or out of scope "User not found."
400 type is not 1, 2 or 3 "Invalid status type."
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • type = 1 sets active = 1, deleted = 0; type = 2 sets active = 0, deleted = 0; type = 3 sets active = 0, deleted = 1 and rewrites the email to (deleted_<id>)<original email> so the address can be reused. All three stamp deleted_at = now().
  • This endpoint applies the change unconditionally. The "dependencies remaining" check is a client-side pre-check that routes to the User Check screen; the server does not block the status change.
  • Writes a user_status_active / user_status_inactive / user_status_deleted audit entry.
POST/api/user/delete

Finalizes the User Check workflow — the "In Active" / "Delete" button that appears once every dependency count reaches zero.

Module Admin > Users
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the target must resolve inside the caller's scope
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id string Yes String; plain or encrypted id Target user
type number Yes Integer 2 = make inactive (renames the user), anything else = delete

Example request

curl -X POST https://api.example.com/api/user/delete \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"id": "1042", "type": 2}'

Success response — 200 OK

{ "error": false }

Errors

Status Condition Message/shape
200 Target not found or out of scope { "error": true, "message": "Something Went Wrong" }
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Unlike POST /api/user/save/active, the inactive path here renames the user to "<name> (inactive)", stamps deleted_at, and rewrites saved Report Builder and Task board filters so they follow the rename.
  • The delete path sets active = 0, deleted = 1 and prefixes the email with (deleted_<id>).
  • Writes a user_check_inactive / user_check_delete audit entry.
  • No subscription-billing side effect is triggered by this endpoint.
POST/api/user/check

Loads the "User Check" screen: every open dependency the user still holds, grouped into sections, with the alternatives available for reassignment.

Module Admin > Users
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the target must resolve inside the caller's scope
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id string Yes String; plain or encrypted id User being checked
type number No Integer The pending action — 2 = deactivate (default), 3 = delete. Echoed back so the screen knows which button to show

Example request

curl -X POST https://api.example.com/api/user/check \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"id": "1042", "type": 2}'

Success response — 200 OK

{
  "error": false,
  "user": { "id": 1042, "name": "Reed, Alex", "email": "alex@acme.example" },
  "sections": [
    {
      "key": "company_invoice_approver",
      "count": 2,
      "rows": [{ "company_id": 412, "company_name": "Northwind Services" }]
    }
  ],
  "alternatives": [{ "id": 1043, "name": "Jordan Vale" }],
  "timesheetAlternatives": [{ "id": 1044, "name": "Sam Ortiz" }],
  "companyServiceTeam": {
    "412": [{ "id": 7, "name": "Bookkeeping", "user": "Jordan Vale" }]
  },
  "totalItems": 2,
  "type": 2
}
Field Type Description
error boolean Always false on success
user object The user being checked (id, name, email)
sections[] array One entry per dependency area: key, count, rows. Row shape varies per section
alternatives[] array Firm users the dependency can be reassigned to
timesheetAlternatives[] array Narrower list used only by the Timesheet Approver popup: users who already hold an authorized timesheet_config row for this firm
companyServiceTeam object Keyed by company id; the service-line team for companies appearing in the Company Level Invoice Approver section
totalItems number Sum of every section's count — the screen enables the finalize button when this reaches zero
type number Echo of the requested action

Errors

Status Condition Message/shape
400 Target not found or out of scope "User not found."
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Section coverage includes firm-level and company-level invoice managers/approvers, service-line approvers, timesheet approvers, Monthly Close primary/reviewer assignments, tasks and service-team memberships. The exact section keys are determined server-side and are not part of the request schema.
  • The Timesheet Approver popup uses timesheetAlternatives, not the generic alternatives list; the Invoice Manager popup uses alternatives.
POST/api/user/check/update

Applies one section's reassignments from the User Check screen and returns the refreshed counts.

Module Admin > Users
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the target must resolve inside the caller's scope
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id string Yes String; plain or encrypted id User being checked
type number Yes Integer The pending action (2 deactivate / 3 delete); passed straight back into the refresh
changes object No Optional, Record<string, unknown> Reassignments to apply, keyed by section. Untyped: the keys are the section keys returned by POST /api/user/check and the values are that section's row payloads

Example request

curl -X POST https://api.example.com/api/user/check/update \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
        "id": "1042",
        "type": 2,
        "changes": { "company_invoice_approver": [{ "company_id": 412, "new_user_id": 1043 }] }
      }'

Success response — 200 OK. The response is exactly the payload of POST /api/user/check with id and type from this request — the recomputed sections, counts and totalItems.

Errors

Status Condition Message/shape
400 Target not found or out of scope "User not found."
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • This endpoint only applies the submitted reassignments and re-reads the counts; it never changes the user's status. The deactivate or delete step is a separate call to POST /api/user/delete.
  • Writes a user_check_update audit entry naming the sections touched.
POST/api/subscription/user/switch

Impersonates another user ("Login as this user"): rewrites the session to that user and pins their first accessible company.

Module Admin > Users
Authentication Required — authenticated session cookie
Authorization Role 1 (Global Master System Administrator) or firm admin. The target must resolve inside the caller's scope.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
user_id string Yes String; plain or encrypted id User to impersonate

Example request

curl -X POST https://api.example.com/api/subscription/user/switch \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"user_id": "1042"}'

Success response — 200 OK

{ "error": false, "msg": "Switched user successfully.", "switchedUserId": 1042 }
Field Type Description
error boolean false on success
msg string Success text
switchedUserId number The impersonated user id. When greater than 0 this endpoint rewrites the session

Errors

Status Condition Message/shape
200 Target is inactive or deleted { "error": true, "msg": "User is inactive or deleted!" }
400 Target not found or out of scope "User not found."
403 Caller is neither role 1 nor a firm admin "You are not allowed to switch to this user."

Notes

  • On a positive switchedUserId the session is rewritten: session.login.userId becomes the impersonated user, session.login.state is set to 'authenticated' and session.login.loginAs is set to true, and the change is persisted to the session store.
  • company, carryOver, authType and otpGeneratedAt are cleared first, then the impersonated user's first accessible company is pinned, so the impersonated session does not retain the administrator's previously selected company.
  • The "global first company" selection branch applies only when the impersonated user's id is 1.
GET/api/user/details/:id

Loads the user detail screen: profile, roles, module/company access, multi-inbox emails, and the flags that lock parts of the form.

Module Admin > Users
Authentication Required — authenticated session cookie
Authorization Opening another user's details requires role 1, role 2, or firm admin. A user may always open their own.
Content type application/json

Path parameters

Name Type Required Description
id string Yes Target user id, plain or encrypted

Example request

curl https://api.example.com/api/user/details/1042 \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "userData": {
    "id": 1042,
    "encryptId": "MTA0Mg==",
    "first_name": "Alex",
    "last_name": "Reed",
    "email": "alex@acme.example",
    "qrImage": null
  },
  "multiInboxes": [],
  "disableEmail": false,
  "menuData": [],
  "menuAccessData": [],
  "company_id": 0,
  "isUserTypeStd": true,
  "isUserTypeAdmin": false,
  "roleArr": [6],
  "accessArr": [],
  "firmType": 2,
  "user": {},
  "new_access": {},
  "allRoles": [],
  "targetRoleNames": "Standard",
  "roleVisible": true,
  "reportDashboard": false,
  "AccessContentShow": true,
  "AuthFirmAdmin": 1,
  "parentUser": 1001,
  "profile": 0,
  "isParentFirmUser": false
}
Field Type Description
userData object The target's users row plus id, encryptId, and qrImage (an inline data-URL TOTP QR when the user's auth type is Google Authenticator, otherwise null)
multiInboxes[] array Multi-inbox email rows
disableEmail boolean The inbox store is unavailable — hides the "+" button
menuData / menuAccessData array Module list and the target's current module access
company_id number Always 0
isUserTypeStd / isUserTypeAdmin boolean Standard vs Administrator user type
roleArr number[] The target's assigned role ids
accessArr array The target's company/group access rows
firmType number firms.plan_type
user / new_access object Current access state and the new-access editor payload
allRoles[] array Roles the caller may assign
targetRoleNames string The target's full role-name list, used for the New Company/Group Access modal warning
roleVisible / AccessContentShow boolean Section visibility flags
reportDashboard boolean Target holds the report/dashboard admin role
AuthFirmAdmin number 1 when the caller is a firm admin
parentUser number The caller's own id
profile number 1 when viewing one's own profile
isParentFirmUser boolean Target is the parent-firm user — User Type / Admin Type stay locked

Errors

Status Condition Message/shape
400 Target not found or out of scope "User not found."
403 Opening another user's details without role 1/2 or firm admin "You do not have access to this user."

Notes

  • Multi-inbox rows come from the email_inboxes table, which is created on first use.
GET/api/user/details/:id/:type

Same as GET /api/user/details/:id, but flags the request as the "profile" variant.

Module Admin > Users
Authentication Required — authenticated session cookie
Authorization Same as GET /api/user/details/:id
Content type application/json

Path parameters

Name Type Required Description
id string Yes Target user id, plain or encrypted
type string Yes Any non-empty value turns on profile mode; the value itself is not inspected further

Example request

curl https://api.example.com/api/user/details/1042/profile \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK. Identical shape to GET /api/user/details/:id; the only difference is that profile is forced to 1.

Errors — identical to GET /api/user/details/:id.

Notes

  • Compatibility route: retained for compatibility with earlier clients; GET /api/user/details/:id is the canonical route. This route serves the same "Your Details" screen with profile forced to 1.
  • Because an id may also be supplied in the <id>_suffix form, some earlier links encode the mode into the id itself; this route is the explicit form.
POST/api/user/details/save

Saves the user detail form: names, emails, phone, profile path and global role assignments.

Module Admin > Users
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the target must resolve inside the caller's scope
Content type application/json

Request body

Field Type Required Validation / allowed values Description
user_id string Yes String; plain or encrypted id Target user
first_name string No String First name
last_name string No String Last name
email string No String — no format validation Primary login email
add_email string No String Additional email
multi_email string[] No Array, each item must be a string Multi-inbox email rows
phone_no string No String Phone number
profile_path string No String Stored profile image path
role number[] No Array, Numeric (numeric strings are coerced) Global role ids. Replaces the target's entire global role set

Example request

curl -X POST https://api.example.com/api/user/details/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
        "user_id": "1042",
        "first_name": "Alex",
        "last_name": "Reed",
        "email": "alex@acme.example",
        "phone_no": "+1 503 555 0142",
        "multi_email": ["alex.reed@acme.example"],
        "role": [6, 8]
      }'

Success response — 200 OK

{ "error": false }

Errors

Status Condition Message/shape
200 The multi-inbox sync failed { "error": true, "message": "<inbox error>" } — the user row and roles are still saved
400 Target not found or out of scope "User not found."
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Global role assignment is destructive: all assign_user_roles rows with a null/zero company_id are deleted and re-inserted from role. Company-scoped role rows are untouched.
  • Writes a user_details_save audit entry with the email, role ids and firm-admin flag.
POST/api/user/details/auth/save

Sets the user's two-factor authentication method and, for Google Authenticator, returns the enrolment secret and QR code.

Module Admin > Users
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the target must resolve inside the caller's scope
Content type application/json

Request body

Field Type Required Validation / allowed values Description
user_id string Yes String; plain or encrypted id Target user
authType number Yes Integer; must be 0, 1, 2 or 3 Two-factor method. 1 = Google Authenticator (TOTP); the other values are the remaining users.auth_type enum members

Example request

curl -X POST https://api.example.com/api/user/details/auth/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"user_id": "1042", "authType": 1}'

Success response — 200 OK

{
  "error": false,
  "authType": 1,
  "qrImage": "data:image/png;base64,iVBORw0KGgo…",
  "otpauthUrl": "otpauth://totp/alex%40acme.example?secret=<base32-secret>&issuer=Genie",
  "google2fa_secret": "JBSWY3DPEHPK3PXP"
}
Field Type Description
error boolean false on success
authType number The stored auth type
qrImage string | null Inline data-URL QR code, or null when the QR library is unavailable (the client then shows the secret as text). Empty for non-TOTP auth types
otpauthUrl string otpauth:// enrolment URL, issuer Genie. '' for non-TOTP auth types
google2fa_secret string Base32 TOTP secret. '' for non-TOTP auth types

Errors

Status Condition Message/shape
400 Target not found or out of scope "User not found."
400 authType is not 0–3 "Invalid auth type."
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • A stable secret is reused across saves; a new one is generated only when switching to TOTP and none is stored.
  • users.auth_type is stored as the enumeration ('0','1','2','3') and is written as a string literal, so the stored value always matches the submitted authType.
  • The QR is rendered server-side; the TOTP secret is never handed to a third-party image host.
  • Best-effort side effect: the remembered-2FA columns (enc_log_on / enc_log) are cleared so the next login re-challenges. A type mismatch there is swallowed and never sinks the auth-type change.
POST/api/user/save_new_access

Grants a user access to a company or a company group from the "New Company/Group Access" modal.

Module Admin > Users
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the target must resolve inside the caller's scope
Content type application/json

Request body

Field Type Required Validation / allowed values Description
user_id string Yes String; plain or encrypted id Target user
company_id number No Integer Company to grant access to
group_id number No Integer Company group to grant access to
accessType string No String Access mode. A value containing group selects group access; anything else selects separate (per-company) access
firmType number No Integer Firm plan type hint from the client
cdataCompanyArr object No Optional, Record<string, unknown> Company selection payload from the modal
gdataCompanyArr object No Optional, Record<string, unknown> Group selection payload
sdataCompanyArr object No Optional, Record<string, unknown> Separate-access selection payload
cdataCompanyAccess object No Optional, Record<string, unknown> Company access-level grid
gdataCompanyAccess object No Optional, Record<string, unknown> Group access-level grid
sdataCompanyAccess object No Optional, Record<string, unknown> Separate access-level grid

Example request

curl -X POST https://api.example.com/api/user/save_new_access \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
        "user_id": "1042",
        "group_id": 88,
        "accessType": "groupAccess",
        "gdataCompanyArr": { "88": true },
        "gdataCompanyAccess": { "88": { "81": 3 } }
      }'

Success response — 200 OK

{ "error": false }

Errors

Status Condition Message/shape
400 Target not found or out of scope "User not found."
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The six *dataCompany* fields are untyped passthroughs of the modal's grids; their inner shape is { scopeId: { menuId: level } } with levels 1 No Access, 2 View Only, 3 Standard, 4 Administrator.
  • Writes a user_access_new_save style audit entry recording the access mode, firm type and the number of company/group rows written.
POST/api/user/new_access

Loads the data behind the "New Company/Group Access" modal: the selectable tree, the module list and the plan-derived menu sets.

Module Admin > Users
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the target must resolve inside the caller's scope
Content type application/json

Request body — the same body as POST /api/user/save_new_access. Only user_id (required), company_id and group_id are read by this endpoint; the remaining fields are accepted and ignored.

Field Type Required Validation / allowed values Description
user_id string Yes String; plain or encrypted id Target user
company_id number No Integer Preselect this company in the tree
group_id number No Integer Preselect this group in the tree. Ignored when company_id > 0

Example request

curl -X POST https://api.example.com/api/user/new_access \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"user_id": "1042", "group_id": 88}'

Success response — 200 OK

{
  "treeView": [],
  "selectedTree": { "id": 88, "name": "Northwest Region", "type": "group", "children": [] },
  "hasCompanies": true,
  "msg": "",
  "menuData": [{ "id": 81, "menu_name": "Tasks & Time" }],
  "allModuleMenu": [81],
  "accountingTierMenu": [],
  "planType": 2,
  "purchaseModule": [81]
}
Field Type Description
treeView[] array The firm's full company-group / company tree
selectedTree object | null The preselected node, or null when nothing was preselected or found
hasCompanies boolean false when a preselected group contains no companies
msg string "No companies found in this company group." when hasCompanies is false, otherwise ''
menuData[] array Modules for the access matrix
allModuleMenu number[] Every menu id in menuData
accountingTierMenu number[] Always []
planType number The firm's plan type
purchaseModule number[] Every menu id in menuData

Errors

Status Condition Message/shape
400 Target not found or out of scope "User not found."
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Although it shares a request body schema with POST /api/user/save_new_access, this is a read endpoint — nothing is persisted. It is that endpoint's companion loader.
POST/api/user/details/access/save

Saves edits to a user's existing company and group access grids from the detail screen.

Module Admin > Users
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the target must resolve inside the caller's scope
Content type application/json

Request body

Field Type Required Validation / allowed values Description
user_id string Yes String; plain or encrypted id Target user
accessType string No String Access mode (group vs separate)
firmType number No Integer Firm plan type hint
comp_access unknown No Optional, untyped Existing company scope mappings
comp_accessNew unknown No Optional, untyped Newly added company scope mappings
group_access unknown No Optional, untyped Existing group scope mappings
group_accessNew unknown No Optional, untyped Newly added group scope mappings
dataGroupArr unknown No Optional, untyped Group access-level matrix
dataGroupArrNew unknown No Optional, untyped Group access-level matrix for new grants
dataCompArr unknown No Optional, untyped Company access-level matrix

Example request

curl -X POST https://api.example.com/api/user/details/access/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
        "user_id": "1042",
        "accessType": "groupAccess",
        "group_access": { "88": true },
        "dataGroupArr": { "88": { "81": 3 } }
      }'

Success response — 200 OK

{ "error": false }

Errors

Status Condition Message/shape
400 Target not found or out of scope "User not found."
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Every payload field beyond user_id, accessType and firmType is untyped: these fields carry the screen's scope maps and access matrices as posted, and are normalized server-side.
  • Writes a user_access_edit_save style audit entry recording the mapping and matrix row counts plus the access mode.
POST/api/user/batch/access/save

Applies one access matrix to many groups and companies at once ("batch access").

Module Admin > Users
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the target must resolve inside the caller's scope
Content type application/json

Request body

Field Type Required Validation / allowed values Description
user_id string Yes String; plain or encrypted id Target user
dataBatchArr unknown No Optional, untyped The access matrix to apply to every selected scope, { menuId: level }
groups unknown No Optional, untyped Group ids to apply the matrix to. Parsed leniently into a numeric list
batch_access_type number No Integer Default access level for rows the matrix does not name. Defaults to 3 (Standard)
notGroupCompanyArr unknown No Optional, untyped Company ids to apply the matrix to directly (companies not reached through a group)

Example request

curl -X POST https://api.example.com/api/user/batch/access/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
        "user_id": "1042",
        "groups": [88, 91],
        "notGroupCompanyArr": [412],
        "batch_access_type": 3,
        "dataBatchArr": { "81": 3, "3": 4 }
      }'

Success response — 200 OK

{ "error": false }

Errors

Status Condition Message/shape
400 Target not found or out of scope "User not found."
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Group rows are written first, then the standalone company rows; both reuse the same parsed matrix.
  • The firm plan type used for the write is resolved from the caller's firm, not the target's.
  • Writes a user_access_batch_save audit entry with the group ids, company ids, batch access type and row count.
POST/api/user/details/profile/save

Uploads a profile picture and returns it as an inline data URL.

Module Admin > Users
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the target must resolve inside the caller's scope
Content type multipart/form-data

Request body

This endpoint accepts a loosely typed request body, so no field-level validation rules apply. The only keys it reads are:

Field Type Required Validation / allowed values Description
user_id string Yes None; read as a string and then resolved as a plain or encrypted id Target user
profile_picture file Yes Extension must be jpg, jpeg, png, gif or webp The image, under the field name profile_picture

Example request

curl -X POST https://api.example.com/api/user/details/profile/save \
  -b 'genie.sid=<session-cookie>' \
  -F 'profile_picture=@avatar.png' \
  -F 'user_id=1042'

Success response — 200 OK

{ "error": false, "logoPath": "data:image/png;base64,iVBORw0KGgo…" }
Field Type Description
error boolean false on success
logoPath string The uploaded image as a data: URL

Errors

Status Condition Message/shape
400 Target not found or out of scope "User not found."
400 No file uploaded "Please upload a profile image."
400 Extension not in the allow-list "The selected file <name> cannot be uploaded. Only files with the following extensions are allowed: jpg, jpeg, png, gif, webp."
400 The uploaded file is empty or unreadable "Invalid profile image."

Notes

  • The endpoint does not persist the image itself — it validates, base64-encodes and returns it. Persisting the resulting path is the caller's job, via profile_path on POST /api/user/details/save.
  • Writes a user_profile_upload audit entry with the file name, MIME type and byte size.

9.4 Reporting Groups

Reporting groups collect companies for consolidated reporting. Three kinds exist, distinguished by type: 1 = firm reporting group, 2 = cross-firm reporting group (Global Master System Administrator only), 3 = Multi-Entity group.

Every endpoint in this subsection runs the same gate: the caller must hold the Global Master System Administrator role, or hold both role 7 (Company Group Company Administrator) and role 8 (Company Group Administrator). Nobody else may reach these routes. Cross-firm capability (canCrossFirm) is the Global Master System Administrator role specifically, matched by role id or by role name, so a deployment whose user_roles ids differ from the standard set is still resolved correctly.

POST/api/reporting-group/list

Returns the three reporting-group tabs (firm, cross-firm, multi-entity) for the reporting-groups landing screen.

Module Admin > Reporting Groups
Authentication Required — authenticated session cookie
Authorization Global Master System Administrator (role 1), or roles 7 and 8 together. Only a Global Master System Administrator sees cross-firm groups or groups outside their own firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
query string No String Free-text name filter, lower-cased before matching

Example request

curl -X POST https://api.example.com/api/reporting-group/list \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"query": "west"}'

Success response — 200 OK

{
  "canCrossFirm": true,
  "firmGroups": [
    { "id": 21, "encryptId": "MjE=", "name": "Western Portfolio", "type": 1 }
  ],
  "crossFirmGroups": [],
  "multiEntityGroups": [
    { "id": 44, "encryptId": "NDQ=", "name": "Westbrook Holdings", "type": 3 }
  ]
}
Field Type Description
canCrossFirm boolean Caller holds the Global Master System Administrator role
firmGroups[] array type = 1 groups
crossFirmGroups[] array type = 2 groups; always [] unless canCrossFirm
multiEntityGroups[] array type = 3 groups

Each row carries id, encryptId, name, type.

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }
403 Caller fails the reporting-group gate "You do not have access to reporting group configuration."

Notes

  • Only this landing render is firm-scoped; POST /api/reporting-group/search is not (see its notes).
POST/api/reporting-group/add

Creates a reporting group.

Module Admin > Reporting Groups
Authentication Required — authenticated session cookie
Authorization Reporting-group gate. Creating a cross-firm group (type = 2) additionally requires the Global Master System Administrator role.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
name string Yes String; non-empty after trim Group name
type number Yes Integer. Values outside 1, 2, 3 fall back to 1 1 firm, 2 cross-firm, 3 multi-entity

Example request

curl -X POST https://api.example.com/api/reporting-group/add \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"name": "Western Portfolio", "type": 1}'

Success response — 200 OK

{ "error": false, "id": 21, "encryptId": "MjE=" }
Field Type Description
error boolean false on success
id / encryptId number / string The new reporting group

Errors

Status Condition Message/shape
200 name blank after trim { "error": true, "message": "Please provide group name." } — or "Please provide multi-entity group name." when type = 3
200 Name already used { "error": true, "message": "Reporting Group name already exist." } — or "Multi-Entity Group name already exist." when type = 3
400 reporting_group table missing "Reporting group table is unavailable."
401 type = 2 without the Global Master System Administrator role "Not allowed to create cross-firm reporting groups."
403 Caller fails the reporting-group gate "You do not have access to reporting group configuration."

Notes

  • The duplicate check is scoped to the caller's firm, except for a Global Master System Administrator, whose names are checked across every firm.
POST/api/reporting-group/search

Type-scoped search across reporting groups.

Module Admin > Reporting Groups
Authentication Required — authenticated session cookie
Authorization Reporting-group gate. Searching type = 2 requires the Global Master System Administrator role; without it the response is { "rows": [] }.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
search_key string No String Name fragment, lower-cased. An empty value matches everything
type number Yes Integer. Values outside 1, 2, 3 fall back to 1 Which tab is being searched

Example request

curl -X POST https://api.example.com/api/reporting-group/search \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"search_key": "west", "type": 1}'

Success response — 200 OK

{
  "rows": [
    { "id": 21, "encryptId": "MjE=", "name": "Western Portfolio", "type": 1 }
  ]
}
Field Type Description
rows[] array Matching groups (id, encryptId, name, type)

Errors

Status Condition Message/shape
400 type missing or not an integer Validation error array
403 Caller fails the reporting-group gate "You do not have access to reporting group configuration."

Notes

  • This search carries no firm predicate: it matches on name, type and deleted = 0 only. An empty search_key therefore returns every firm's groups of that type. Only POST /api/reporting-group/list is firm-scoped.
POST/api/reporting-group/delete

Soft-deletes a reporting group.

Module Admin > Reporting Groups
Authentication Required — authenticated session cookie
Authorization Reporting-group gate only — no ownership, type or cross-firm validation
Content type application/json

Request body

Field Type Required Validation / allowed values Description
rg_id string Yes String; plain or encrypted id Reporting group to delete
type number Yes Integer Accepted by the request body schema but not used; the response echoes the group's stored type instead

Example request

curl -X POST https://api.example.com/api/reporting-group/delete \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"rg_id": "MjE=", "type": 1}'

Success response — 200 OK

{ "error": false, "type": 1 }
Field Type Description
error boolean false on success
type number The deleted group's stored type, so the client knows which tab to refresh. Defaults to 1 when the row could not be read

Errors

Status Condition Message/shape
403 Caller fails the reporting-group gate "You do not have access to reporting group configuration."

Notes

  • The group is soft-deleted by rg_id with no ownership, type or cross-firm check; the role gate is the only guard. The row is read solely to report its stored type in the response.
  • Falls back to a hard DELETE only when the table has neither deleted nor deleted_at.
POST/api/reporting-group/clone

Duplicates an existing reporting group under a new name.

Module Admin > Reporting Groups
Authentication Required — authenticated session cookie
Authorization Reporting-group gate; the source group must be reachable (a non-Global-Master caller cannot clone another firm's group)
Content type application/json

Request body

Field Type Required Validation / allowed values Description
cloneName string Yes String; non-empty after trim Name for the copy
cloneId string Yes String; plain or encrypted id Source reporting group

Example request

curl -X POST https://api.example.com/api/reporting-group/clone \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"cloneId": "MjE=", "cloneName": "Western Portfolio (2027)"}'

Success response — 200 OK

{ "error": false, "id": 22, "encryptId": "MjI=" }
Field Type Description
error boolean false on success
id / encryptId number / string The new clone

Errors

Status Condition Message/shape
400 cloneName blank after trim "Please provide reporting group name."
400 Source group not found "Reporting group not found."
400 reporting_group table missing "Reporting group table is unavailable."
401 Source group belongs to another firm and the caller is not a Global Master System Administrator "Not allowed to access cross-firm reporting groups."
403 Caller fails the reporting-group gate "You do not have access to reporting group configuration."

Notes

  • The clone inherits the source's type and group_id; the requested type is not consulted.
  • There is no server-side block on cloning a Multi-Entity (type = 3) group; the Clone button is simply not rendered on that tab.
GET/api/reporting-group/:id

Loads the reporting-group editor: the group, the company-group dropdown, the candidate companies and the currently selected ones.

Module Admin > Reporting Groups
Authentication Required — authenticated session cookie
Authorization Reporting-group gate; the group must be reachable by the caller's firm unless they hold the Global Master System Administrator role
Content type application/json

Path parameters

Name Type Required Description
id string Yes Reporting group id, plain or encrypted

Example request

curl https://api.example.com/api/reporting-group/MjE%3D \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "group": { "id": 21, "encryptId": "MjE=", "name": "Western Portfolio", "type": 1, "group_id": 88 },
  "companyGroupOptions": [{ "id": 88, "name": "Northwest Region" }],
  "availableCompanies": [
    { "id": 413, "name": "Cascade Logistics", "disabled": false }
  ],
  "selectedCompanies": [{ "id": 412, "name": "Northwind Services" }]
}
Field Type Description
group object The reporting group; group_id is the company group it draws candidates from (0 when unset)
companyGroupOptions[] array Company-group dropdown — all groups for a Global Master System Administrator / Global Company Administrator / Global Company Group Administrator, the caller's firm's groups otherwise
availableCompanies[] array Candidates from the selected company group's whole subtree. disabled: true marks a company already selected
selectedCompanies[] array Companies currently attached to the reporting group

Errors

Status Condition Message/shape
400 Group not found "Reporting group not found."
400 reporting_group table missing "Reporting group table is unavailable."
401 Group belongs to another firm and the caller is not a Global Master System Administrator "Not allowed to access cross-firm reporting groups."
403 Caller fails the reporting-group gate "You do not have access to reporting group configuration."

Notes

  • The company-group dropdown is not scoped by the reporting group's firm_id; a Global Master System Administrator, who has no firm of their own, receives every company group and can attach companies from any of them.
  • availableCompanies is empty when the group has no group_id yet.
POST/api/reporting-group/company/group

Returns the companies available under a company group; the reporting-group editor calls it whenever the company-group dropdown changes.

Module Admin > Reporting Groups
Authentication Required — authenticated session cookie
Authorization Reporting-group gate
Content type application/json

Request body

Field Type Required Validation / allowed values Description
gid number Yes Integer Company group id to resolve candidates from
sel_company number[] No Array Already-selected company ids; these come back with disabled: true

Example request

curl -X POST https://api.example.com/api/reporting-group/company/group \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"gid": 88, "sel_company": [412]}'

Success response — 200 OK

{
  "companies": [
    { "id": 412, "name": "Northwind Services", "disabled": true },
    { "id": 413, "name": "Cascade Logistics", "disabled": false }
  ]
}
Field Type Description
companies[] array Candidates from the group's whole subtree, each with id, name, disabled

Errors

Status Condition Message/shape
400 gid missing or not an integer Validation error array
403 Caller fails the reporting-group gate "You do not have access to reporting group configuration."

Notes

  • The candidate set is resolved from the group subtree alone, with no viewer-dependent firm scope, so the initial editor render and every subsequent dropdown change produce the same list.
  • Candidates come from company_group_companies only — there is no active = 1 filter and no union with company.company_group_id. Soft-deleted companies (deleted IN (0, 1)) are included.
  • Returns { "companies": [] } when gid <= 0 or the company_group table is absent.
POST/api/reporting-group/company/save

Saves the reporting-group editor: renames the group, re-points its company group, and syncs its member companies.

Module Admin > Reporting Groups
Authentication Required — authenticated session cookie
Authorization Reporting-group gate; the group must be reachable by the caller's firm unless they hold the Global Master System Administrator role
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id string Yes String; plain or encrypted id Reporting group to save
name string Yes String; non-empty after trim New name
type number Yes Integer Sent by the client, but the stored type wins; this is used only as a fallback
group_id number No Integer Company group the reporting group draws from. Default 0
company_child number[] No Array Member company ids. Positive values are de-duplicated

Example request

curl -X POST https://api.example.com/api/reporting-group/company/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"id": "MjE=", "name": "Western Portfolio", "type": 1, "group_id": 88, "company_child": [412, 413]}'

Success response — 200 OK

{ "error": false }

Errors

Status Condition Message/shape
200 name blank after trim { "error": true, "message": "Reporting Group field is required." } — or "Multi-Entity Group field is required." when the stored type is 3
200 Another non-deleted group of the same type already uses that name { "error": true, "message": "Reporting Group name already exist." } — or "Multi-Entity Group name already exist." for type 3
400 Group not found "Reporting group not found."
401 Group belongs to another firm and the caller is not a Global Master System Administrator "Not allowed to access cross-firm reporting groups."
403 Caller fails the reporting-group gate "You do not have access to reporting group configuration."

Notes

  • Company membership is synced only when company_child is non-empty: an empty array leaves the existing members untouched rather than clearing them.
  • The duplicate check is by name and type, excluding the group itself, and is not firm-scoped.

9.5 Logs (Task Audit / Activity Log)

Admin > Logs is the global activity log: who changed whose module access, when, and from what to what. Three endpoints back the screen: logs/filters supplies the filter bar's options, logs/filter returns the filtered rows, and logs/details returns the before/after detail for one row.

All three endpoints require the Global Master System Administrator role, matched by role id or by the role name "Global Master System Administrator", so a deployment whose user_roles ids differ from the standard set is still resolved correctly. users.global_admin and users.firm_admin are explicitly not accepted; only the assigned role grants access.

GET/api/logs/filters

Returns everything the log page's filter bar needs: the user dropdown and the company/company-group lists.

Module Admin > Logs
Authentication Required — authenticated session cookie
Authorization Global Master System Administrator only (role id 1 or the role name)
Content type application/json

Example request

curl https://api.example.com/api/logs/filters \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "users": [
    { "id": 1042, "name": "Alex Reed", "email": "alex@acme.example", "profile_image": "" }
  ],
  "groups": [{ "id": 88, "name": "Northwest Region", "deleted": 0 }],
  "companies": [{ "id": 412, "name": "Northwind Services", "deleted": 0 }]
}
Field Type Description
users[] array Non-deleted users ordered by last name; id, name ("First Last"), email, profile_image
groups[] array Company groups with deleted = 0, ordered by name. Blank-named rows are dropped
companies[] array Companies with deleted IN (0, 1) — soft-deleted companies are included — ordered by name. Blank-named rows are dropped

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }
403 Caller is not a Global Master System Administrator "You do not have access to the logs."

Notes

  • For a Global Master System Administrator both lists are unfiltered by firm, because such a user has no firm_id of their own.
  • The soft-delete asymmetry between the two lists is intentional: companies are returned for deleted IN (0, 1) while company groups are returned only for deleted = 0.
POST/api/logs/filter

Returns the filtered activity-log rows.

Module Admin > Logs
Authentication Required — authenticated session cookie
Authorization Global Master System Administrator only
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id string No String Numeric company or group id. '' when nothing is picked
cgType string No String 'group', 'company' or '' — which kind of entity id refers to
allUsers string No String Selected user id; '' for "All Users"
type string No String The dropdown's label text: 'All', 'Created', 'Edited' or 'Deleted'. Mapped back to user_logs.event_type 1/2/3; anything else means no event filter
startDate string No String mm-dd-yyyy
endDate string No String mm-dd-yyyy

Example request

curl -X POST https://api.example.com/api/logs/filter \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"id": "88", "cgType": "group", "allUsers": "", "type": "Edited", "startDate": "08-01-2026", "endDate": "08-31-2026"}'

Success response — 200 OK

{
  "error": false,
  "records": 1,
  "rows": [
    {
      "id": 90312,
      "date": "Aug 21, 2026",
      "time": "09:14 AM",
      "type": "Edited",
      "user": "Alex Reed",
      "event": "Access edited for Jordan Vale",
      "hasDetails": true
    }
  ]
}
Field Type Description
error boolean Always false
records number Number of rows returned (equal to rows.length; there is no pagination)
rows[].id number user_logs.id, the value POST /api/logs/details expects as log_id
rows[].date / rows[].time string Formatted from created_at
rows[].type string "Created" / "Edited" / "Deleted", or "" for an unknown event type
rows[].user string The acting user's full name
rows[].event string Sanitized event description (HTML stripped/escaped)
rows[].hasDetails boolean The log row has a details payload, so the detail view is available

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }
403 Caller is not a Global Master System Administrator "You do not have access to the logs."

Notes

  • Returns { "error": false, "records": 0, "rows": [] } when the user_logs table is absent.
  • Rows are ordered by created_at descending, with no limit.
  • Date behaviour: a start date alone matches that exact day; a start and end date are applied as BETWEEN '<start> 00:00:00' AND '<end> 00:00:00', so rows recorded on the end day after midnight fall outside the range. Set the end date one day later to include a full final day.
  • Scope behaviour: picking a group narrows the rows to that group plus its companies; picking a company does not narrow the rows at all. With no selection, the scope is every company (deleted IN (0,1)) and every group (deleted = 0) a Global Master System Administrator can see.
POST/api/logs/details

Returns the module-access before/after detail for one log row.

Module Admin > Logs
Authentication Required — authenticated session cookie
Authorization Global Master System Administrator only
Content type application/json

Request body

Field Type Required Validation / allowed values Description
log_id string Yes String; coerced to a number user_logs.id from a POST /api/logs/filter row

Example request

curl -X POST https://api.example.com/api/logs/details \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"log_id": "90312"}'

Success response — 200 OK

{
  "user": { "name": "Jordan Vale", "email": "jordan@acme.example", "profile_image": "" },
  "cgName": "Northwest Region",
  "accessBy": "Alex Reed",
  "accessType": "edited",
  "accessOn": "Aug 21, 2026 09:14 AM",
  "description": "Access edited for Jordan Vale",
  "hasOldAccess": true,
  "rows": [
    { "module": "Tasks & Time", "oldPermission": "View Only", "newPermission": "Standard" },
    { "module": "Monthly Close", "oldPermission": "No Access", "newPermission": "Administrator" }
  ]
}
Field Type Description
user object The user whose access changed (name, email, profile_image)
cgName string Company or company-group name, first letter upper-cased
accessBy string Who made the change
accessType string Lower-cased event label — "created" / "edited" / "deleted". The UI reuses it inside "Access edited By :" / "Access edited on:"
accessOn string Formatted updated_at
description string Sanitized event text
hasOldAccess boolean false when the log recorded no prior state; oldPermission is then "" on every row
rows[] array One row per module in the new-access map, in the map's original key order
rows[].oldPermission string "No Access" / "View Only" / "Standard" / "Administrator", "No Access" for a module missing from the old map, or "" when hasOldAccess is false
rows[].newPermission string Same vocabulary

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }
403 Caller is not a Global Master System Administrator "You do not have access to the logs."
404 log_id is 0/unparseable, the user_logs table is absent, or the row does not exist "Log not found."

Notes

  • Module order is preserved by scanning the raw JSON's key order rather than re-sorting, so the detail view matches how the access was actually recorded.
  • Menus that are excluded from module access — Home (1), Forecast (6), Analytics (7), Knowledge (8), Feedback Center (61) — are filtered out of the rows.

9.6 QBO Sync Status

A single read-only endpoint backing the Admin > QBO Sync Status board, which shows the last QuickBooks Online sync result for every company and every client.

GET/api/qbo/sync/history

Returns the sync-history board: every company, every client, and the Lambda sync runs their histories reference.

Module Admin > QBO Sync Status
Authentication Required — authenticated session cookie
Authorization Global Master System Administrator only — resolved the same way the session endpoint computes its global_admin flag (role id 1 or the role name "Global Master System Administrator"), so server and client can never disagree about who is allowed in
Content type application/json

Example request

curl https://api.example.com/api/qbo/sync/history \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "companies": [
    {
      "id": 412,
      "name": "Northwind Services",
      "connected": 1,
      "syncHistory": { "manual": [9014], "cron": [9013] }
    }
  ],
  "clients": [
    {
      "id": 77,
      "name": "Harbor Dental",
      "firm_name": "Acme Accounting Partners",
      "connected": 0,
      "syncHistory": {}
    }
  ],
  "lambdaSync": [
    {
      "id": 9014,
      "json_status": {
        "companies": {
          "412": {
            "status": "low_error",
            "timestamp": 1787000000,
            "modules": ["gl", "ap", "ar", "bs"],
            "errorModules": ["ap"],
            "moduleMessages": ["Vendor 214 could not be resolved"],
            "message": null
          }
        },
        "clients": {}
      }
    }
  ]
}
Field Type Description
companies[].id / name number / string Company identity
companies[].connected number company.qbo_connection, returned under the key connected
companies[].syncHistory object company.qbo_sync_history JSON: manual and cron arrays of qbo_lambda_sync.id values. Both keys are genuinely optional on the wire
clients[].firm_name string | null firms.account_name; null when the LEFT JOIN has no match
clients[].connected number clients.qbo_connection
clients[].syncHistory object Same shape as the company one
lambdaSync[].id number qbo_lambda_sync.id
lambdaSync[].json_status object | null companies and clients maps keyed by id
…json_status.<id>.status string | null completed, low_error, medium_error, high_error, full_error or inprogress
…json_status.<id>.timestamp number | string | null Unix seconds; the board renders it in America/New_York
…json_status.<id>.modules string[] | null Module short codes, e.g. gl, ap, ar, bs
…json_status.<id>.errorModules string[] | null Subset of modules that failed
…json_status.<id>.moduleMessages string[] | null Positionally paired with errorModules
…json_status.<id>.message string | null Whole-run failure message; when it is present the board hides the module lists

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }
403 Caller is not a Global Master System Administrator { "error": true, "msg": "Not Found" } — mirrors the "404 | Not Found" the page itself shows

Notes

  • Because the response covers every company and client in the system, the Global Master System Administrator requirement is enforced server-side and not left to the client.
  • The manual and cron keys inside syncHistory are genuinely optional and are not defaulted by the server. Clients must treat a missing key as an empty list.
  • Read-only. company.qbo_sync_history, clients.qbo_sync_history and qbo_lambda_sync.json_status are written by an external AWS Lambda sync worker, not by this application, so the board reports exactly what the worker has recorded and is empty when it has recorded nothing.
  • The board is an Admin section item (menu id 111, parent 11 = Admin), and its path is distinct from qbo/connect, qbo/callback and qbo/syncstatus.
  • Ordering: companies by name ascending; clients by firm account name then client name.

↑ Back to Table of Contents


10. Client Success

Client Success is the area of Genie where a firm defines what it records about its clients and who services them. It has two halves. Client Info owns the per-company data itself: the firm-level and global definitions of the Company Info and Client Contacts fields, the values stored against each company, and the Excel template download/import used to load that data in bulk. Configure owns the settings behind it: the firm's client service roles and their pricing, the per-company Service Team (which user holds which role, at what rate), the Service Line invoice-approver matrix, the Client Info start-date defaults, and the firm's client tags.

Every route in this section is mounted under the api prefix and is authenticated by the server-side session (genie.sid cookie); there is no bearer token and no CSRF protection on these particular routes. Request validation runs on every request, so any body property that is not declared in the request body schema is silently stripped before the endpoint sees it.

Two distinct permission models apply, and it matters which one a given route uses:

  • Configuration scope — used for anything that edits definitions (field definitions, service roles, tags). global scope requires users.global_admin = 1 (or the Global Master System Administrator assigned role, which is promoted to global_admin); firm scope requires users.firm_admin = 1. Failure is 403.
  • Per-company Client Success access — used for anything that edits a company's data. A global admin or firm admin always passes. Anyone else must hold access_type = ACCESS_ADMIN (4) on CLIENT_SUCCESS_MENU_ID = 10 for that company, resolved from user_company_access, then inherited user_group_access up the company-group tree. The levels are ACCESS_NO = 1, ACCESS_VIEW = 2, ACCESS_STANDARD = 3, ACCESS_ADMIN = 4. The edit check additionally rejects soft-deleted (inactive) companies as read-only. Two routes use relaxed variants: the Start Date save needs only ACCESS_STANDARD, and the template download needs only ACCESS_VIEW.

Many of these routes answer HTTP 200/201 with { "error": true, "message": "..." } rather than a 4xx status. This convention is documented per endpoint; callers must inspect the error property of the body rather than relying on the status code alone.


10.1 Client Info

Client Info owns the per-company data itself: the firm-level and global definitions of the Company Info and Client Contacts fields, the values stored against each company, and the Excel template download/import used to load that data in bulk.


10.1.1 Company Info Configuration

Field definitions for the Company Info tab. Each definition lives in company_info and is scoped either globally (firm_id IS NULL OR 0) or to one firm (firm_id = <firm>).

GET/api/cs/company-info/:type

Lists the Company Info field definitions for either the global or the firm scope, together with whether the current user may edit them.

Module Client Success > Client Info > Company Info Configuration
Authentication Required — authenticated session cookie
Authorization No hard gate — this endpoint returns canEdit/readOnly instead of throwing. canEdit is global_admin = 1 for type=global, firm_admin = 1 otherwise
Content type application/json

Path parameters

Name Type Required Description
type string Yes Case-insensitive. global selects the global definitions; any other value selects the caller's firm scope

Example request

curl -X GET "https://api.example.com/api/cs/company-info/firm" \
  -H "Accept: application/json" \
  -b "genie.sid=<session-cookie>"

Success response — 200 OK

{
  "company_info": [
    {
      "id": "eyJpdiI6IjhmMj...",
      "row_id": 412,
      "company_id": null,
      "field": "Client Since",
      "field_type": 2,
      "field_values": "",
      "default_values": "",
      "order": 1
    }
  ],
  "canEdit": true,
  "readOnly": false
}
Field Type Description
company_info[].id string Encrypted row id; the value to pass back to the PATCH/DELETE routes
company_info[].row_id number Plain numeric company_info.id
company_info[].company_id number | null Always null for definitions
company_info[].field string Field label
company_info[].field_type number 1 = picklist (uses field_values), otherwise free text. Defaults to 2
company_info[].field_values string Picklist options; empty unless field_type = 1
company_info[].default_values string Default value for new companies
company_info[].order number Display order
canEdit boolean Whether the user passes the configuration-scope check
readOnly boolean true whenever the scope is global, the user cannot edit, or a firm admin is viewing global

Errors

Status Condition Message/shape
401 No session.login.userId, session state is not authenticated, or the user row is missing/deleted { "message": "These credentials do not match our records." }

Notes

  • readOnly is always true for the global scope, for every user, including a global admin. canEdit still reports the underlying permission, so the two fields can legitimately disagree.
  • Results exclude soft-deleted rows (COALESCE(deleted, 0) = 0) and are ordered by order, then id.
PUT/api/cs/company-info

Creates a new Company Info field definition in the global or firm scope.

Module Client Success > Client Info > Company Info Configuration
Authentication Required — authenticated session cookie
Authorization Configuration-scope check on the scope named by scope: global requires global_admin = 1, firm requires firm_admin = 1
Content type application/json

Request body

Field Type Required Validation / allowed values Description
field string Yes String Field label. Trimmed; rejected if empty after trimming
scope string No String global (case-insensitive) selects the global scope; anything else, including omission, means firm scope
fieldType number No Integer; numeric strings are coerced 1 = picklist, otherwise free text. Defaults to 2
fieldValues string No String Picklist options. Persisted only when fieldType = 1; otherwise stored as ''
defaultValues string No String Default value. Defaults to ''
order number No Integer Display order. Defaults to 0
id string No String Accepted but not read by this endpoint
staticField number No Integer Accepted but not read by this endpoint

Example request

curl -X PUT "https://api.example.com/api/cs/company-info" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{
    "scope": "firm",
    "field": "Client Since",
    "fieldType": 2,
    "defaultValues": "",
    "order": 5
  }'

Success response — 200 OK

{ "error": false, "companyId": "eyJpdiI6IjhmMj..." }
Field Type Description
error boolean Always false on success
companyId string Encrypted id of the newly inserted company_info row; '' if the insert id could not be read

Errors

Status Condition Message/shape
400 field missing/not a string, or a supplied field fails its validator Request validation error array
400 field is empty after trimming { "message": "Field is required." }
401 Session invalid or user not found { "message": "These credentials do not match our records." }
403 Caller fails the configuration-scope check { "message": "You do not have permission to modify this configuration." }

Notes

  • Rows are inserted with company_id = NULL; the global scope also writes firm_id = NULL.
  • The response key is companyId even though the value is a company_info row id. It is named exactly as shown.
PATCH/api/cs/company-info/:id

Updates an existing Company Info field definition.

Module Client Success > Client Info > Company Info Configuration
Authentication Required — authenticated session cookie
Authorization Configuration-scope check on the scope of the stored row (resolved from its firm_id), not on any scope supplied in the body
Content type application/json

Path parameters

Name Type Required Description
id string Yes The row's encrypted id, or its plain numeric id — both are accepted

Request body (same shape as PUT /api/cs/company-info above). This endpoint reads field, fieldType, fieldValues, defaultValues and order; scope, id and staticField are ignored.

Example request

curl -X PATCH "https://api.example.com/api/cs/company-info/412" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "field": "Client Since (FY)", "fieldType": 1, "fieldValues": "2023,2024,2025", "order": 5 }'

Success response — 200 OK

{ "error": false }

Errors

Status Condition Message/shape
400 :id does not resolve to a positive integer { "message": "Invalid row id." }
401 Session invalid or user not found { "message": "These credentials do not match our records." }
403 Caller fails the configuration-scope check for the row's scope { "message": "You do not have permission to modify this configuration." }

Notes

  • The update writes all five columns unconditionally, so an omitted field becomes '' and an omitted order becomes 0. Send the full record, not a partial patch, despite the HTTP verb.
  • field_values is cleared to '' unless fieldType = 1.
DELETE/api/cs/company-info/:id

Soft-deletes a Company Info field definition.

Module Client Success > Client Info > Company Info Configuration
Authentication Required — authenticated session cookie
Authorization Configuration-scope check on the stored row's scope
Content type application/json

Path parameters

Name Type Required Description
id string Yes Encrypted or plain row id

Example request

curl -X DELETE "https://api.example.com/api/cs/company-info/412" \
  -b "genie.sid=<session-cookie>"

Success response — 200 OK

{ "error": false }

Errors

Status Condition Message/shape
400 :id does not resolve to a positive integer { "message": "Invalid row id." }
401 Session invalid or user not found { "message": "These credentials do not match our records." }
403 Caller fails the configuration-scope check { "message": "You do not have permission to modify this configuration." }

Notes

  • Soft delete only: sets deleted = 1, deleted_at and updated_at. Existing company_info_details values are left untouched.
POST/api/cs/company-info/change/order

Persists a drag-and-drop reordering of the Company Info field definitions.

Module Client Success > Client Info > Company Info Configuration
Authentication Required — authenticated session cookie
Authorization Configuration-scope check on the scope of the first row in data; falls back to firm scope when data is empty or the first id is unreadable
Content type application/json

Request body

Field Type Required Validation / allowed values Description
data array Yes Array; each item is validated The full ordered list
data[].id string Yes String Encrypted or plain company_info.id
data[].order number Yes Integer New display order

Example request

curl -X POST "https://api.example.com/api/cs/company-info/change/order" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "data": [ { "id": "412", "order": 1 }, { "id": "413", "order": 2 } ] }'

Success response — 201 Created

{ "error": false }

Errors

Status Condition Message/shape
400 data missing, not an array, or an item fails String/Integer Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }
403 Caller fails the configuration-scope check { "message": "You do not have permission to modify this configuration." }

Notes

  • The permission check samples only the first item's scope. Every item is then updated by id, one UPDATE per row, without re-checking scope — so a caller must not mix global and firm rows in one payload.
  • Items whose id does not resolve to a positive integer are skipped silently.
  • The default status for POST applies: 201, not 200.

10.1.2 Client Contacts Configuration

The same five operations against company_contacts, which defines the columns of the Client Contacts grid. The one structural difference is static_field: rows flagged static_field = 1 are system-defined and cannot be edited or deleted.

GET/api/cs/company-contacts/:type

Lists the Client Contacts column definitions for the global or firm scope.

Module Client Success > Client Info > Client Contacts Configuration
Authentication Required — authenticated session cookie
Authorization No hard gate — returns canEdit/readOnly. canEdit is global_admin = 1 for type=global, firm_admin = 1 otherwise
Content type application/json

Path parameters

Name Type Required Description
type string Yes global (case-insensitive) for the global scope; anything else for the caller's firm

Example request

curl -X GET "https://api.example.com/api/cs/company-contacts/firm" \
  -b "genie.sid=<session-cookie>"

Success response — 200 OK

{
  "error": false,
  "companyContacts": [
    {
      "id": "eyJpdiI6Ik9wcT...",
      "row_id": 77,
      "company_id": null,
      "field": "Contact Name",
      "field_type": 2,
      "field_values": "",
      "default_values": "",
      "order": 1,
      "static_field": 1
    }
  ],
  "canEdit": true,
  "readOnly": false
}
Field Type Description
error boolean Always false
companyContacts[].id string Encrypted row id
companyContacts[].row_id number Plain company_contacts.id
companyContacts[].field string Column label
companyContacts[].field_type number 1 = picklist, otherwise free text
companyContacts[].field_values string Picklist options
companyContacts[].default_values string Default value
companyContacts[].order number Column order
companyContacts[].static_field number 1 = system column, not editable or deletable
canEdit boolean Configuration-scope permission
readOnly boolean true for global scope, for non-editors, or for a firm admin viewing global

Errors

Status Condition Message/shape
401 Session invalid or user not found { "message": "These credentials do not match our records." }

Notes

  • Unlike GET /api/cs/company-info/:type, this response includes a top-level error: false and nests the rows under companyContacts.
  • The same forced readOnly on the global scope applies here.
PUT/api/cs/company-contacts

Creates a new Client Contacts column definition.

Module Client Success > Client Info > Client Contacts Configuration
Authentication Required — authenticated session cookie
Authorization Configuration-scope check on the scope named by scope
Content type application/json

Request body (identical to PUT /api/cs/company-info). This endpoint reads scope, field, fieldType, fieldValues, defaultValues and order; id and staticField are ignored, and new rows are always written with static_field = 0.

Example request

curl -X PUT "https://api.example.com/api/cs/company-contacts" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "scope": "firm", "field": "Portal Access", "fieldType": 1, "fieldValues": "Yes,No", "order": 6 }'

Success response — 200 OK

{ "error": false, "companyContactId": "eyJpdiI6Ik9wcT..." }
Field Type Description
error boolean Always false on success
companyContactId string Encrypted id of the new company_contacts row; '' if unavailable

Errors

Status Condition Message/shape
400 Validation failure, or field empty after trimming ("Field is required.") Request validation error array / { "message": "Field is required." }
401 Session invalid or user not found { "message": "These credentials do not match our records." }
403 Caller fails the configuration-scope check { "message": "You do not have permission to modify this configuration." }
PATCH/api/cs/company-contacts/:id

Updates a Client Contacts column definition, unless it is a static system column.

Module Client Success > Client Info > Client Contacts Configuration
Authentication Required — authenticated session cookie
Authorization Configuration-scope check on the stored row's scope
Content type application/json

Path parameters

Name Type Required Description
id string Yes Encrypted or plain company_contacts.id

Request body. Reads field, fieldType, fieldValues, defaultValues, order.

Example request

curl -X PATCH "https://api.example.com/api/cs/company-contacts/77" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "field": "Primary Contact", "fieldType": 2, "order": 1 }'

Success response — 200 OK

{ "error": false }

Static column rejection, also 200 OK:

{ "error": true, "message": "Static fields cannot be modified." }

Errors

Status Condition Message/shape
400 :id does not resolve to a positive integer { "message": "Invalid row id." }
401 Session invalid or user not found { "message": "These credentials do not match our records." }
403 Caller fails the configuration-scope check { "message": "You do not have permission to modify this configuration." }
200 Target row has static_field = 1 { "error": true, "message": "Static fields cannot be modified." }

Notes

  • The static-field rejection is returned as a 200 body, not a 4xx. Callers must inspect error.
  • As with Company Info, all five columns are overwritten, so send a full record.
DELETE/api/cs/company-contacts/:id

Soft-deletes a Client Contacts column definition, unless it is static.

Module Client Success > Client Info > Client Contacts Configuration
Authentication Required — authenticated session cookie
Authorization Configuration-scope check on the stored row's scope
Content type application/json

Path parameters

Name Type Required Description
id string Yes Encrypted or plain company_contacts.id

Example request

curl -X DELETE "https://api.example.com/api/cs/company-contacts/77" \
  -b "genie.sid=<session-cookie>"

Success response — 200 OK

{ "error": false }

Errors

Status Condition Message/shape
400 :id does not resolve to a positive integer { "message": "Invalid row id." }
401 Session invalid or user not found { "message": "These credentials do not match our records." }
403 Caller fails the configuration-scope check { "message": "You do not have permission to modify this configuration." }
200 Target row has static_field = 1 { "error": true, "message": "Static fields cannot be deleted." }

Notes

  • Sets deleted = 1, deleted_at, updated_at. Stored contact values in the details table are not removed.
POST/api/cs/company-contacts/change/order

Persists a reordering of the Client Contacts column definitions.

Module Client Success > Client Info > Client Contacts Configuration
Authentication Required — authenticated session cookie
Authorization Configuration-scope check on the scope of the first row in data; firm when the list is empty
Content type application/json

Request body

Field Type Required Validation / allowed values Description
data array Yes Array; each item is validated Ordered list of columns
data[].id string Yes String Encrypted or plain company_contacts.id
data[].order number Yes Integer New order

Example request

curl -X POST "https://api.example.com/api/cs/company-contacts/change/order" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "data": [ { "id": "77", "order": 1 }, { "id": "78", "order": 2 } ] }'

Success response — 201 Created

{ "error": false }

Errors

Status Condition Message/shape
400 Validation failure on data or an item Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }
403 Caller fails the configuration-scope check { "message": "You do not have permission to modify this configuration." }

Notes

  • Static columns are reordered like any other; the static-field restriction applies only to update and delete.
  • Unresolvable ids are skipped without error.

10.1.3 Company Data

The per-company values behind the definitions above: the Company Info tab, the Client Contacts grid, and the company-level email-capture flag. All of these routes are POST (default status 201) and resolve the company either from an explicit companyId or, failing that, from a task id via tasks.company_id.

POST/api/cs/company-info/details

Returns one company's Company Info tab: every field definition in the company's scope, joined to that company's stored value.

Module Client Success > Client Info > Company Data
Authentication Required — authenticated session cookie
Authorization No hard gate — returns canEdit/readOnly from the per-company Client Success check (global admin, firm admin, or ACCESS_ADMIN on menu 10 for this company)
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId string No String Encrypted or plain company id. Takes precedence over task
task string No String Encrypted or plain task id; the company is read from tasks.company_id when companyId is absent or unresolvable

Example request

curl -X POST "https://api.example.com/api/cs/company-info/details" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "companyId": "1042" }'

Success response — 201 Created

{
  "data": [
    {
      "id": "eyJpdiI6IjhmMj...",
      "info_id": "eyJpdiI6IjhmMj...",
      "list_id": "eyJpdiI6InJUcT...",
      "field": "Client Since",
      "field_type": 2,
      "field_values": "",
      "default_values": "",
      "value": "2021-04-01",
      "order": 1
    }
  ],
  "head": ["Company Info"],
  "canEdit": true,
  "readOnly": false
}
Field Type Description
data[].id / data[].info_id string Encrypted company_info.id — both keys carry the same value
data[].list_id string Encrypted company_info_details.id, or '' when no value row exists yet
data[].field string Field label
data[].field_type number 1 = picklist, otherwise free text
data[].field_values string Picklist options
data[].default_values string Configured default
data[].value string Stored value, falling back to default_values
data[].order number Display order
head string[] Grid heading, always ["Company Info"]
canEdit boolean Per-company edit permission
readOnly boolean !canEdit

When the company cannot be resolved, or the company_info table is absent, the response is instead:

{ "data": [], "company": null, "head": [], "canEdit": false, "readOnly": true }

Errors

Status Condition Message/shape
400 companyId/task fail String Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }

Notes

  • Historic data can contain more than one company_info_details row per (field, company). The reader collapses duplicates by keeping the row with the highest list_id, and POST /api/cs/company-info/details-save targets that same row, so reader and writer always agree.
  • Only the empty-company shape includes a company key; the populated shape does not.
  • The field definitions are scoped by the company's own firm (resolved from company.firm_id), not the caller's firm.
POST/api/cs/company-info/details-save

Saves one Company Info value for one company — an upsert into company_info_details.

Module Client Success > Client Info > Company Data
Authentication Required — authenticated session cookie
Authorization Requires per-company Client Success edit rights — global admin, firm admin, or ACCESS_ADMIN on menu 10 for this company. The company must also be active (not soft-deleted)
Content type application/json

Request body

Field Type Required Validation / allowed values Description
infoId string Yes String Encrypted or plain company_info.id
companyId string Yes String Encrypted or plain company id
listId string No String Accepted but not read — the target row is located by (infoId, companyId)
value unknown No Optional; no type validation The value to store. null/undefined is stored as ''; anything else is stringified

Example request

curl -X POST "https://api.example.com/api/cs/company-info/details-save" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "infoId": "412", "companyId": "1042", "value": "2021-04-01" }'

Success response — 201 Created

{ "error": false, "id": "eyJpdiI6InJUcT..." }
Field Type Description
error boolean false on success
id string Encrypted company_info_details.id — the updated row, or the newly inserted one

Errors

Status Condition Message/shape
400 infoId or companyId missing / not a string Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }
400 Company row does not exist { "message": "Company not found." }
403 Caller lacks per-company edit rights { "message": "You do not have permission to modify client info for this company." }
403 Company is soft-deleted { "message": "Inactive companies are read-only in Client Info." }
201 companyId or infoId does not resolve to a positive integer { "error": true, "message": "Invalid identifiers." }
201 company_info_details table absent { "error": true, "message": "Company info details table not available." }

Notes

  • The existing-row lookup is ORDER BY id DESC LIMIT 1, matching the reader's duplicate-collapsing rule, so an edit always lands on the row the grid displays.
  • An update also clears deleted back to 0 where that column exists, reviving a previously removed value.
  • value accepts any JSON type and is coerced to a string on write.
POST/api/cs/company-contacts/details

Returns one company's Client Contacts grid: the column definitions plus the row/value matrix, along with the company's email-capture flag.

Module Client Success > Client Info > Company Data
Authentication Required — authenticated session cookie
Authorization No hard gate — returns canEdit (ACCESS_ADMIN or admin) and canViewResource (>= ACCESS_VIEW or admin)
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId string No String Encrypted or plain company id
task string No String Encrypted or plain task id, used to resolve the company when companyId is absent

Example request

curl -X POST "https://api.example.com/api/cs/company-contacts/details" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "companyId": "1042" }'

Success response — 201 Created

{
  "data": {
    "infoData": [
      {
        "id": "eyJpdiI6Ik9wcT...",
        "raw_id": 77,
        "field": "Contact Name",
        "field_type": 2,
        "field_values": "",
        "default_values": "",
        "static_field": 1,
        "order": 1
      }
    ],
    "valueDetailArray": [
      {
        "order": 0,
        "77": { "listId": "eyJpdiI6IlZ4...", "value": "Dana Reyes" },
        "eyJpdiI6Ik9wcT...": { "listId": "eyJpdiI6IlZ4...", "value": "Dana Reyes" }
      }
    ]
  },
  "emailCapture": 1,
  "head": ["Client Contacts"],
  "canEdit": true,
  "readOnly": false,
  "canViewResource": true
}
Field Type Description
data.infoData[] object[] Column definitions: encrypted id, plain raw_id, field, field_type, field_values, default_values, static_field, order
data.valueDetailArray[] object[] One entry per contact row. order is the row's order key; every other key is a column id (present twice — once as the plain numeric id, once as the encrypted id) mapping to { listId, value }
emailCapture number company.email_capture, 0 or 1
head string[] Always ["Client Contacts"]
canEdit / readOnly boolean Per-company edit permission and its inverse
canViewResource boolean Whether the Resource tab may be viewed (>= ACCESS_VIEW)

When no company resolves, or no contacts table exists:

{
  "data": { "infoData": [], "valueDetailArray": [] },
  "emailCapture": 0,
  "head": [],
  "canEdit": false,
  "readOnly": true,
  "canViewResource": false
}

Errors

Status Condition Message/shape
400 companyId/task fail String Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }

Notes

  • Row order follows detail-row id ascending, and rows are grouped in first-seen order — not sorted by the numeric order value.
  • Detail rows are restricted to the column ids currently visible in this scope, so rows belonging to the other (global vs firm) configuration scope cannot shift a legitimate cell into the wrong row.
  • When a company has no contact values at all, one empty placeholder row is returned with order: 0 and blank cells for every column. There is no cross-company fallback: an empty grid never shows another company's contacts.
  • Each cell is emitted under both the plain and the encrypted column id so either key works client-side.
POST/api/cs/company-contacts/details-save

Saves one cell of the Client Contacts grid — an upsert keyed on (field, company, row order).

Module Client Success > Client Info > Company Data
Authentication Required — authenticated session cookie
Authorization Requires per-company Client Success edit rights — admin or ACCESS_ADMIN on menu 10 for this company; company must be active
Content type application/json

Request body

Field Type Required Validation / allowed values Description
infoId string Yes String Encrypted or plain column id
companyId number Yes Integer; numeric strings are coerced Plain numeric company id (this endpoint does not decrypt it)
order number Yes Integer Row order key — which contact row within the grid
listId string No String Accepted but not read; the row is located by (infoId, companyId, order)
value unknown No Optional; no type validation Cell value. null/undefined stores ''

Example request

curl -X POST "https://api.example.com/api/cs/company-contacts/details-save" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "infoId": "77", "companyId": 1042, "order": 0, "value": "Dana Reyes" }'

Success response — 201 Created

{ "error": false, "id": "eyJpdiI6IlZ4..." }
Field Type Description
error boolean false on success
id string Encrypted id of the updated or inserted detail row

Errors

Status Condition Message/shape
400 Validation failure on infoId, companyId or order Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }
400 Company row does not exist { "message": "Company not found." }
403 Caller lacks per-company edit rights { "message": "You do not have permission to modify client info for this company." }
403 Company is soft-deleted { "message": "Inactive companies are read-only in Client Info." }
201 companyId is falsy { "error": true, "message": "Invalid identifiers: company is missing." }
201 infoId does not resolve { "error": true, "message": "Invalid identifiers: unrecognised field id \"…\"." }
201 No contact-details table exists { "error": true, "message": "Company contact details table not available." }
201 The details table lacks a required column { "error": true, "message": "Company contact details schema is unsupported." }

Notes

  • companyId must be sent as a plain numeric company id; this endpoint does not decrypt it.
  • The two "invalid identifiers" cases carry distinct messages so they can be told apart from the response alone.
  • Like Company Info, the existing-row lookup is ORDER BY id DESC LIMIT 1 so the writer targets the row the reader displays.
POST/api/cs/company-contacts/deleteDetail

Deletes an entire contact row (all columns at one row-order position) for a company.

Module Client Success > Client Info > Company Data
Authentication Required — authenticated session cookie
Authorization Requires per-company Client Success edit rights; company must be active
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Integer; numeric strings are coerced Plain numeric company id
order number Yes Integer Row order key identifying the contact row to remove

Example request

curl -X POST "https://api.example.com/api/cs/company-contacts/deleteDetail" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "companyId": 1042, "order": 2 }'

Success response — 201 Created

{ "error": false }

Errors

Status Condition Message/shape
400 Validation failure on companyId or order Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }
400 Company row does not exist { "message": "Company not found." }
403 Caller lacks per-company edit rights { "message": "You do not have permission to modify client info for this company." }
403 Company is soft-deleted { "message": "Inactive companies are read-only in Client Info." }
201 companyId is falsy { "error": true, "message": "Invalid identifiers." }

Notes

  • The operation targets every detail row matching (company_id, order) — it removes the whole grid row, not a single cell.
  • Where the details table has a deleted column the rows are soft-deleted (deleted = 1); where it does not, the value column is blanked to '' instead.
  • When no contacts-details table exists, or the table lacks an order/company column, this endpoint returns { "error": false } having done nothing.
POST/api/cs/company-contacts

Toggles the company-level email-capture flag (company.email_capture).

Module Client Success > Client Info > Company Data
Authentication Required — authenticated session cookie
Authorization Requires per-company Client Success edit rights; company must be active
Content type application/json

Request body

Field Type Required Validation / allowed values Description
company_id number Yes Integer; numeric strings are coerced Plain numeric company id. Note the snake_case key
email_capture number Yes Integer Truthy stores 1, 0 stores 0

Example request

curl -X POST "https://api.example.com/api/cs/company-contacts" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "company_id": 1042, "email_capture": 1 }'

Success response — 201 Created

{ "error": false }

Errors

Status Condition Message/shape
400 Validation failure on company_id or email_capture Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }
400 Company row does not exist { "message": "Company not found." }
403 Caller lacks per-company edit rights { "message": "You do not have permission to modify client info for this company." }
403 Company is soft-deleted { "message": "Inactive companies are read-only in Client Info." }
201 company_id is falsy { "error": true, "message": "Invalid identifiers." }

Notes

  • This route shares its path with PUT /api/cs/company-contacts (create column definition) and is distinguished only by HTTP method — POST is the email-capture toggle, PUT is the column create. Its snake_case body keys are unique in this module and must be sent exactly as shown.
  • The stored value is normalised to 0 or 1; any truthy integer becomes 1.
  • The current value is read back by POST /api/cs/company-contacts/details as emailCapture.

10.1.4 Template Download and Import

Bulk load of Client Info data via Excel. Download is a two-step handshake: the POST builds the workbook server-side and returns a one-shot URL, and a follow-up GET streams the file and deletes it.

POST/api/cs/company-info/download/file

Generates an Excel import template for one of four Client Info models and returns a single-use download link.

Module Client Success > Client Info > Template Download and Import
Authentication Required — authenticated session cookie
Authorization Requires per-company Client Success view rights — global admin, firm admin, or >= ACCESS_VIEW (2) on menu 10 for this company. This is the relaxed view gate, not the admin gate
Content type Request application/json; response application/json (the file itself is fetched separately)

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Integer; numeric strings are coerced The company whose scope determines the template's fields and company list
model string Yes One of company_info, company_contact, service_team, company_resource Which template to build
comType string No String Accepted but not read by this endpoint
type string No String Accepted but not read by this endpoint

Example request

curl -X POST "https://api.example.com/api/cs/company-info/download/file" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "companyId": 1042, "model": "company_info" }'

Success response — 201 Created

{ "error": false, "redirect": "/api/cs/company-info/download/generated/9f2c8b1e-4a7d-4c33-93a1-6e0f2b5c7a11" }
Field Type Description
error boolean Always false on success
redirect string Relative URL to GET in order to receive the .xlsx file. Single use

Errors

Status Condition Message/shape
400 model not one of the four allowed values, or companyId not an integer Request validation error array
400 companyId or model resolves falsy after coercion { "message": "Company and model are required." }
400 model passes validation but reaches the fallback branch { "message": "Unsupported template model." }
401 Session invalid or user not found { "message": "These credentials do not match our records." }
403 Caller lacks view rights on this company { "message": "You do not have permission to access client info resources for this company." }

Notes

  • The generated workbook is held as temporary server-side storage and referenced by a single-use token. The follow-up GET must be served by the same application instance that produced the export, so multi-instance deployments must preserve instance affinity for these routes. See Deployment & Operational Considerations.
  • Sheet layout per model:
  • company_info → Company Info (header row only) + Available Data. File name Company Info Import Template.xlsx.
  • company_contact → Company Contact (header row only) + Available Data. File name Company Contacts Import Template.xlsx.
  • service_team → Override Price (one row per company with Yes/No), Service Team (header row only: Company, Service Role, User, Price per Hour($)), and Available Data with four columns: company list, Service Role(s), Users, Override Default Price. File name Service Team Import Template.xlsx.
  • company_resource → Company Resource (header row only: Company, Name, Link) + Available Data (company column only). File name Company Resource Import Template.xlsx.
  • The Available Data sheet is written in columns, not rows, and column A's header is the literal string Company followed by the company names.
  • Data sheets contain the header row and nothing else — they are templates to fill in, not exports.
  • Service-role options for the service_team template are filtered deleted IN (0, 2) and ordered by the order column.
POST/api/cs/company-info/import/file

Uploads a filled-in Excel template and imports its rows.

Module Client Success > Client Info > Template Download and Import
Authentication Required — authenticated session cookie
Authorization Requires per-company Client Success edit rights — admin or ACCESS_ADMIN on menu 10 for this company; company must be active
Content type multipart/form-data (the file part is named file); response application/json

Request body — multipart/form-data. The file part is file; the remaining parts are the fields listed below.

Field Type Required Validation / allowed values Description
file file Yes Extension must be .xlsx, .xls or .xlx, checked on upload The completed workbook
companyId number Yes Integer, coerced The company driving field scope and the permitted company list
model string Yes One of company_info, company_contact, service_team, company_resource Which import to run
comType string No String Not read by this endpoint
type string No String Not read by this endpoint

Example request

curl -X POST "https://api.example.com/api/cs/company-info/import/file" \
  -b "genie.sid=<session-cookie>" \
  -F "file=@Company Info Import Template.xlsx" \
  -F "companyId=1042" \
  -F "model=company_info"

Success response — 201 Created

{ "error": false, "message": "Data are Imported Successfully." }

Partial failure (also 201), returning an error log plus a downloadable error workbook:

{
  "error": true,
  "errorLog": {
    "head": ["Company", "Client Since", "Reason"],
    "body": [["Acme Holdings", "2021-04-01", "Company Not found in the Available data."]]
  },
  "errorExcelLink": "/api/cs/company-info/download/generated/3b7e...",
  "toBeExportArray": [["Acme Holdings", "2021-04-01", "Company Not found in the Available data."]]
}
Field Type Description
error boolean false when every row imported
message string Present on the all-or-nothing outcomes
errorLog.head string[] The expected header row plus a trailing Reason column
errorLog.body string[][] One entry per rejected row: the original cells plus the reason
errorExcelLink string Relative URL to a generated Error Log workbook, same single-use mechanism as the template download
toBeExportArray string[][] The same rejected rows, repeated for compatibility with earlier clients

Errors

Status Condition Message/shape
400 model not in the allowed set, or companyId not an integer Request validation error array
400 companyId or model falsy after coercion { "message": "Company and model are required." }
400 model reaches the fallback branch { "message": "Unsupported import model." }
401 Session invalid or user not found { "message": "These credentials do not match our records." }
400 Company row does not exist { "message": "Company not found." }
403 Caller lacks per-company edit rights { "message": "You do not have permission to modify client info for this company." }
403 Company is soft-deleted { "message": "Inactive companies are read-only in Client Info." }
201 No file part, or the file has no buffer { "error": true, "message": "Upload file is required." }
201 Extension not .xlsx/.xls/.xlx { "error": true, "message": "Please provide Excel sheet format supported file.", "hidePopUp": true }
201 First sheet has no rows { "error": true, "message": "Empty Excel Sheet.Please put on some data to import." }
201 Header row does not match the expected columns (company_info / company_contact) { "error": true, "message": "The present input fields are not exactly present in the uploaded document. Please download the latest template and upload." }
201 Header row wrong for service_team { "error": true, "message": "Service Team Excel Formation is Wrong." }
201 Header row wrong for company_resource { "error": true, "message": "Company Resource Excel Formation is Wrong." }
201 Workbook parsed but contained no data rows { "error": true, "message": "No Data to Import." }

Notes

  • Only the first sheet of the uploaded workbook is read. Rows that are entirely blank are dropped before validation.
  • A trailing Reason column is stripped from the uploaded header before comparison, so a downloaded error log can be corrected and re-uploaded directly rather than rebuilt by hand.
  • The expected header set is the same one the download route builds its template from, so the template handed out always matches the one the importer accepts.
  • The company column is matched case-insensitively against the companies the caller may import into; an unmatched name produces Company Not found in the Available data.
  • company_resource rows additionally validate the Link column (Invalid Link format.) and require both Name and Link (Name/Link Not Found.).
  • After a service_team import the firm role map is re-applied for the affected company, including approver updates. This runs best-effort: a failure is not reported and the imported rows are never rolled back.
  • Rejected rows never block accepted ones: valid rows in the same file are written.
GET/api/cs/company-info/download/generated/:token

Streams a previously generated template or error workbook, then deletes it.

Module Client Success > Client Info > Template Download and Import
Authentication Not required — possession of the single-use token is the only credential
Authorization No role requirement; the single-use token is the only credential
Content type Binary download — application/octet-stream with Content-Disposition: attachment; application/json on the 404 path

Path parameters

Name Type Required Description
token string Yes The UUID from the redirect / errorExcelLink returned by the download or import route

Example request

curl -X GET "https://api.example.com/api/cs/company-info/download/generated/9f2c8b1e-4a7d-4c33-93a1-6e0f2b5c7a11" \
  -OJ

Success response — 200 OK, body is the .xlsx file. The suggested filename comes from the generating route (for example Company Info Import Template.xlsx).

Errors

Status Condition Message/shape
404 Token unknown, already consumed, or issued by a different application instance { "error": true, "message": "Download expired." }

Notes

  • Strictly single use: on completion the token is invalidated and the temporary file is removed.
  • The token is valid only on the application instance that issued it, and tokens do not survive a restart of that instance. Deployments must preserve instance affinity for these routes, otherwise this GET can return 404 even immediately after a successful POST. See Deployment & Operational Considerations.
  • Because there is no session check here, treat the token as a bearer credential and do not log or share generated links.

10.2 Configure

Configure owns the settings behind Client Info: the firm's client service roles and their pricing, the per-company Service Team (which user holds which role, at what rate), the Client Info start-date defaults, and the firm's client tags.


10.2.1 Service Roles

The firm's client service role catalogue (service_roles): creation, editing, pricing, ordering, activation state, and the reassignment flow used when an in-use role is retired. All routes are POST (default status 201).

POST/api/utilities/configure/get

Loads the Client Success Configure screen for either the Global or the Firm tab: the service role list with pricing and flags, the service lines, and the firm-level settings.

Module Client Success > Configure > Service Roles
Authentication Required — authenticated session cookie
Authorization No hard gate — returns canEdit/readOnly from the configuration-scope check (global_admin for the Global tab, firm_admin for the Firm tab)
Content type application/json

Request body

Field Type Required Validation / allowed values Description
isGlobal number Yes Integer; numeric strings are coerced 1 = Global tab (roles with firm_id IS NULL OR 0); anything else = the caller's firm
deleted number No Integer Status filter: 1 = active only (deleted = 0), 2 = inactive only (deleted = 3), anything else (including omission) = both

Example request

curl -X POST "https://api.example.com/api/utilities/configure/get" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "isGlobal": 0, "deleted": 1 }'

Success response — 201 Created

{
  "error": false,
  "firmSetting": 1,
  "firmAdmin": 1,
  "configGlobal": [],
  "configCustom": [],
  "cloneData": true,
  "userAssignedRole": [],
  "firmId": 17,
  "useSLApprover": 0,
  "canEdit": true,
  "readOnly": false,
  "serviceLine": [{ "id": 3, "name": "Bookkeeping" }],
  "clientServiceRole": [
    {
      "id": 51,
      "role": "Account Manager",
      "primary_accountant": 1,
      "mc_manager": 0,
      "invoice_approver": 0,
      "price_per_hour": "85.00",
      "deleted": 0,
      "order": 1,
      "firm_id": 17,
      "is_global": 0,
      "service_line_based_approvers": [{ "service_line_id": 3, "invoice_approver": 0 }],
      "view_only": 0
    }
  ]
}
Field Type Description
error boolean Always false
firmSetting number firms.use_service_role — whether the firm uses its own role set rather than the global one
firmAdmin number The caller's users.firm_admin value
configGlobal, configCustom, userAssignedRole array Always empty; retained for compatibility with earlier clients
cloneData boolean true when the Global tab is showing and the firm has not yet been stamped with a firm-setting role — the signal for the "overwrite Global setting with Firm setting?" confirm. Always true on the Firm tab
firmId number The resolved firms.id row id
useSLApprover number firms.use_service_line_approver
canEdit / readOnly boolean Configuration-scope permission and its inverse
serviceLine[] object[] The firm's service lines
clientServiceRole[].price_per_hour string Fixed to two decimal places
clientServiceRole[].deleted number 0 active, 3 inactive
clientServiceRole[].is_global number 1 when the role has no firm
clientServiceRole[].service_line_based_approvers[] object[] One entry per service line, with that line's invoice_approver flag for this role
clientServiceRole[].view_only number 1 when the Global tab is showing to a non-global-admin

Errors

Status Condition Message/shape
400 isGlobal missing or not an integer Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }

Notes

  • Roles are ordered by order then role.
  • cloneData reflects whether this firm has already been stamped with a firm-setting role, and therefore whether the Global → Firm clone confirmation is required.
  • firmId resolves to the actual firms.id row. Write routes on this screen resolve it the same way, so read and write always address the same row.
POST/api/utilities/configure/save

Creates a new client service role.

Module Client Success > Configure > Service Roles
Authentication Required — authenticated session cookie
Authorization Configuration-scope check on global when isGlobal = 1, else firm
Content type application/json

Request body

Field Type Required Validation / allowed values Description
serviceRole string Yes String Role name. Trimmed; rejected if empty
isGlobal number Yes Integer, coerced 1 creates a global role (firm_id = NULL, is_global = 1); otherwise a firm role
pricePerHour number No Numeric; numeric strings are coerced, with no further validation Default hourly rate. Defaults to 0

Example request

curl -X POST "https://api.example.com/api/utilities/configure/save" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "serviceRole": "Controller", "pricePerHour": 120, "isGlobal": 0 }'

Success response — 201 Created

{ "error": false, "id": 138 }
Field Type Description
error boolean false on success
id number Plain numeric id of the new service_roles row

Errors

Status Condition Message/shape
400 serviceRole or isGlobal missing / wrong type Request validation error array
400 serviceRole empty after trimming { "message": "Client Service Role is required." }
401 Session invalid or user not found { "message": "These credentials do not match our records." }
403 Caller fails the configuration-scope check { "message": "You do not have permission to modify this configuration." }
201 A role with the same name already exists in this scope { "error": true, "message": "Client Service Role already exists." }

Notes

  • Duplicate detection is case-insensitive within the scope and considers both active (deleted = 0) and inactive (deleted = 3) roles.
  • order is assigned automatically as MAX(order) + 1 within the scope.
  • The three role flags (primary_accountant, mc_manager, invoice_approver) are always inserted as 0; set them afterwards via POST /api/utilities/configure/update.
POST/api/utilities/configure/update

Multi-purpose role/setting update. The colName field selects one of three distinct behaviours: the firm-level Service Line Approver master switch, one cell of the role × service-line approver matrix, or a direct edit of a service_roles row.

Module Client Success > Configure > Service Roles
Authentication Required — authenticated session cookie
Authorization Branch-dependent. colName = 'use_service_line_approver' requires firm scope (firm_admin = 1); colName = 'service_line' requires the scope of the role named by serviceRoleId; every other case requires the scope of the role named by id
Content type application/json

Request body

Field Type Required Validation / allowed values Description
colName string | null No One of use_service_line_approver, service_line, primary_accountant, mc_manager, invoiceApprover Selects the operation. Absent/null means a plain role save or rename
id number No Integer, coerced service_roles.id. Required for every branch except the two named above
role string No String New role name. Written only when non-empty after trimming
pricePerHour number No Numeric; numeric strings are coerced, with no further validation New hourly rate. Written when the key is present
primaryAccountant boolean No Boolean Sets primary_accountant
mcManager boolean No Boolean Sets mc_manager
invoiceApprover boolean No Boolean Sets invoice_approver; also read as the on/off value in the service_line branch
serviceRoleId number No Integer Role id for the service_line branch
serviceLineId number No Integer Service line id for the service_line branch
firmId number No Integer Target firm for the use_service_line_approver branch; defaults to the caller's firm
value number No Numeric; numeric strings are coerced, with no further validation On/off value for the use_service_line_approver branch

Example request

curl -X POST "https://api.example.com/api/utilities/configure/update" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "id": 51, "colName": "primary_accountant", "primaryAccountant": true }'

Success response — 201 Created

{ "error": false }

Errors

Status Condition Message/shape
400 colName present but not one of the five allowed values, or a field fails its validator Request validation error array
400 service_line branch with no serviceRoleId { "message": "Service role is required." }
400 service_line branch with no serviceLineId { "message": "Service role and service line are required." }
400 Default branch with no id { "message": "Role id is required." }
400 Default branch and id matches no row { "message": "Role not found." }
401 Session invalid or user not found { "message": "These credentials do not match our records." }
403 Caller fails the configuration-scope check for the branch { "message": "You do not have permission to modify this configuration." }

Notes

  • colName is restricted to the values listed above; an unrecognised value is rejected rather than accepted as a no-op success.
  • primary_accountant and mc_manager are exclusive within a scope: setting either first clears that flag on every other role in the same scope, then sets it on the target role. invoice_approver is not exclusive.
  • Only the properties actually present in the body are written. role is additionally skipped when blank after trimming.
  • The use_service_line_approver branch writes firms.use_service_line_approver and returns { "error": false } without writing if the deployment has no such column.
  • The service_line branch upserts cs_invoice_approver_based_sl for the (role, line) pair and reactivates a previously soft-deleted row. It returns { "error": false } without writing if the table is absent. Note this is the firm-level matrix — the per-company Service Line approver checkboxes are POST /api/utilities/configure/save-sl-invoice-approver.
POST/api/utilities/configure/delete

Changes a service role's activation state — restore, inactivate, or delete. Inactivating a role that is still assigned returns the list of companies needing reassignment instead of applying the change.

Module Client Success > Configure > Service Roles
Authentication Required — authenticated session cookie
Authorization Configuration-scope check on the role's own scope (global if firm_id is null/0, else firm)
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number Yes Integer, coerced service_roles.id
status number Yes Integer; further restricted to 1, 2 or 3 1 = delete (deleted = 1, name suffixed (deleted)); 2 = restore (deleted = 0, suffix stripped); 3 = inactivate (deleted = 3, name suffixed (inactive))

Example request

curl -X POST "https://api.example.com/api/utilities/configure/delete" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "id": 51, "status": 3 }'

Success response — 201 Created

{ "error": false }

Blocked inactivation (also 201), when the role is still assigned to companies:

{
  "error": true,
  "needAlternativeRole": true,
  "message": "Role is assigned to companies. Please choose alternate roles.",
  "companyRoleData": [
    { "companyId": 1042, "companyName": "Acme Holdings", "alternateRoleId": 0 }
  ]
}
Field Type Description
needAlternativeRole boolean true signals the caller must run the reassignment flow
companyRoleData[].companyId number A company where the role is still assigned
companyRoleData[].companyName string Company name, falling back to Company <id>
companyRoleData[].alternateRoleId number Always 0; the client fills this in before calling updateAlterRoles

Errors

Status Condition Message/shape
400 id/status missing or not integers Request validation error array
400 id falsy or status not in [1, 2, 3] { "message": "Invalid role status update request." }
400 No service_roles row with that id { "message": "Role not found." }
401 Session invalid or user not found { "message": "These credentials do not match our records." }
403 Caller fails the configuration-scope check { "message": "You do not have permission to modify this configuration." }

Notes

  • Despite the route name this endpoint is a state change, not a hard delete; no row is ever removed.
  • The assigned-company check runs only for status = 3. status = 1 (delete) applies unconditionally.
  • Any existing (inactive) / (deleted) suffix is stripped from the role name before the new suffix is applied, so repeated state changes do not accumulate suffixes.
  • The blocked response is the entry point to POST /api/utilities/configure/getRolesAndCompany followed by POST /api/utilities/configure/updateAlterRoles.
POST/api/utilities/service/role/update

Persists a drag-and-drop reordering of the service role list.

Module Client Success > Configure > Service Roles
Authentication Required — authenticated session cookie
Authorization Configuration-scope check on the scope of the first role in the array — and only when that first id is greater than zero. An array whose first id is 0 performs no permission check at all
Content type application/json

Request body

Field Type Required Validation / allowed values Description
clientServiceRole array Yes Array; each item is validated The full ordered role list
clientServiceRole[].id number Yes Integer, coerced service_roles.id
clientServiceRole[].order number Yes Integer, coerced New display order

Example request

curl -X POST "https://api.example.com/api/utilities/service/role/update" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "clientServiceRole": [ { "id": 51, "order": 1 }, { "id": 52, "order": 2 } ] }'

Success response — 201 Created

{ "error": false }

Errors

Status Condition Message/shape
400 clientServiceRole missing, not an array, or an item fails Integer Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }
403 Caller fails the configuration-scope check for the first role's scope { "message": "You do not have permission to modify this configuration." }

Notes

  • The scope check samples only the first entry, and every row is then updated by id without re-checking. Do not mix global and firm roles in one payload.
  • Each row is written with an individual UPDATE; there is no transaction, so a partial reorder is possible if the connection drops mid-list.
POST/api/utilities/configure/firm/update

Toggles "Use Firm Settings" (firms.use_service_role). Turning it on clones the global configuration into the firm and queues a role-map rebuild.

Module Client Success > Configure > Service Roles
Authentication Required — authenticated session cookie
Authorization Configuration-scope check on the firm scope — requires firm_admin = 1 (a global admin also passes)
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmSetting number Yes Integer; numeric strings are coerced Truthy enables firm settings (stored as 1), 0 disables

Example request

curl -X POST "https://api.example.com/api/utilities/configure/firm/update" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "firmSetting": 1 }'

Success response — 201 Created

{ "error": false }

Errors

Status Condition Message/shape
400 firmSetting missing or not an integer Request validation error array
400 Deployment has no firms.use_service_role column { "message": "This deployment has no firms.use_service_role column." }
400 No firms row resolves for this user { "message": "No firm record found for this user, so the setting cannot be saved." }
401 Session invalid or user not found { "message": "These credentials do not match our records." }
403 Caller is not a firm admin { "message": "You do not have permission to modify this configuration." }

Notes

  • The target firms row is resolved the same way the read path resolves it, and its existence is asserted before the write, so the request fails rather than reporting success without saving.
  • When the flag goes on, the global service_roles, client_company_info and company_contacts are copied into the firm and firm_setting is stamped, inside a single transaction. The copy is self-guarding: a firm that already has its copy is never overwritten. When the flag goes off, nothing is cloned.
  • The clone is non-fatal: the flag write has already committed, so a clone failure is logged rather than returned as an error.
  • A firm-wide role-map rebuild is queued, not run inline: one queue_manager row is written and claimed by a background worker on its next five-second poll. It is queued after the clone so the rebuild sees the cloned roles. See Deployment & Operational Considerations.
POST/api/utilities/configure/getRolesAndCompany

For a role being retired: lists the alternate roles available and the companies currently using it. Step one of the reassignment flow.

Module Client Success > Configure > Service Roles
Authentication Required — authenticated session cookie
Authorization Configuration-scope check on the scope of the role named by id
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number Yes Integer, coerced The service_roles.id being retired
name string No String Accepted but not read by this endpoint

Example request

curl -X POST "https://api.example.com/api/utilities/configure/getRolesAndCompany" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "id": 51 }'

Success response — 201 Created

{
  "error": false,
  "roles": [ { "id": 52, "role": "Senior Accountant" } ],
  "companyRoleData": [
    { "companyId": 1042, "companyName": "Acme Holdings", "alternateRoleId": 0 }
  ]
}
Field Type Description
roles[] object[] Every other active role in the caller's firm, ordered by name — the candidates for reassignment
companyRoleData[].companyId number A company with a live service_role_users row for the role
companyRoleData[].companyName string Company name, falling back to Company <id>
companyRoleData[].alternateRoleId number Always 0; the client fills this in

Errors

Status Condition Message/shape
400 id missing or not an integer Request validation error array
400 id resolves to 0 { "message": "Role id is required." }
401 Session invalid or user not found { "message": "These credentials do not match our records." }
403 Caller fails the configuration-scope check { "message": "You do not have permission to modify this configuration." }

Notes

  • The candidate list is scoped strictly to firm_id = <caller's firm> and excludes the role itself and any soft-deleted role — global roles never appear as reassignment candidates.
  • The companyRoleData shape matches the one returned by the blocked POST /api/utilities/configure/delete, so the client can populate either result into the same form.
POST/api/utilities/configure/updateAlterRoles

Reassigns every company using a role to a chosen alternate, repoints tasks to a budget role, and inactivates the original. Step two of the reassignment flow.

Module Client Success > Configure > Service Roles
Authentication Required — authenticated session cookie
Authorization Configuration-scope check on the scope of the role named by mainId
Content type application/json

Request body

Field Type Required Validation / allowed values Description
mainId number Yes Integer, coerced The role being retired
alternateRoleBudget number Yes Integer, coerced The role that inherits this role's task assignments and reviewer slots
companyRoleData array Yes Array; each item is validated and at least one item is required Per-company reassignment instructions
companyRoleData[].companyId number Yes Integer, coerced Company to reassign
companyRoleData[].alternateRoleId number Yes Integer, coerced Role to move that company's service-team rows to

Example request

curl -X POST "https://api.example.com/api/utilities/configure/updateAlterRoles" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{
    "mainId": 51,
    "alternateRoleBudget": 52,
    "companyRoleData": [ { "companyId": 1042, "alternateRoleId": 52 } ]
  }'

Success response — 201 Created

{ "error": false, "message": "Role reassigned and inactivated successfully." }

Errors

Status Condition Message/shape
400 Any required field missing or of the wrong type Request validation error array
400 mainId or alternateRoleBudget resolves to 0 { "message": "Main role and budget role are required." }
400 companyRoleData is empty { "message": "At least one company reassignment is required." }
401 Session invalid or user not found { "message": "These credentials do not match our records." }
403 Caller fails the configuration-scope check { "message": "You do not have permission to modify this configuration." }

Notes

  • Three writes happen, in order: (1) each listed company's live service_role_users rows move from mainId to that entry's alternateRoleId, joined through company.firm_id so the caller's firm boundary is enforced per row; (2) firm-scoped tasks.assigned_roles and tasks.reviewer values equal to mainId are repointed to alternateRoleBudget; (3) the original role is set deleted = 3 and its name suffixed (inactive).
  • Entries with a falsy companyId or alternateRoleId are skipped silently — the role is still inactivated at the end, so a malformed list can retire a role without reassigning every company.
  • The task repoint applies to the whole firm, not only the companies listed.
  • There is no enclosing transaction; the steps are independent statements.

10.2.2 Client Info Start Date

The "Start Date" panel on Client Info, backed by utilities_config.todo_start_date. It has two independently-gated halves: a per-company Custom Value and a Global Default.

POST/api/utilities/firm-configure

Reads the effective Start Date values for one company: its custom value and the global default.

Module Client Success > Configure > Client Info Start Date
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond authentication — any authenticated user may read this
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Integer; numeric strings are coerced Plain numeric company id

Example request

curl -X POST "https://api.example.com/api/utilities/firm-configure" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "companyId": 1042 }'

Success response — 201 Created

{ "custom_value": "Apr-2021", "global_value": "Jan-2020" }
Field Type Description
custom_value string This company's own start date, formatted Mon-YYYY
global_value string The global default start date, same format

Errors

Status Condition Message/shape
400 companyId missing or not an integer Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }

Notes

  • When companyId is falsy, the utilities_config table is absent, or it has no todo_start_date column, both values fall back to the current month formatted Mon-YYYY (for example Sep-2026).
  • The custom row is matched on company_id alone; the global row requires company_id IS NULL AND is_global = 1. Both exclude soft-deleted rows via deleted_at IS NULL, and ties resolve to the lowest id. The updater uses the identical locator so read and write can never disagree about which row is "the" config row.
POST/api/utilities/firm-configure/update

Saves the Start Date custom value and/or the global default.

Module Client Success > Configure > Client Info Start Date
Authentication Required — authenticated session cookie
Authorization Requires per-company Client Success rights of at least ACCESS_STANDARD — global admin, firm admin, or >= ACCESS_STANDARD (3) on menu 10 for this company; company must be active. Additionally, supplying globalDefault requires global_admin = 1
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Integer, coerced Plain numeric company id
customValue string No String This company's start date. Written only when a non-empty value is supplied
globalDefault string No String The global default start date. Requires global_admin = 1

Example request

curl -X POST "https://api.example.com/api/utilities/firm-configure/update" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "companyId": 1042, "customValue": "2021-04-01" }'

Success response — 201 Created

{ "error": false, "message": "Configuration Updated Successfully." }

Errors

Status Condition Message/shape
400 companyId missing or not an integer Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }
401 globalDefault supplied by a non-global-admin { "message": "These credentials do not match our records." }
400 Company row does not exist { "message": "Company not found." }
403 Caller lacks ACCESS_STANDARD on this company { "message": "You do not have permission to modify client info for this company." }
403 Company is soft-deleted { "message": "Inactive companies are read-only in Client Info." }
201 companyId falsy or utilities_config table absent { "error": true, "message": "Utilities config not available." }
201 No todo_start_date column { "error": true, "message": "Start date not supported." }

Notes

  • This route uses the relaxed ACCESS_STANDARD gate, not the ACCESS_ADMIN gate that the rest of Client Info editing uses: a company-Standard user may save that company's own Custom Value start date.
  • The global-default half is gated separately and strictly on global-admin rights, and the rejection is a 401, not a 403.
  • Both halves are upserts against the same located row: an update also clears deleted back to 0, and an insert supplies company_id (NULL for the global row) and is_global where those columns exist.
  • Omitting a half simply skips it — sending only customValue leaves the global default untouched, and vice versa.
  • The success message is returned even if neither half was supplied and nothing was written.

10.2.3 Service Team

The per-company Service Team grid: which service role is held by which user, at what rate, plus the per-company pricing override, role overrides and Service Line invoice-approver checkboxes.

POST/api/utilities/configure/get-roles

Loads the whole Service Team panel for one company: the role catalogue, the assigned team, selectable users, service lines, override state and the company's project list.

Module Client Success > Configure > Service Team
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond authentication — the response carries no canEdit flag
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId string No String Encrypted or plain company id
task string No String Encrypted or plain task id; resolves the company when companyId is absent
userId number No Numeric; numeric strings are coerced, with no further validation Filters the team to rows held by this user
roleId number No Numeric; numeric strings are coerced, with no further validation Filters the role list to this role
fromFilter boolean No Optional; no type validation Accepted but not read by this endpoint

Example request

curl -X POST "https://api.example.com/api/utilities/configure/get-roles" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "companyId": "1042" }'

Success response — 201 Created

{
  "error": false,
  "clientServiceRole": [
    {
      "id": 51,
      "role": "Account Manager",
      "price_per_hour": "85.00",
      "deleted": 0,
      "primary_accountant": 1,
      "mc_manager": 0,
      "invoice_approver": 0,
      "service_line_based_approvers": [{ "service_line_id": 3, "invoice_approver": 0 }]
    }
  ],
  "clientServiceTeam": [
    {
      "id": "eyJpdiI6IkJ2...",
      "role_id": 51,
      "role": "Account Manager",
      "user_id": 204,
      "user": "Reyes, Dana",
      "price_per_hour": "85.00"
    }
  ],
  "users": [{ "id": 204, "name": "Reyes, Dana" }],
  "serviceLine": [
    {
      "id": 3,
      "name": "Bookkeeping",
      "invoice_approver": 0,
      "role_based_approver": []
    }
  ],
  "firmId": 17,
  "useSLApprover": 0,
  "overRideDefaultPricing": 0,
  "defaultRoles": [
    { "id": 51, "role": "Account Manager", "primary_accountant": 1, "mc_manager": 0, "invoice_approver": 0 }
  ],
  "overrideRoles": [
    {
      "id": 51,
      "role": "Account Manager",
      "override_primary_accountant": 0,
      "override_mc_manager": 0,
      "override_invoice_approver": 0
    }
  ],
  "projectList": [
    {
      "id": 900,
      "name": "Monthly Close",
      "company_id": 1042,
      "monthly_budget": "2500.00",
      "monthly_minimum": "0.00",
      "is_primary_budget": 1
    }
  ]
}
Field Type Description
clientServiceRole[] object[] The roles selectable for this company, with default pricing, the three role flags, and per-service-line approver state
clientServiceTeam[].id string | number Encrypted service_role_users.id, or literal 0 for a placeholder row that has no stored record
clientServiceTeam[].user_id / .user number | null / string Assigned user and display name (lf_name, else "Last, First"); blank when the role is unassigned
clientServiceTeam[].price_per_hour string Two decimal places
users[] object[] Selectable users: firm-scoped, not deleted, active
serviceLine[] object[] Each line with its own invoice_approver checkbox state and role_based_approver list
firmId number Resolved firms.id
useSLApprover number 1/0. The per-company row wins when present — including when it is 0 — otherwise the firm-level firms.use_service_line_approver applies
overRideDefaultPricing number company.override_price
defaultRoles[] object[] The firm-default Primary Accountant / MC Manager / Invoice Approver flags per role
overrideRoles[] object[] The per-company overrides of those three flags, from cs_override_roles
projectList[] object[] The company's active projects with budget figures

When service_role_users or service_roles is missing, a reduced shape is returned:

{
  "error": false,
  "clientServiceRole": [],
  "clientServiceTeam": [],
  "users": [],
  "serviceLine": [],
  "useSLApprover": 0,
  "overRideDefaultPricing": 0,
  "projectList": []
}

Errors

Status Condition Message/shape
400 companyId/task fail String Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }

Notes

  • Role scoping is exclusive, not additive: when firms.use_service_role = 1 only firm_id = <firm> roles are returned; when it is 0 only global (firm_id IS NULL) roles are.
  • Roles are filtered deleted IN (0, 2) and ordered by role ascending. deleted = 3 (inactive) roles are hidden.
  • cs_override_roles rows are filtered by deleted_at IS NULL, not by a deleted flag.
  • The reduced early-return shape omits firmId, defaultRoles and overrideRoles entirely — clients should not assume those keys are always present.
POST/api/utilities/configure/save-roles

Assigns a user to a service role for a company, or updates the company's role overrides. Which of the two it does is selected by override.

Module Client Success > Configure > Service Team
Authentication Required — authenticated session cookie
Authorization Requires per-company Client Success edit rights — admin or ACCESS_ADMIN on menu 10 for this company; company must be active
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Integer; numeric strings are coerced Plain numeric company id
override number No Integer, coerced 0/absent = assign a user. 1 = Primary Accountant override, 2 = MC Manager override, any other positive value = Invoice Approver override
overrideRoles number[] No Array of numbers; numeric strings are coerced In override mode, the role ids that should carry the override. Every other role's flag is cleared first
role number No Numeric; numeric strings are coerced, with no further validation In assign mode, the service_roles.id — required
user number No Numeric; numeric strings are coerced, with no further validation In assign mode, the user to assign. 0/absent stores NULL

Example request

curl -X POST "https://api.example.com/api/utilities/configure/save-roles" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "companyId": 1042, "role": 51, "user": 204 }'

Success response — 201 Created (assign mode)

{ "error": false, "id": "eyJpdiI6IkJ2...", "name": "Reyes, Dana", "price_per_hour": 85 }
Field Type Description
error boolean false on success
id string Encrypted service_role_users.id of the saved row
name string The assigned user's display name; '' when the role was left unassigned
price_per_hour number The rate stored on the row

Override mode (also 201):

{ "error": false, "message": "Override Role Updated Successfully." }

Errors

Status Condition Message/shape
400 companyId missing or not an integer Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }
400 Company row does not exist { "message": "Company not found." }
403 Caller lacks per-company edit rights { "message": "You do not have permission to modify client info for this company." }
403 Company is soft-deleted { "message": "Inactive companies are read-only in Client Info." }
201 companyId falsy { "error": true, "message": "Company is required." }
201 Assign mode with no role { "error": true, "message": "Company and role are required." }
201 service_role_users table absent { "error": true, "message": "Service team table not found." }
201 Override mode and cs_override_roles table absent { "error": false }

Notes

  • service_role_users holds one row per (company, service role); the user is a column on that row, not a row of its own. The match key excludes user_id, so assigning a user to a role that already has an unassigned row updates that row instead of inserting a second one.
  • Duplicate rows for the same (company, service role) are repaired on save: extras beyond the lowest-id live row are soft-deleted, or hard-deleted where there is no deleted column.
  • Pricing rule: when the company is not on override pricing, the role's own price_per_hour is pushed onto the row on every save. When it is, the row keeps its price unless it had none.
  • After an assign the firm role map is re-applied for the company, including approver updates. In override mode the approver update runs only for the Invoice Approver override; the Primary Accountant and MC Manager overrides rebuild the role map without touching approvers.
  • Override mode first clears the selected override column for every role on the company, then sets it to 1 for each id in overrideRoles — so an empty overrideRoles clears the override entirely.
POST/api/utilities/configure/save-roles-only

Adds a service role to a company's Service Team without assigning anybody — the row is created (or reset) with user_id = NULL.

Module Client Success > Configure > Service Team
Authentication Required — authenticated session cookie
Authorization Requires per-company Client Success edit rights — admin or ACCESS_ADMIN on menu 10; company must be active
Content type application/json

Request body (same shape as save-roles). This endpoint reads only companyId and role; user, override and overrideRoles are ignored.

Field Type Required Validation / allowed values Description
companyId number Yes Integer, coerced Plain numeric company id
role number Yes (by this endpoint) Numeric; numeric strings are coerced, with no further validation The service_roles.id to add

Example request

curl -X POST "https://api.example.com/api/utilities/configure/save-roles-only" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "companyId": 1042, "role": 51 }'

Success response — 201 Created

{ "error": false, "id": "eyJpdiI6IkJ2...", "name": "", "price_per_hour": 85 }
Field Type Description
id string Encrypted service_role_users.id
name string Always '' — the row is unassigned by definition
price_per_hour number The role's default rate

Errors

Status Condition Message/shape
400 companyId missing or not an integer Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }
400 Company row does not exist { "message": "Company not found." }
403 Caller lacks per-company edit rights { "message": "You do not have permission to modify client info for this company." }
403 Company is soft-deleted { "message": "Inactive companies are read-only in Client Info." }
201 companyId or role falsy { "error": true, "message": "Company and role are required." }
201 service_role_users table absent { "error": true, "message": "Service team table not found." }

Notes

  • This route unconditionally writes user_id = NULL. Use it, rather than save-roles, to add a role with nobody assigned: on save-roles a falsy user means "not supplied", not "set to NULL".
  • Same one-row-per-(company, role) model and the same pricing rule as save-roles.
  • The role map is re-applied without approver updates: an unassigned role cannot contribute an approver, so an approver rebuild here would risk clearing the company's existing approvers on a write that assigned nobody.
  • The resulting row renders in the grid as the role with an empty User cell, ready to be assigned via save-roles.
POST/api/utilities/configure/remove-roles

Removes the user from a Service Team row, leaving the role in place.

Module Client Success > Configure > Service Team
Authentication Required — authenticated session cookie
Authorization Requires per-company Client Success edit rights on the company resolved from the row, not from the body; company must be active
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id string Yes String Encrypted or plain service_role_users.id
companyId number Yes Integer, coerced Required by validation but not read — the company is looked up from the row
userId number No Numeric; numeric strings are coerced, with no further validation Accepted but not read by this endpoint

Example request

curl -X POST "https://api.example.com/api/utilities/configure/remove-roles" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "id": "eyJpdiI6IkJ2...", "companyId": 1042 }'

Success response — 201 Created

{ "error": false }

Blocked removal (also 201):

{
  "error": true,
  "message": "You cannot delete this role because this user has been mapped in the Service Team for this company."
}

Errors

Status Condition Message/shape
400 id not a string, or companyId missing/not an integer Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }
400 Resolved company row does not exist { "message": "Company not found." }
403 Caller lacks per-company edit rights { "message": "You do not have permission to modify client info for this company." }
403 Company is soft-deleted { "message": "Inactive companies are read-only in Client Info." }
201 id does not resolve to a positive integer { "error": true, "message": "Invalid service-team row." }
201 No service_role_users row with that id { "error": true, "message": "Service-team row not found." }
201 Role is the company's effective PA / MC Manager / Invoice Approver { "error": true, "message": "You cannot delete this role because this user has been mapped in the Service Team for this company." }

Notes

  • This clears the user, it does not delete the row: user_id is set to NULL and deleted stays 0. The role therefore remains in the grid afterwards with an empty User cell, and stays available for reassignment.
  • The block rule: an explicit cs_override_roles entry on this role for any of the three slots blocks outright. Otherwise, a role carrying the firm-default flag for a slot blocks unless some override exists on the company to take that slot over.
  • Afterwards the role map is re-applied, including approver updates.
  • companyId is a required body field that this endpoint ignores; supply it for validation, but the authoritative company is the one on the row.
POST/api/utilities/configure/save-role-price

Sets the hourly rate on one Service Team row.

Module Client Success > Configure > Service Team
Authentication Required — authenticated session cookie
Authorization Requires per-company Client Success edit rights on the company resolved from the row; company must be active
Content type application/json

Request body

Field Type Required Validation / allowed values Description
encId string Yes String Encrypted or plain service_role_users.id
pricePerHour number Yes Number; numeric strings are coerced The new rate. Written as-is (0 when null/undefined)

Example request

curl -X POST "https://api.example.com/api/utilities/configure/save-role-price" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "encId": "eyJpdiI6IkJ2...", "pricePerHour": 95.5 }'

Success response — 201 Created

{ "error": false }

Errors

Status Condition Message/shape
400 encId not a string or pricePerHour not a number Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }
400 Resolved company row does not exist { "message": "Company not found." }
403 Caller lacks per-company edit rights { "message": "You do not have permission to modify client info for this company." }
403 Company is soft-deleted { "message": "Inactive companies are read-only in Client Info." }
201 encId does not resolve to a positive integer { "error": true, "message": "Invalid row id." }
201 No service_role_users row with that id { "error": true, "message": "Service-team row not found." }

Notes

  • pricePerHour must be sent as a number or a numeric string; it is required.
  • The rate written here is a per-row override and survives only while the company is on override pricing; a later save-roles on a non-override company overwrites it with the role default. override-default-price with a restore signal also resets it.
POST/api/utilities/configure/override-default-price

Toggles a company's "override default pricing" flag, and optionally restores every one of its Service Team rows back to the role default rates.

Module Client Success > Configure > Service Team
Authentication Required — authenticated session cookie
Authorization Requires per-company Client Success edit rights — admin or ACCESS_ADMIN on menu 10; company must be active
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Integer, coerced Plain numeric company id
value number Yes Integer, coerced Truthy stores company.override_price = 1, 0 stores 0
restoreDefaults number No Integer, coerced > 0 triggers the price restore
roles unknown[] No Array; no nested schema A non-empty array also triggers the restore on its own. Only service_role_users[].enc_id (or .id) is read from each entry; prices inside are ignored

Example request

curl -X POST "https://api.example.com/api/utilities/configure/override-default-price" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "companyId": 1042, "value": 0, "restoreDefaults": 1 }'

Success response — 201 Created

{ "error": false, "message": "Override Pricing Updated Successfully.", "restoredRows": 12 }
Field Type Description
error boolean false on success
message string Always "Override Pricing Updated Successfully."
restoredRows number How many service_role_users rows were repriced. 0 when no restore was requested — the field distinguishes a real restore from a silent no-op

Errors

Status Condition Message/shape
400 companyId or value missing / not integers, or roles not an array Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }
400 Company row does not exist { "message": "Company not found." }
403 Caller lacks per-company edit rights { "message": "You do not have permission to modify client info for this company." }
403 Company is soft-deleted { "message": "Inactive companies are read-only in Client Info." }
201 companyId falsy { "error": true, "message": "Invalid company id." }

Notes

  • Two restore signals, both honoured. restoreDefaults: 1 triggers the restore, and so does a non-empty roles array on its own — a caller that sends only the array does not need the flag.
  • Prices always come from service_roles, never from the request body. Any price supplied inside roles is ignored; each affected row is repriced from its role's own price_per_hour.
  • Restore scope: when the caller names row ids (via roles[].service_role_users[].enc_id or .id) only those rows are repriced; otherwise every live row for the company is. Ids may be encrypted or plain.
  • The flag write and the restore are separate statements with no enclosing transaction; the flag is written first and always.
  • The restore is logged server-side with the company, the flag value, the scope and the affected-row count.
POST/api/utilities/configure/save-sl-invoice-approver

Saves one of the two per-company Service Line Invoice Approver checkboxes — the master switch, or one specific service line.

Module Client Success > Configure > Service Team
Authentication Required — authenticated session cookie
Authorization Requires per-company Client Success edit rights — admin or ACCESS_ADMIN on menu 10; company must be active
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Integer, coerced Plain numeric company id
colName string Yes String (no enum constraint) Exactly use_service_line_approver selects the master switch (cs_use_service_line_invoice_approver, one row per company). Any other string selects the per-line table (cs_service_line_invoice_approvers, one row per company + line)
invoiceApprover number Yes Integer, coerced Truthy stores 1, 0 stores 0
serviceLineId number No Integer, coerced Required in practice when colName is not the master switch

Example request

curl -X POST "https://api.example.com/api/utilities/configure/save-sl-invoice-approver" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "companyId": 1042, "colName": "service_line", "serviceLineId": 3, "invoiceApprover": 1 }'

Success response — 201 Created

{ "error": false, "message": "Service Line Approver Updated Successfully." }

Errors

Status Condition Message/shape
400 Any required field missing or of the wrong type Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }
400 Company row does not exist { "message": "Company not found." }
403 Caller lacks per-company edit rights { "message": "You do not have permission to modify client info for this company." }
403 Company is soft-deleted { "message": "Inactive companies are read-only in Client Info." }
201 companyId falsy { "error": true, "message": "Invalid company id." }
201 Target table absent from the schema { "error": true, "message": "Service line approver table not found." }
201 Per-line mode with no serviceLineId { "error": true, "message": "Invalid service line id." }

Notes

  • colName chooses which table is written; the two checkboxes are backed by different tables and must not be conflated. Both are per-company settings and must be posted to this route, not to POST /api/utilities/configure/update, which writes firm-level configuration.
  • Neither checkbox writes cs_invoice_approver_based_sl; that table is the firm-level role × service-line matrix and belongs to POST /api/utilities/configure/update with colName = 'service_line'.
  • The upsert matches without a deleted_at filter and clears the flag on write, so re-enabling a previously removed line revives the tombstoned row rather than inserting a duplicate.
  • Afterwards the company's existing invoices are re-resolved onto the new approver and the approver role map is re-run, so existing invoices follow the new configuration. Both steps run best-effort: a failure does not undo the checkbox write, which has already succeeded.
  • colName is not constrained to a fixed set of values: any value other than use_service_line_approver routes to the per-line table rather than being rejected.

10.2.4 Client Tags

Firm-scoped Client Success tags (client_success_tags), used to label clients. Two routes save the tag list: a canonical one and a compatibility alias.

POST/api/utilities/configure/tags/get

Lists the firm's active client tags.

Module Client Success > Configure > Client Tags
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond authentication
Content type application/json

Request body — none. This endpoint reads no request body; any body sent is ignored.

Example request

curl -X POST "https://api.example.com/api/utilities/configure/tags/get" \
  -H "Content-Length: 0" \
  -b "genie.sid=<session-cookie>"

Success response — 201 Created

{ "clientSuccessTags": [ { "id": 8, "name": "Onboarding" }, { "id": 12, "name": "At Risk" } ] }
Field Type Description
clientSuccessTags[].id number Plain client_success_tags.id
clientSuccessTags[].name string Tag label

Errors

Status Condition Message/shape
401 Session invalid or user not found { "message": "These credentials do not match our records." }

Notes

  • Scoped to the caller's firm and to deleted = 0.
  • Ordered by id ASC, so tags are returned in insertion order rather than alphabetically.
POST/api/utilities/configure/tags/save

Saves the firm's tag list: creates new names, revives previously deleted ones, and soft-deletes any active tag not present in the payload.

Module Client Success > Configure > Client Tags
Authentication Required — authenticated session cookie
Authorization Configuration-scope check on the firm scope — requires firm_admin = 1
Content type application/json

Request body — a bare JSON array of { text, tiClasses? } objects. No request-body validation is applied to this payload, and no properties are stripped from it. This endpoint reads only text from each element; tiClasses is accepted and ignored.

Field Type Required Validation / allowed values Description
[].text string Yes (in practice) None Tag name. Trimmed; empty strings and duplicates (case-insensitive) are dropped
[].tiClasses string[] No None Tag-input CSS classes from the frontend widget; not read

Example request

curl -X POST "https://api.example.com/api/utilities/configure/tags/save" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '[ { "text": "Onboarding" }, { "text": "At Risk" } ]'

Success response — 201 Created

{
  "error": false,
  "message": "Client Success Tags saved Successfully.",
  "template_id": "eyJpdiI6IlF3..."
}
Field Type Description
error boolean false on success
message string Always "Client Success Tags saved Successfully."
template_id string Encrypted id of the last tag touched (newly inserted, or the last matched existing tag); '' if none

Errors

Status Condition Message/shape
401 Session invalid or user not found { "message": "These credentials do not match our records." }
403 Caller is not a firm admin { "message": "You do not have permission to modify this configuration." }
201 Payload contains no usable names after trimming and de-duplication { "error": true, "message": "At least one tag is required." }

Notes

  • This is a full-list replace, not an append: any currently active tag whose name is absent from the payload is soft-deleted (deleted = 1). Send the complete list every time.
  • Matching is case-insensitive. An existing tag whose name reappears in the payload is revived (deleted = 0) rather than re-created, so tag ids — and therefore any task↔tag associations — survive.
  • Because this payload is not validated, malformed elements are not rejected; an element without a usable text is simply skipped.
POST/api/utilities/configure/save/tags

Compatibility route: retained for compatibility with earlier clients. POST /api/utilities/configure/tags/save is the canonical route; behaviour is identical.

Module Client Success > Configure > Client Tags
Authentication Required — authenticated session cookie
Authorization Configuration-scope check on the firm scope — requires firm_admin = 1
Content type application/json

Request body — identical to POST /api/utilities/configure/tags/save: a bare array of { text, tiClasses? }, with no request-body validation.

Example request

curl -X POST "https://api.example.com/api/utilities/configure/save/tags" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '[ { "text": "Onboarding" }, { "text": "At Risk" } ]'

Success response — 201 Created

{
  "error": false,
  "message": "Client Success Tags saved Successfully.",
  "template_id": "eyJpdiI6IlF3..."
}

Errors — identical to POST /api/utilities/configure/tags/save.

Status Condition Message/shape
401 Session invalid or user not found { "message": "These credentials do not match our records." }
403 Caller is not a firm admin { "message": "You do not have permission to modify this configuration." }
201 No usable tag names in the payload { "error": true, "message": "At least one tag is required." }

Notes

  • New integrations should use POST /api/utilities/configure/tags/save; this path is available for existing clients that already call it.
  • The same full-list-replace semantics apply, so a caller sending a partial list through this alias will soft-delete the tags it omits.
POST/api/utilities/configure/tags/update

Renames a single tag by id, preserving its task associations.

Module Client Success > Configure > Client Tags
Authentication Required — authenticated session cookie
Authorization Configuration-scope check on the firm scope — requires firm_admin = 1
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number Yes Integer; numeric strings are coerced client_success_tags.id
name string Yes String New tag name. Trimmed; rejected if empty

Example request

curl -X POST "https://api.example.com/api/utilities/configure/tags/update" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "id": 8, "name": "Onboarding (2026)" }'

Success response — 201 Created

{ "error": false, "message": "Tag updated successfully.", "id": 8, "name": "Onboarding (2026)" }
Field Type Description
error boolean false on success
message string "Tag updated successfully."
id number The tag id
name string The trimmed new name

Errors

Status Condition Message/shape
400 id not an integer or name not a string Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }
403 Caller is not a firm admin { "message": "You do not have permission to modify this configuration." }
201 id resolves to 0 { "error": true, "message": "Tag id is required." }
201 name empty after trimming { "error": true, "message": "Tag name is required." }
201 Tag not found in this firm, or already deleted { "error": true, "message": "Tag not found." }
201 Another active tag in the firm has the same name { "error": true, "message": "<name> already exist in the Tags." }

Notes

  • Renaming by id, rather than delete-and-recreate through tags/save, is what preserves existing task↔tag associations.
  • Ownership is verified before the write: the tag must belong to the caller's firm and be active. The UPDATE itself is also constrained by firm_id.
  • The duplicate check is case-insensitive and ignores soft-deleted tags.
POST/api/utilities/configure/tags/delete

Soft-deletes a single tag by id.

Module Client Success > Configure > Client Tags
Authentication Required — authenticated session cookie
Authorization Configuration-scope check on the firm scope — requires firm_admin = 1
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number Yes Integer; numeric strings are coerced client_success_tags.id

Example request

curl -X POST "https://api.example.com/api/utilities/configure/tags/delete" \
  -H "Content-Type: application/json" \
  -b "genie.sid=<session-cookie>" \
  -d '{ "id": 8 }'

Success response — 201 Created

{ "error": false, "message": "Tag deleted successfully." }

Errors

Status Condition Message/shape
400 id missing or not an integer Request validation error array
401 Session invalid or user not found { "message": "These credentials do not match our records." }
403 Caller is not a firm admin { "message": "You do not have permission to modify this configuration." }
201 id resolves to 0 { "error": true, "message": "Tag id is required." }

Notes

  • Soft delete only (deleted = 1), constrained to the caller's firm. The row survives, so re-adding the same name through tags/save revives this record and its associations.
  • No existence check is performed: deleting an id that does not exist, or belongs to another firm, still answers { "error": false, "message": "Tag deleted successfully." } having written nothing.

↑ Back to Table of Contents


11. Tasks & Time

Tasks & Time is the core work-management area of Genie. It covers task creation and tracking (Tasks), saved list views (Task Board), scheduling and drag-reschedule (Calendar), client Projects carrying the monthly budget and service-role allocation used by invoicing, the reusable Task & Time Templates catalogue, the module's firm-level reference data (Configure), the user-defined reporting engine (Reports / Reports Builder), time capture and approval (Timesheet), and turning approved time and fixed-price lines into QuickBooks invoices (Invoicing). Where a route is retained purely for compatibility with earlier clients, that is called out in the Notes for that endpoint.

Conventions that apply to every endpoint in this section

  • Base path. Every route in this section is served under an /api-prefixed route group, so every route below is /api/<path>.
  • Authentication. Authentication is enforced by each endpoint individually: the session must carry session.login.userId, and session.login.state must be authenticated. A missing or unauthenticated session yields 401 with { "message": "These credentials do not match our records." }. The session cookie is genie.sid and requests must be sent with credentials.
  • Validation. Global request validation is applied. Any body property that is not part of the documented request body schema is silently stripped before the endpoint runs; a validation violation returns 400 with { "statusCode": 400, "message": [ ... ], "error": "Bad Request" }. Fields marked Numeric accept numeric strings and coerce them.
  • Request size. JSON and form-encoded request bodies are accepted up to 20 MB, specifically because a Reports Builder save (POST /api/tat/reports/save) can carry a per-value conditional-format rule for every distinct value of a column. A larger body returns 413 request entity too large.
  • Error envelope. Most business failures are not HTTP errors: they are returned as 200 OK with { "error": true, "message": "..." } for Tasks, Templates, Configure and Invoicing (a few task routes return only { "message": "Task Not Found" }), and with { "error": true, "msg": "..." } for Reports. Genuine HTTP errors are limited to 401 (no session / firm mismatch / global-board write by a non-global admin), 403 (Client Info resource permissions) and 400 (validation, invalid project, invalid firm context).
  • Encrypted identifiers. Task, project, user and email ids are frequently exchanged in an encrypted form (enc_id, enc_cid, taskId), produced and consumed by the API. Routes that accept a task id accept either the plain numeric id or its encrypted form.
  • List convention. Task and audit lists use the same shape: offset (default 0), limit (default 30), sort_field (a display column name, not a SQL column), sort_order (asc/desc, case-insensitive), and filters — an array of { column, filter } entries where column is again the display name. Unknown column names are ignored rather than rejected. Projects use a different, object-shaped filter.
  • Access model. Tasks & Time is menu id 81. Per-company access levels are ACCESS_NO = 1, ACCESS_VIEW = 2, ACCESS_STANDARD = 3, ACCESS_ADMIN = 4. Editing requires >= ACCESS_STANDARD; admin-only affordances require ACCESS_ADMIN. A global admin is a user with users.global_admin = 1 (or the assigned Global Master System Administrator role); a firm admin is a user with users.firm_admin = 1; both bypass the access-level checks. Configure additionally requires the Global Master System Administrator role for global-scoped rows. Access enforcement is applied server-side and is documented per endpoint below.
  • Disposition / progress vocabulary. Stored disposition_status values are Completed, Skipped, Deferred, Reassigned, Open (any other value is title-cased). The derived Progress display value is Completed, Deferred, Skipped, Overdue (open and past due), or Open. Invoicing is 1 = Hourly, 2 = Fixed Price.

The Tasks & Time domain is the operational core of Genie: firms create and track client work as Tasks, arrange saved views of those tasks on the Task Board, schedule and drag-reschedule them on the Calendar, and group them under client Projects that carry the monthly budget and service-role allocation used by invoicing. Where a route is retained purely for compatibility with earlier clients, that is called out in the Notes for that endpoint.

Conventions that apply to every endpoint in this section

  • Base path. Every route in this section is served under /api, so every path below begins with /api/.
  • Authentication. Authentication is enforced by each endpoint individually: the session must carry session.login.userId, and session.login.state must be authenticated. A missing or unauthenticated session yields 401 with { "message": "These credentials do not match our records." }. The session cookie is genie.sid and requests must be sent with credentials.
  • Validation. Global request validation is applied. Any body property that is not part of the documented request body schema is silently stripped before the endpoint runs; a validation violation returns 400 with { "statusCode": 400, "message": [ ... ], "error": "Bad Request" }. Fields marked Numeric accept numeric strings and coerce them.
  • Error envelope. Most business failures are not HTTP errors: they are returned as 200 OK with { "error": true, "message": "..." } (a few task routes return only { "message": "Task Not Found" }). Genuine HTTP errors are limited to 401 (no session / firm mismatch / global-board write by a non-global admin), 403 (Client Info resource permissions) and 400 (validation, invalid project, invalid firm context).
  • Encrypted identifiers. Task, project, user and email ids are frequently exchanged in an encrypted form (enc_id, enc_cid, taskId), produced and consumed by the API. Routes that accept a task id accept either the plain numeric id or its encrypted form.
  • List convention. Task and audit lists use the same shape: offset (default 0), limit (default 30), sort_field (a display column name, not a SQL column), sort_order (asc/desc, case-insensitive), and filters — an array of { column, filter } entries where column is again the display name. Unknown column names are ignored rather than rejected. Projects use a different, object-shaped filter.
  • Access model. Tasks & Time is menu id 81 (TASK_TIME_MENU_ID). Per-company access levels are ACCESS_NO = 1, ACCESS_VIEW = 2, ACCESS_STANDARD = 3, ACCESS_ADMIN = 4. Editing requires >= ACCESS_STANDARD; admin-only affordances require ACCESS_ADMIN. Global Master System Administrators and firm admins bypass these checks. Access enforcement is applied server-side and is documented per endpoint below.
  • Disposition / progress vocabulary. Stored disposition_status values are Completed, Skipped, Deferred, Reassigned, Open (any other value is title-cased). The derived Progress display value is Completed, Deferred, Skipped, Overdue (open and past due), or Open. Invoicing is 1 = Hourly, 2 = Fixed Price.

11.1 Tasks

The Tasks subsection covers task creation and editing, the task list and its exports, per-task steps/resources/time history, disposition (completion) and recurrence, bulk multi-task operations, and the task audit log. It comprises 39 routes.

GET/api/task/filter-options

Returns every dropdown option set the Task List filter panel and the Add/Edit Task form need, scoped to the caller's firm and company access.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Project options are scoped by company access (global admin → all firms; firm user → whole firm; mapped user → only mapped companies), and the firm's use_service_line / use_service_role flags are applied.
Content type application/json

Example request

curl -s 'https://api.example.com/api/task/filter-options' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "priorityOptions": [{ "id": 3, "name": "High" }],
  "assignOptions": [
    { "group": "Roles", "options": [{ "id": 12, "name": "Accountant", "icon": "fa-user-tie", "deleted": 0 }] },
    { "group": "Users", "options": [{ "id": 88, "name": "Doe, Jane", "icon": "fa-user-tie", "active": 1, "deleted": 0 }] }
  ],
  "serviceLineOptions": [{ "id": 4, "name": "Bookkeeping" }],
  "projectOptions": [{ "id": 91, "name": "Monthly Close" }],
  "typeOptions": [{ "id": 2, "name": "Recurring" }],
  "billingOptions": [{ "id": 1, "name": "Standard" }],
  "tagOptions": [{ "id": 7, "name": "Urgent" }],
  "statusOption": [{ "id": 5, "name": "In Progress" }],
  "companyGroupOptions": [{ "id": 3, "name": "West Region" }],
  "roleUserMap": { "12": [88, 91] },
  "options": []
}
Field Type Description
priorityOptions array Priority rows for firm 0 (global) and the caller's firm.
assignOptions array Two groups, Roles and Users, used by the Assigned-To picker.
serviceLineOptions array Service lines; falls back to the global set when the firm has use_service_line = 0.
projectOptions array Projects visible under the caller's company access scope.
typeOptions / billingOptions / tagOptions / statusOption array Task type, invoice-detail, tag and status option sets.
companyGroupOptions array Company-group options for the Company Group filter column.
roleUserMap object Map of service-role id → array of user ids holding that role, so the UI can narrow the User picker after a Role is chosen.
options array Always empty; retained for compatibility with earlier clients.

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Inactive/soft-deleted options are still returned with a deleted flag rather than being filtered out, so the client can render "(inactive)" suffixes.
GET/api/task/multi-task/list

Returns a flat { roleOptions, userOptions } pair for the Reports Builder preview's Role/User global-filter dropdowns.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; scoped to the caller's firm.
Content type application/json

Example request

curl -s 'https://api.example.com/api/task/multi-task/list' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "roleOptions": [{ "id": 12, "name": "Accountant" }],
  "userOptions": [{ "id": 88, "name": "Doe, Jane" }]
}
Field Type Description
roleOptions[].id / .name number / string Firm service roles.
userOptions[].id / .name number / string Firm users; lf_name when present, otherwise a formatted first/last name.

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Compatibility route: retained for compatibility with earlier clients. It returns the same firm-roles/firm-users data that GET /api/task/filter-options already provides inside assignOptions, re-shaped flat for the Reports Preview page.
  • Despite the multi-task path segment this is a read-only dropdown feed, unrelated to the multi-task update/delete/skip routes.
POST/api/task/company/projects

Returns the assignable roles/users and the project list for one company — the payload the Add/Edit Task form loads after a company is picked.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Integer, coerced from string Company whose projects and assignable people are wanted.

Example request

curl -s -X POST 'https://api.example.com/api/task/company/projects' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "companyId": 412 }'

Success response — 200 OK

{
  "assignOptions": [
    { "group": "Roles", "options": [{ "id": 12, "name": "Accountant", "icon": "fa-user-tie", "deleted": 0 }] },
    { "group": "Users", "options": [{ "id": 88, "name": "Doe, Jane", "lf_name": "Doe, Jane", "icon": "fa-user-tie", "active": 1, "deleted": 0 }] }
  ],
  "projectOptions": [
    { "id": 91, "name": "Monthly Close", "serviceLine": 4, "enc_id": "b2FkOTk..." }
  ],
  "roleOptions": [{ "id": 12, "name": "Accountant", "icon": "fa-user-tie", "deleted": 0 }],
  "userOptions": [{ "id": 88, "name": "Doe, Jane", "lf_name": "Doe, Jane", "icon": "fa-user-tie", "active": 1, "deleted": 0 }]
}
Field Type Description
assignOptions array Grouped Roles / Users picker payload.
projectOptions[].serviceLine number | null The project's service line, used to auto-fill the task's service line.
projectOptions[].enc_id string Encrypted project id.
roleOptions / userOptions array The same two sets, flat.

Errors

Status Condition Message/shape
400 companyId missing or not an integer Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }
POST/api/task/get-assigned-name

Resolves the single user who should be auto-assigned for a given service role at a given company.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number Yes Integer Service role id.
companyId number Yes Integer Company the role is being resolved against.

Example request

curl -s -X POST 'https://api.example.com/api/task/get-assigned-name' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "id": 12, "companyId": 412 }'

Success response — 200 OK

{ "id": 88, "name": "Doe, Jane", "icon": "user-tie" }
Field Type Description
id number Resolved user id, or 0 when nobody holds the role at that company.
name string User display name, or the literal "Unassigned".
icon string "user-tie" when resolved, "user-ban" when unassigned.

Errors

Status Condition Message/shape
400 Missing/non-integer id or companyId Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }
POST/api/task/company/authUserRoles

Given a company and a set of candidate service roles, returns the role/user pair to pre-select when exactly one role matches.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Integer Company to resolve roles against.
serviceRoleIds number[] Yes Array (elements not individually validated) Candidate service role ids.

Example request

curl -s -X POST 'https://api.example.com/api/task/company/authUserRoles' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "companyId": 412, "serviceRoleIds": [12, 15] }'

Success response — 200 OK

{ "role": { "id": 12, "name": "Accountant" }, "user": { "id": 88, "name": "Doe, Jane" } }
Field Type Description
role object | [] { id, name } when exactly one distinct role is resolved, otherwise an empty array.
user object | [] { id, name } for the user holding that role, otherwise an empty array.

Errors

Status Condition Message/shape
400 Missing companyId / serviceRoleIds Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The empty-array form is returned whenever zero or more than one distinct role resolves — the client is expected to leave the pickers untouched in that case.
POST/api/task/bulk/fetch/clients

Lists the company/project pairs eligible for a bulk task creation run, optionally narrowed by project, service line, invoice detail or invoicing type.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
byProject string No String Project-name filter.
byServiceLine number No Integer Service line id filter.
byInvoiceDetails number No Integer Invoice-detail (billing) id filter.
byInvoicing number No Integer Invoicing type filter (1 Hourly, 2 Fixed Price).

Example request

curl -s -X POST 'https://api.example.com/api/task/bulk/fetch/clients' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "byServiceLine": 4, "byInvoicing": 1 }'

Success response — 200 OK

{
  "clients": [
    {
      "company_id": 412,
      "company_name": "Acme Holdings",
      "project_id": 91,
      "project_name": "Monthly Close",
      "invoice_details": 1,
      "invoicing": 1,
      "invoicing_label": "Hourly"
    }
  ]
}
Field Type Description
clients[] array One row per company/project pair, ordered by company name then project name.
clients[].invoicing_label string Human label derived from invoicing (Hourly / Fixed Price, "" otherwise).

Errors

Status Condition Message/shape
400 A supplied filter fails its type check Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • All four filters are optional; with none supplied the full eligible set for the caller's scope is returned. This endpoint feeds the client picker of POST /api/task/bulk/create/tasks.
POST/api/task/bulk/create/tasks

Creates one task per selected client/project from a task template, in a single call.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. The per-task assigned-user access check is skipped here because users are resolved server-side from company-scoped service roles.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
templateId number Yes Integer Task template to instantiate.
assignedRole number Yes Integer Service role assigned to every created task.
selectedClients object[] Yes Array; each item is validated against the item fields below The company/project pairs to create tasks for.
selectedClients[].company_id number Yes Integer Company id.
selectedClients[].project_id number Yes Integer Project id.
selectedClients[].invoice_details number No Integer Per-row invoice detail override.
selectedClients[].invoicing number No Integer Per-row invoicing override.
taskDue string Yes String Due date for every created task.
taskName string Yes String Task name for every created task.
invoiceDetails number No Integer Default invoice detail.
invoicing number No Integer Default invoicing type.
serviceLine number Yes Integer Service line for every created task.
taskType number Yes Integer Task type for every created task.

Example request

curl -s -X POST 'https://api.example.com/api/task/bulk/create/tasks' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{
        "templateId": 33,
        "assignedRole": 12,
        "selectedClients": [{ "company_id": 412, "project_id": 91 }],
        "taskDue": "2026-10-15",
        "taskName": "September Close",
        "serviceLine": 4,
        "taskType": 2
      }'

Success response — 200 OK

{ "error": false, "message": "Task(s) Created Successfully." }
Field Type Description
error boolean false on success.
message string "Task(s) Created Successfully."

Errors

Status Condition Message/shape
200 selectedClients empty { "error": true, "message": "Select at least one client/project." }
200 templateId does not resolve { "error": true, "message": "Template not found." }
400 Missing/invalid required fields Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The template's steps are replicated onto each created task.
  • If the template carries a recurrence with a non-empty cron_string, a recurrence row is created for each task with startOn taken from the first 10 characters of taskDue and no end date.
PUT/api/task

Creates a single task.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization Enforced server-side: the chosen company must belong to the caller's firm, and the caller needs Standard or Admin access on menu 81 for that company; firm and global admins bypass. The assigned and reviewer users are additionally checked for menu-81 access to that company.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
name string Yes String (must be non-blank after trim) Task name.
company number Yes Integer Client company id.
priority number No Integer Priority id.
assignedRoles number No Integer Assigned service role id.
assignedName number No Integer Assigned user id.
serviceLine number No Integer Service line id; derived from the project when a project is supplied.
project number No Integer Project id.
type number No Integer Task type id.
billing number No Integer Invoice detail (billing) id.
estimatedTime string No String Estimated time.
requireReview boolean No Boolean Whether the task requires review.
reviewer number No Integer Reviewer service role id.
reviewerName number No Integer Reviewer user id.
timeNotes string No String Time notes.
due string No String Due date/time; invalid dates are stored as null.
status number No Integer Service-line status id.
tags number[] No Array Tag ids; also written to tasks_tags.
invoicing number No Integer 1 Hourly, 2 Fixed Price. Optional — stored NULL when absent.
allOccurrence string No String Recurrence-scope hint.
emailId number No Integer Linked email id.
fromTemplate boolean No Boolean Marks the audit source as Template.
fromTemplateGroup boolean No Boolean Marks the audit source as Template Group.
fromQuickTask boolean No Boolean Marks the audit source as Quick Task and switches the timesheet check to the soft-alert path.
fromClone boolean No Boolean Marks the audit source as Clone Task.
fromTodoToTask boolean No Boolean Only meaningful on PATCH /api/task/:id; see that endpoint.
taskDate string No String Date used for the timesheet-lock check.

Example request

curl -s -X PUT 'https://api.example.com/api/task' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{
        "name": "Reconcile bank accounts",
        "company": 412,
        "project": 91,
        "assignedRoles": 12,
        "assignedName": 88,
        "due": "2026-10-15",
        "invoicing": 1,
        "tags": [7]
      }'

Success response — 200 OK

{
  "error": false,
  "message": "Task created successfully.",
  "taskId": "b2FkOTk...",
  "rolePrice": 145,
  "haveAlert": false
}
Field Type Description
error boolean false on success.
message string "Task created successfully."
taskId string Encrypted id of the new task.
rolePrice number | null Role override price resolved for company + assignedRoles.
haveAlert boolean Quick Task only — true when saving will move the user's already-submitted timesheet week back to Unsubmitted.

Errors

Status Condition Message/shape
200 Blank name { "error": true, "message": "The name field is required." }
200 Missing company { "error": true, "message": "The company field is required." }
200 taskDate falls in a SUBMIT/APPROVE timesheet week and this is not a Quick Task { "error": true, "message": "<timesheet lock message>" }
200 Assigned or reviewer user lacks menu-81 access to the company { "error": true, "message": "<assigned-user access message>" }
200 Insert produced no id { "error": true, "message": "task failed" }
400 Request validation failure Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }
401/403 Company outside the caller's firm, or caller lacks write access on menu 81 Raised by the company firm and write-access checks

Notes

  • This endpoint uses PUT, not POST.
  • A Created audit record is written with a From value of Quick Task, Template Group, Template, Clone Task or Scratch, chosen from the from* booleans in that order.
PATCH/api/task/:id

Updates an existing task using the same field set as create.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the task must belong to the caller's firm and to a non-deleted company.
Content type application/json

Path parameters

Name Type Required Description
id string Yes Task id — plain numeric id or encrypted id.

Request body (same field set as PUT /api/task; see that endpoint's table)

Field Type Required Validation / allowed values Description
(all fields as for PUT /api/task)
fromTodoToTask boolean No Boolean Promotes an existing is_todo = 1 row into a real task: is_todo is cleared and the audit event is written as Created with From: 'ToDo' rather than as an Updated diff.

Example request

curl -s -X PATCH 'https://api.example.com/api/task/10482' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "name": "Reconcile bank accounts (Q3)", "company": 412, "priority": 3 }'

Success response — 200 OK

{
  "error": false,
  "message": "Task updated successfully.",
  "taskId": "b2FkOTk...",
  "rolePrice": 145,
  "haveAlert": false
}
Field Type Description
error boolean false on success.
message string "Task updated successfully."
taskId string Encrypted id of the updated task.
rolePrice number | null Role override price for company + assignedRoles.
haveAlert boolean Always false on update.

Errors

Status Condition Message/shape
200 id does not decode, or the task is not visible to the caller { "error": true, "message": "Task not found." }
200 Blank name { "error": true, "message": "The name field is required." }
200 Missing company { "error": true, "message": "The company field is required." }
400 Request validation failure Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Normal updates write an Updated audit record containing a before/after diff computed from full task snapshots.
  • With fromTodoToTask, the promotion of a ToDo into a task is logged as a creation, not an update.
POST/api/task/get-task

Loads the full Task Details payload for one task: field values, resolved dropdown labels, steps, resources, recurrence, latest time history, the caller's per-company permission flags, and the dropdown option sets.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. The caller's menu-81 access level for the task's company is resolved and returned as accessType / canEdit / canAdmin for client-side gating: an explicit menu row gives its own level, group-inherited access gives ACCESS_VIEW (2) only, no access gives ACCESS_NO (1). Firm and global admins get ACCESS_ADMIN (4).
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id string Yes String Task id — plain or encrypted.
createHistory number No Integer Requests that a history row be created when the task is opened.
fromViewTask number No Integer Marks the call as coming from the View Task screen.

Example request

curl -s -X POST 'https://api.example.com/api/task/get-task' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "id": "b2FkOTk...", "fromViewTask": 1 }'

Success response — 200 OK

{
  "error": false,
  "userId": 88,
  "taskDate": "2026-10-15",
  "taskValues": {
    "id": 10482,
    "enc_id": "b2FkOTk...",
    "name": "Reconcile bank accounts",
    "company": { "id": 412, "name": "Acme Holdings", "deleted": 0 },
    "assignedRole": { "id": 12, "name": "Accountant" },
    "assignedName": { "id": 88, "name": "Doe, Jane" },
    "project": { "id": 91, "name": "Monthly Close", "serviceLine": 4 },
    "serviceLine": { "id": 4, "name": "Bookkeeping" },
    "status": { "id": 5, "name": "In Progress" },
    "priority": { "id": 3, "name": "High" },
    "type": { "id": 2, "name": "Recurring" },
    "billing": { "id": 1, "name": "Standard" },
    "invoicing": { "id": 1, "name": "Hourly" },
    "due": "2026-10-15 00:00:00",
    "estimated_time": "2:00",
    "requires_review": 0,
    "time_notes": "",
    "disposition_status": "Open",
    "tags": [{ "id": 7, "name": "Urgent" }],
    "tag_ids": [7],
    "recurrence": null,
    "recurrence_id": null,
    "haveAccess": true,
    "accessType": 4,
    "canEdit": true,
    "canAdmin": true,
    "companyDeleted": false,
    "isFirmAdmin": true,
    "isGlobalAdmin": false,
    "steps": [],
    "resources": [],
    "task_comments": [],
    "time_history": []
  },
  "dropDownValues": { "statusOption": [{ "id": 5, "name": "In Progress" }] },
  "haveAlert": false,
  "timeHistory": null,
  "projectRoleStatus": { "monthlyEstimateHours": "0.00", "last4Wk": "0.00", "mothToDoDate": "0.00", "query": [] }
}
Field Type Description
error boolean false on success.
userId number The calling user's id.
taskDate string | null The due date, date part only.
taskValues object The task itself. Reference fields (company, assignedRole, assignedName, reviewerRole, reviewerName, serviceLine, project, billing, priority, type, status, invoicing) are { id, name } objects or null. Inactive/deleted references carry an (inactive) / (deleted) suffix in name.
taskValues.enc_id / email_id string | null Encrypted identifiers.
taskValues.haveAccess, hasCompanyAccess, accessType, canEdit, canAdmin boolean / number Permission flags for gating Edit / Status / Notes / Disposition / Reopen / Comments and Company-Resource editing.
taskValues.isFirmAdmin, isGlobalAdmin, isAdminUser boolean Caller's admin flags — also gate "Make into Task Template".
taskValues.companyDeleted boolean true when the client company is inactive; the details view still loads so the UI can show a read-only banner.
taskValues.steps, resources, tags, tag_ids, recurrence, time_history array/object Child collections. task_comments is always [] from this endpoint.
dropDownValues object Option sets plus statusOption.
haveAlert boolean Warns that opening/logging time will unsubmit the current week's timesheet.
timeHistory object | null The latest time-history row.
projectRoleStatus object Monthly estimate / last-4-week / month-to-date role hours.

Errors

Status Condition Message/shape
200 id does not decode, or the task is not visible { "error": true, "message": "Task not found." }
400 id missing Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Several fields are returned in both snake_case and camelCase (estimated_time/estimatedTime, requires_review/requireReview, time_notes/timeNotes) for compatibility with earlier clients.
  • Two option tables use an inverted is_active flag: for service_line and task_type, is_active = 1 means deactivated. The (inactive) suffix logic here follows that convention.
  • This is the only task read path that loads a task whose company is soft-deleted; every mutation path keeps the strict company filter.
POST/api/task/clone

Creates a copy of an existing task, including its steps and resources, under a new name.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the clone is created through the task-create path, so the company write-access rules of PUT /api/task apply.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
taskId string Yes String Source task id — plain or encrypted.
taskName string Yes String (must be non-blank after trim) Name for the cloned task.

Example request

curl -s -X POST 'https://api.example.com/api/task/clone' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "taskId": "b2FkOTk...", "taskName": "Reconcile bank accounts (copy)" }'

Success response — 200 OK

{
  "error": false,
  "message": "Task cloned successfully.",
  "taskId": "Y2xvbmU...",
  "rolePrice": 145,
  "haveAlert": false
}
Field Type Description
(all fields from PUT /api/task) The create response is spread into the clone response.
message string Overridden to "Task cloned successfully."

Errors

Status Condition Message/shape
200 Source task not found or not visible { "error": true, "message": "Task not found." }
200 Blank taskName { "error": true, "message": "Clone Task Name field is required." }
400 Missing taskId / taskName Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The clone's audit record is written with From: 'Clone Task'.
  • Child rows (steps, resources) are replicated after the new task id is known.
POST/api/task/makeToTemplate

Converts an existing task into a reusable task template, carrying over its settings, steps and recurrence.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. The client gates the action on the admin flags returned by POST /api/task/get-task.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
taskId string Yes String Source task id — plain or encrypted.
templateName string Yes String (must be non-blank after trim) Name for the new template.

Example request

curl -s -X POST 'https://api.example.com/api/task/makeToTemplate' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "taskId": "b2FkOTk...", "templateName": "Monthly bank reconciliation" }'

Success response — 200 OK

{ "error": false, "message": "Task converted to template successfully.", "template_id": 57 }
Field Type Description
error boolean false on success.
message string "Task converted to template successfully."
template_id number Id of the created template.

Errors

Status Condition Message/shape
200 Task not found or not visible { "error": true, "message": "Task not found." }
200 Blank templateName { "error": true, "message": "Template Name field is required." }
200 The template settings save fails The error envelope returned by the template save
400 Missing taskId / templateName Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Template creation is handled by the Task Templates area, so the template creation rules documented there apply. The task's due date is split into date and time parts; due_time is truncated to HH:MM:SS.
GET/api/task/:id/time-history

Returns the caller's most recent time-history row for a task — the timer state the Task Details view resumes from.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the task must be visible to the caller's firm.
Content type application/json

Path parameters

Name Type Required Description
id string Yes Task id — plain or encrypted.

Example request

curl -s 'https://api.example.com/api/task/10482/time-history' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "getTimeHistory": {
    "id": 9931,
    "task_id": 10482,
    "start_time": 1759312800,
    "end_time": 1759316400,
    "override_time": null
  }
}
Field Type Description
getTimeHistory object The latest row for this task and user, or a zeroed default row when none exists (or when the id does not resolve).

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • This endpoint never returns an error envelope: an unresolvable id or an invisible task both yield a default (empty) time-history row, so the timer UI always has something to bind to.
PUT/api/task/time-history

Creates (or updates, when id is supplied) a time-history row for a task — used when the timer is stopped.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the task must be visible to the caller.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
task_id string Yes String Task id — plain or encrypted.
id number No Integer Existing time-history row id to update.
start_time number Yes Integer Start timestamp (epoch seconds).
end_time number Yes Integer End timestamp (epoch seconds).
override_time number | null No Integer Manually reported duration overriding the captured span.

Example request

curl -s -X PUT 'https://api.example.com/api/task/time-history' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "task_id": "b2FkOTk...", "start_time": 1759312800, "end_time": 1759316400 }'

Success response — 200 OK

{
  "error": false,
  "message": "Time history saved successfully.",
  "data": { "id": 9931, "task_id": 10482, "start_time": 1759312800, "end_time": 1759316400, "override_time": null }
}
Field Type Description
error boolean false on success.
message string "Time history saved successfully."
data object The persisted time-history row.

Errors

Status Condition Message/shape
200 task_id does not decode, or the task is not visible { "error": true, "message": "Task not found." }
400 Missing task_id / start_time / end_time Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • This endpoint uses PUT, while the companion update route POST /api/task/time-history/update uses POST.
POST/api/task/time-history/update

Updates only the end time of an existing time-history row.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the row's task must be visible to the caller.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number Yes Integer Time-history row id.
end_time number No Integer New end timestamp.
endTime number No Integer camelCase alias, accepted for compatibility with earlier clients.

Example request

curl -s -X POST 'https://api.example.com/api/task/time-history/update' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "id": 9931, "end_time": 1759318000 }'

Success response — 200 OK

{
  "error": false,
  "message": "Time history updated successfully.",
  "data": { "id": 9931, "task_id": 10482, "start_time": 1759312800, "end_time": 1759318000, "override_time": null }
}
Field Type Description
error boolean false on success.
message string "Time history updated successfully."
data object The updated row.

Errors

Status Condition Message/shape
200 id missing or zero { "error": true, "message": "Invalid time-history id." }
200 Row not found { "error": true, "message": "Time history not found." }
200 The row's task is not visible to the caller { "error": true, "message": "Task not found." }
400 id missing/non-integer Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • start_time and override_time are always taken from the stored row; only the end time can be changed here. If neither end_time nor endTime is supplied, the existing end time is retained.
POST/api/task/task-history

Returns the per-entry time log for a task: who logged time, when, how much was captured and how much was reported.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the task must be visible to the caller.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id string Yes String Task id — plain or encrypted.
flagHistory boolean No Boolean Selects the history variant to return.

Example request

curl -s -X POST 'https://api.example.com/api/task/task-history' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "id": "b2FkOTk..." }'

Success response — 200 OK

{
  "taskHistory": [
    {
      "id": 9931,
      "entry_number": 1,
      "user_name": "Doe, Jane",
      "date": "2026-10-01 09:00:00",
      "time_capture": 3600,
      "time_reported": 3600,
      "progress": "Open"
    }
  ]
}
Field Type Description
taskHistory[].time_capture number Seconds actually captured (end_time − start_time, floored at 0).
taskHistory[].time_reported number override_time when greater than 0, otherwise the captured value.
taskHistory[].progress string Derived progress label for the task at read time.

Errors

Status Condition Message/shape
200 id does not decode, or the task is not visible { "taskHistory": [] }
400 id missing Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • A failure reading the underlying rows produces an empty taskHistory array rather than an error response.
POST/api/task/time/disposition

Records the outcome of working a task — Complete / Skip / Defer / Reassign, or Reopen — together with the time captured and its billable/non-billable/no-charge allocation.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the task must be visible to the caller.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
task_id string Yes String Task id — plain or encrypted.
id number No Integer Existing time-history row to attach the disposition to.
start_time number Yes Integer Start timestamp (epoch seconds).
end_time number Yes Integer End timestamp (epoch seconds).
override_time number | null No Integer Manually reported duration.
disposition object[] Yes Array; each item is validated against the item fields below, and only the first element is used The disposition to apply.
disposition[].status string Yes String; normalised to Completed, Skipped, Deferred, Reassigned, Open (any other value is title-cased) Outcome.
disposition[].reason string | null No String Reason text (deferral/reassignment).
disposition[].due string No String New due date when deferring.
disposition[].assigned_roles number No Integer New assigned role when reassigning.
disposition[].assigned_name number No Integer New assigned user when reassigning.
recurrence_due string No String Client-supplied next due date (the server computes its own; see Notes).
allocate_time object No Nested object; its fields are validated individually Time split from the Allocate Time modal.
allocate_time.billableTime number No Number Billable seconds.
allocate_time.noChargeTime number No Number No-charge seconds.
allocate_time.nonBillableTime number No Number Non-billable seconds.
allocate_time.timeNotes string No String Client-visible time notes.
allocate_time.internalNotes string No String Internal notes.
allocate_time.isModified boolean No Boolean Whether the user edited the split.
allocate_time.error boolean No Boolean Client-side validation marker.
isReopen boolean No Boolean Reopen path — see Notes.
revertTimesheet boolean No Boolean Requests the associated timesheet week be reverted.

Example request

curl -s -X POST 'https://api.example.com/api/task/time/disposition' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{
        "task_id": "b2FkOTk...",
        "start_time": 1759312800,
        "end_time": 1759316400,
        "disposition": [{ "status": "Completed" }],
        "allocate_time": { "billableTime": 3600, "noChargeTime": 0, "nonBillableTime": 0 }
      }'

Success response — 200 OK

{
  "error": false,
  "message": "Task disposition saved successfully.",
  "data": { "id": 9931, "task_id": 10482, "start_time": 1759312800, "end_time": 1759316400, "override_time": null }
}
Field Type Description
error boolean false on success.
message string "Task disposition saved successfully."
data object The saved (or reopened) time-history row.

Errors

Status Condition Message/shape
200 task_id does not decode, or the task is not visible { "error": true, "message": "Task not found." }
200 disposition[0].status empty or unnormalisable { "error": true, "message": "Disposition status is required." }
400 Missing task_id / start_time / end_time / disposition Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Reopen. The Reopen action sends an empty disposition array together with isReopen; that path bypasses the status check, writes an Updated audit record with New: { "Disposition Status": "REOPEN" }, and returns the reopened history row.
  • Recurrence. After a disposition is saved, if the task carries a recurrence and the whole family is completed or skipped, the next occurrence is cloned synchronously. The next due date is computed server-side; recurrence_due in the body is not the authority. A failure here is logged as a warning and never fails the disposition save.
  • allocate_time is validated as a nested object, so its numeric fields are not removed by unknown-property stripping and the submitted split is applied as sent.
POST/api/task/recurrence/all

Creates or updates the recurrence rule attached to a task.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the task must be visible to the caller.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number No Integer Existing recurrence row id.
taskId string Yes String Task id — plain or encrypted.
due string No String Due date to apply alongside the rule.
cronString string No String The recurrence rule, stored in recurrence.cron_string.
startOn string No String First occurrence date.
endOn string No String Last occurrence date.
weekendRule string No String — free text; no fixed set of values How occurrences landing on a weekend are shifted.
holidayRule string No String — free text; no fixed set of values How occurrences landing on a holiday are shifted.
holidayCalendar number No Integer Holiday calendar id used by holidayRule.
allOccurence number No Integer Whether the change applies to the whole occurrence family. The field name is spelled exactly as shown, with a single r, and must be sent that way.

Example request

curl -s -X POST 'https://api.example.com/api/task/recurrence/all' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "taskId": "b2FkOTk...", "cronString": "0 0 1 * *", "startOn": "2026-10-01", "allOccurence": 1 }'

Success response — 200 OK

{ "error": false, "message": "Recurrence saved successfully.", "recurrenceId": 771 }
Field Type Description
error boolean false on success.
message string "Recurrence saved successfully."
recurrenceId number Id of the created/updated recurrence row.

Errors

Status Condition Message/shape
200 taskId does not decode, or the task is not visible { "error": true, "message": "Task not found." }
200 The recurrence table is absent { "error": true, "message": "Recurrence table not found." }
200 The recurrence row could not be created { "error": true, "message": "Something went wrong." }
400 Missing taskId Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Writes are schema-adaptive: weekend_rule, holiday_rule, updated_at and the step/resource snapshot columns (r_step, r_resource) are only written when those columns exist on the recurrence table.
  • Saving a recurrence also snapshots the task's current steps and resources onto the recurrence row so generated occurrences inherit them.
  • Allowed values for weekendRule / holidayRule are not specified in the current implementation — they are stored as free-form strings.
PATCH/api/task/update/status/:id

Changes a task's service-line status (the workflow status, not the disposition).

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the task must be visible to the caller. The Task Details view gates the control on canEdit.
Content type application/json

Path parameters

Name Type Required Description
id string Yes Task id — plain or encrypted.

Request body

Field Type Required Validation / allowed values Description
status number Yes Integer service_line_status.id to set.

Example request

curl -s -X PATCH 'https://api.example.com/api/task/update/status/10482' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "status": 5 }'

Success response — 200 OK

{ "error": false, "message": "Status updated successfully." }
Field Type Description
error boolean false on success.
message string "Status updated successfully."

Errors

Status Condition Message/shape
200 id does not decode, or the task is not visible { "error": true, "message": "Task not found." }
400 Missing/non-integer status Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • An Updated audit record is written carrying the resolved status names (before/after), not the raw ids.
PATCH/api/task/time-notes/:id

Replaces a task's time notes.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the task must be visible to the caller.
Content type application/json

Path parameters

Name Type Required Description
id string Yes Task id — plain or encrypted.

Request body

Field Type Required Validation / allowed values Description
timeNotes string Yes String New time-notes text (empty string allowed).

Example request

curl -s -X PATCH 'https://api.example.com/api/task/time-notes/10482' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "timeNotes": "Waiting on October bank statement." }'

Success response — 200 OK

{ "error": false, "message": "Time notes updated successfully." }
Field Type Description
error boolean false on success.
message string "Time notes updated successfully."

Errors

Status Condition Message/shape
200 id does not decode, or the task is not visible { "error": true, "message": "Task not found." }
400 timeNotes missing or not a string Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • An Updated audit record is written carrying the before and after Time Notes text.
GET/api/task/:id/step/list

Lists the checklist steps attached to a task, in order.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the task must be visible to the caller.
Content type application/json

Path parameters

Name Type Required Description
id string Yes Task id — plain or encrypted.

Example request

curl -s 'https://api.example.com/api/task/10482/step/list' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "step_list": [
    { "id": 401, "task_id": 10482, "name": "Download statements", "link": "", "order": 1, "done": null }
  ]
}
Field Type Description
step_list array Step rows as stored; empty when the id does not resolve or the task is not visible.

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The step table name is resolved at runtime, so column availability varies by deployment; the exact set of row columns is therefore not fixed.
POST/api/task/step/all

Saves the full set of checklist steps for a task in one call — updating existing rows and inserting new ones.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the task must be visible to the caller.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
task_id string Yes String Task id — plain or encrypted.
all_occurence number No Integer Apply to the whole recurrence family. The field name is spelled exactly as shown and must be sent that way.
allOccurence number No Integer camelCase alias of the same flag.
step object[] Yes Array; each item is validated against the item fields below The complete step list.
step[].step_id number No Integer Existing step id (update).
step[].id number No Integer Alias for step_id.
step[].name string No String Step name.
step[].description string No String Step description.
step[].link string No String Reference link.
step[].order number No Integer Display order; defaults to the array index + 1.
step[].done string | null No String Completion marker.

Example request

curl -s -X POST 'https://api.example.com/api/task/step/all' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{
        "task_id": "b2FkOTk...",
        "allOccurence": 0,
        "step": [
          { "step_id": 401, "name": "Download statements", "order": 1 },
          { "name": "Match transactions", "order": 2 }
        ]
      }'

Success response — 200 OK

{ "error": false, "message": "Steps saved successfully." }
Field Type Description
error boolean Always false on a reachable task.
message string "Steps saved successfully.", or "No steps were saved." when nothing was persisted.

Errors

Status Condition Message/shape
200 task_id does not decode, or the task is not visible { "error": true, "message": "Task not found." }
400 Missing task_id / step Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Saving zero steps is valid and returns success; a task may have no checklist.
  • The task's updated_at is touched on every call.
DELETE/api/task/step/:id

Deletes one checklist step.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the step's owning task must be visible to the caller.
Content type application/json

Path parameters

Name Type Required Description
id string Yes Step id. Coerced to a number by this endpoint — a non-numeric value is rejected as "Step not found."

Example request

curl -s -X DELETE 'https://api.example.com/api/task/step/401' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{ "error": false, "message": "Step deleted successfully." }
Field Type Description
error boolean false on success.
message string "Step deleted successfully."

Errors

Status Condition Message/shape
200 Missing/zero id, step row not found, or its task is not visible { "error": true, "message": "Step not found." }
401 No authenticated session { "message": "These credentials do not match our records." }
GET/api/task/:id/resource/list

Lists the reference links (resources) attached to a task.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the task must be visible to the caller.
Content type application/json

Path parameters

Name Type Required Description
id string Yes Task id — plain or encrypted.

Example request

curl -s 'https://api.example.com/api/task/10482/resource/list' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "resource": [
    { "id": 55, "task_id": 10482, "name": "Bank portal", "link": "https://bank.example.com", "order": 1 }
  ]
}
Field Type Description
resource array Resource rows as stored; empty when the id does not resolve or the task is not visible.

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }
POST/api/task/resource/all

Saves the full set of resources for a task.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the task must be visible to the caller.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
taskId string Yes String Task id — plain or encrypted.
allOccurence number No Integer Apply to the whole recurrence family.
resource object[] Yes Array; each item is validated against the item fields below The complete resource list.
resource[].resource_id number No Integer Existing resource id (update).
resource[].id number No Integer Alias for resource_id.
resource[].name string No String Display name. Falls back to description when blank.
resource[].description string No String Alternate source for the name.
resource[].link string No String URL. Rows with a blank name or blank link are silently skipped.
resource[].order number No Integer Display order; defaults to the array index + 1.

Example request

curl -s -X POST 'https://api.example.com/api/task/resource/all' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{
        "taskId": "b2FkOTk...",
        "resource": [{ "name": "Bank portal", "link": "https://bank.example.com", "order": 1 }]
      }'

Success response — 200 OK

{}
Field Type Description
(none) On success the endpoint returns an empty object — no error/message envelope.

Errors

Status Condition Message/shape
200 taskId does not decode, or the task is not visible { "error": true, "message": "Task not found." }
400 Missing taskId / resource Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Unlike the step-save endpoint, this endpoint returns no message on success; clients should treat the absence of an error key as success.
DELETE/api/task/resource/:id

Deletes one task resource.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the resource's owning task must be visible to the caller.
Content type application/json

Path parameters

Name Type Required Description
id string Yes Resource id; coerced to a number by this endpoint.

Example request

curl -s -X DELETE 'https://api.example.com/api/task/resource/55' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{ "error": false, "message": "Resource deleted successfully." }
Field Type Description
error boolean false on success.
message string "Resource deleted successfully."

Errors

Status Condition Message/shape
200 Missing/zero id, row not found, or its task is not visible { "error": true, "message": "Resource not found." }
401 No authenticated session { "message": "These credentials do not match our records." }
POST/api/task/list

The main Task List query: filtered, sorted and paginated tasks for the caller's company scope.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Company scope is applied automatically: a Global Master System Administrator sees every firm, a firm user or firm admin sees the whole firm, and a non-firm mapped user sees only their user_company_mapping companies.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
offset number No Integer, minimum value 0; default 0 Row offset.
limit number No Integer, minimum value 1; default 30 Page size.
sort_field string No String; must be one of the display names below or it is ignored Sort column.
sort_order string No One of asc, desc, ASC, DESC, ""; anything other than desc (case-insensitive) sorts ascending Sort direction.
filters object[] No Array; each item is validated against the entry fields below Per-column filters.
filters[].column string Yes (within an entry) String Display column name.
filters[].filter string | null No String Filter value.
filters[].id number No Integer Saved board column id (round-tripped from the board).
filters[].task_view_id number No Integer Owning board id.
filters[].order number No Integer Column order.
includeInactive number No Integer; 1 includes inactive rows Include inactive companies/options.
globalFilter object No Nested object; its fields are validated individually Board-level Role/User filter.
globalFilter.role string No String Role scope.
globalFilter.user string No String User scope.
fromHome boolean No Boolean Marks the call as the Home dashboard variant.
skipTodo boolean No Boolean Exclude ToDo rows.
isPriority string No String High-priority-only scope.
asOn string No String As-of date for the query.
company number No Integer, minimum value 0 Restricts the incomplete/all drill-down to a single company.

Sortable columns (sort_field): Company, Task Name, Company Group, Assigned To, Assigned To Person, Reviewer, Review Person, Progress, Project, Service Line, Status, Due, Priority, Type, Invoice Detail, Invoicing, Tags, Recurrence.

Filterable columns (filters[].column): Company, Company Group, Priority, Project, Service Line, Type, Status, Assigned To, Assigned To Person, Reviewer, Review Person, Invoice Detail, Requires Review.

Example request

curl -s -X POST 'https://api.example.com/api/task/list' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{
        "offset": 0,
        "limit": 30,
        "sort_field": "Due",
        "sort_order": "asc",
        "filters": [{ "column": "Status", "filter": "In Progress" }],
        "includeInactive": 0
      }'

Success response — 200 OK — an array of task rows (not an envelope).

[
  {
    "id": 10482,
    "company_id": 412,
    "assigned_name": 88,
    "company_group_id": 3,
    "Company Group": "West Region",
    "Task Name": "Reconcile bank accounts",
    "Due": "2026-10-15 00:00:00",
    "Estimate Time": "2:00",
    "isTodo": 0,
    "Company": "Acme Holdings",
    "company_deleted": 0,
    "Project": "Monthly Close",
    "Service Line": "Bookkeeping",
    "Invoice Detail": "Standard",
    "Priority": "High",
    "Type": "Recurring",
    "Status": "In Progress",
    "Invoicing": "Hourly",
    "Tags": "Urgent",
    "Assigned To": "Accountant",
    "Assigned To Person": "Doe, Jane",
    "Reviewer": null,
    "Review Person": null,
    "Requires Review": "No",
    "Progress": "Open",
    "Recurrence": "Monthly",
    "Recurrence Rule": "On the 1st of every month",
    "Next Recurrence": "2026-11-01",
    "haveAccess": true,
    "getComment": [],
    "enc_id": "b2FkOTk...",
    "steps": [],
    "time_history_count": 1,
    "time_history": [],
    "task_comments": []
  }
]
Field Type Description
Display-name keys (Task Name, Company, Due, …) string | null Human-readable column values, keyed exactly as the grid displays them.
id, company_id, assigned_name, company_group_id number Raw identifiers.
enc_id string Encrypted task id, used by the detail/clone/delete routes.
Progress string Completed / Deferred / Skipped / Overdue / Open (or a title-cased raw status).
Recurrence, Recurrence Rule, Next Recurrence string Derived recurrence display values.
haveAccess boolean Whether the caller has task access to this row's company (true for unrestricted scopes).
steps, time_history, task_comments, getComment array Eager-loaded children. time_history_count is the row count.

Errors

Status Condition Message/shape
400 Request validation failure (e.g. sort_order outside the allowed set) Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • No total-count field is returned; paging is driven purely by offset/limit.
  • Unknown sort_field or filters[].column values are ignored rather than rejected — the maps above are the authoritative allow-lists.
  • The company field applies only to the incomplete/all company drill-downs.
POST/api/task/list/export-excel

Exports the full filtered Task List as an Excel workbook, returned as a base64 data: URI.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the same company scope as POST /api/task/list applies.
Content type application/json (the workbook is embedded as a base64 data URI, not streamed)

Request body (extends the POST /api/task/list request body)

Field Type Required Validation / allowed values Description
(all task-list fields) No See POST /api/task/list Same filters, sort and global scope as the on-screen list.
columns string[] No Array; each item must be a string Visible column set to emit, by display name.
fileName string No String Requested file name.
viewType string No String View type label from the export modal.
fromDue string No String From-due-date bound.
toDue string No String To-due-date bound.

Example request

curl -s -X POST 'https://api.example.com/api/task/list/export-excel' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{
        "filters": [{ "column": "Status", "filter": "In Progress" }],
        "columns": ["Task Name", "Company", "Due", "Progress"],
        "fileName": "October tasks",
        "fromDue": "2026-10-01",
        "toDue": "2026-10-31"
      }'

Success response — 200 OK

{
  "error": false,
  "redirect": "data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,UEsDBBQ...",
  "exportName": "October tasks.xlsx",
  "total": 148
}
Field Type Description
error boolean false on success.
redirect string data: URI containing the base64 workbook — the client navigates to or downloads this.
exportName string File name to save as.
total number Number of rows exported.

Errors

Status Condition Message/shape
400 Request validation failure Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The export runs over the full filtered dataset, not just the current page — offset/limit from the list request do not cap it.
  • Column widths are auto-sized within a 12–60 character band.
  • The Excel and Google Sheet exports are produced from the same workbook definition, so their contents always match.
POST/api/task/list/export-google-sheet

Builds the same Task List workbook and uploads it straight into a chosen Google Drive folder, where Drive converts it to a native Sheet.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie plus a Google account connected for the calling user
Authorization No additional role requirement beyond an authenticated session. The Drive upload runs against a token minted for this session's user, so a folder id belonging to another account simply fails the write.
Content type application/json

Request body (extends the POST /api/task/list/export-excel request body)

Field Type Required Validation / allowed values Description
(all Excel-export fields) No See POST /api/task/list/export-excel Identical filters and column set.
folderId string Yes String; must be non-blank after trim Drive folder id chosen in the Google Drive Picker.
folderName string No String Display name of the folder, echoed back in the success message.

Example request

curl -s -X POST 'https://api.example.com/api/task/list/export-google-sheet' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{
        "columns": ["Task Name", "Company", "Due"],
        "fileName": "October tasks",
        "folderId": "1AbCdEfGhIjKlMnOpQrStUvWxYz",
        "folderName": "Client Reports"
      }'

Success response — 200 OK

{
  "error": false,
  "file": "1XyZ...",
  "name": "October tasks.xlsx",
  "view_link": "https://docs.google.com/spreadsheets/d/1XyZ.../edit",
  "folderName": "Client Reports",
  "exportName": "October tasks.xlsx",
  "total": 148
}
Field Type Description
error boolean false on success.
file string Drive file id of the created Sheet.
name string File name as stored in Drive.
view_link string Direct link to open the Sheet.
folderName string | null Echo of the picked folder name.
exportName, total string / number Same as the Excel export.

Errors

Status Condition Message/shape
200 Blank folderId { "error": true, "message": "<message stating that no Drive folder was selected>" }
200 Google account not connected / access revoked { "error": true, "needsAuth": true, "message": "<reason>" }
200 Drive API rejected the upload { "error": true, "message": "<Drive error message>" }
200 Any other failure { "error": true, "message": "<message stating that the Google Sheet could not be created>" }
400 folderId missing Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The workbook is built and uploaded in a single server round-trip, so it never passes through the browser.
  • needsAuth: true is the signal for the client to re-open "Configure Google Account" rather than showing a plain error toast.
POST/api/task/multi-task/update

Applies one or more field changes to a set of tasks at once (the Multi-Edit grid action).

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; only tasks visible to the caller's firm are touched.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
ids string[] Yes Array; each item must be a string Task ids — plain or encrypted. Duplicates and unresolvable entries are dropped.
fieldset object[] Yes Array; each item is validated against the entry fields below The changes to apply.
fieldset[].field string Yes String; see the allowed list below Field name. Unrecognised names are silently ignored.
fieldset[].value any No No type validation New value.
allOccurrences string No String Apply to the whole recurrence family. The field name is spelled exactly as shown, with a double r, unlike the step and resource endpoints, and must be sent that way.

Allowed fieldset[].field values: Task Name, Role, User, Requires Reviewer, Reviewer Role, Reviewer User, Priority, Due, Invoice Detail, Invoicing, Estimated Time, Service Line, Type, Tags.

Example request

curl -s -X POST 'https://api.example.com/api/task/multi-task/update' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{
        "ids": ["b2FkOTk...", "Y2xvbmU..."],
        "fieldset": [
          { "field": "Priority", "value": 3 },
          { "field": "Due", "value": "2026-11-30" }
        ]
      }'

Success response — 200 OK

{ "error": false, "message": "Task Updated successfully", "mismatchServiceLine": [] }
Field Type Description
error boolean false on success.
message string "Task Updated successfully".
mismatchServiceLine array Populated when a Type change conflicts with a task's project service line — each entry is { name, project, serviceLine }.

Errors

Status Condition Message/shape
200 No usable ids, or none of them resolve to visible tasks { "message": "Task Not Found" } (note: no error key in this response shape)
400 Missing ids / fieldset Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Invoicing accepts either an id (1/2) or a label containing "hourly"/"fixed"; it falls back to 1 when neither resolves.
  • Tags accepts an array or a comma-separated string, of ids or { id }/{ value } objects.
  • If a Role is set without a User (or a Reviewer Role without a Reviewer User), the matching users are auto-populated server-side from the company's service-role assignments.
  • Each affected task gets its own Updated audit record with a before/after diff.
POST/api/task/multi-task/delete

Soft-deletes a set of tasks.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; only tasks visible to the caller are deleted.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
ids string[] Yes Array; each item must be a string Task ids — plain or encrypted.

Example request

curl -s -X POST 'https://api.example.com/api/task/multi-task/delete' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "ids": ["b2FkOTk...", "10483"] }'

Success response — 200 OK

{ "error": false, "message": "Task Deleted Successfully" }
Field Type Description
error boolean false on success.
message string "Task Deleted Successfully".

Errors

Status Condition Message/shape
200 No usable ids, or none resolve to visible tasks { "message": "Task Not Found" }
400 ids missing Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • A Deleted audit record carrying a pre-delete snapshot is written for each task.
DELETE/api/task/:id

Deletes a single task.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; same visibility rules as the multi-delete.
Content type application/json

Path parameters

Name Type Required Description
id string Yes Task id — plain or encrypted.

Example request

curl -s -X DELETE 'https://api.example.com/api/task/10482' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{ "error": false, "message": "Task Deleted Successfully" }
Field Type Description
error boolean false on success.
message string "Task Deleted Successfully".

Errors

Status Condition Message/shape
200 Task not found or not visible { "message": "Task Not Found" }
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Compatibility route: retained for compatibility with earlier clients. The canonical route is POST /api/task/multi-task/delete; this endpoint applies exactly the same processing to the single id, including the Deleted audit write, and returns an identical response shape.
POST/api/task/multi-task/skip

Marks a set of tasks as skipped.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; only visible tasks are affected.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
ids string[] Yes Array; each item must be a string Task ids — plain or encrypted.

Example request

curl -s -X POST 'https://api.example.com/api/task/multi-task/skip' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "ids": ["b2FkOTk..."] }'

Success response — 200 OK

{ "error": false, "message": "Task Skipped Successfully" }
Field Type Description
error boolean false on success.
message string "Task Skipped Successfully".

Errors

Status Condition Message/shape
200 No usable ids, or none resolve to visible tasks { "message": "Task Not Found" }
400 ids missing Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • disposition_status is set to the upper-case literal SKIPPED, while the audit record stores New: { "Disposition Status": "Skipped" }. Clients must handle both spellings.
POST/api/task/home/list

Returns the Home dashboard's saved view definition together with the task list rendered through it.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization Enforced server-side: a firmId that differs from the caller's own firm throws 401.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmId number No Integer; must equal the caller's firm Firm to load the home view for. Defaults to the caller's firm.

Example request

curl -s -X POST 'https://api.example.com/api/task/home/list' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "firmId": 45 }'

Success response — 200 OK

{
  "homeView": {
    "id": 12,
    "name": "Home View",
    "priority": null,
    "pinned": 0,
    "firm_id": 45,
    "user_id": null,
    "default": 0,
    "sort_field": "Due",
    "sort_order": "asc",
    "global_role": null,
    "global_user": null,
    "is_home_view": 1,
    "order": 1,
    "columns": [
      { "id": 301, "task_view_id": 12, "column": "Task Name", "order": 1, "filter": null }
    ]
  },
  "taskList": []
}
Field Type Description
homeView object The home board row plus its columns. When no home board exists a synthetic placeholder named "Home View" with id: 0 is returned.
taskList array Task rows in the same shape as POST /api/task/list, produced with fromHome: true and skipTodo: false.

Errors

Status Condition Message/shape
400 Non-integer firmId Validation error array
401 No session, or firmId differs from the caller's firm { "message": "These credentials do not match our records." }
GET/api/task/getOpenTask/list

Returns the caller's currently open tasks with their captured time, for the timer/open-task widget.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; scoped to the caller's firm.
Content type application/json

Example request

curl -s 'https://api.example.com/api/task/getOpenTask/list' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK — an array.

[
  {
    "id": 10482,
    "task_id": 10482,
    "name": "Reconcile bank accounts",
    "enc_id": "b2FkOTk...",
    "companyName": "Acme Holdings",
    "capture_time": 3600,
    "lastLogged": "2026-10-01 09:00:00",
    "disposition_status": ""
  }
]
Field Type Description
id / task_id number The task id, duplicated for compatibility with earlier clients.
enc_id string Encrypted task id.
capture_time number Total captured seconds.
lastLogged string | null Formatted timestamp of the most recent time entry.
disposition_status string Raw stored status, "" when unset.

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Returns [] when the tasks or company table is unavailable, rather than erroring.
POST/api/task/audit/list

The Task Audit Log: paginated, filtered audit rows, optionally accompanied by the dropdown option sets the filter bar needs.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Company scope mirrors the task list: global admin → all firms; firm user/admin → whole firm; mapped user → mapped companies only.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
offset number No Integer, minimum value 0; default 0 Row offset.
limit number No Integer, minimum value 1; default 30 Page size.
sort_field string No String; one of Date, Time, User, Client, Name, Due Date, Action — anything else falls back to audit-created-at Sort column.
sort_order string No One of asc, desc, ASC, DESC, ""; defaults to DESC (only an explicit asc sorts ascending) Sort direction.
filters object[] No Array; each item is validated against the entry fields below Per-column filters.
filters[].column string Yes (within an entry) String; one of Date, User, Client, Name, Due Date, Action, Original Value, New Value Filter column.
filters[].filter string | null No String; blank values are skipped Filter value.
includeInactiveOptions number No Integer; 1 includes inactive users/companies Include inactive rows and dropdown entries.
taskId number No Integer Restrict to one task's audit trail.
fetchDropDown boolean No Boolean When true, wrap the list in an envelope that also carries the filter dropdown data.

Filter semantics: Date accepts a preset name, a Custom JSON payload ({"preset":"Custom","start":"YYYY-MM-DD","end":"YYYY-MM-DD"}), or a single date. User and Client accept a numeric id (preferred) or a display name. Name and Original Value / New Value are LIKE substring matches. Action is an exact match.

Example request

curl -s -X POST 'https://api.example.com/api/task/audit/list' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{
        "offset": 0,
        "limit": 30,
        "sort_field": "Date",
        "sort_order": "desc",
        "filters": [{ "column": "Action", "filter": "Updated" }],
        "fetchDropDown": true
      }'

Success response — 200 OK

With fetchDropDown: true:

{
  "dropDownData": {
    "users": [{ "id": 88, "name": "Doe, Jane", "deleted": 0 }],
    "clients": [{ "id": 412, "name": "Acme Holdings", "deleted": 0 }],
    "actions": ["Created", "Updated", "Deleted"]
  },
  "listData": [
    {
      "id": 55210,
      "task_id": 10482,
      "audit_date": "10-01-2026",
      "audit_time": "09:14 AM",
      "user": "Doe, Jane",
      "user_deleted": 0,
      "client": "Acme Holdings",
      "name": "Reconcile bank accounts",
      "due_date": "10-15-2026",
      "action": "Updated",
      "original_data": { "Status": "Open" },
      "new_data": { "Status": "In Progress" }
    }
  ]
}

Without fetchDropDown, the response is the bare listData array.

Field Type Description
listData[].audit_date / audit_time string Pre-formatted MM-DD-YYYY and hh:mm AM/PM.
listData[].original_data / new_data object | null Normalised key/value diff; a non-JSON string is wrapped as { "value": "..." }.
dropDownData.actions string[] Always exactly ["Created","Updated","Deleted"].

Errors

Status Condition Message/shape
400 Request validation failure Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The response shape changes with fetchDropDown: an envelope when true, a plain array when absent/false. Clients must branch on the flag they sent.
  • The Action dropdown is fixed to Created/Updated/Deleted. Audit rows with other actions (Completed, Deferred, …) exist and are returned by the list, but are not filterable.
  • With includeInactiveOptions off, the main list is gated on users.active = 1 and company.deleted = 0; the dropdown additionally folds users.deleted_at into its deleted flag.
GET/api/task/company/resource/:companyId

Lists the Client Info company resources (shared reference links) for a company.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization Enforced server-side: the caller must be able to view the company's Client Info resources; otherwise 403.
Content type application/json

Path parameters

Name Type Required Description
companyId string Yes Company id; coerced to a number by this endpoint.

Example request

curl -s 'https://api.example.com/api/task/company/resource/412' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "companyResource": [
    {
      "id": 88,
      "cr_id": 88,
      "name": "Client portal",
      "description": "Client portal",
      "link": "https://portal.example.com",
      "order": 1
    }
  ]
}
Field Type Description
companyResource[].id / cr_id number The same id under both keys (earlier clients read cr_id).
companyResource[].name / description string The same text under both keys.
companyResource[].link string URL.
companyResource[].order number Display order.

Errors

Status Condition Message/shape
403 Caller lacks Client Info resource access for the company { "message": "You do not have permission to access client info resources for this company." }
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Returns { "companyResource": [] } — without running the permission check — when companyId is falsy or the company_resources table is absent.
  • Rows are filtered on both the deleted flag and the deleted_at column, so a row marked deleted by either is excluded.
POST/api/task/company/resource/all

Saves the full set of Client Info company resources for a company.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization Enforced server-side: 403 when the caller cannot modify the company's Client Info, and 403 again when the company is inactive (Client Info is read-only for inactive companies).
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Integer Company the resources belong to.
companyResource object[] Yes Array; elements have the shape below but are not individually validated Complete resource list.
companyResource[].cr_id number | null No — Existing row id; absent/0 inserts a new row.
companyResource[].name string No — Display name.
companyResource[].link string No — URL; must be a valid link.
companyResource[].order number No — Display order.

Example request

curl -s -X POST 'https://api.example.com/api/task/company/resource/all' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{
        "companyId": 412,
        "companyResource": [
          { "cr_id": 88, "name": "Client portal", "link": "https://portal.example.com", "order": 1 },
          { "name": "Shared drive", "link": "https://drive.example.com", "order": 2 }
        ]
      }'

Success response — 200 OK

{ "error": false }
Field Type Description
error boolean false on success. No message is returned.

Errors

Status Condition Message/shape
200 One invalid link { "error": true, "message": "Please enter a valid Link for \"<name>\"." }
200 Several invalid links { "error": true, "message": "Please enter a valid Link for: <name>, <name>." }
200 The company_resources table is absent { "error": true, "message": "Company resources table not found." }
400 Missing companyId / companyResource; company row missing Validation error array, or { "message": "Company not found." }
403 No edit permission, or the company is inactive { "message": "You do not have permission to modify client info for this company." } / { "message": "Inactive companies are read-only in Client Info." }
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Rows where both name and link are blank are skipped entirely — neither validated nor persisted.
  • Updating an existing row also clears its deleted flag where that column exists (an undelete side effect).
DELETE/api/task/company/resource/:id

Deletes one Client Info company resource.

Module Tasks & Time > Tasks
Authentication Required — authenticated session cookie
Authorization Enforced server-side: the caller must be able to modify Client Info for the resource's owning company.
Content type application/json

Path parameters

Name Type Required Description
id string Yes Resource id; coerced to a number by this endpoint.

Example request

curl -s -X DELETE 'https://api.example.com/api/task/company/resource/88' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{ "error": false }
Field Type Description
error boolean false on success.

Errors

Status Condition Message/shape
200 Missing/zero id { "error": true, "message": "Invalid resource id." }
200 The row's company cannot be resolved { "error": true, "message": "Resource not found." }
200 The company_resources table is absent { "error": false } (treated as a no-op success)
403 No edit permission, or the company is inactive Forbidden message as above
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Soft-deletes by setting deleted = 1 (plus updated_at / deleted_at when those columns exist). Only when the table has no deleted column does it fall back to a hard DELETE.

11.2 Task Board

Task Boards ("task views") are saved column/filter/sort configurations over the task list. A board is either personal (owned by one user) or firm-wide; a firm-wide board stored with no firm id is a global board and only a Global Master System Administrator may create, edit or remove it. Per-user state (default board, pinned board) is stored separately from the board itself, so two users can pin and default different boards from the same shared set. This subsection comprises 6 routes.

POST/api/task/board/view

Returns the boards visible to the caller, optionally together with the task list rendered through the default board.

Module Tasks & Time > Task Board
Authentication Required — authenticated session cookie
Authorization Enforced server-side: requesting a firmId other than the caller's own throws 401 unless the caller is a global admin. A firm-less Global Master System Administrator is pinned to firm 0 (the global board set).
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmId number No Integer; defaults to the caller's firm Firm whose boards to load. 0 selects the global board set.
isList boolean No Boolean When true, also run the default board's filters and return the resulting task list.
shouldFirm boolean No Boolean Forces firm scoping: a zero/absent firmId falls back to the caller's own firm instead of the global set.

Example request

curl -s -X POST 'https://api.example.com/api/task/board/view' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "firmId": 45, "isList": true }'

Success response — 200 OK

With isList: true:

{
  "taskList": [],
  "data": [
    {
      "id": 12,
      "name": "Full View",
      "priority": null,
      "pinned": 1,
      "firm_id": 45,
      "user_id": null,
      "default": 1,
      "sort_field": "Due",
      "sort_order": "asc",
      "global_role": null,
      "global_user": null,
      "is_home_view": 0,
      "order": 1,
      "type": "firm",
      "columns": [
        { "id": 301, "task_view_id": 12, "column": "Task Name", "order": 1, "filter": null }
      ]
    }
  ],
  "globalFirm": false
}

Without isList, the response is the bare boards array (the data value above).

Field Type Description
data[].type string "personal" when the board has a user_id, otherwise "firm".
data[].pinned number The caller's pin state from task_pinned_boards, falling back to the board's own value.
data[].default number 1 on exactly one board: the caller's task_board_default, else the board flagged default = 1, else the first board.
data[].columns[].filter string | null Saved filter, with inactive-option tokens stripped.
taskList array Only when isList: true — task rows in the POST /api/task/list shape, run with limit: 30, offset: 0 and the default board's filters, sort and global Role/User filter.
globalFirm boolean true when the caller was steered to the global board set because they are not a global (or, for a firm-scoped request, firm) admin.

Errors

Status Condition Message/shape
401 No session, or a non-global-admin requested another firm's boards { "message": "These credentials do not match our records." }
400 Request validation failure Validation error array

Notes

  • The response shape changes with isList: an envelope when true, a plain array otherwise.
  • With isList: true but no boards at all, the response is { "taskList": [], "data": [], "globalFirm": <bool> }.
  • Calling this endpoint has a side effect: the firm's built-in system boards are seeded if they do not yet exist (skipped for firm 0).
  • Default resolution uses no name-based heuristic and never auto-creates a personal default.
POST/api/task/board/addEditBoard

Creates a new board or replaces an existing one, including its full column list.

Module Tasks & Time > Task Board
Authentication Required — authenticated session cookie
Authorization Enforced server-side: writing a global board (no firm id and a non-personal type) requires a global admin, otherwise 401.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number No Integer Board id to update. Absent or 0 creates a new board.
name string Yes String Board name; must be unique within its scope.
firmId number Yes Integer; a value <= 0 is stored as NULL (global) Owning firm.
type string Yes One of personal, firm, global Board scope. 'global' is normalised to 'firm' before storage; globality is expressed by a null firm id.
columns object[] Yes Array; each item is validated against the entry fields below Ordered column list.
columns[].name string Yes String Display column name.
columns[].filter string | null No String Saved filter for that column.
sortField string | null No String Default sort column.
sortOrder string | null No String Default sort direction.
globalRole string | null No String Board-level Role global filter.
globalUser string | null No String Board-level User global filter.
pinned number No Integer Accepted by the request body but not persisted here — use POST /api/task/board/updatePin.

Example request

curl -s -X POST 'https://api.example.com/api/task/board/addEditBoard' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{
        "name": "Overdue work",
        "firmId": 45,
        "type": "personal",
        "sortField": "Due",
        "sortOrder": "asc",
        "columns": [
          { "name": "Task Name" },
          { "name": "Company" },
          { "name": "Progress", "filter": "Overdue" }
        ]
      }'

Success response — 200 OK

{
  "error": false,
  "message": "Task view added successfully.",
  "board": {
    "id": 19,
    "name": "Overdue work",
    "firm_id": 45,
    "user_id": 88,
    "sort_field": "Due",
    "sort_order": "asc",
    "global_role": null,
    "global_user": null,
    "pinned": 0,
    "type": "personal",
    "columns": [{ "id": 512, "task_view_id": 19, "column": "Task Name", "order": 1, "filter": null }]
  },
  "data": {
    "id": 19,
    "name": "Overdue work",
    "global_role": null,
    "global_user": null,
    "pinned": 0,
    "table": { "columns": [{ "name": "Task Name" }] }
  }
}
Field Type Description
error boolean false on success.
message string "Task view added successfully." or "Task view updated successfully."
board object The stored board row plus its persisted columns and derived type.
data object A trimmed echo, retained for compatibility with earlier clients, carrying the submitted columns under table.columns.

Errors

Status Condition Message/shape
200 A board of the same name already exists in the same scope { "error": true, "message": "<name> already exist in the Task View." }
200 Insert produced no id, or the board could not be re-read { "error": true, "message": "Something went wrong." }
400 Missing/invalid name, firmId, type or columns Validation error array
401 No session, or a non-global admin attempted a global board { "message": "These credentials do not match our records." }

Notes

  • Updating a board replaces its columns wholesale: all existing task_column rows for the board are soft-deleted (deleted_at stamped) and the submitted list is re-inserted with fresh ids.
  • A personal board stores the caller's user_id; a firm board stores user_id = NULL.
  • New boards receive order = MAX(order) + 1 computed across all non-deleted boards, pinned = 0, default = 0 and is_home_view = 0.
  • Uniqueness is scope-aware: a firm board collides only with same-named firm boards for that firm; a personal board only with the caller's own.
POST/api/task/board/removeBoard

Soft-deletes a board and its columns.

Module Tasks & Time > Task Board
Authentication Required — authenticated session cookie
Authorization Enforced server-side: removing a global board (null/zero firm id and no owning user) requires a global admin, otherwise 401.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number Yes Integer Board id.
index number No Integer Client-side list index; not used by this endpoint.
colName string No String Not used by this endpoint.
firm_id number No Integer Not used by this endpoint.

Example request

curl -s -X POST 'https://api.example.com/api/task/board/removeBoard' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "id": 19 }'

Success response — 200 OK

{ "error": false, "message": "Task view deleted successfully." }
Field Type Description
error boolean false on success.
message string "Task view deleted successfully."

Errors

Status Condition Message/shape
200 Board id does not resolve { "error": true, "message": "Task view not found." }
400 Missing/non-integer id Validation error array
401 No session, or a non-global admin attempted to remove a global board { "message": "These credentials do not match our records." }

Notes

  • Both task_board and its task_column rows are stamped with the same deleted_at; nothing is hard-deleted.
  • index, colName and firm_id belong to the shared board-id request body used by other board routes and are ignored here.
POST/api/task/board/default

Sets — or clears — the caller's personal default board.

Module Tasks & Time > Task Board
Authentication Required — authenticated session cookie
Authorization No additional role requirement — the default is stored per user in task_board_default, so it never affects other users.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number Yes Integer; a value <= 0 clears the caller's default Board to make default.
index, colName, firm_id number/string No Integer / String Not used by this endpoint.

Example request

curl -s -X POST 'https://api.example.com/api/task/board/default' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "id": 19 }'

Success response — 200 OK

{ "error": false, "message": "Task board set as defaulted successfully." }
Field Type Description
error boolean false on success.
message string "Task board set as defaulted successfully.", or "Task board default removed successfully." when id <= 0.

Errors

Status Condition Message/shape
200 Board id does not resolve { "error": true, "message": "Task view not found." }
400 id missing or non-integer Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Passing id: 0 (or any non-positive value) deletes the caller's task_board_default row entirely; the "default" then falls back to the board flagged default = 1, or the first board.
  • Exactly one task_board_default row is kept per user — this endpoint updates in place when one already exists.
POST/api/task/board/updatePin

Pins or unpins a board for the calling user.

Module Tasks & Time > Task Board
Authentication Required — authenticated session cookie
Authorization No additional role requirement — pin state is stored per user in task_pinned_boards.
Content type application/json

Request body (extends the shared board-id request body)

Field Type Required Validation / allowed values Description
id number Yes Integer Board id.
pinned number Yes Normalised server-side: true/"true" → 1, false/"false" → 0, any other value is converted to a number (absent becomes 0). Any non-zero value is stored as 1. Pin state.
index, colName, firm_id number/string No Integer / String Not used by this endpoint.

Example request

curl -s -X POST 'https://api.example.com/api/task/board/updatePin' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "id": 19, "pinned": true }'

Success response — 200 OK

{ "error": false, "message": "Pin updated successfully." }
Field Type Description
error boolean false on success.
message string "Pin updated successfully."

Errors

Status Condition Message/shape
200 Board id does not resolve { "error": true, "message": "Task view not found." }
400 Missing id / pinned Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Server-side normalisation means the endpoint accepts both a boolean and a 0/1 value for pinned.
POST/api/task/board/reorderBoards

Re-orders boards to match the submitted sequence.

Module Tasks & Time > Task Board
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Note that ordering is stored on task_board itself, so this affects every user who can see the boards, not just the caller.
Content type application/json

Request body — a bare JSON array of objects with the fields below

Field Type Required Validation / allowed values Description
(root) object[] Yes The request body itself is an array, not an object Boards in their new display order.
[].id number Yes Integer Board id. Entries whose id resolves to 0 are skipped.

Example request

curl -s -X POST 'https://api.example.com/api/task/board/reorderBoards' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '[{ "id": 19 }, { "id": 12 }, { "id": 7 }]'

Success response — 200 OK

{ "error": false, "message": "Task Board reordered successfully." }
Field Type Description
error boolean false on success.
message string "Task Board reordered successfully."

Errors

Status Condition Message/shape
200 Body is not an array, or is an empty array { "error": true, "message": "No boards to reorder." }
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • order is assigned as the 1-based array index, so the submitted list must contain every board the user wants ordered — omitted boards keep their previous order value and may collide.
  • Only non-deleted boards are updated.

11.3 Calendar

The Calendar shows each user's tasks laid out by date and supports drag-and-drop to reschedule, reorder within a day, or reassign to another user. This subsection comprises 2 routes.

POST/api/task/calendar

Returns the calendar grid for a date range: tasks grouped by user and day, plus the user picker and the caller's permission to view other users.

Module Tasks & Time > Calendar
Authentication Required — authenticated session cookie
Authorization Enforced server-side: a firm admin, or a user whose timesheet config has view_others, sees every firm user; a user with authorized sees themselves plus everyone who submits to them as approver; everyone else sees only themselves.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
start string Yes String Range start date (compared as DATE(...), so YYYY-MM-DD).
end string Yes String Range end date, inclusive.
firstTime boolean No Boolean First-load hint from the client.
userId string No String Encrypted user id to display. Defaults to the caller.
calendarFormat string No One of day, week, month Requested view granularity.
isShowAllUsers boolean No Boolean When true, return tasks for every permitted user instead of just the selected one.

Example request

curl -s -X POST 'https://api.example.com/api/task/calendar' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "start": "2026-10-01", "end": "2026-10-31", "calendarFormat": "month", "isShowAllUsers": false }'

Success response — 200 OK

{
  "tasks": {
    "dXNlcjg4...": {
      "2026-10-15": [
        {
          "id": 10482,
          "enc_id": "b2FkOTk...",
          "name": "Reconcile bank accounts",
          "company": "Acme Holdings",
          "estimated_time": "2:00",
          "date": "2026-10-15"
        }
      ]
    }
  },
  "userOptions": [
    { "name": "Doe, Jane", "enc_id": "dXNlcjg4...", "deleted": 0, "active": 1 }
  ],
  "allowViewUsers": true,
  "userId": "dXNlcjg4..."
}
Field Type Description
tasks object Two-level map: encrypted assigned-user id → ISO date (YYYY-MM-DD) → array of task cards. Empty object when no users resolve.
tasks[user][date][].enc_id string Encrypted task id, used as the drag payload for POST /api/task/calendar/position.
userOptions[] array Permitted users, each { name, enc_id, deleted, active }. name prefers lf_name.
allowViewUsers boolean true when the caller is a firm admin or has view_others/authorized on their timesheet config — the client shows the user picker only then.
userId string Encrypted id of the calling user, used as the picker's default selection.

Errors

Status Condition Message/shape
400 Missing start / end, or calendarFormat outside the allowed set Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • A task appears on calendar_date when one is set, otherwise on due (COALESCE(calendar_date, due)), and only within the requested range.
  • A task is eligible when it is not deleted, has a due date, is not a ToDo (is_todo = 0), is not a timesheet entry (timesheet_entry = 0), and its disposition_status is not COMPLETED or SKIPPED. The company must belong to the caller's firm.
  • Grouping is keyed by full ISO date (YYYY-MM-DD), so week views that straddle a month boundary cannot collide on the day number.
  • Date values are built from local date components, so a positive server UTC offset cannot roll dates back a day.
POST/api/task/calendar/position

Applies a drag: reschedules a task to a new day, optionally reassigns it to another user, and persists the per-day ordering of the affected columns.

Module Tasks & Time > Calendar
Authentication Required — authenticated session cookie
Authorization Enforced server-side when reassigning: the target user must have task access to the task's company and be an active, non-deleted user. There is no admin bypass for this check.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id string Yes String Encrypted task id being dragged.
from object Yes Nested object; its fields are validated individually The source day.
from.date string Yes String Source date.
from.list object[] No Array; each item is validated against the entry fields below The source day's remaining tasks in order.
to object Yes Nested object; its fields are validated individually The destination day.
to.date string Yes String Destination date; written to tasks.calendar_date.
to.list object[] No Same shape as from.list The destination day's tasks in order.
*.list[].order number Yes Integer Position within the day.
*.list[].id string Yes String Encrypted task id. Entries that fail to decrypt are dropped.
isSame boolean No Boolean true when source and destination are the same day — the from list is then not written again.
userId string No String Encrypted id of the user to reassign the task to. Omit to keep the current assignee.
dropDataArr unknown[] No Array Opaque client state, echoed back only on the noAccess path; never persisted.

Example request

curl -s -X POST 'https://api.example.com/api/task/calendar/position' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{
        "id": "b2FkOTk...",
        "from": { "date": "2026-10-15", "list": [{ "order": 1, "id": "YWJjMTIz..." }] },
        "to":   { "date": "2026-10-16", "list": [{ "order": 1, "id": "b2FkOTk..." }] },
        "isSame": false,
        "userId": "dXNlcjkx..."
      }'

Success response — 200 OK

{ "success": true }

When the target user lacks access to the task's company, the call still returns 200 with a recovery payload:

{
  "noAccess": true,
  "data": {
    "id": "b2FkOTk...",
    "from": { "date": "2026-10-15", "list": [] },
    "to": { "date": "2026-10-16", "list": [] },
    "isSame": false,
    "userId": "dXNlcjkx...",
    "dropDataArr": [],
    "company": { "id": 412, "name": "Acme Holdings" },
    "fromUser": "dXNlcjg4...",
    "toUser": "dXNlcjkx..."
  }
}
Field Type Description
success boolean true when the move was applied.
noAccess boolean Present instead of success when the target user cannot be assigned.
data object Echo of the submitted drag payload plus company, fromUser and toUser, so the client can restore the pre-drag UI and explain the refusal.

Errors

Status Condition Message/shape
200 to.date or from.date missing, id fails to decrypt, or the task row is not found { "success": false, "message": "Something went wrong" }
200 Target user lacks company access { "noAccess": true, "data": { ... } }
400 Missing id / from / to Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Reassignment is skipped entirely when the target user is already the assignee.
  • Ordering is stored in task_calendar as a JSON positions array keyed by (user_id, date), upserted per day.
  • Both the destination and source day orderings are written under the post-move owner, even when the drag crosses users.
  • The from list is only rewritten when isSame is false or absent.

11.4 Projects

Projects group a client's work under a service line and billing arrangement, and carry the monthly budget plus the service-role allocation that drives invoicing. This subsection also covers the Excel import/export round-trip for bulk project setup and the budget configuration endpoints. This subsection comprises 19 routes.

Unless noted otherwise, the create/update/delete/status/import routes require global admin or firm admin rights and then confirm that the target company is reachable: a global admin may target any firm's company; everyone else is restricted to their own firm.

POST/api/task/project/based/data

Returns the service line, billing, invoicing and fixed-fee facts for one project — used to auto-fill the Add/Edit Task form once a project is chosen.

Module Tasks & Time > Projects
Authentication Required — authenticated session cookie
Authorization No admin gate. The project's company must belong to the caller's firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
project_id number Yes Integer Project to inspect.
company_id number No Integer Company to use for the fixed-fee lookup; defaults to the project's own company.

Example request

curl -s -X POST 'https://api.example.com/api/task/project/based/data' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "project_id": 91, "company_id": 412 }'

Success response — 200 OK

{
  "serviceLine": { "id": 4, "name": "Bookkeeping", "firm_id": 45 },
  "billing": { "id": 1, "name": "Standard" },
  "invoicing": { "id": 1, "name": "Hourly" },
  "fixedFee": false
}
Field Type Description
serviceLine object | null The project's service line, or null when unset.
billing object | null The project's invoice detail, or null.
invoicing object { id: 1, name: "Hourly" } or { id: 2, name: "Fixed Price" }.
fixedFee boolean true when at least one invoice_price_list row exists for this company/project pair.

Errors

Status Condition Message/shape
400 Project not found or soft-deleted { "message": "Invalid project." }
400 Missing/non-integer project_id Validation error array
401/403 Project's company is outside the caller's firm Raised by the company firm check
401 No authenticated session { "message": "These credentials do not match our records." }
POST/api/task/project/getProjectData

The Projects grid query: a paginated, filtered project list plus the dropdown option sets the page needs.

Module Tasks & Time > Projects
Authentication Required — authenticated session cookie
Authorization No admin gate — this is the read path. Company scope follows the caller's access: global admin → every firm; firm user/admin → all firm companies; mapped user → only user_company_mapping companies.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
offset number No Integer; default 0 Row offset.
limit number No Integer; default 100 Page size.
filter object No Nested object; its fields are validated individually Column filters.
filter.name string No String Project name — LIKE %value%.
filter.company string No String Company name — LIKE %value%.
filter.serviceLine string No String Service line name — LIKE %value%.
filter.billing string No String Invoice detail — matched as an id (p.billing = Number(value)).
filter.invoicing string No String Invoicing type — matched as an id (p.invoicing = Number(value)).
filter.status string No String Accepted but not applied to the result set.
firm_id number No Integer Accepted but not applied; the caller's own firm is always used.
includeInactive boolean No Boolean Include inactive companies and projects (active_status <> 1).
listOnly boolean No Boolean Return only projectList, skipping the dropdown queries.

Example request

curl -s -X POST 'https://api.example.com/api/task/project/getProjectData' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "offset": 0, "limit": 100, "filter": { "company": "Acme" }, "includeInactive": false }'

Success response — 200 OK

{
  "dropDownValues": {
    "status": [],
    "serviceRole": [{ "id": 12, "role": "Accountant" }],
    "serviceLine": [{ "id": 4, "name": "Bookkeeping", "firm_id": 45 }],
    "billing": [{ "id": 1, "name": "Standard" }],
    "project": [{ "id": 91, "company_id": 412, "name": "Monthly Close", "monthly_budget": 2500, "monthly_minimum": 500 }]
  },
  "projectList": [
    {
      "id": 91,
      "company_id": 412,
      "company_name": "Acme Holdings",
      "name": "Monthly Close",
      "service_line_id": 4,
      "service_line": "Bookkeeping",
      "billing_id": 1,
      "billing": "Standard",
      "status_id": null,
      "status": null,
      "active_status": 1,
      "is_primary_budget": 1,
      "invoicing_id": 1,
      "invoicing": "Hourly",
      "monthly_budget": 2500,
      "monthly_minimum": 500,
      "count": 0,
      "task_id": 3,
      "enc_id": "cHJvajkx...",
      "enc_cid": "Y29tcDQxMg...",
      "primary_budget_present": 1,
      "comments_count": 2
    }
  ]
}
Field Type Description
projectList[].invoicing string "Hourly" when invoicing = 1, otherwise "Fixed Price".
projectList[].count number Number of invoice_price_list rows (fixed-price entries) for the project.
projectList[].task_id number Open task count for the project, despite the name — used to block inactivation.
projectList[].enc_id / enc_cid string Encrypted project and company ids.
projectList[].primary_budget_present number 1 when any project of the same company in this list is the primary budget — powers the "only one primary budget per company" guard.
projectList[].comments_count number Number of per-period comment entries.
projectList[].status / status_id null Always null; the columns are selected as literals.
dropDownValues.status array Always [].
dropDownValues.serviceLine array Either the firm's own service lines or the global set — an exclusive choice driven by firms.use_service_line, never a union.

Errors

Status Condition Message/shape
400 Request validation failure Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • With listOnly: true the response is { "projectList": [...] } only — no dropDownValues.
  • When the caller's scope resolves to zero companies, the response is the full envelope with every array empty.
  • No total-count field is returned; paging is driven by offset/limit. Results are ordered by company name then project name.
PUT/api/task/project

Creates a project.

Module Tasks & Time > Projects
Authentication Required — authenticated session cookie
Authorization Global or firm admin only; the company must be reachable by the caller.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firm_id number Yes Integer; a falsy value is stored as NULL Owning firm.
company_id number Yes Integer Client company.
name string Yes String, must not be empty; trimmed before storage Project name; must be unique per company (case- and whitespace-insensitive).
service_line number Yes Integer Service line id.
billing number Yes Integer Invoice detail id.
invoicing number Yes Integer 1 Hourly, 2 Fixed Price.
monthly_budget number No Numeric (numeric strings are coerced) (no Integer, so decimals are allowed); defaults to 0 Monthly budget amount.
monthly_minimum number No Numeric (numeric strings are coerced); defaults to 0 Monthly minimum amount.
status number No Integer Accepted but not applied on create; new projects are always inserted with active_status = 1.
is_primary_budget number No Integer Marks the project as its company's primary budget.

Example request

curl -s -X PUT 'https://api.example.com/api/task/project' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{
        "firm_id": 45,
        "company_id": 412,
        "name": "Monthly Close",
        "service_line": 4,
        "billing": 1,
        "invoicing": 1,
        "monthly_budget": 2500,
        "monthly_minimum": 500
      }'

Success response — 200 OK

{
  "error": false,
  "message": "Project created successfully.",
  "data": {
    "id": 91,
    "company_id": 412,
    "company_name": "Acme Holdings",
    "name": "Monthly Close",
    "service_line_id": 4,
    "service_line": "Bookkeeping",
    "billing_id": 1,
    "billing": "Standard",
    "active_status": 1,
    "invoicing_id": 1,
    "invoicing": "Hourly",
    "monthly_budget": 2500,
    "monthly_minimum": 500,
    "enc_id": "cHJvajkx...",
    "enc_cid": "Y29tcDQxMg...",
    "count": 0
  }
}
Field Type Description
error boolean false on success.
message string "Project created successfully."
data object | null The created project re-read with its joined labels, plus enc_id, enc_cid and count: 0. null if the re-read finds nothing.

Errors

Status Condition Message/shape
200 A project of the same name already exists for the company { "error": true, "message": "<name> already exist in the Projects." }
400 Company not found (global-admin path) { "message": "Company not found." }
400 Request validation failure Validation error array
401/403 Caller is not a firm/global admin, or the company is outside their firm Raised by the project management access check
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • This endpoint uses PUT, matching the task-create convention.
  • Firm or global admin rights are enforced server-side, not only in the user interface.
PATCH/api/task/project/:id

Updates a project.

Module Tasks & Time > Projects
Authentication Required — authenticated session cookie
Authorization Global or firm admin. The project itself is additionally verified to belong to the caller's firm before any mutation.
Content type application/json

Path parameters

Name Type Required Description
id string Yes Project id; coerced to a number by this endpoint.

Request body (same fields as PUT /api/task/project)

Field Type Required Validation / allowed values Description
(all project-save fields) See PUT /api/task/project status is meaningful here; use POST /api/task/project/active/status for the dedicated activate/inactivate action.

Example request

curl -s -X PATCH 'https://api.example.com/api/task/project/91' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{
        "firm_id": 45,
        "company_id": 412,
        "name": "Monthly Close (US)",
        "service_line": 4,
        "billing": 1,
        "invoicing": 1
      }'

Success response — 200 OK

{
  "error": false,
  "message": "Project Updated Successfully",
  "data": {
    "id": 91,
    "company_id": 412,
    "company_name": "Acme Holdings",
    "name": "Monthly Close (US)",
    "service_line_id": 4,
    "service_line": "Bookkeeping",
    "billing_id": 1,
    "billing": "Standard",
    "active_status": 1,
    "invoicing_id": 1,
    "invoicing": "Hourly",
    "monthly_budget": 2500,
    "monthly_minimum": 500,
    "count": 0,
    "enc_id": "cHJvajkx...",
    "enc_cid": "Y29tcDQxMg..."
  }
}
Field Type Description
error boolean false on success.
message string "Project Updated Successfully".
data object | null The refreshed project row with joined labels and encrypted ids.

Errors

Status Condition Message/shape
200 Project not found, or a same-named project already exists for the company { "error": true, "message": "..." }
400 Company not found (global-admin path) { "message": "Company not found." }
400 Request validation failure Validation error array
401/403 Caller is not a firm/global admin, or the project/company is outside their firm Raised by the project management access and firm checks
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Renaming a project rewrites saved Task List "Project" column filters, so board filters keep matching after the rename.
DELETE/api/task/project/:id

Soft-deletes a project, appending (deleted) to its name.

Module Tasks & Time > Projects
Authentication Required — authenticated session cookie
Authorization Global or firm admin. Non-global admins are additionally restricted to projects whose company belongs to their firm.
Content type application/json

Path parameters

Name Type Required Description
id string Yes Project id; coerced to a number.

Example request

curl -s -X DELETE 'https://api.example.com/api/task/project/91' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{ "error": false, "message": "Project deleted successfully." }
Field Type Description
error boolean false on success.
message string "Project deleted successfully."

Errors

Status Condition Message/shape
200 Project not found, already deleted, or outside the caller's firm { "error": true, "message": "Project not found." }
200 The project is active and still has open tasks { "error": true, "message": "The project was unable to be inactive. This project has N open task(s)." }
401/403 Caller is not a firm/global admin Raised by the project management access check
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The open-task guard counts tasks that are not deleted, not timesheet entries, not email-derived, and whose disposition_status is neither completed nor skipped (or is null with recent activity).
  • The stored name has any existing (inactive) / (deleted) suffix stripped before (deleted) is appended, so repeated operations do not stack suffixes.
  • Saved Task List "Project" filters are re-synced afterwards, as on rename.
POST/api/task/project/active/status

Activates or inactivates a project.

Module Tasks & Time > Projects
Authentication Required — authenticated session cookie
Authorization Global or firm admin; non-global admins are firm-scoped.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number Yes Integer Project id.
active_status number Yes Integer; 1 activates, any other value inactivates New status.

Example request

curl -s -X POST 'https://api.example.com/api/task/project/active/status' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "id": 91, "active_status": 0 }'

Success response — 200 OK

{ "error": false }
Field Type Description
error boolean false on success. No message is returned.

Errors

Status Condition Message/shape
200 Project not found, deleted, or outside the caller's firm { "error": true, "message": "Project not found." }
200 Inactivating an active project that still has open tasks { "error": true, "message": "The project was unable to be inactive. This project has N open task(s)." }
400 Missing id / active_status Validation error array
401/403 Caller is not a firm/global admin Raised by the project management access check
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Inactivating appends (inactive) to the project name; activating strips it. Existing (inactive)/(deleted) suffixes are removed first.
  • Saved Task List "Project" filters are re-synced after the rename.
POST/api/task/project/comment

Returns a project's per-period comments, guaranteeing an entry exists for the current month.

Module Tasks & Time > Projects
Authentication Required — authenticated session cookie
Authorization No admin gate; the project must belong to the caller's firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
projectId number Yes Integer Project id.

Example request

curl -s -X POST 'https://api.example.com/api/task/project/comment' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "projectId": 91 }'

Success response — 200 OK

{
  "error": false,
  "title": "Comments",
  "comments": [
    { "month": "September 2026", "comment": "" },
    { "month": "August 2026", "comment": "Budget raised for year-end." }
  ],
  "periodLabel": "August 2026"
}
Field Type Description
error boolean false on success.
title string Always "Comments".
comments array { month, comment } entries sorted newest first. A blank entry for the current month is inserted when absent.
periodLabel string Month label derived from the project's updated_at.

Errors

Status Condition Message/shape
200 Project not found, deleted, or outside the caller's firm { "error": true, "message": "Project not found." }
400 Missing projectId Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }
POST/api/task/project/comment/save

Saves a project's per-period comments.

Module Tasks & Time > Projects
Authentication Required — authenticated session cookie
Authorization No admin gate; the project must belong to the caller's firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
projectId number Yes Integer Project id.
comments object[] No (preferred) Array; elements shaped { month: string; comment: string } but not individually validated The full edited per-period array.
comment string No String A single comment string, treated as the current month's entry. Used only when comments is absent.

Example request

curl -s -X POST 'https://api.example.com/api/task/project/comment/save' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{
        "projectId": 91,
        "comments": [
          { "month": "September 2026", "comment": "On track." },
          { "month": "August 2026", "comment": "Budget raised for year-end." }
        ]
      }'

Success response — 200 OK

{ "error": false, "message": "Comment saved successfully.", "periodLabel": "September 2026" }
Field Type Description
error boolean false on success.
message string "Comment saved successfully."
periodLabel string Month label for the save timestamp.

Errors

Status Condition Message/shape
200 Project not found or outside the caller's firm { "error": true, "message": "Project not found." }
200 Any submitted period's comment is blank after trimming { "error": true, "message": "Please enter your comment." }
400 Missing projectId Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The whole array is replaced, not merged — clients must send every period they want to keep, including the ones they did not edit.
  • The blank check applies to every submitted period, not just the current month.
  • comment (singular) is retained for compatibility with earlier clients that send a single string; comments is the canonical field.
POST/api/task/project/download/file

Generates the Project Import Excel template and returns a one-time download link.

Module Tasks & Time > Projects
Authentication Required — authenticated session cookie
Authorization No admin gate on this route; a firm_id other than the caller's own is rejected.
Content type application/json (the workbook itself is fetched separately as a binary download)

Request body

Field Type Required Validation / allowed values Description
firm_id number Yes Integer; a non-zero value must equal the caller's resolved firm Firm whose reference data seeds the template.

Example request

curl -s -X POST 'https://api.example.com/api/task/project/download/file' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "firm_id": 45 }'

Success response — 200 OK

{ "redirect": "/api/task/project/download/generated/9f1c2b7e-6d4a-4f2b-9a10-3e5c8d7b1f22" }
Field Type Description
redirect string Relative URL of the generated file; fetch it with GET /api/task/project/download/generated/:token.

Errors

Status Condition Message/shape
400 firm_id is non-zero and differs from the caller's firm { "message": "Invalid firm context." }
400 Missing/non-integer firm_id Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The workbook contains three sheets: Project (the entry sheet), Existing Data (the firm's current projects) and Available Data (the valid Company / Service Line / Invoice Detail / Invoicing values).
  • The Available Data sheet is written transposed — each category is a column header with its values listed downward; the other two sheets use normal orientation.
  • Column widths are set to 23 for columns A–O on every sheet, and row 1 is bolded on all three (centre-aligned only on Project).
  • The generated workbook is held as temporary server-side storage and referenced by a single-use token. The download request must be served by the same application instance that produced the file, so multi-instance deployments must preserve instance affinity for these routes. See Deployment & Operational Considerations.
POST/api/task/project/import/file

Uploads a filled-in Project Import template, validates every row, imports the valid ones and reports the rest.

Module Tasks & Time > Projects
Authentication Required — authenticated session cookie
Authorization A firm_id other than the caller's own is rejected.
Content type multipart/form-data

Request body — multipart form

Field Type Required Validation / allowed values Description
file file Yes Sent as the file part of the multipart request; no size or MIME validation is applied by this endpoint The filled-in .xlsx template.
firm_id number Yes Integer; must equal the caller's firm when non-zero Firm context.

Example request

curl -s -X POST 'https://api.example.com/api/task/project/import/file' \
  -b 'genie.sid=<session-cookie>' \
  -F 'firm_id=45' \
  -F 'file=@"Project Import Template.xlsx"'

Success response — 200 OK

{
  "error": false,
  "header": { "project": "Project", "company": "Company" },
  "excelData": [],
  "defaultVal": { "company": [], "serviceLine": [], "billing": [], "invoicing": [] },
  "errorData": [],
  "message": "Projects are imported successfully",
  "downloadLink": "#"
}

When some rows fail:

{
  "error": true,
  "header": { "project": "Project", "company": "Company" },
  "excelData": [{ "project": "Payroll", "company": 0, "reason": "Invalid Company" }],
  "defaultVal": { "company": [], "serviceLine": [], "billing": [], "invoicing": [] },
  "errorData": [["Payroll", "Invalid Company"]],
  "message": "The following projects are failed to import due to invalid input or missing fields",
  "downloadLink": "/api/task/project/download/generated/<token>"
}
Field Type Description
error boolean true when at least one row failed. Valid rows are still imported.
header object Column key → display header map for rendering the error grid.
excelData array The failed rows. When more than 20 rows failed this is errorData instead of the raw rows.
defaultVal object Reference option sets (company, serviceLine, billing, invoicing) for re-rendering the pickers.
errorData array Row/reason pairs for the generated error workbook.
message string Success or failure summary.
downloadLink string "#" on full success; otherwise a one-time link to the generated error workbook.

Errors

Status Condition Message/shape
400 firm_id is non-zero and differs from the caller's firm { "message": "Invalid firm context." }
400 Missing firm_id Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • This is a partial-success endpoint: valid rows are written immediately even when others fail, and each row is inserted or updated as appropriate. Re-uploading the same file will therefore update, not duplicate, the already-imported rows.
  • The error workbook is made available through the same single-use token mechanism as the template download.
POST/api/task/project/import/save

Saves a set of reviewed/corrected import rows without re-uploading a file.

Module Tasks & Time > Projects
Authentication Required — authenticated session cookie
Authorization Global or firm admin — import is an admin action. A per-row menu-81 company access check runs in addition. A firm_id other than the caller's own is rejected.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firm_id number Yes Integer; must equal the caller's firm when non-zero Firm context.
projects object[] Yes Array; each item is validated against the row fields below Rows to import.
projects[].id number No Integer Existing project id (update rather than insert).
projects[].project string Yes String Project name.
projects[].company number Yes Integer Company id.
projects[].serviceLine number Yes Integer Service line id.
projects[].billing number Yes Integer Invoice detail id.
projects[].invoicing number Yes Integer 1 Hourly, 2 Fixed Price.
projects[].monthly_budget number No Numeric (numeric strings are coerced); must be finite and >= 0 Monthly budget.
projects[].monthly_minimum number No Numeric (numeric strings are coerced); must be finite and >= 0 Monthly minimum.
projects[].reason string No String Failure reason echoed back on rejected rows.

Example request

curl -s -X POST 'https://api.example.com/api/task/project/import/save' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{
        "firm_id": 45,
        "projects": [
          { "project": "Payroll", "company": 412, "serviceLine": 4, "billing": 1, "invoicing": 1, "monthly_budget": 800 }
        ]
      }'

Success response — 200 OK

{ "error": false, "message": "Projects are imported successfully", "projects": [], "errorCollection": [] }
Field Type Description
error boolean true when any row was rejected.
message string Success text, or "Some projects could not be imported. Please fix the highlighted rows."
projects array The rejected rows, each carrying a reason string listing its issues.
errorCollection string[] Flat list of every issue encountered, for a summary banner.

Errors

Status Condition Message/shape
200 One or more rows invalid (bad budget/minimum, or "Don't access with this company" when the caller has no menu-81 access to that company) The failure envelope above
400 firm_id mismatch { "message": "Invalid firm context." }
400 Request validation failure Validation error array
401/403 Caller is not a firm/global admin Raised by the admin access check
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Like the file import, this is a partial-success endpoint: valid rows are persisted even when others are rejected.
GET/api/task/project/download/generated/:token

Streams a previously generated project template or import-error workbook.

Module Tasks & Time > Projects
Authentication Not required — possession of the single-use token is the only credential; this is the only route in this section that does not require a session
Authorization No role requirement; the single-use token is the only credential
Content type binary download (Content-Disposition: attachment)

Path parameters

Name Type Required Description
token string Yes The UUID returned in the redirect / downloadLink field of a generate or import call.

Example request

curl -s -OJ 'https://api.example.com/api/task/project/download/generated/9f1c2b7e-6d4a-4f2b-9a10-3e5c8d7b1f22'

Success response — 200 OK

Binary .xlsx stream, sent as an attachment named Project Import Template.xlsx (or the error workbook's own name).

Response body: binary file download — no JSON body.

Errors

Status Condition Message/shape
404 Token unknown, already consumed, or expired { "error": true, "message": "File not found." }

Notes

  • The token is single-use: it is invalidated before the stream starts, and the temporary server-side file is removed once the download completes.
  • The download must be served by the same application instance that generated the file, so multi-instance deployments must preserve instance affinity for these routes. See Deployment & Operational Considerations.
  • Because the route is unauthenticated, the token should be treated as a bearer credential and not logged or shared.
POST/api/task/project/budget/default

Returns the firm's default budget configuration data: service roles with their default percentages, service lines, and the projects the defaults can be applied to.

Module Tasks & Time > Projects
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; scoped to the caller's firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
serviceRoleUser boolean No Normalised server-side: true, 1 and "1" map to true; everything else maps to false Switches to the service-role-user variant of the payload.
serviceLine number No Integer; 0 means "all service lines" Restrict projects to one service line.

Example request

curl -s -X POST 'https://api.example.com/api/task/project/budget/default' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "serviceLine": 4 }'

Success response — 200 OK

{
  "error": false,
  "dropDownValues": {
    "project": [
      {
        "id": 91,
        "name": "Monthly Close",
        "service_line": 4,
        "company_id": 412,
        "monthly_budget": 2500,
        "monthly_minimum": 500,
        "company_name": "Acme Holdings",
        "project_budget_id": 77,
        "company": { "name": "Acme Holdings" },
        "project_budget": { "id": 77 }
      }
    ],
    "serviceLine": [{ "id": 4, "name": "Bookkeeping" }],
    "serviceLineData": 4,
    "serviceRole": [{ "id": 12, "role": "Accountant", "percentage": "40.00" }]
  }
}
Field Type Description
error boolean false on success.
dropDownValues.project array Active, non-deleted projects for active firm companies, optionally filtered by service line, each annotated with its latest budget allocation.
dropDownValues.serviceLine array Firm service lines.
dropDownValues.serviceLineData number Echo of the requested service line id.
dropDownValues.serviceRole array Firm service roles with their default percentages.
dropDownValues.serviceUserRole object Present instead of the above on the serviceRoleUser: true variant.

Errors

Status Condition Message/shape
400 Request validation failure Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Service roles use the deleted IN (0, 2) convention (0 and 2 are both active states; 1 is deleted and 3 inactive) and are ordered by TRIM(role).
  • The role scope is exclusive to the firm: global roles are not included, because a firm gets its own row as soon as it customises a role and including both would produce duplicates.
POST/api/task/project/budget/servicerole

Returns the Primary Budget grid for one project: its monthly budget/minimum plus the per-service-role price, default percentage, custom percentage and monthly hours.

Module Tasks & Time > Projects
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the project must belong to a company in the caller's firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Integer Company id.
projectId number Yes Integer Project id.

Example request

curl -s -X POST 'https://api.example.com/api/task/project/budget/servicerole' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "companyId": 412, "projectId": 91 }'

Success response — 200 OK

{
  "error": false,
  "primaryBudgetData": {
    "companyId": 412,
    "projectId": 91,
    "budgetMonthlyBudget": "2500.00",
    "budgetMonthlyMinimum": "500.00"
  },
  "overRidePricing": true,
  "isFirmAdmin": true,
  "serviceRole": [
    {
      "id": 12,
      "service_role_id": 12,
      "role": "Accountant",
      "price_per_hour": "145.00",
      "percentage": "40.00",
      "custom_percentage": "35.00",
      "monthly_hours_budget": "6.00"
    }
  ]
}
Field Type Description
error boolean false on success.
primaryBudgetData object The project's monthly budget and minimum, formatted to 2 decimals.
overRidePricing boolean The company's override_price flag — whether Price Per Hour may be edited here.
isFirmAdmin boolean The caller's firm-admin flag. The client enables the Price Per Hour inputs only when both this and overRidePricing are true.
serviceRole[] array One row per firm service role, all money/percentage values as 2-decimal strings.

Errors

Status Condition Message/shape
200 companyId or projectId missing/zero { "error": true, "message": "Company and Project are required." }
200 Project not found for that company in the caller's firm { "error": true, "message": "Project not found." }
400 Missing companyId / projectId Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Once a project has any override rows saved, a role without its own override row reports custom_percentage: "0.00" — it does not silently fall back to the default percentage. This prevents the Custom (%) total inflating past 100.
  • percentage (the Default column) is resolved from the project's own allocation first, falling back to the firm default — not from the Configure Default Budget page's numbers.
POST/api/task/project/budget/override

Returns the per-role override grid (custom percentage and monthly hours) for one project.

Module Tasks & Time > Projects
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; firm-scoped.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Integer Company id.
projectId number Yes Integer Project id.

Example request

curl -s -X POST 'https://api.example.com/api/task/project/budget/override' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "companyId": 412, "projectId": 91 }'

Success response — 200 OK

{
  "error": false,
  "projectBudgetOverride": [
    {
      "serviceRoleId": 12,
      "encId": "cm9sZTEy...",
      "monthlyHoursBudget": "6.00",
      "customPercentage": "35.00",
      "defaultPercentage": "40.00"
    }
  ]
}
Field Type Description
error boolean false on success.
projectBudgetOverride[].encId string Encrypted service-role id.
projectBudgetOverride[].monthlyHoursBudget / customPercentage / defaultPercentage string 2-decimal strings.

Errors

Status Condition Message/shape
200 companyId or projectId missing/zero { "error": true, "message": "Company and Project are required." }
400 Missing companyId / projectId Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The same "any override rows exist ⇒ missing roles read 0.00" rule as the service-role grid applies here.
POST/api/task/project/save/budget

Saves a project's primary monthly budget and its per-service-role allocation, transactionally.

Module Tasks & Time > Projects
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the project must belong to the caller's firm. Price Per Hour rows are only rewritten when the request sets overRidePricing.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
primaryBudgetData object Yes Object; its inner fields have the shapes below but are not individually validated Project identity and budget amounts.
primaryBudgetData.companyId number Yes — Company id; must be non-zero.
primaryBudgetData.projectId number Yes — Project id; must be non-zero.
primaryBudgetData.budgetMonthlyBudget number | string No — Monthly budget.
primaryBudgetData.budgetMonthlyMinimum number | string No — Monthly minimum.
serviceRole object[] Yes Array (element fields not individually validated) Per-role allocation rows.
serviceRole[].id / .service_role_id number No — Service role id (either key is accepted).
serviceRole[].role string No — Role name.
serviceRole[].price_per_hour number | string No — Hourly price; written only when overRidePricing is set.
serviceRole[].percentage / .custom_percentage number | string No — Default and custom percentages.
serviceRole[].monthly_hours_budget number | string No — Monthly hours for the role.
overRidePricing boolean No Normalised server-side: true, 1 and "1" map to true; everything else maps to false Whether to also rewrite the company's service-team Price Per Hour rows.

Example request

curl -s -X POST 'https://api.example.com/api/task/project/save/budget' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{
        "primaryBudgetData": {
          "companyId": 412, "projectId": 91,
          "budgetMonthlyBudget": 2500, "budgetMonthlyMinimum": 500
        },
        "serviceRole": [
          { "service_role_id": 12, "role": "Accountant", "custom_percentage": 35, "monthly_hours_budget": 6, "price_per_hour": 145 }
        ],
        "overRidePricing": true
      }'

Success response — 200 OK

{ "error": false, "message": "Project budget saved successfully." }
Field Type Description
error boolean false on success.
message string "Project budget saved successfully."

Errors

Status Condition Message/shape
200 companyId or projectId missing/zero { "error": true, "message": "Company and Project are required." }
200 Project not found for that company in the caller's firm { "error": true, "message": "Project not found." }
400 Missing primaryBudgetData / serviceRole Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The allocation header and every per-role row are written inside a single transaction, so a mid-loop failure cannot leave some roles updated and others stale.
  • overRidePricing is the company's override_price flag (returned by POST /api/task/project/budget/servicerole). Sending it when the company does not allow price override would rewrite Price Per Hour rows it should not touch, so clients must echo the value the server gave them.
POST/api/task/project/budget/save/servicerole

Saves the firm's default budget allocation, or one of the two per-project override variants — the branch is chosen by type.

Module Tasks & Time > Projects
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; firm-scoped.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
type string Yes One of default, monthlyBudget, serviceTeam default saves the firm default (optionally applying it to many projects); monthlyBudget and serviceTeam take the override-save branch.
serviceRoleData object No Object Map of service-role id → percentage (either a number or { percentage }). Values must be finite and >= 0; anything else becomes 0.00. For type: 'default' the total must be 100 % (±0.005).
isApplied boolean No Normalised server-side: true, 1 and "1" map to true; everything else maps to false Apply the default to projects as well as storing it.
selectedType string No One of all, selected Whether the apply targets all companies or the listed ones.
serviceLine number No Integer; required (non-zero) for type: 'default' Service line the default belongs to.
projectId object[] No Array; elements shaped { company, project } Explicit apply targets.
companyId string No String Company scope for the apply.
overrideProjectId number No Integer Target project for the override branch.
monthly_price_budget number No Numeric (numeric strings are coerced) Override monthly price budget.
budget number No Numeric (numeric strings are coerced) Override budget amount.
allocationData object[] No Array; elements shaped { serviceRoleId, monthlyHoursBudget, customPercentage } Per-role override values.

Example request

curl -s -X POST 'https://api.example.com/api/task/project/budget/save/servicerole' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{
        "type": "default",
        "serviceLine": 4,
        "serviceRoleData": { "12": 40, "15": 60 },
        "isApplied": true,
        "selectedType": "selected",
        "projectId": [{ "company": 412, "project": 91 }]
      }'

Success response — 200 OK

{ "error": false }
Field Type Description
error boolean false on success. No message is returned on the default path.

Errors

Status Condition Message/shape
200 type: 'default' with no serviceLine { "error": true, "message": "Service line is required." }
200 Percentages do not total 100 % { "error": true, "message": "Please Update Default values as 100 %." }
200 isApplied with selectedType: 'all' and a price-per-hour validation failure The validation envelope from POST /api/task/project/budget/validate
200 The default_budget_mapping table lacks an allocation reference column { "error": true, "message": "Budget mapping table is missing allocation reference." }
400 type missing or outside the allowed set Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The route name says "servicerole" but it is the firm default budget save; the per-project primary budget is POST /api/task/project/save/budget.
  • type: 'monthlyBudget' and type: 'serviceTeam' both take the per-project override-save branch.
  • The allocation header upsert and every per-role default_budget_mapping upsert run in one transaction.
POST/api/task/project/budget/validate

Checks that every service role referenced by a budget allocation has a usable Price Per Hour before the allocation is applied.

Module Tasks & Time > Projects
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; firm-scoped.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
serviceRoleData object Yes Object Map of service-role id → percentage, as sent to the save endpoint.
allCompany boolean No Normalised server-side: true, 1 and "1" map to true; everything else maps to false Validate across every company in the firm.
companyId number | number[] No No type restriction is applied, so either a single id or an array is accepted Companies to validate. Ignored when allCompany is true.

Example request

curl -s -X POST 'https://api.example.com/api/task/project/budget/validate' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "serviceRoleData": { "12": 40, "15": 60 }, "companyId": [412, 413] }'

Success response — 200 OK

{ "error": false, "data": [] }
Field Type Description
error boolean true when at least one company/role pair fails the price check.
data array The offending company/role rows; empty on success.

Errors

Status Condition Message/shape
200 Validation failures found { "error": true, "data": [ ... ] }
400 serviceRoleData missing or not an object Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • allCompany: true (or omitting companyId entirely) validates without a company restriction; otherwise only positive ids are used.
  • The same validation runs implicitly inside POST /api/task/project/budget/save/servicerole when isApplied is set with selectedType: 'all'; this endpoint exists so the UI can pre-flight the apply.
POST/api/task/project/budget/error/export

Turns a list of budget-apply error messages into a downloadable CSV.

Module Tasks & Time > Projects
Authentication Required — authenticated session cookie
Authorization No role requirement.
Content type application/json (the CSV is embedded as a data: URI)

Request body

Field Type Required Validation / allowed values Description
export unknown[] Yes Array; elements may be of any type Error lines. A string is used as-is; a nested array is flattened to its first (or first-of-first) element.

Example request

curl -s -X POST 'https://api.example.com/api/task/project/budget/error/export' \
  -b 'genie.sid=<session-cookie>' \
  -H 'Content-Type: application/json' \
  -d '{ "export": ["Acme Holdings - Accountant has no price per hour", "Beta LLC - CA has no price per hour"] }'

Success response — 200 OK

{ "redirect": "data:text/csv;charset=utf-8,Project%20Budget%20Apply%20to%20Clients%20Errors%0A..." }
Field Type Description
redirect string data:text/csv URI, URL-encoded, that the client navigates to in order to download.

Errors

Status Condition Message/shape
400 export missing or not an array Validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The CSV always begins with the header line Project Budget Apply to Clients Errors, followed by one escaped line per entry.
  • No file name is supplied by the API; the browser derives one from the data: URI.

11.5 Task & Time Templates

Path parameters accept either a plain numeric id or the application's encrypted id form, and responses
hand back encrypted ids under encrypt_id / template_id.

POST/api/task/template/groupList

Lists template groups for the Global or Firm Templates tab, with optional service-line / task-type
filtering, plus the firm checkbox state that drives the "Use Firm Task/Project Templates" toggle.

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization Any authenticated user, but a non-global-admin may only request their own firm: firmId/firm_id must equal the caller's firm, else 401
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmId number No Integer (coerced) Target firm. Falls back to firm_id, then the caller's firm.
firm_id number No Integer (coerced) Alternative spelling of firmId; accepted and treated identically.
type number No Integer (coerced) Template kind discriminator (template_groups.type). Applied only when > 0.
fromTemplate boolean No Boolean "Apply template" flow: scopes to the firm's own groups when firms.use_task = 1, otherwise to global groups.
isFirmTemplate boolean No Boolean Firm tab. A global admin sees every firm's groups; anyone else sees only their own firm's.
filter object No Validated nested object Optional post-filter.
filter.serviceLine array No Array of { id?, name? } objects Keeps only groups containing a template whose settings use one of these service lines.
filter.taskType array No Array of { id?, name? } objects Keeps only groups containing a template whose settings use one of these task types.

Example request

curl -X POST 'https://api.example.com/api/task/template/groupList' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"firmId":12,"type":1,"isFirmTemplate":true,"filter":{"serviceLine":[{"id":4}]}}'

Success response — 200 OK

{
  "templateGroups": [
    {
      "id": 31,
      "name": "Monthly Close",
      "type": 1,
      "reference_date": "2026-01-31",
      "reference_note": "Period end",
      "firm_id": 12,
      "is_global": 0,
      "encrypt_id": { "id": "aE9xR2..." }
    }
  ],
  "checkBoxData": {
    "use_task": 1,
    "use_project": 0,
    "use_service_line": 0,
    "task": false,
    "project": true
  }
}
Field Type Description
templateGroups[] array template_groups rows (id, name, type, reference_date, reference_note, firm_id, is_global) sorted by name.
templateGroups[].encrypt_id.id string Encrypted group id to use in subsequent path parameters.
checkBoxData object The firm's firms usage flags.
checkBoxData.task boolean true when the firm has no task groups and no task templates yet (so the checkbox may still trigger the first-time clone).
checkBoxData.project boolean Same, for project groups/templates.

Errors

Status Condition Message/shape
400 Request validation failure validation error message array
401 No/expired session, or non-global-admin requesting another firm { "message": "These credentials do not match our records." }

Notes

  • The fromTemplate branch reads firms.use_task for the resolved firm; if that lookup throws it falls
    back to 0, i.e. the global catalogue.
  • On the Firm tab a Global Master System Administrator sees every firm's groups
    (firm_id IS NOT NULL AND firm_id != 0).
GET/api/task/template/group/included/:id

Returns one template group together with the templates included in it — the "included templates" panel.

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization Non-global-admins may only read a group belonging to their own firm (global groups are readable by all)
Content type application/json

Path parameters

Name Type Required Description
id string Yes Template group id — plain numeric or encrypted form.

Example request

curl 'https://api.example.com/api/task/template/group/included/31' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "templateGroups": {
    "id": 31,
    "name": "Monthly Close",
    "type": 1,
    "reference_date": "2026-01-31",
    "reference_note": "Period end",
    "firm_id": 12,
    "is_global": 0,
    "encrypt_id": { "id": "aE9xR2..." },
    "templates_array": [{ "template_id": 88 }],
    "templates": [{ "id": 88, "name": "Bank Reconciliation" }]
  },
  "templates": [{ "id": 88, "name": "Bank Reconciliation" }]
}
Field Type Description
templateGroups object | null The group, or null when the id cannot be decoded.
templateGroups.templates_array[] array { template_id } pairs, the shape the editor binds to.
templates[] array The same template rows, hoisted to the top level for convenience.

Errors

Status Condition Message/shape
401 No/expired session, or group belongs to another firm and caller is not a global admin { "message": "These credentials do not match our records." }

Notes

  • An unresolvable id is not an error: this endpoint returns { "templateGroups": null, "templates": [] }.
GET/api/task/template/edit/group/:id

Backs the Edit Template Group page: the group plus both sides of the template picker (already selected
and still available).

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization Non-global-admins may only read a group belonging to their own firm
Content type application/json

Path parameters

Name Type Required Description
id string Yes Template group id — plain numeric or encrypted form.

Example request

curl 'https://api.example.com/api/task/template/edit/group/aE9xR2...' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "getGroupData": {
    "id": 31,
    "name": "Monthly Close",
    "type": 1,
    "reference_date": "2026-01-31",
    "reference_note": "Period end",
    "firm_id": 12,
    "encrypt_id": { "id": "aE9xR2..." },
    "templates_array": [{ "template_id": 88 }]
  },
  "selected_template": [{ "id": 88, "name": "Bank Reconciliation" }],
  "available_template": [{ "id": 91, "name": "Payroll Journal" }]
}
Field Type Description
getGroupData object | null The template_groups row plus encrypt_id and templates_array. null when the id is unresolvable or the group does not exist.
selected_template[] array Templates currently mapped to the group.
available_template[] array Templates in scope that are not yet mapped.

Errors

Status Condition Message/shape
401 No/expired session, or cross-firm access without global admin { "message": "These credentials do not match our records." }
GET/api/task/template/group/based/template/:id

Returns a template group and the templates it contains — the read-only "apply this group" view used when
creating tasks from a group.

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; no firm or admin restriction is applied to this read
Content type application/json

Path parameters

Name Type Required Description
id string Yes Template group id — plain numeric or encrypted form.

Example request

curl 'https://api.example.com/api/task/template/group/based/template/31' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "group": {
    "id": 31,
    "name": "Monthly Close",
    "type": 1,
    "reference_date": "2026-01-31",
    "reference_note": "Period end",
    "encrypt_id": { "id": "aE9xR2..." }
  },
  "template": [{ "id": 88, "name": "Bank Reconciliation" }]
}
Field Type Description
group object | null The group row plus encrypt_id; null when not found.
template[] array Templates mapped to the group.

Errors

Status Condition Message/shape
401 No/expired session { "message": "These credentials do not match our records." }

Notes

  • Returns { "group": null, "template": [] } rather than an error when the group cannot be resolved.
PUT/api/task/template/groupList

Creates a template group (or updates one when id is supplied in the body), including its template
membership.

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization Global rows (firm_id 0/NULL): global admin only. Firm rows: that firm's firm admin only (firm_admin = 1 and the row's firm equals the caller's firm)
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number No Integer (coerced) Present ⇒ update. Absent ⇒ create.
firmId number No Integer (coerced) Target firm. Optional: when it is omitted, the group's own firm_id is read from the database and used instead.
name string Yes String Group name; must be unique within the scope.
type number Yes Integer (coerced) Group kind discriminator.
referenceDate string No String Reference date stored on the group.
referenceNote string No String Free-text note.
templates number[] No Array Template ids to map into the group.

Example request

curl -X PUT 'https://api.example.com/api/task/template/groupList' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"name":"Monthly Close","type":1,"firmId":12,"referenceDate":"2026-01-31","templates":[88,91]}'

Success response — 200 OK

{
  "error": false,
  "message": "Template Group created successfully.",
  "template_id": "aE9xR2...",
  "type": 1,
  "firm_id": 12
}
Field Type Description
error boolean false on success.
message string Template Group created successfully. or ... updated successfully.
template_id string Encrypted id of the saved group.
type number Echo of the submitted type.
firm_id number Resolved target firm (0 for a global group).

Errors

Status Condition Message/shape
200 Duplicate name in scope { "error": true, "message": "<name> already exist in the Template Groups." }
400 Missing name or type validation error message array
401 Cross-firm write, non-firm-admin on a firm row, or non-global-admin on a global row { "message": "These credentials do not match our records." }

Notes

  • On update with firmId omitted or 0, the target firm is looked up from the existing
    template_groups.firm_id row before the permission check runs.
  • templates replaces the group's membership.
PATCH/api/task/template/groupList/:id

Updates an existing template group. Identical to the PUT above except the id comes from the path.

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization Global rows: global admin. Firm rows: that firm's firm admin
Content type application/json

Path parameters

Name Type Required Description
id string Yes Template group id. Used as the id of the record to update.

Request body, exactly as for PUT /api/task/template/groupList. Any id in the body is overwritten
by the path parameter.

Example request

curl -X PATCH 'https://api.example.com/api/task/template/groupList/31' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"name":"Monthly Close (Revised)","type":1,"templates":[88]}'

Success response — 200 OK

{
  "error": false,
  "message": "Template Group updated successfully.",
  "template_id": "aE9xR2...",
  "type": 1,
  "firm_id": 12
}

Response fields as for PUT /api/task/template/groupList.

Errors

Status Condition Message/shape
200 Duplicate name in scope { "error": true, "message": "<name> already exist in the Template Groups." }
400 Missing name or type validation error message array
401 Scope/role violation { "message": "These credentials do not match our records." }

Notes

  • The raw path segment is used as the record id without further validation, so a non-numeric path segment
    is passed through as a string.
DELETE/api/task/template/groupList/:id

Soft-deletes a template group.

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization Global groups: global admin only. Firm groups: that firm's firm admin only
Content type application/json

Path parameters

Name Type Required Description
id string Yes Template group id — plain numeric or encrypted form.

Example request

curl -X DELETE 'https://api.example.com/api/task/template/groupList/31' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{ "error": false, "message": "Template Group deleted successfully." }
Field Type Description
error boolean false on success.
message string Confirmation text.

Errors

Status Condition Message/shape
200 Id cannot be decoded { "error": true, "message": "Template Group not found." }
401 Scope/role violation { "message": "These credentials do not match our records." }

Notes

  • Soft delete: deleted_at is stamped; rows are not physically removed.
POST/api/task/template/cloneTemplateGroup

Clones an existing template group under a new name, optionally re-selecting which templates it contains.

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization Determined by the source group's scope: global source ⇒ global admin; firm source ⇒ that firm's firm admin
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number Yes Integer (coerced) Source template group id.
name string Yes String Name for the clone; must not already exist.
reference_date string No String Reference date for the clone.
reference_note string No String Note for the clone.
templates array No Array of { id: number } objects; id is a required integer Templates to map into the clone.

Example request

curl -X POST 'https://api.example.com/api/task/template/cloneTemplateGroup' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"id":31,"name":"Monthly Close FY27","reference_date":"2027-01-31","templates":[{"id":88}]}'

Success response — 200 OK

{
  "error": false,
  "message": "Template Group Cloned successfully.",
  "data": {
    "id": 44,
    "name": "Monthly Close FY27",
    "type": 1,
    "reference_date": "2027-01-31",
    "reference_note": null,
    "encrypt_id": { "id": "bK2mV8..." }
  }
}
Field Type Description
error boolean false on success.
data.id number New group id.
data.type number Inherited from the source group.
data.encrypt_id.id string Encrypted new group id.

Errors

Status Condition Message/shape
200 Source group missing { "error": true, "message": "Template Group not found." }
200 Name collision { "error": true, "group_error": true, "message": "<name> already exist." }
400 Missing id/name validation error message array
401 Scope/role violation on the source group { "message": "These credentials do not match our records." }
POST/api/task/template/templateList

Lists individual templates for the Global or Firm Templates tab, with the same scoping and filtering
semantics as the group list.

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization Non-global-admins may only request their own firm, else 401
Content type application/json

Request body — the identical field set to the group list: firmId, firm_id, type, fromTemplate,
isFirmTemplate, filter.serviceLine, filter.taskType. See POST /api/task/template/groupList for the
full table.

Example request

curl -X POST 'https://api.example.com/api/task/template/templateList' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"firmId":12,"type":1,"isFirmTemplate":true}'

Success response — 200 OK

{
  "templateList": [
    { "id": 88, "name": "Bank Reconciliation", "type": 1, "firm_id": 12 }
  ]
}
Field Type Description
templateList[] array Distinct template rows: id, name, type, firm_id, ordered by name.

Errors

Status Condition Message/shape
400 Request validation failure validation error message array
401 No/expired session, or non-global-admin requesting another firm { "message": "These credentials do not match our records." }

Notes

  • Service-line / task-type filters join template_settings, so a template with no settings row is
    excluded once any filter is supplied.
PUT/api/task/template/templateList

Creates a template (or updates one when id is supplied).

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization Global templates: global admin only. Firm templates: that firm's firm admin only
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number No Integer (coerced) Present ⇒ update.
name string Yes String Template name; unique within scope.
type number Yes Integer (coerced) Template kind discriminator.
firmId number No Integer (coerced) Target firm; omitted/0 means the global catalogue.

Example request

curl -X PUT 'https://api.example.com/api/task/template/templateList' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"name":"Bank Reconciliation","type":1,"firmId":12}'

Success response — 200 OK

{
  "error": false,
  "message": "Template created successfully.",
  "template_id": "cQ7zL4..."
}
Field Type Description
error boolean false on success.
message string Template created successfully. or Template updated successfully.
template_id string Encrypted id of the saved template.

Errors

Status Condition Message/shape
200 Duplicate name in scope { "error": true, "message": "<name> already exist in the Templates." }
400 Missing name or type validation error message array
401 Scope/role violation { "message": "These credentials do not match our records." }

Notes

  • The same save contract also backs the Tasks area's "make task into template" flow.
PATCH/api/task/template/templateList/:id

Updates an existing template; the id comes from the path.

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization Global templates: global admin. Firm templates: that firm's firm admin
Content type application/json

Path parameters

Name Type Required Description
id string Yes Template id, interpreted as a number. A non-numeric value resolves to no id, so the request is treated as a create.

Request body, as for PUT /api/task/template/templateList.

Example request

curl -X PATCH 'https://api.example.com/api/task/template/templateList/88' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"name":"Bank Reconciliation (Monthly)","type":1,"firmId":12}'

Success response — 200 OK

{
  "error": false,
  "message": "Template updated successfully.",
  "template_id": "cQ7zL4..."
}

Response fields as for PUT /api/task/template/templateList.

Errors

Status Condition Message/shape
200 Duplicate name in scope { "error": true, "message": "<name> already exist in the Templates." }
400 Missing name or type validation error message array
401 Scope/role violation { "message": "These credentials do not match our records." }

Notes

  • Only a plain numeric path id maps to an update; an encrypted id in this path creates a new template
    instead.
DELETE/api/task/template/templateList/:id

Soft-deletes a template, its settings and its group mappings — with a two-step confirmation when the
template is still mapped to a group.

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization Global templates: global admin only. Firm templates: that firm's firm admin only
Content type application/json

Path parameters

Name Type Required Description
id string Yes Template id. Plain numeric = first pass (may return the confirmation prompt); encrypted = confirmed delete.

Example request

# First pass
curl -X DELETE 'https://api.example.com/api/task/template/templateList/88' -b 'genie.sid=<session-cookie>'
# Confirmed pass, using the encrypted id returned above
curl -X DELETE 'https://api.example.com/api/task/template/templateList/cQ7zL4...' -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "error": false,
  "message": "Template deleted successfully.",
  "template_id": "cQ7zL4..."
}
Field Type Description
error boolean false on success; true when the confirmation prompt is returned.
message string Confirmation text, or the "mapped with other template groups" prompt.
template_id string Encrypted template id — resend it in the path to confirm.

Errors

Status Condition Message/shape
200 Id cannot be decoded { "error": true, "message": "Template not found." }
200 Template is mapped to a group and the path id was plain numeric { "error": true, "message": "This template has been mapped with other template groups. Do you still want to delete it? By confirming, it will remove from its respective template groups as well.", "template_id": "<encrypted>" }
401 Scope/role violation { "message": "These credentials do not match our records." }

Notes

  • The confirmation prompt is returned only when the template is mapped to at least one group and the
    path id was plain numeric; an encrypted id bypasses the prompt.
  • On confirmation, template, template_settings and template_groups_templates rows are all soft-deleted.
POST/api/task/template/cloneTemplate

Clones a single template (and its settings) under a new name.

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization Determined by the source template's scope: global ⇒ global admin; firm ⇒ that firm's firm admin
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number Yes Integer (coerced) Source template id.
name string Yes String Name for the clone.

Example request

curl -X POST 'https://api.example.com/api/task/template/cloneTemplate' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"id":88,"name":"Bank Reconciliation (Copy)"}'

Success response — 200 OK

{
  "error": false,
  "message": "Template Cloned successfully.",
  "data": { "id": 102, "name": "Bank Reconciliation (Copy)" }
}
Field Type Description
error boolean false on success.
data.id number New template id.
data.name string Echo of the requested name.

Errors

Status Condition Message/shape
200 Source template missing { "error": true, "message": "Template not found." }
200 Name collision { "error": true, "message": "<name> already exist in the Templates." }
400 Missing id/name validation error message array
401 Scope/role violation on the source template { "message": "These credentials do not match our records." }
POST/api/task/template/template-setting

Loads the Template Settings editor: the saved settings for a template plus every dropdown's option set.

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization Non-global-admins may only request their own firm (firmId must equal the caller's firm)
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmId number No Integer (coerced) Target firm; defaults to the caller's firm.
templateId number No Integer (coerced) Template whose settings to load.
sLine number No Integer (coerced) Service line to scope the task-type / task-status option lists to.
fromTemplate boolean No Boolean Signals the "apply template" flow rather than the editor.

Example request

curl -X POST 'https://api.example.com/api/task/template/template-setting' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"firmId":12,"templateId":88,"sLine":4}'

Success response — 200 OK

{
  "defaultValues": {
    "serviceLine": [{ "id": 4, "name": "Bookkeeping" }],
    "taskType": [{ "id": 7, "name": "Recurring" }],
    "priority": [{ "id": 2, "name": "High" }],
    "billing": [{ "id": 1, "name": "Billable" }],
    "tags": [{ "id": 5, "name": "Month End" }],
    "assignRoles": [{ "id": 3, "role": "Bookkeeper" }],
    "serviceRole": [{ "id": 3, "role": "Bookkeeper" }],
    "taskStatus": [{ "id": 9, "name": "In Progress" }]
  },
  "template": { "id": 88, "name": "Bank Reconciliation", "type": 1, "firm_id": 12 },
  "templateSettings": { "template_id": 88, "task_type": 7, "service_line": 4, "priority": 2 },
  "steps": [{ "name": "Download statement", "description": "", "action_link": "" }],
  "stepHaveData": true,
  "recurrence": { "id": 14, "template_id": 88, "cron_string": "0 0 1 * *" }
}
Field Type Description
defaultValues object Option lists for every dropdown in the editor. assignRoles and serviceRole carry the same list under two keys.
template object The template row.
templateSettings object The enriched template_settings row.
steps[] array template_steps rows.
stepHaveData boolean steps.length > 0.
recurrence object | null The template's recurrence row, if any.

Errors

Status Condition Message/shape
400 Request validation failure validation error message array
401 No/expired session, or non-global-admin requesting another firm { "message": "These credentials do not match our records." }

Notes

  • Service-role options are additionally narrowed to either the firm's own role catalogue or the global
    one, according to firms.use_service_role.
  • Option names containing (deleted) or (inactive) are filtered out of the dropdown lists.
PUT/api/task/template/template-setting

Saves the Template Settings editor: the settings row, its steps and its recurrence.

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization Determined by the target template's scope: global template ⇒ global admin; firm template ⇒ that firm's firm admin
Content type application/json

Request body

Field Type Required Validation / allowed values Description
template_id number Yes Integer (coerced) Template these settings belong to.
task_type unknown No Optional (untyped) Task type — id or { id } object.
priority unknown No Optional (untyped) Priority.
assinged_role unknown No Optional (untyped) Assigned role. The field name is spelled exactly as shown and must be sent that way.
requires_review unknown No Optional (untyped) Review-required flag.
service_line unknown No Optional (untyped) Service line.
module unknown No Optional (untyped) Owning module.
billing unknown No Optional (untyped) Billing type.
tags (number|string)[] No Array Tag ids or names.
status unknown No Optional (untyped) Status value.
estimated_time unknown No Optional (untyped) Estimated time.
service_role unknown No Optional (untyped) Service role.
steps array No Array of step objects Step list; each step has optional name, description, action_link (all String) and deleted_at (untyped).
task_status unknown No Optional (untyped) Task status.
relative_due_date unknown No Optional (untyped) Relative due-date rule.
due string No String Due-date expression.
invoicing unknown No Optional (untyped) Invoicing type.
due_time string No String Due time.
recurrence object No Recurrence object See below.

Recurrence object: id? (int), template_id? (int), cron_string? (string), start_on?
(string), end_on? (string), weekend_rule? (string), holiday_rule? (string), holiday_calendar?
(untyped) — all optional.

Example request

curl -X PUT 'https://api.example.com/api/task/template/template-setting' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
        "template_id": 88,
        "service_line": 4,
        "task_type": 7,
        "priority": 2,
        "tags": [5],
        "steps": [{ "name": "Download statement", "description": "From the bank portal" }],
        "recurrence": { "cron_string": "0 0 1 * *", "start_on": "2026-02-01", "weekend_rule": "next" }
      }'

Success response — 200 OK

{
  "error": false,
  "message": "Template Settings created successfully.",
  "template_id": "dR3nY9..."
}
Field Type Description
error boolean false on success.
message string Template Settings created successfully. or ... updated successfully.
template_id string Encrypted id of the saved template_settings row.

Errors

Status Condition Message/shape
200 template_id resolves to nothing { "error": true, "message": "Template is required." }
400 Missing template_id validation error message array
401 Scope/role violation on the target template { "message": "These credentials do not match our records." }

Notes

  • Several fields accept either a scalar id or a { id, name } object, depending on the control that
    produced the value; both forms are normalised.
  • Steps are replaced wholesale; deleted_at on a step marks it removed.
POST/api/task/template/template-setting/service-line/options

Returns the task-type and task-status option lists for one service line — the dependent-dropdown refresh
in the Template Settings editor.

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization The resolved firm must equal the caller's firm, else 401
Content type application/json

Request body

Field Type Required Validation / allowed values Description
serviceLineId number Yes Integer (coerced) Service line whose dependent options to load.
firmId number No Integer (coerced) Defaults to the caller's firm.

Example request

curl -X POST 'https://api.example.com/api/task/template/template-setting/service-line/options' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"serviceLineId":4,"firmId":12}'

Success response — 200 OK

{
  "taskType": [{ "id": 7, "name": "Recurring" }],
  "taskStatus": [{ "id": 9, "name": "In Progress" }]
}
Field Type Description
taskType[] array Task types belonging to the service line.
taskStatus[] array Statuses belonging to the service line.

Errors

Status Condition Message/shape
400 Missing serviceLineId validation error message array
401 Requested firm is not the caller's firm { "message": "These credentials do not match our records." }

Notes

  • A falsy serviceLineId (e.g. 0) returns { "taskType": [], "taskStatus": [] } rather than an error.
POST/api/task/template/removeStep

Soft-deletes one step from a template, addressed by its ordinal position.

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization Global template ⇒ global admin; firm template ⇒ that firm's firm admin
Content type application/json

Request body

Field Type Required Validation / allowed values Description
order number Yes Integer (coerced) The step's order value within the template.
template_id number Yes Integer (coerced) Owning template id.

Example request

curl -X POST 'https://api.example.com/api/task/template/removeStep' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"template_id":88,"order":2}'

Success response — 200 OK

{ "error": false, "message": "Step deleted successfully." }
Field Type Description
error boolean Always false on this path.
message string Confirmation text.

Errors

Status Condition Message/shape
400 Missing order or template_id validation error message array
401 Scope/role violation on the owning template { "message": "These credentials do not match our records." }

Notes

  • Deleting a step whose order does not exist is a no-op that still returns success.
POST/api/task/template/clone

The "Use Firm Task/Project Templates" first-time switch: copies the whole global template catalogue into
the firm, or (when turning the flag off) soft-deletes the firm's cloned set.

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization firm_id must equal the caller's firm and the caller must be a firm admin (firm_admin = 1)
Content type application/json

Request body

Field Type Required Validation / allowed values Description
use_firm number Yes Integer (coerced) 1 = clone global templates into the firm; anything else = remove the firm's cloned set.
type number Yes Integer (coerced) Template kind to clone/remove.
firm_id number Yes Integer (coerced) Target firm; must be the caller's own.

Example request

curl -X POST 'https://api.example.com/api/task/template/clone' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"use_firm":1,"type":1,"firm_id":12}'

Success response — 200 OK

{ "error": false, "message": "Global Templates are cloned successfully." }
Field Type Description
error boolean false on success.
message string Global Templates are cloned successfully. when use_firm = 1, Clone Templates are removed successfully. otherwise.

Errors

Status Condition Message/shape
400 Missing use_firm, type or firm_id validation error message array
401 firm_id is not the caller's firm, or caller is not a firm admin { "message": "These credentials do not match our records." }

Notes

  • This is a destructive toggle: use_firm ≠ 1 soft-deletes the firm's template_groups, template and
    template_settings rows for the given type. That is why it is firm-admin-only, even for a Global
    Master System Administrator without firm_admin.
  • The front end calls this route only when turning the checkbox on for the very first time; every
    other toggle goes to POST /api/task/firm instead.
POST/api/task/template/firm/checkBoxData

Returns the firm's template usage flags and whether the firm already has task/project data — the state the
"Use Firm Templates" checkboxes render from.

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization firm_id must equal the caller's firm, else 401
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firm_id number Yes Integer (coerced) Target firm; must be the caller's own.

Example request

curl -X POST 'https://api.example.com/api/task/template/firm/checkBoxData' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"firm_id":12}'

Success response — 200 OK

{
  "use_task": 1,
  "use_project": 0,
  "use_service_line": 0,
  "use_service_role": 1,
  "use_timesheet": 1,
  "use_service_line_approver": 0,
  "clone_task_config": 1,
  "close_deadline_config": 0,
  "task": false,
  "project": true
}
Field Type Description
use_task, use_project, use_service_line, use_service_role, use_timesheet, use_service_line_approver, clone_task_config, close_deadline_config number The firms row's flags. All default to 0 when no firm row exists.
task boolean true when the firm has no task template data yet (first-time clone still possible).
project boolean true when the firm has no project template data yet.

Errors

Status Condition Message/shape
400 Missing firm_id validation error message array
401 firm_id is not the caller's firm { "message": "These credentials do not match our records." }

Notes

  • The firm_id comparison is strict, against the already-coerced numeric value.
POST/api/task/firm duplicate path

Flips the firm's template usage flags. This path is also documented in the Configure subsection, and that entry is the canonical one — see the note below.

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization firmId must equal the caller's firm and the caller must be a firm admin
Content type application/json

Request body

Field Type Required Validation / allowed values Description
templateId number Yes Integer (coerced) Which flag to write: 1 ⇒ firms.use_task, 2 ⇒ firms.use_project, anything else ⇒ firms.use_service_line.
firmId number Yes Integer (coerced) Target firm; must be the caller's own.
value number Yes Integer (coerced) Truthy ⇒ stored as 1, falsy ⇒ 0.

Example request

curl -X POST 'https://api.example.com/api/task/firm' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"templateId":1,"firmId":12,"value":0}'

Success response — 200 OK

{ "error": false, "message": "Record updated successfully." }
Field Type Description
error boolean false when at least one firms row was updated.
message string Record updated successfully.

Errors

Status Condition Message/shape
200 UPDATE matched no firms row { "error": true, "message": "failed" }
400 Missing templateId, firmId or value validation error message array
401 firmId is not the caller's firm, or caller is not a firm admin { "message": "These credentials do not match our records." }

Notes

  • Duplicate path. The same path is declared in the Configure subsection, and that entry is the one that serves requests to /api/task/firm. The two behave almost identically; the Configure entry additionally accepts cloneDone and resolves the target firms row first. Clients should integrate against the Configure entry and treat this one as documentation of the shared path only.
  • This endpoint exists so that unchecking "Use Firm Task Templates" has a call of its own; the
    destructive /api/task/template/clone route is used only for the first-time enable.
POST/api/task/template/from/group

Creates real tasks from a template group's templates — the "apply template group" action. Takes a raw
JSON array, one element per task to create.

Module Tasks & Time > Task & Time Templates
Authentication Required — authenticated session cookie
Authorization Per company: the company must belong to the caller's firm and the caller must hold ACCESS_STANDARD (3) or ACCESS_ADMIN (4) on TASK_TIME_MENU_ID (81) for that company; global and firm admins bypass the access-level check
Content type application/json (top-level array)

Request body — a JSON array validated item by item. Unknown properties are not stripped from array
items on this route, so every nested field (steps, resources, recurrence, …) reaches the endpoint
intact.

Only two fields per item are validated:

Field Type Required Validation / allowed values Description
company any Yes Must be present — may be a scalar id or a nested { id, name } object Target company.
name string No String Task name.

Because unknown properties are preserved, the following additional keys are also read and applied to
the created task — each accepts a scalar id or a nested { id } object:

Key Description
priority Priority.
assigned Assigned role.
assignedName Assigned user; must have access to company or the request is rejected.
serviceLine Service line.
project Project.
type Task type.
billing Billing type.
estimatedTime Estimated time (string).
requireReview Coerced to a boolean.
assignedReviewer Reviewer role.
assignedReviewerName Reviewer user; must have access to company.
due Due date (string).
status Task status.
tags Tag list.
invoicing Invoicing type; defaults to 1.
timeNotes Time notes (string).
steps Written as the new task's steps.
resources Written as the new task's resources.
recurrence Written as the new task's recurrence.

Example request

curl -X POST 'https://api.example.com/api/task/template/from/group' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '[
        {
          "company": { "id": 501, "name": "Acme Ltd" },
          "name": "Bank Reconciliation",
          "serviceLine": { "id": 4 },
          "type": { "id": 7 },
          "assignedName": { "id": 88 },
          "due": "2026-02-15",
          "invoicing": 1,
          "steps": [{ "name": "Download statement" }]
        }
      ]'

Success response — 200 OK

{ "error": false, "message": "Task created successfully." }
Field Type Description
error boolean false when every task was created.
message string Task created successfully.

Errors

Status Condition Message/shape
200 Empty array { "error": true, "message": "No template task payload found." }
200 Per-row validation problems (missing company, assignee/reviewer without company access) { "error": true, "message": "<msg1><br><msg2>" } — messages joined with <br>
200 Task creation reports a failure The creation result { error: true, message, taskId? } is returned unchanged
400 An array element fails item validation (e.g. no company key at all) validation error message array
401 No session, company outside the caller's firm, or insufficient Tasks & Time access on that company { "message": "These credentials do not match our records." }

Notes

  • Validation runs as a pre-pass over every row before any task is created; if any row fails, nothing is
    created. Creation itself is not transactional across rows — a failure on row n returns immediately,
    leaving rows 1..n-1 created.
  • The assignee and reviewer access checks are performed by this endpoint itself and are not repeated
    during task creation.
  • Task names are stored exactly as submitted, with no trimming and no fallback value.

11.6 Configure

Configure's permission model. Every mutating endpoint applies one of two rules, so that a hand-built
request cannot bypass the restrictions the UI enforces:

  • Payload-scope rule — used by endpoints that carry a firm id in the payload. firmId <= 0 means
    global configuration and requires the Global Master System Administrator role; a positive firmId
    must equal the caller's own firm and the caller must be a firm admin. The rule also resolves the SQL
    firm predicate: global rows are (firm_id IS NULL OR firm_id = 0), firm rows are firm_id = ?.
  • Row-scope rule — used by endpoints addressed by row id only. The row's own firm_id decides
    which restriction applies: NULL/0 ⇒ Global Master System Administrator; otherwise that firm's firm
    admin. service_line_status rows carry no firm_id of their own, so their owning service_line's firm
    is used.

is_active semantics. In priority, service_line and task_type, is_active = 1 is written when
inactivating a row and is_active = 0 when reactivating. Read the column as "is inactive".

POST/api/task/configs/serviceline/group

Loads the whole Tasks & Time Configure page in one call: priorities, billing types, service lines with
their task types, service lines with their statuses, and the firm's Home View column order.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Read-only: any authenticated user, but a non-zero firm must be the caller's own firm, else 401
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firm number Yes Integer (coerced) 0 ⇒ the Global tab; a firm id ⇒ that firm's tab (must be the caller's own).
from string No String Caller-supplied origin hint.

Example request

curl -X POST 'https://api.example.com/api/task/configs/serviceline/group' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"firm":12}'

Success response — 200 OK

{
  "billing": [{ "id": 1, "name": "Billable" }],
  "priority": [
    { "id": 2, "name": "High", "order": 1, "high_priority": 1, "assigned_id": 14, "is_active": 0 }
  ],
  "servicelineList": [
    { "id": 4, "name": "Bookkeeping", "order": 1, "assigned_id": null, "is_active": 0,
      "child": [{ "id": 7, "name": "Recurring" }] }
  ],
  "servicelineStatus": [
    { "id": 4, "name": "Bookkeeping", "order": 1, "assigned_id": null, "is_active": 0,
      "child": [{ "id": 9, "name": "In Progress" }] }
  ],
  "homeViwColumns": [
    { "id": 55, "task_view_id": 3, "column": "company", "order": 1, "filter": null }
  ]
}
Field Type Description
billing[] array Billing type lookup rows.
priority[] array Priorities with high_priority flag and assigned_id (non-null when the priority is in use, blocking deletion).
servicelineList[] array Service lines, each with child[] = its task types.
servicelineStatus[] array The same service lines, each with child[] = its statuses.
homeViwColumns[] array task_column rows for the firm's is_home_view board, ordered by order. Empty array when firm = 0. The response key is spelled exactly as shown.

Errors

Status Condition Message/shape
400 Missing firm validation error message array
401 No session, or firm is a firm other than the caller's { "message": "These credentials do not match our records." }
POST/api/task/configs/clone/status

Reports whether the firm is set to use its own configuration and whether the global-to-firm clone has
already been run — the state of the "Use Firm Settings" checkbox.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; no admin restriction is applied to this read
Content type application/json

Request body (firm required, from optional). See above.

Example request

curl -X POST 'https://api.example.com/api/task/configs/clone/status' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"firm":12}'

Success response — 200 OK

{ "error": false, "clone": 1, "cloneDone": 1 }
Field Type Description
error boolean false when the firm row was found.
clone number firms.use_service_line — whether the firm runs on its own configuration.
cloneDone number firms.clone_task_config — whether the global catalogue has already been copied in.

Errors

Status Condition Message/shape
200 Firm row not found { "error": true, "message": "Firm not found", "clone": 0, "cloneDone": 0 }
400 Missing firm validation error message array
401 No/expired session { "message": "These credentials do not match our records." }
POST/api/task/configs/serviceline/clone

Copies the global priorities, service lines, task types and statuses into the firm — the "Use Firm
Settings" first-run action.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Firm admin (firm_admin = 1) of the requested firm; the firm must be the caller's own
Content type application/json

Request body (firm required, from optional).

Example request

curl -X POST 'https://api.example.com/api/task/configs/serviceline/clone' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"firm":12}'

Success response — 200 OK

{ "error": false, "message": "Global Configuration are Cloned Successfully." }
Field Type Description
error boolean false on success.
message string Confirmation text.

Errors

Status Condition Message/shape
200 firm is 0/absent, or not the caller's firm { "error": true, "message": "Firm not found" }
400 Missing firm validation error message array
401 Caller is not a firm admin { "message": "These credentials do not match our records." }

Notes

  • The clone is idempotent per row: each global row is inserted only when a row with the same name does not
    already exist in the firm's scope.
POST/api/task/configs/set/priority/high

Marks one priority as the firm's (or the global) "high priority", clearing the flag on all others in the
same scope.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Payload-scope rule: firm = 0 ⇒ Global Master System Administrator; firm > 0 ⇒ that firm's firm admin
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number Yes Integer (coerced) Priority to flag as high.
firm number Yes Integer (coerced) 0 ⇒ global scope, otherwise the caller's firm.

Example request

curl -X POST 'https://api.example.com/api/task/configs/set/priority/high' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"id":2,"firm":12}'

Success response — 200 OK

{ "error": false, "message": "Priority updated successfully." }
Field Type Description
error boolean Always false on this path.
message string Confirmation text.

Errors

Status Condition Message/shape
400 Missing id or firm validation error message array
401 Not a Global Master System Administrator (global scope), or not that firm's firm admin { "message": "These credentials do not match our records." }

Notes

  • Two statements run: clear high_priority across the scope, then set it on id within the same scope
    — an id outside the scope silently sets nothing while still returning success.
POST/api/task/configs/priority/inactive

Inactivates a priority after repointing everything that used it to a replacement priority.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Row-scope rule, applied to the old priority's own firm_id: global row ⇒ Global Master System Administrator, firm row ⇒ that firm's firm admin
Content type application/json

Request body

Field Type Required Validation / allowed values Description
oldPriorityId number Yes Integer (coerced) Priority being inactivated.
priorityId number Yes Integer (coerced) Replacement priority to repoint tasks and template settings to.

Example request

curl -X POST 'https://api.example.com/api/task/configs/priority/inactive' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"oldPriorityId":2,"priorityId":3}'

Success response — 200 OK

{ "error": false, "message": "Priority inactivated successfully." }
Field Type Description
error boolean false on success.
message string Confirmation text.

Errors

Status Condition Message/shape
400 Missing either id validation error message array
401 Old priority does not exist, or role/scope violation { "message": "These credentials do not match our records." }

Notes

  • Side effects, in order: tasks.priority is repointed for all live tasks; template_settings.priority is
    repointed; the old priority row is then set is_active = 1 (inactive).
  • A missing old priority is reported as 401, not 404.
POST/api/task/config/service-line/inactive

Inactivates a service line, repointing tasks to a replacement service line, task type and task status.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Row-scope rule, applied to the old service line's firm_id
Content type application/json

Request body

Field Type Required Validation / allowed values Description
oldServiceLineId number Yes Integer (coerced) Service line being inactivated.
serviceLineId number Yes Integer (coerced) Replacement service line.
taskTypeId number Yes Integer (coerced) Replacement task type (must belong to the replacement service line).
taskStatusId number Yes Integer (coerced) Replacement task status.

Example request

curl -X POST 'https://api.example.com/api/task/config/service-line/inactive' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"oldServiceLineId":4,"serviceLineId":5,"taskTypeId":11,"taskStatusId":14}'

Success response — 200 OK

{ "error": false, "message": "Service Line inactivated successfully." }
Field Type Description
error boolean false on success.
message string Confirmation text.

Errors

Status Condition Message/shape
400 Any of the four ids missing validation error message array
401 Old service line does not exist, or role/scope violation { "message": "These credentials do not match our records." }

Notes

  • Repoints live tasks (service line, task type and status together, since type and status are service-line
    scoped) and template settings, then marks the old service line inactive.
POST/api/task/type/inactive

Inactivates a task type after repointing everything that used it.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Row-scope rule, applied to the old task type's firm_id
Content type application/json

Request body

Field Type Required Validation / allowed values Description
oldTaskTypeId number Yes Integer (coerced) Task type being inactivated.
taskTypeId number Yes Integer (coerced) Replacement task type.

Example request

curl -X POST 'https://api.example.com/api/task/type/inactive' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"oldTaskTypeId":7,"taskTypeId":11}'

Success response — 200 OK

{ "error": false, "message": "Task Type inactivated successfully." }
Field Type Description
error boolean false on success.
message string Confirmation text.

Errors

Status Condition Message/shape
400 Missing either id validation error message array
401 Old task type does not exist, or role/scope violation { "message": "These credentials do not match our records." }
POST/api/task/change/priority/order

Persists the drag-and-drop ordering of priorities.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Per row: the row-scope rule, applied to that priority's firm_id
Content type application/json

Request body

Field Type Required Validation / allowed values Description
data array Yes Array of { id, order } objects The new ordering.
data[].id number Yes Integer (coerced) Priority id.
data[].order number Yes Integer (coerced) New order value.
from string No String Ignored by this endpoint (see POST /api/task/config/change/order).
index number No Integer (coerced) Accepted but not used.

Example request

curl -X POST 'https://api.example.com/api/task/change/priority/order' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"data":[{"id":2,"order":1},{"id":3,"order":2}]}'

Success response — 200 OK

{ "error": false }
Field Type Description
error boolean Always false on this path.

Errors

Status Condition Message/shape
400 data missing or malformed validation error message array
401 Any listed row fails the per-row scope/role check { "message": "These credentials do not match our records." }

Notes

  • Rows are processed one at a time with no transaction: a 401 on item n leaves items 1..n-1 already
    reordered. An id that matches no row is skipped silently.
POST/api/task/config/change/order

Persists the drag-and-drop ordering of service lines, task types or statuses, selected by from.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Per row: the row-scope rule, applied to that row's firm_id (for a status, the owning service line's firm)
Content type application/json

Request body

Field Type Required Validation / allowed values Description
data array Yes Array of { id: int, order: int } objects The new ordering.
from string No String. Compared lower-cased: tasktype ⇒ task_type, status ⇒ service_line_status, anything else (including omitted) ⇒ service_line Which table to reorder.
index number No Integer (coerced) Accepted but not used.

Example request

curl -X POST 'https://api.example.com/api/task/config/change/order' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"from":"taskType","data":[{"id":7,"order":1},{"id":11,"order":2}]}'

Success response — 200 OK

{ "error": false }
Field Type Description
error boolean Always false on this path.

Errors

Status Condition Message/shape
400 data missing or malformed validation error message array
401 Any listed row fails the per-row scope/role check { "message": "These credentials do not match our records." }

Notes

  • The scope and role check is applied to every row individually, so a caller can reorder only the rows
    their own scope permits.
POST/api/task/home/order

Replaces the firm's Home View column list and ordering, creating the Home View board if it does not exist.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Firm admin of the caller's own firm
Content type application/json

Request body

Field Type Required Validation / allowed values Description
columns array Yes Array of column objects (see the two rows below) Full replacement column list.
columns[].column string Yes String Column key stored in task_column.column.
columns[].order number Yes Integer (coerced) Display order.
firmId number No Integer (coerced) Defaults to the caller's firm; must equal it when supplied.

Example request

curl -X POST 'https://api.example.com/api/task/home/order' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"firmId":12,"columns":[{"column":"company","order":1},{"column":"due","order":2}]}'

Success response — 200 OK

{ "error": false, "message": "Home view column(s) are updated sucessfully." }
Field Type Description
error boolean false on success.
message string Confirmation text. The message string is spelled exactly as shown, including sucessfully.

Errors

Status Condition Message/shape
200 Home View board could neither be found nor created { "error": true, "message": "Unable to save Home View order." }
400 columns missing or malformed validation error message array
401 firmId is not the caller's firm, or caller is not a firm admin { "message": "These credentials do not match our records." }

Notes

  • Destructive replace: all existing task_column rows for the board are soft-deleted, then the submitted
    list is inserted. Submitting an empty columns array clears the Home View.
  • A missing Home View board is created as a task_board row named Home View with user_id = 0 and
    is_home_view = 1.
POST/api/task/config/save

Adds a configuration value. Only colName = "priority" is implemented.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Payload-scope rule: firmId = 0 ⇒ Global Master System Administrator; firmId > 0 ⇒ that firm's firm admin
Content type application/json

Request body

Field Type Required Validation / allowed values Description
colName string Yes String. Only "priority" is supported Which configuration list to add to.
colVal string Yes String The new value's name (trimmed).
firmId number Yes Integer (coerced) 0 ⇒ global scope.
order number No Integer (coerced) Explicit order; otherwise appended.

Example request

curl -X POST 'https://api.example.com/api/task/config/save' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"colName":"priority","colVal":"Critical","firmId":12}'

Success response — 200 OK

{
  "error": false,
  "message": "Priority created successfully.",
  "data": { "id": 21, "name": "Critical", "is_active": 0, "order": 4, "high_priority": 0 }
}
Field Type Description
error boolean false on success.
data.id number New priority id.
data.is_active number Always 0 (active) for a new row.
data.order number Assigned order.
data.high_priority number Always 0 for a new row.

Errors

Status Condition Message/shape
200 colName !== "priority" { "error": true, "message": "Only priority add is supported in this POC." }
200 colVal blank after trim { "error": true, "message": "Priority name is required." }
200 Name already exists in scope (case-insensitive) { "error": true, "message": "<name> already exist in the Priority." }
400 Missing colName, colVal or firmId validation error message array
401 Not a Global Master System Administrator (global scope), or not that firm's firm admin { "message": "These credentials do not match our records." }

Notes

  • The role check runs before the colName check, so an unauthorised caller gets 401 even for an
    unsupported colName.
PUT/api/task/configs/serviceline

Creates or updates a service line.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Payload-scope rule: firmId = 0 ⇒ Global Master System Administrator; firmId > 0 ⇒ that firm's firm admin
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number No Integer (coerced) Present ⇒ update.
name string Yes String Service line name (trimmed); unique in scope.
isGlobalFirm number Yes Integer (coerced) Global/firm discriminator carried by the editor.
firmId number Yes Integer (coerced) 0 ⇒ global scope.
order number No Integer (coerced) Explicit order; otherwise appended.

Example request

curl -X PUT 'https://api.example.com/api/task/configs/serviceline' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"name":"Payroll","isGlobalFirm":1,"firmId":12}'

Success response — 200 OK

{
  "error": false,
  "message": "Service Line created successfully.",
  "id": 33,
  "name": "Payroll",
  "order": 5,
  "is_active": 0
}
Field Type Description
error boolean false on success.
message string Service Line created successfully. or Service Line updated successfully.
id number Saved row id.
name string Trimmed name.
order number Assigned order.
is_active number 0.

Errors

Status Condition Message/shape
200 name blank after trim { "error": true, "message": "Service Line name is required." }
200 Name already exists in scope { "error": true, "message": "<name> already exist in the Service Line." }
400 Missing name, isGlobalFirm or firmId validation error message array
401 Not a Global Master System Administrator (global scope), or not that firm's firm admin { "message": "These credentials do not match our records." }
PUT/api/task/type

Creates or updates a task type.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Payload-scope rule: firmId = 0 ⇒ Global Master System Administrator; firmId > 0 ⇒ that firm's firm admin
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number No Integer (coerced) Present ⇒ update.
name string Yes String Task type name (trimmed); unique in scope.
type number Yes Integer (coerced) Owning service line id (task_type.type).
firmId number Yes Integer (coerced) 0 ⇒ global scope.

Example request

curl -X PUT 'https://api.example.com/api/task/type' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"name":"Ad hoc","type":4,"firmId":12}'

Success response — 200 OK

{
  "error": false,
  "message": "Task Type created successfully.",
  "id": 42,
  "name": "Ad hoc",
  "type": 4,
  "order": 3,
  "is_active": 0
}
Field Type Description
error boolean false on success.
message string Task Type created successfully. or Task Type updated successfully.
id number Saved row id.
type number Echo of the submitted type.
order number Assigned order.
is_active number 0.

Errors

Status Condition Message/shape
200 name blank after trim { "error": true, "message": "Task Type name is required." }
200 Name already exists in scope { "error": true, "message": "<name> already exist in the Task Types." }
400 Missing name, type or firmId validation error message array
401 Not a Global Master System Administrator (global scope), or not that firm's firm admin { "message": "These credentials do not match our records." }
PATCH/api/task/type/:id

Updates a task type; the id comes from the path.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Payload-scope rule — as for PUT /api/task/type
Content type application/json

Path parameters

Name Type Required Description
id string Yes Task type id; interpreted as a number.

Request body, as for PUT /api/task/type. Any id in the body is overwritten by the path parameter.

Example request

curl -X PATCH 'https://api.example.com/api/task/type/42' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"name":"Ad hoc (revised)","type":4,"firmId":12}'

Success response — 200 OK

{
  "error": false,
  "message": "Task Type updated successfully.",
  "id": 42,
  "name": "Ad hoc (revised)",
  "type": 4,
  "order": 3,
  "is_active": 0
}

Response fields as for PUT /api/task/type.

Errors

Status Condition Message/shape
200 Blank name / duplicate name { "error": true, "message": "..." }
400 Missing name, type or firmId validation error message array
401 Scope/role violation { "message": "These credentials do not match our records." }

Notes

  • A non-numeric path id does not match any row.
DELETE/api/task/type/:id/:status

Deletes or (in)activates a task type, depending on the status segment.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Row-scope rule, applied to the task type's own firm_id
Content type application/json

Path parameters

Name Type Required Description
id string Yes Task type id; interpreted as a number.
status string Yes Interpreted as a number. 1 ⇒ soft delete (deleted_at). 2 ⇒ set is_active = 0 (activate). Any other value ⇒ set is_active = 1 (inactivate).

Example request

curl -X DELETE 'https://api.example.com/api/task/type/42/1' -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{ "error": false, "message": "Task Type deleted successfully." }
Field Type Description
error boolean Always false on this path.
message string Same message regardless of which branch ran.

Errors

Status Condition Message/shape
401 Task type not found, or role/scope violation { "message": "These credentials do not match our records." }
PUT/api/task/status

Creates or updates a task status under a service line.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Row-scope rule, applied to the owning service line's firm_id
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number No Integer (coerced) Present ⇒ update.
name string Yes String Status name (trimmed); unique within the service line.
service_line_id number Yes Integer (coerced) Owning service line — also decides the permission scope.
status number No Integer (coerced) Status flag persisted on the row.
order number No Integer (coerced) Explicit order; otherwise appended.

Example request

curl -X PUT 'https://api.example.com/api/task/status' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"name":"Awaiting Client","service_line_id":4,"status":1}'

Success response — 200 OK

{
  "error": false,
  "message": "Task Status created successfully.",
  "id": 61,
  "name": "Awaiting Client",
  "status": 1,
  "service_line_id": 4,
  "order": 6
}
Field Type Description
error boolean false on success.
message string Task Status created successfully. or Task Status updated successfully.
id number Saved row id.
status number Persisted status flag.
service_line_id number Echo of the submitted service line.
order number Assigned order.

Errors

Status Condition Message/shape
200 name blank after trim { "error": true, "message": "Status name is required." }
200 Name already exists under that service line { "error": true, "message": "<name> already exist in Task Status." }
400 Missing name or service_line_id validation error message array
401 Service line not found, or role/scope violation { "message": "These credentials do not match our records." }
PATCH/api/task/status/:id

Updates a task status; the id comes from the path.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Row-scope rule, applied to the owning service line's firm_id
Content type application/json

Path parameters

Name Type Required Description
id string Yes Task status id; interpreted as a number.

Request body, as for PUT /api/task/status.

Example request

curl -X PATCH 'https://api.example.com/api/task/status/61' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"name":"Awaiting Client Docs","service_line_id":4}'

Success response — 200 OK

{
  "error": false,
  "message": "Task Status updated successfully.",
  "id": 61,
  "name": "Awaiting Client Docs",
  "status": 1,
  "service_line_id": 4,
  "order": 6
}

Response fields as for PUT /api/task/status.

Errors

Status Condition Message/shape
200 Blank name / duplicate name { "error": true, "message": "..." }
400 Missing name or service_line_id validation error message array
401 Service line not found, or role/scope violation { "message": "These credentials do not match our records." }
DELETE/api/task/status/:id

Soft-deletes a task status.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Row-scope rule, applied to the owning service line's firm_id
Content type application/json

Path parameters

Name Type Required Description
id string Yes Task status id; interpreted as a number.

Example request

curl -X DELETE 'https://api.example.com/api/task/status/61' -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{ "error": false, "message": "Task Status deleted successfully." }
Field Type Description
error boolean Always false on this path.
message string Confirmation text.

Errors

Status Condition Message/shape
401 Status not found, or role/scope violation { "message": "These credentials do not match our records." }

Notes

  • Where the deleted_at column is not available, the row is marked removed by setting
    service_line_status.status = 0 instead.
POST/api/task/manage/status

Renames a task status.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Row-scope rule, applied to the owning service line's firm_id
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id number Yes Integer (coerced) Task status id.
name string Yes String New name (trimmed before writing).

Example request

curl -X POST 'https://api.example.com/api/task/manage/status' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"id":61,"name":"Awaiting Client Docs"}'

Success response — 200 OK

{ "error": false, "message": "Task Status updated successfully." }
Field Type Description
error boolean Always false on this path.
message string Confirmation text.

Errors

Status Condition Message/shape
400 Missing id or name validation error message array
401 Status not found, or role/scope violation { "message": "These credentials do not match our records." }

Notes

  • No duplicate-name check on this path (unlike PUT /api/task/status).
POST/api/task/status/inactive

Inactivates a task status after repointing tasks to a replacement status.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Row-scope rule, applied to the old status's owning service line's firm_id
Content type application/json

Request body

Field Type Required Validation / allowed values Description
oldTaskStatusId number Yes Integer (coerced) Status being inactivated.
taskStatusId number Yes Integer (coerced) Replacement status.

Example request

curl -X POST 'https://api.example.com/api/task/status/inactive' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"oldTaskStatusId":61,"taskStatusId":9}'

Success response — 200 OK

{ "error": false, "message": "Task Status inactivated successfully." }
Field Type Description
error boolean false on success.
message string Confirmation text.

Errors

Status Condition Message/shape
400 Missing either id validation error message array
401 Old status not found, or role/scope violation { "message": "These credentials do not match our records." }

Notes

  • Repoints tasks.status on live tasks before inactivating the status row.
DELETE/api/task/configs/serviceline/:id/:deleted

Deletes or (in)activates a service line, depending on the deleted segment.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Row-scope rule, applied to the service line's own firm_id
Content type application/json

Path parameters

Name Type Required Description
id string Yes Service line id; interpreted as a number.
deleted string Yes Interpreted as a number. 1 ⇒ soft delete (deleted_at). 2 ⇒ set is_active = 0 (activate). Any other value ⇒ set is_active = 1 (inactivate).

Example request

curl -X DELETE 'https://api.example.com/api/task/configs/serviceline/4/1' -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{ "error": false, "message": "Service Line deleted successfully." }
Field Type Description
error boolean Always false on this path.
message string Same message regardless of which branch ran.

Errors

Status Condition Message/shape
401 Service line not found, or role/scope violation { "message": "These credentials do not match our records." }
POST/api/task/config/update

Renames a configuration value. Only colName = "priority" is implemented.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Payload-scope rule: firmId = 0 ⇒ Global Master System Administrator; firmId > 0 ⇒ that firm's firm admin
Content type application/json

Request body

Field Type Required Validation / allowed values Description
colName string Yes String. Only "priority" is supported Which configuration list.
colVal string Yes String New name (trimmed).
firmId number Yes Integer (coerced) 0 ⇒ global scope.
id number Yes Integer (coerced) Row to rename.

Example request

curl -X POST 'https://api.example.com/api/task/config/update' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"colName":"priority","colVal":"Urgent","firmId":12,"id":21}'

Success response — 200 OK

{ "error": false, "message": "Priority updated successfully." }
Field Type Description
error boolean false on success.
message string Confirmation text.

Errors

Status Condition Message/shape
200 colName !== "priority" { "error": true, "message": "Only priority update is supported in this POC." }
200 colVal blank after trim { "error": true, "message": "Priority name is required." }
200 Name already exists in scope { "error": true, "message": "<name> already exist in the Priority." }
400 Missing any of colName, colVal, firmId, id validation error message array
401 Not a Global Master System Administrator (global scope), or not that firm's firm admin { "message": "These credentials do not match our records." }
POST/api/task/config/delete

Deletes or (in)activates a configuration value. Only colName = "priority" is implemented.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization Row-scope rule, applied to the priority row's own firm_id
Content type application/json

Request body

Field Type Required Validation / allowed values Description
colName string Yes String. Only "priority" is supported Which configuration list.
colVal number Yes Integer (coerced) The row id to act on (despite the name).
deleted number Yes Integer (coerced) 1 ⇒ soft delete. 2 ⇒ set is_active = 0 (activate). Any other value ⇒ is_active = 1 (inactivate).

Example request

curl -X POST 'https://api.example.com/api/task/config/delete' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"colName":"priority","colVal":21,"deleted":1}'

Success response — 200 OK

{ "error": false, "message": "Priority deleted sucessfully." }
Field Type Description
error boolean false on success.
message string Confirmation text. The message string is spelled exactly as shown, including sucessfully.

Errors

Status Condition Message/shape
200 colName !== "priority" { "error": true, "message": "Only priority delete is supported in this POC." }
400 Missing any of colName, colVal, deleted validation error message array
401 Priority row not found, or role/scope violation { "message": "These credentials do not match our records." }
POST/api/task/update-alternate-type

Repoints one task's lookup columns after a configuration value it referenced was inactivated — the
"alternate selection" modal.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the task must be visible to the caller, otherwise the endpoint reports "Task not found."
Content type application/json

Request body

Field Type Required Validation / allowed values Description
taskId string Yes String — plain numeric or encrypted id Task to update.
updateArray array Yes Array of { name, value } objects Column/value pairs to write.
updateArray[].name string Yes String Column name. Silently ignored unless it is one of priority, project, service_line, billing, invoicing, type, assigned_roles, assigned_name, reviewer, reviewer_name and the column exists on tasks.
updateArray[].value any Yes Must be present Written as a number, or as null when the value is not numeric.

Example request

curl -X POST 'https://api.example.com/api/task/update-alternate-type' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"taskId":"eL5wT1...","updateArray":[{"name":"service_line","value":5},{"name":"type","value":11}]}'

Success response — 200 OK

{
  "error": false,
  "message": "Alternate Data Updated successfully.",
  "statusOptionUpdate": true,
  "statusOption": [{ "id": 14, "name": "In Progress" }]
}
Field Type Description
error boolean false on success.
statusOptionUpdate boolean true when service_line was among the updated columns, meaning the client must reload the status dropdown.
statusOption[] array The replacement status options for the new service line; [] when service_line was not changed.

Errors

Status Condition Message/shape
200 taskId cannot be decoded, or the task is not visible to the caller { "error": true, "message": "Task not found." }
200 No entry in updateArray maps to an allowed, existing column { "error": true, "message": "No alternate values to update." }
400 Missing taskId or updateArray validation error message array
401 No/expired session { "message": "These credentials do not match our records." }

Notes

  • Changing service_line also nulls tasks.status, because statuses are service-line scoped.
  • Anything not in the allow-list is dropped without comment — the endpoint cannot be used to write
    arbitrary tasks columns.
POST/api/task/firm

Flips a firm-level usage flag (use_task / use_project / use_service_line), optionally recording that the configuration clone has been done. This is the canonical entry for /api/task/firm — the same path also appears in the Task & Time Templates subsection.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization firmId must equal the caller's firm and the caller must be a firm admin
Content type application/json

Request body

Field Type Required Validation / allowed values Description
templateId number Yes Integer (coerced) Which flag: 1 ⇒ firms.use_task, 2 ⇒ firms.use_project, anything else ⇒ firms.use_service_line.
firmId number Yes Integer (coerced) Target firm; must be the caller's own.
value number Yes Integer (coerced) Written to the flag column as-is.
cloneDone boolean No Boolean When present (a real boolean), also writes firms.clone_task_config = 1/0.

Example request

curl -X POST 'https://api.example.com/api/task/firm' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"templateId":1,"firmId":12,"value":1,"cloneDone":true}'

Success response — 200 OK

{ "error": false, "message": "Record updated successfully." }
Field Type Description
error boolean false on success.
message string Confirmation text.

Errors

Status Condition Message/shape
200 No firms row matches user_id = firmId OR id = firmId { "error": true, "message": "Firm configuration not found." }
400 Missing templateId, firmId or value validation error message array
401 firmId is not the caller's firm, or caller is not a firm admin { "message": "These credentials do not match our records." }

Notes

  • Duplicate path. The Task & Time Templates subsection documents the same path; this entry is the canonical one and serves
    every request to /api/task/firm. Compared with the Templates variant, this one accepts cloneDone,
    resolves the target firms row id before updating, and writes value exactly as supplied rather than
    normalising it to 0/1.
  • firmId is compared strictly against the caller's own firm id.
POST/api/task/utilization/config

Loads the Utilization configuration tab: the firm's service-role targets, its users' overrides, and the
current user's own role assignment.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization firmId must be non-zero and equal the caller's firm; no admin flag required for the read
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmId number Yes Integer (coerced) Target firm; must be the caller's own and non-zero.
type string No String + One of role, user. Defaults to 'role' Which tab to populate; 'user' additionally loads firmUsers.

Example request

curl -X POST 'https://api.example.com/api/task/utilization/config' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"firmId":12,"type":"user"}'

Success response — 200 OK

{
  "defaultServiceRoles": [
    { "id": 3, "role": "Bookkeeper", "target_utilization": 75, "target_bill_week": 30, "target_bill_day": 6 }
  ],
  "firmUsers": [
    {
      "id": 88,
      "lf_name": "Doe, Jane",
      "override_total_hours": 40,
      "override_target_utilization": 80,
      "override_target_bill_week": 32,
      "override_target_bill_day": 6.4
    }
  ],
  "loginUserRole": 3
}
Field Type Description
defaultServiceRoles[] array Service roles in scope with their utilization targets (utilization_config_role values falling back to service_roles columns, then 0).
firmUsers[] array Firm users with their utilization_config_user overrides. Populated only for type = 'user'; otherwise []. Derived override_target_bill_week/_day are nulled when both stored overrides are null, so a cleared override renders blank rather than 0.00.
loginUserRole number | null The calling user's own utilization_config_user.role_id, or null.

Errors

Status Condition Message/shape
400 Missing firmId, or type outside role/user validation error message array
401 firmId is 0 or not the caller's firm { "message": "These credentials do not match our records." }

Notes

  • Role scoping is exclusive and follows firms.use_service_role: 1 ⇒ the firm's own roles,
    otherwise the global catalogue — never a union of both.
  • service_roles rows with deleted IN (0, 2) are included; both values denote an active role.
POST/api/task/utilization/config/save

Saves one utilization row — either a role-level target or a user-level override.

Module Tasks & Time > Configure
Authentication Required — authenticated session cookie
Authorization firmId must be non-zero and equal the caller's firm
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmId number Yes Integer (coerced) Target firm; must be the caller's own.
type string Yes String + One of role, user Which row kind to save.
roleId number No Integer (coerced) Required in practice when type = 'role'; also stored alongside a user row.
userId number No Integer (coerced) Required in practice when type = 'user'.
value string No String The target utilization value for a role row.
override_total_hours number | string | null No Optional; not numerically coerced User override for weekly hours. A blank string or null is preserved and means "no override", which applies the 40-hour default; it is never stored as an explicit 0.
override_target_utilization number | string | null No Optional; not numerically coerced User override for target utilization %. A blank value means "no override", which is treated as 0%.
override_target_bill_week number No Number (coerced) Derived weekly billable target.
override_target_bill_day number No Number (coerced) Derived daily billable target.

Example request

curl -X POST 'https://api.example.com/api/task/utilization/config/save' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"firmId":12,"type":"user","userId":88,"roleId":3,"override_total_hours":40,"override_target_utilization":80}'

Success response — 200 OK (user branch)

{
  "firm_id": 12,
  "user_id": 88,
  "role_id": 3,
  "override_total_hours": 40,
  "override_target_utilization": 80,
  "override_target_bill_week": 32,
  "override_target_bill_day": 6.4
}

Success response — 200 OK (role branch)

{
  "firm_id": 12,
  "role_id": 3,
  "target_utilization": 75,
  "target_bill_week": 30,
  "target_bill_day": 6
}
Field Type Description
firm_id number Echo of the requested firm.
user_id number User branch only.
role_id number The saved/derived role id.
override_total_hours / override_target_utilization number | null null when the caller sent a blank override.
override_target_bill_week / override_target_bill_day number | null Derived values, rounded to 2 decimals; null when both stored overrides are blank.
target_utilization / target_bill_week / target_bill_day number Role branch only; derived values rounded to 2 decimals.

Errors

Status Condition Message/shape
200 type = 'user' with no usable userId { "error": true, "message": "User is required." }
200 type = 'role' with no usable roleId { "error": true, "message": "Role is required." }
400 Missing firmId or type, or type outside role/user validation error message array
401 firmId is 0 or not the caller's firm { "message": "These credentials do not match our records." }

11.7 Reports (Reports Builder)

This subsection covers report list management and placement, report definitions, live preview, XLSX export
and calculated-column evaluation.

Enumerations used across this subsection

Enum Values Where enforced
Report module / type (list) task, time, project, invoice, profitability, company, custom The module field of POST /api/tat/reports/list — restricted value set
Placement global, firm, personal The placement field of the clone, share and save requests — restricted value set
Reorder bucket global, firm, personal, favorite, shared The type field of POST /api/tat/reports/update/order — restricted value set
List action fav, unfav, delete The action field of POST /api/tat/reports/update — restricted value set
Standard report monthly_budget, mc_section_status, billable_hours, fp_profitability The type field of the standard report preview and export requests — restricted value set

Note that the type field of the clone, share, getcolumn and save requests is a plain string with no
restricted value set — the module vocabulary above is enforced only on the module field of the report
list request.

Column detail structure — one entry per column on a report, persisted to
reports_builder_column_details. Every field is optional; an entry whose columnId is blank is dropped.

Field Type Validation Persisted column Meaning
columnId string String column_id The reports_builder_column id, or a synthetic id for a custom/calculated column (Company reports use company_info_<id> / company_contacts_<id>; calculated columns use their own generated id).
coumnType string String coumn_type normal (a real SQL column) or anything else (a calculated/formula column). Defaults to normal. The persisted column name is spelled exactly as shown.
order number Integer order Display order; defaults to the array index + 1.
sort string String sort Saved sort direction for the column.
sumType string String sum_type Saved aggregation choice. Persisted but not what drives the preview's subtotals — see the row-group note below.
format any (JSON) untyped format Presentation flags. The exporter reads is_bold, is_italic, is_underline, is_double_underline and is_box.
displayName string String display_name Column heading; defaults to columnId.
is_display boolean untyped is_display Presentation-only filter; defaults to true. A hidden column is still fetched, so it can still be filtered on, grouped by and subtotalled.
filters any (JSON) untyped filters The saved column filter — see the filter structure below.
customFilters any (JSON) untyped custom_filters Additional saved filter payload.
condFormat any (JSON) untyped cond_format Conditional-formatting rules. This is the field that can make a save payload very large.
formula any (JSON) untyped formula For a calculated column: { formula: "<expression>", output: "<field type>", decimal: <n> }.
decimal number Integer decimal Decimal places applied to the rendered value; defaults to 0.
isHeaderFilter any untyped is_header_filter Stored as an integer 0/1 (never JSON-encoded), or NULL when omitted.

Row-group structure — { column_id?: string }. The rowGroup field of a save request is
mapped to a plain string array and stored JSON-encoded in reports_builder_list.row_group. Row groups
drive the grouped preview; when a request asks for neededData: 'details' the grouping is skipped and
detail rows are returned instead. Row-group subtotal columns are derived from each column's field_type
being one of number, time or percentage.

Filter structure — a saved/inline filter entry is
{ field_name, field_type, filter: { input, option } }, where option is either a string or
{ value: "<op>" }. Recognised operators:

Operator Behaviour
equal Numeric equality when both sides parse as numbers, otherwise string equality.
not_equal Inverse of equal.
contains String includes.
starts_with String startsWith.
ends_with String endsWith.
greater / greater_equal_to Numeric only; false when either side is non-numeric.
lower / lower_equal_to Numeric only; false when either side is non-numeric.

field_type of jsonstring or link is skipped entirely. A boolean field is the one type that does
not require an option — only input (0/1) — matching the preview sidebar's boolean editor. An
absent or empty option on any other type means the filter is ignored (not "match nothing").

Formula (calculated column) grammar — formulas are parsed by a dedicated expression parser and are
never evaluated as executable code. Operands are bare field names resolved against the row; the editor
writes them wrapped in parentheses, e.g. (billable_time) / (total_reported_time).

  • Operators, lowest to highest precedence: ||, &&, comparisons (==, !=, >=, <=, >, <),
    + -, * / %, ^ (right-associative), unary - + !.
  • Functions: DATEDIFF(a, b), ABS, ROUND (1 or 2 args), CEIL, FLOOR, SQRT, POW, MIN,
    MAX, IF(cond, then, else). Names are case-insensitive; any other function name throws.
  • Literals: numbers, single- or double-quoted strings. Identifiers may be written $name or name.
  • DATEDIFF: returns '' if either operand is empty or unparseable; returns whole seconds when
    the column's output type is time, otherwise whole days; always an absolute value.
  • Failure semantics: an unknown variable, a division (or modulo) by zero, unbalanced parentheses,
    trailing tokens or any other parse/eval failure makes the entire cell evaluate to 0 — never a
    partial result.
  • Output: always a string. percentage output multiplies by 100. Decimals come from the formula's
    decimal (only applied when > 0 and the result is numeric), and are formatted with fixed decimals,
    no thousands separator and halves rounded away from zero. decimal is clamped to 0..100. A
    non-numeric string result passes through unformatted.
  • Results are written to the row under both calc_<column detail id> and the column's own
    column_id, so a later formula can reference an earlier calculated column by name. For row-group
    summary rows the variables are rebuilt from the __SUBTOTAL__<field> keys first.

Standard preview payload — every report engine returns the same envelope, which is then decorated
with calculated columns and decimal places:

Field Type Description
columnDetail[] array Detail rows.
columnSummary[] array Deepest row-group summary rows (empty for a plain detail request).
colSummarySubTotal array | object Per-level row-group subtotals, keyed by level when grouping is active.
rowGroupColumns[] array Field names the result is grouped by.
neededData string details on the detail path.
name, type, globalType, placement string Report identity.
columnList[] array { column_id, display_name, field_name, field_type, order, format, cond_format, filters } per displayed column.
total_record number Row count of the returned set.
reportTotal array | object Report-wide totals (object on the subtotal-only path).
reportRowGroupTotal array | object Grand total across row groups.
POST/api/tat/reports/list

Returns every report the caller can see for one module, bucketed by placement, plus the user list used by
the Share dialog.

Module Tasks & Time > Reports (Reports Builder)
Authentication Required — authenticated session cookie
Authorization No additional role requirement; visibility is scoped in SQL to global reports, the caller's firm's reports, the caller's own personal reports and reports shared with the caller
Content type application/json

Request body

Field Type Required Validation / allowed values Description
module string Yes String + One of task, time, project, invoice, profitability, company, custom Which report module's list to return.

Example request

curl -X POST 'https://api.example.com/api/tat/reports/list' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"module":"task"}'

Success response — 200 OK

{
  "error": false,
  "reports": {
    "global": [{ "id": 5, "name": "Open Tasks", "type": "task", "placement": "global", "order": 1, "encryptId": "fM8kP3..." }],
    "firm": [],
    "personal": [{ "id": 12, "name": "My Overdue", "type": "task", "placement": "personal", "order": 1, "encryptId": "gN9lQ4..." }],
    "favorite": [{ "id": 5, "name": "Open Tasks", "order": 1, "encryptId": "fM8kP3..." }],
    "shared": []
  },
  "roleId": 1,
  "firmAdmin": 0,
  "userList": [
    { "id": 88, "user_name": "jdoe", "lf_name": "Doe, Jane", "first_name": "Jane", "last_name": "Doe",
      "email": "jane@example.com", "firm_admin": 0, "firm_id": 12, "role": 6, "user_type": 1, "active": 1 }
  ]
}
Field Type Description
error boolean Always false on this path.
reports object Five buckets — global, firm, personal, favorite, shared — each sorted by order. Every row is the reports_builder_list row plus encryptId. A favourited report appears in both its placement bucket and favorite.
roleId 1 | "" 1 for a global admin, empty string otherwise. This is a flag, not a role id; the UI gates Global-report management on it.
firmAdmin number The caller's users.firm_admin value, returned directly.
userList[] array Active, non-deleted users in the caller's firm excluding the caller — the Share dialog's candidate list.

Errors

Status Condition Message/shape
400 module missing or outside the allowed set validation error message array
401 No/expired session { "message": "These credentials do not match our records." }
GET/api/tat/reports/profitability/access

Tells the client whether the current user may see profitability reports.

Module Tasks & Time > Reports (Reports Builder)
Authentication Required — authenticated session cookie
Authorization Reads the caller's own timesheet_config.view_profitablity_reports flag
Content type application/json

Example request

curl 'https://api.example.com/api/tat/reports/profitability/access' -b 'genie.sid=<session-cookie>'

Success response — 200 OK

true
Field Type Description
(body) boolean A bare JSON boolean — true when the caller's timesheet_config.view_profitablity_reports equals 1, false otherwise (including when no config row exists).

Errors

Status Condition Message/shape
401 No/expired session { "message": "These credentials do not match our records." }

Notes

  • The response is a scalar, not an envelope — unlike every other Reports endpoint.
  • The column name view_profitablity_reports is spelled exactly as shown.
POST/api/tat/reports/update

Favourites, un-favourites or deletes a report from the list screen.

Module Tasks & Time > Reports (Reports Builder)
Authentication Required — authenticated session cookie
Authorization fav/unfav: any authenticated user (favourites are per-user). delete: global reports (user_id IS NULL) require a global admin; firm reports (user_id = firmId) require a firm admin or global admin; personal reports require the owner
Content type application/json

Request body

Field Type Required Validation / allowed values Description
action string Yes String + One of fav, unfav, delete What to do.
reportId string Yes String — plain numeric or encrypted id Target report.

Example request

curl -X POST 'https://api.example.com/api/tat/reports/update' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"action":"fav","reportId":"fM8kP3..."}'

Success response — 200 OK

{ "error": false, "msg": "Report marked as favorite successfully" }
Field Type Description
error boolean false on success.
msg string Report marked as favorite successfully, This Report has been already marked as favorites, Report removed from favorite successfully, or Report deleted successfully.

Errors

Status Condition Message/shape
200 Report not found { "error": true, "msg": "Report not found." }
200 Caller may not delete this report's placement { "error": true, "msg": "Not allowed to delete this report." }
400 reportId cannot be decoded { "statusCode": 400, "message": "Invalid report id", "error": "Bad Request" }
400 action outside the allowed set validation error message array
401 No/expired session { "message": "These credentials do not match our records." }

Notes

  • Favouriting assigns the next order within the report's own module, so favourites keep a per-module
    ordering. Un-favouriting sets is_fav = 0, order = 0 rather than deleting the row.
POST/api/tat/reports/clone

Copies a report — its list row and all of its column details — under a new name and placement.

Module Tasks & Time > Reports (Reports Builder)
Authentication Required — authenticated session cookie
Authorization placement: "global" requires a global admin; placement: "firm" requires a firm admin or global admin; placement: "personal" is open
Content type application/json

Request body

Field Type Required Validation / allowed values Description
reportId string Yes String — plain numeric or encrypted id Source report.
name string Yes String Name for the clone.
placement string Yes String + One of global, firm, personal Where the clone lives.
type string Yes String (no Restricted value set) Fallback module; the source report's own type wins when present.

Example request

curl -X POST 'https://api.example.com/api/tat/reports/clone' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"reportId":"fM8kP3...","name":"Open Tasks (Copy)","placement":"personal","type":"task"}'

Success response — 200 OK

{ "error": false, "id": "hP1sR6...", "message": "Report cloned successfully." }
Field Type Description
error boolean false on success.
id string Encrypted id of the new report.
message string Confirmation text. Note this endpoint uses message, not msg.

Errors

Status Condition Message/shape
200 Source report missing { "error": true, "msg": "Report not found." }
200 placement: "global" without global admin { "error": true, "msg": "Not allowed to place this report globally." }
200 placement: "firm" without firm/global admin { "error": true, "msg": "Not allowed to place this report at the firm level." }
200 Name already used in the target placement { "error": true, "msg": "Report name already Exist. Please provide another name" }
400 reportId cannot be decoded { "statusCode": 400, "message": "Invalid report id", "error": "Bad Request" }
400 Missing/invalid fields validation error message array
401 No/expired session { "message": "These credentials do not match our records." }

Notes

  • The duplicate-name check excludes the source report, so cloning under the same name into the same
    placement is permitted.
  • global_type, description and row_group are carried over from the source. row_group is stored as a JSON string.
POST/api/tat/reports/share

Shares a report with a set of users in the caller's firm.

Module Tasks & Time > Reports (Reports Builder)
Authentication Required — authenticated session cookie
Authorization No placement role gate on this route; recipients outside the caller's firm are silently skipped
Content type application/json

Request body

Field Type Required Validation / allowed values Description
reportId string Yes String — plain numeric or encrypted id Report to share.
name string Yes String Report name carried by the dialog.
placement string Yes String + One of global, firm, personal Placement carried by the dialog.
type string Yes String (no Restricted value set) Module carried by the dialog.
isShared number[] Yes Array + Numeric (numeric strings are coerced) User ids to share with.

Example request

curl -X POST 'https://api.example.com/api/tat/reports/share' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"reportId":"fM8kP3...","name":"Open Tasks","placement":"personal","type":"task","isShared":[88,91]}'

Success response — 200 OK

{ "error": false, "msg": "Report shared successfully." }
Field Type Description
error boolean false on success.
msg string Report shared successfully., or Report shared successfully. N selected user(s) were skipped because they are outside your firm.

Errors

Status Condition Message/shape
200 isShared empty { "error": true, "msg": "Please select users to share this report." }
200 The report is already shared with one of the selected users { "error": true, "msg": "Selected report is already shared to one of these users." }
200 Report missing { "error": true, "msg": "Report not found." }
200 Every selected user is outside the caller's firm { "error": true, "msg": "None of the selected users can be shared with." }
400 reportId cannot be decoded { "statusCode": 400, "message": "Invalid report id", "error": "Bad Request" }
400 Missing/invalid fields validation error message array
401 No/expired session { "message": "These credentials do not match our records." }

Notes

  • Recipients outside the caller's firm are dropped rather than rejected; the count appears in msg.
  • Shared reports show up in the recipient's reports.shared bucket from POST /api/tat/reports/list,
    ordered by reports_builder_shared_list.order.
POST/api/tat/reports/update/order

Persists drag-and-drop ordering within one bucket of the report list.

Module Tasks & Time > Reports (Reports Builder)
Authentication Required — authenticated session cookie
Authorization No additional role requirement. favorite/shared writes are scoped to the caller's own rows; global/firm/personal update reports_builder_list by id only
Content type application/json

Request body

Field Type Required Validation / allowed values Description
type string Yes String + One of global, firm, personal, favorite, shared Which bucket is being reordered.
data array Yes Array of { id, order } objects The new ordering.
data[].id number Yes Integer (coerced) Report id (plain numeric — not the encrypted form).
data[].order number Yes Integer (coerced) New order value.

Example request

curl -X POST 'https://api.example.com/api/tat/reports/update/order' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"type":"personal","data":[{"id":12,"order":1},{"id":18,"order":2}]}'

Success response — 200 OK

{ "error": false }
Field Type Description
error boolean Always false on this path.

Errors

Status Condition Message/shape
400 type outside the allowed set, or data malformed validation error message array
401 No/expired session { "message": "These credentials do not match our records." }

Notes

  • For global/firm/personal the update carries no owner predicate: rows are matched by id alone,
    so global rows stored with user_id = 0 rather than NULL still reorder correctly.
POST/api/tat/reports/details

Loads the report editor: the report's saved columns and row groups, plus the full catalogue of columns
available for its type.

Module Tasks & Time > Reports (Reports Builder)
Authentication Required — authenticated session cookie
Authorization No additional role requirement; the report must be visible to the caller
Content type application/json

Request body — all fields optional

Field Type Required Validation / allowed values Description
reportId string No String — plain numeric or encrypted id Existing report to load. Omit (with report_id also omitted) for the new-report case.
report_id string No String Alternative spelling; used when reportId is absent.
type string No String Module, used to pick the column catalogue when no report id is supplied. Overwritten by the report's own type when a report is loaded.
placement string No String Placement for the new-report case.
search_val string No String Filters the available-column catalogue by display_name LIKE %search_val%.

Example request

curl -X POST 'https://api.example.com/api/tat/reports/details' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"reportId":"fM8kP3...","search_val":"hours"}'

Success response — 200 OK

{
  "name": "Open Tasks",
  "type": "task",
  "placement": "global",
  "columns": [
    { "id": 210, "column_id": 210, "field_name": "company_name", "display_name": "Company",
      "field_type": "text", "type": "task", "default_values": null }
  ],
  "selectedColumns": [
    { "id": 900, "report_id": 5, "column_id": "210", "coumn_type": "normal", "order": 1,
      "display_name": "Company", "is_display": 1, "sort": null, "sum_type": null,
      "format": null, "filters": null, "custom_filters": null, "cond_format": null,
      "formula": null, "decimal": 0, "is_header_filter": null }
  ],
  "rowGroup": [],
  "shared": 0,
  "globalType": "",
  "error": false
}
Field Type Description
name, type, placement, globalType string Report identity (empty strings for a new report).
columns[] array Available columns: reports_builder_column rows for the type (each with column_id aliased from id), merged with the report's calculated columns (column_id, display_name, field_name = the column id, field_type from the formula's output, default_values: null), sorted by display_name.
selectedColumns[] array The report's reports_builder_column_details rows, ordered by order.
rowGroup[] array The subset of selectedColumns named in the report's row_group, reversed.
shared number 1 when the report is shared with the caller, else 0.
error boolean false on success.

Errors

Status Condition Message/shape
200 reportId resolves to a report the caller cannot see { "error": true, "msg": "Report not found" }
401 No/expired session { "message": "These credentials do not match our records." }

Notes

  • The available-column catalogue is filtered by globalType: utilization and timeCompare restrict to
    the matching sub_type; a plain time report excludes
    billable_hour, non_billable_hour, no_charge_hour, fp_time_sheet_hour, fp_time_sheet_amount; a
    plain profitability report excludes pweek, pmonth, reported_hours, client_billable_hours.
POST/api/tat/reports/getcolumn

Returns the default value sets used to populate filter dropdowns in the report editor.

Module Tasks & Time > Reports (Reports Builder)
Authentication Required — authenticated session cookie
Authorization No additional role requirement; results are scoped to the caller's firm
Content type application/json

Request body

Field Type Required Validation / allowed values Description
type string Yes String (no Restricted value set) Report module; selects which reports_builder_column dropdown defaults to include.

Example request

curl -X POST 'https://api.example.com/api/tat/reports/getcolumn' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"type":"task"}'

Success response — 200 OK

{
  "error": false,
  "columnDefaultValues": {
    "company": ["Acme Ltd", "Beta Inc"],
    "users": ["Doe, Jane"],
    "user": ["Doe, Jane"],
    "assigned_name": ["Doe, Jane", "Unassigned"],
    "status": ["In Progress", "No Status"],
    "tags": ["Month End", "No Tags"],
    "item_type": ["Service", "Expense", "Product"]
  }
}
Field Type Description
error boolean Always false on this path.
columnDefaultValues object Map of field name → de-duplicated, blank-stripped option list. users and user carry the same list; assigned_name appends Unassigned; status appends No Status; tags appends No Tags; item_type is the fixed list ["Service","Expense","Product"]. Any reports_builder_column row of field_type = 'dropdown' with default_values for this type adds/overrides its own entry.

Errors

Status Condition Message/shape
400 Missing type validation error message array
401 No/expired session { "message": "These credentials do not match our records." }
POST/api/tat/reports/getdate

Returns the report's reference date column and the date window the preview should open with.

Module Tasks & Time > Reports (Reports Builder)
Authentication Required — authenticated session cookie
Authorization No additional role requirement
Content type application/json

Request body

Field Type Required Validation / allowed values Description
reportId string Yes String — plain numeric or encrypted id Report whose date defaults to compute.

Example request

curl -X POST 'https://api.example.com/api/tat/reports/getdate' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"reportId":"fM8kP3..."}'

Success response — 200 OK

{
  "doesDateNeed": true,
  "type": "",
  "displayName": "Due Date",
  "fieldName": "due",
  "filterText": "Range from 01/01/2026 to 01/31/2026",
  "dateColumns": [
    { "displayName": "Completion Date", "fieldName": "completion_date",
      "start": "2026-01-01", "end": "2026-01-31",
      "filterText": "Range from 01/01/2026 to 01/31/2026", "isFirst": true }
  ],
  "start": "2026-01-01",
  "end": "2026-01-31",
  "isFirst": false,
  "haveProjectActualBillable": false
}
Field Type Description
doesDateNeed boolean Whether the preview needs a date window at all. Forced true for a Project report that selects actual_billable_hours.
type string invoiceBudget when the report's global_type is invoiceBudget, else "".
displayName string Label for the date control; Actual Billable Hours Date on the Project/actual-billable path.
fieldName string The reference date column.
filterText string Human-readable Range from MM/DD/YYYY to MM/DD/YYYY.
dateColumns[] array Every date column on the report with its own derived range and isFirst flag (isFirst marks completion_date).
start, end string The derived YYYY-MM-DD window.
haveProjectActualBillable boolean Whether the report selects actual_billable_hours on a Project report.

Errors

Status Condition Message/shape
200 Report not found { "error": true }
400 reportId cannot be decoded { "statusCode": 400, "message": "Invalid report id", "error": "Bad Request" }
400 Missing reportId validation error message array
401 No/expired session { "message": "These credentials do not match our records." }

Notes

  • The window is derived from the column's saved filter: a greater/lower operator shifts one end by a
    day, a two-ended operator orders the pair, and a single value collapses to start === end.
POST/api/tat/reports/save

Creates or updates a report definition — the list row, its row groups and its full column-detail set.

Module Tasks & Time > Reports (Reports Builder)
Authentication Required — authenticated session cookie
Authorization placement: "global" requires a global admin; placement: "firm" requires a firm admin or global admin; personal is open. On update the report must also be visible to the caller
Content type application/json

Request body

Field Type Required Validation / allowed values Description
reportId string No String — plain numeric or encrypted id Present ⇒ update.
report_id string No String Alternative spelling; used when reportId is absent.
name string Yes String Report name; unique within (type, placement, owner).
type string Yes String (no Restricted value set) Report module.
placement string Yes String + One of global, firm, personal Where the report lives.
details array No Array of column detail objects The column definitions — see the column detail structure table above.
rowGroup array No Array of { column_id? } objects Row-group column ids, stored JSON-encoded on the report.
isShared any No untyped Accepted but not used on this path.

Example request

curl -X POST 'https://api.example.com/api/tat/reports/save' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
        "reportId": "fM8kP3...",
        "name": "Open Tasks",
        "type": "task",
        "placement": "global",
        "rowGroup": [{ "column_id": "210" }],
        "details": [
          { "columnId": "210", "coumnType": "normal", "order": 1, "displayName": "Company", "is_display": true },
          { "columnId": "calc_1", "coumnType": "calculated", "order": 2, "displayName": "Utilisation",
            "decimal": 2,
            "formula": { "formula": "(billable_time) / (total_reported_time)", "output": "percentage", "decimal": 2 } }
        ]
      }'

Success response — 200 OK

{
  "reportId": "fM8kP3...",
  "isNewReport": false,
  "error": false,
  "msg": "Reports data has been saved successfully"
}
Field Type Description
reportId string Encrypted id of the saved report.
isNewReport boolean true when a new reports_builder_list row was inserted.
error boolean false on success.
msg string Confirmation text.

Errors

Status Condition Message/shape
200 placement: "global" without global admin { "error": true, "msg": "Not allowed to place this report globally." }
200 placement: "firm" without firm/global admin { "error": true, "msg": "Not allowed to place this report at the firm level." }
200 Name already used in the target placement { "error": true, "msg": "Report name already Exist. Please provide another name" }
200 Update targets a report the caller cannot see { "error": true, "msg": "Report not found" }
400 Missing name, type or placement, or placement outside the allowed set validation error message array
401 No/expired session { "message": "These credentials do not match our records." }
413 Request body exceeds 20 MB request entity too large

Notes

  • This is the endpoint the 20 MB body limit exists for. A column carrying a per-value
    conditional-formatting rule for every distinct value (Company or Project columns with hundreds of
    entries) can produce a very large payload, so the request body limit for JSON and form-encoded bodies is
    raised to 20 MB.
  • Column sync is a soft-delete-and-upsert: details whose column_id is not in the payload are
    soft-deleted; each remaining column_id updates its live row, or inserts a new one. Re-adding a
    previously removed column creates a new row rather than reviving the soft-deleted one, so its old
    configuration and id do not come back.
  • format, filters, custom_filters, cond_format and formula are JSON-encoded on write.
    is_header_filter is an integer column and is coerced to 0/1/NULL, never JSON.
  • placement also determines the stored owner (reports_builder_list.user_id): global reports store
    NULL, firm reports store the firm id, personal reports store the user id.
POST/api/tat/reports/preview

Runs a saved report and returns rows, row-group summaries and totals — the main reporting call.

Module Tasks & Time > Reports (Reports Builder)
Authentication Required — authenticated session cookie
Authorization No additional role requirement; the report must be visible to the caller, and all data is firm-scoped (a Global Master System Administrator with no firm of their own gets an all-firms scope)
Content type application/json

Request body — all fields optional

Field Type Required Validation / allowed values Description
reportId string No String — plain numeric or encrypted id Report to run. Required in practice: without it (and without report_id) the call returns Report not found.
report_id string No String Alternative spelling; used when reportId is absent.
globalFilter object No Validated nested object { role?: string, user?: string } (both String) Cross-report role/user narrowing. Both must be set for the company narrowing to apply.
filters any No untyped Inline filter list (same structure as saved column filters).
inputDate / input_date any No untyped The header date window, typically { start, end }.
needSubtotal boolean No untyped Request the totals-only pass. Ignored for invoiceBudget reports.
neededData string No untyped details requests the flat detail rows and suppresses row grouping.
expandBy any No untyped { rowGroupFieldName: value } pairs pinning a detail fetch to one group combination; used together with neededData: 'details'.
paginate any No untyped { page?, limit?, offset? }. limit is clamped to 1..100000, default 200.
export boolean No untyped Marks the call as an export pass.
includeInactiveData boolean No untyped Include inactive records.
sorting any No untyped { field_name?, direction? }.

Example request

curl -X POST 'https://api.example.com/api/tat/reports/preview' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
        "reportId": "fM8kP3...",
        "inputDate": { "start": "2026-01-01", "end": "2026-01-31" },
        "globalFilter": { "role": "3", "user": "88" },
        "sorting": { "field_name": "due", "direction": "asc" },
        "paginate": { "page": 1, "limit": 200 },
        "includeInactiveData": false
      }'

Success response — 200 OK

{
  "columnDetail": [{ "company_name": "Acme Ltd", "due": "2026-01-15", "billable_time": 3600 }],
  "columnSummary": [],
  "colSummarySubTotal": [],
  "rowGroupColumns": [],
  "neededData": "details",
  "name": "Open Tasks",
  "type": "task",
  "globalType": "",
  "placement": "global",
  "columnList": [
    { "column_id": "company_name", "display_name": "Company", "field_name": "company_name",
      "field_type": "text", "order": 0, "format": null, "cond_format": null, "filters": null }
  ],
  "total_record": 1,
  "reportTotal": [],
  "reportRowGroupTotal": []
}

Response fields as in the Standard preview payload table above. When needSubtotal is requested the
response collapses to { "reportTotal": { ... } }.

Errors

Status Condition Message/shape
200 No usable report id, or report not visible { "error": true, "msg": "Report not found" }
200 Report has no columns { "error": true, "msg": "There is no columns in this report" }
200 A row-group query fails { "error": true, "msg": "Failed to build task row groups.", "debug": "<driver message>" }
200 Any other failure inside the preview { "error": true, "msg": "Failed to build the report preview.", "debug": "<message>" }
401 No/expired session { "message": "These credentials do not match our records." }

Notes

  • Non-HTTP failures are returned as a readable JSON envelope with a debug field and are logged
    server-side. Real HTTP errors (notably the 401) are returned unchanged, so client-side session-expiry
    handling still fires.
  • The report's type and global_type select a dedicated engine: invoice (non-budget), invoice +
    invoiceBudget, time, time + timeCompare, project, company, profitability + utilization
    (computed by dedicated database routines),
    profitability, and a generic Task engine as the fallback.
  • Hidden columns (is_display = 0) are still fetched, so they can be filtered on, grouped by and
    subtotalled — is_display is applied as a presentation filter only.
  • Calculated columns are evaluated after the query, on both detail rows and row-group summary rows.
  • BIGINT values are returned as numbers where they fit in a JavaScript number and as strings where they
    do not, so a large integer can never break response serialisation.
POST/api/tat/reports/make/export

Generates an XLSX workbook for a report and returns a one-time download link.

Module Tasks & Time > Reports (Reports Builder)
Authentication Required — authenticated session cookie
Authorization Same as preview — the report must be visible to the caller. The generated token is bound to the calling user
Content type application/json (the workbook itself is fetched separately)

Request body

Field Type Required Validation / allowed values Description
reportId string Yes String — plain numeric or encrypted id Report to export.
inputDate any No untyped The header date window, forwarded to the preview.
filters any No untyped Inline filters, forwarded to the preview.
globalFilter object No untyped; same shape as the preview's globalFilter Forwarded so the workbook matches the on-screen grid.
sorting any No untyped Forwarded so the workbook matches the on-screen order.
includeInactiveData boolean No untyped Forwarded.
exportName string No untyped File name for the download. Sanitised by removing \ / # ? " %; falls back to the report name, then report.
exportWithSummary boolean No untyped Switches to the recursive row-group hierarchy export.

Example request

curl -X POST 'https://api.example.com/api/tat/reports/make/export' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
        "reportId": "fM8kP3...",
        "inputDate": { "start": "2026-01-01", "end": "2026-01-31" },
        "globalFilter": { "role": "3", "user": "88" },
        "sorting": { "field_name": "due", "direction": "asc" },
        "includeInactiveData": false,
        "exportName": "Open Tasks January",
        "exportWithSummary": true
      }'

Success response — 200 OK

{
  "error": false,
  "message": "Export generated.",
  "redirect": "/api/tat/reports/export/generated/4f3c2b1a-9d8e-4c7b-a6f5-0e1d2c3b4a59"
}
Field Type Description
error boolean false on success.
message string Export generated.
redirect string Relative URL of the one-time download — GET /api/tat/reports/export/generated/:token.

Errors

Status Condition Message/shape
200 The underlying preview failed { "error": true, "msg": "<preview msg>" }, or { "error": true, "msg": "Failed to generate export." }
400 Missing reportId validation error message array
401 No/expired session { "message": "These credentials do not match our records." }

Notes

  • This re-runs POST /api/tat/reports/preview with the same payload plus
    paginate: { page: 1, limit: 100000 }, export: true and neededData: 'details', so a row-grouped
    report exports real detail rows rather than the grouped-summary early return. A separate pass fetches
    reportTotal, and — for exportWithSummary — the row-group summary levels.
  • The workbook opens with a header block (Report Name / Generated On / Date Range) before the column
    header row. Per-column format flags (is_bold, is_italic, is_underline, is_double_underline,
    is_box) are applied as cell styles; column widths are auto-sized to at least 20 characters.
  • The generated workbook is held as temporary server-side storage and referenced by a single-use UUID
    token recorded together with the owning user id and a creation timestamp; expired entries are swept on
    each export. The download request must be served by the same application instance that produced the
    export, so multi-instance deployments must preserve instance affinity for these routes. See
    Deployment & Operational Considerations.
GET/api/tat/reports/export/generated/:token

Streams a previously generated export workbook. One-time, session-bound.

Module Tasks & Time > Reports (Reports Builder)
Authentication Required — authenticated session cookie; token ownership is verified against the session
Authorization The token must belong to the user who generated it
Content type Binary download — application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, with a Content-Disposition attachment filename. JSON on error

Path parameters

Name Type Required Description
token string Yes The UUID from the redirect URL returned by POST /api/tat/reports/make/export (or by the standard-report export).

Example request

curl -OJ 'https://api.example.com/api/tat/reports/export/generated/4f3c2b1a-9d8e-4c7b-a6f5-0e1d2c3b4a59' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

The XLSX file itself, named from the export's exportName (or the report name), e.g.
Open Tasks January.xlsx. There is no JSON body.

Errors

Status Condition Message/shape
404 Token unknown, already used, or swept as expired { "error": true, "message": "File not found." }
403 No valid session, or the token belongs to a different user { "error": true, "message": "Not allowed to download this export." }

Notes

  • The download requires a valid session and is served only to the user who generated the token; a shared
    or forwarded link is of no use to anyone else.
  • The download is genuinely one-time: the token is invalidated before streaming begins, and the
    temporary file is removed once the download completes. A second request for the same token returns
    404.
  • Responses on this route are written directly to the HTTP response, so a 403 or 404 here is returned as
    the JSON body shown above rather than through the standard error envelope.
POST/api/tat/reports/standard/report/dropdown

Returns the option lists for the Custom tab's pre-built ("standard") report filters.

Module Tasks & Time > Reports (Reports Builder)
Authentication Required — authenticated session cookie
Authorization No additional role requirement; scoping is resolved server-side from the session, never from the payload
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmId any No untyped, optional Accepted but ignored — the firm is resolved from the session.

Example request

curl -X POST 'https://api.example.com/api/tat/reports/standard/report/dropdown' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{}'

Success response — 200 OK

{
  "error": false,
  "userOptions": [{ "id": 88, "lf_name": "Doe, Jane" }],
  "roleOptions": [{ "id": 3, "role": "Bookkeeper" }],
  "companyGroupOptions": [{ "id": 2, "name": "Retail Clients" }],
  "companyOptions": [{ "id": 501, "name": "Acme Ltd" }]
}
Field Type Description
error boolean Always false on this path.
userOptions[] array Firm users. Only deleted_at IS NULL is applied; no deleted or active filtering.
roleOptions[] array Service roles in scope.
companyGroupOptions[] array Company groups in scope.
companyOptions[] array Companies in scope.

Errors

Status Condition Message/shape
401 No/expired session { "message": "These credentials do not match our records." }

Notes

  • Firm scoping is three-way: a real firm id scopes to that firm; a Global Master System Administrator with
    no firm of their own sees everything; a firm-less non-admin sees nothing (AND firm_id = 0).
POST/api/tat/reports/standard/report/preview

Runs one of the four pre-built standard reports.

Module Tasks & Time > Reports (Reports Builder)
Authentication Required — authenticated session cookie
Authorization No additional role requirement; the firm is resolved from the session
Content type application/json

Request body

Field Type Required Validation / allowed values Description
type string Yes String + One of monthly_budget, mc_section_status, billable_hours, fp_profitability Which standard report to run.
filter object No Validated nested object Report-specific filters — see below.
isPivot boolean No untyped Request the pivoted layout.
firmId any No untyped Accepted but not used; the firm is resolved from the session.

Filter object — every field optional, grouped by the report that uses it:

Field Type Validation Used by Description
startMonth string String monthly_budget Whole-month picker, start.
endMonth string String monthly_budget Whole-month picker, end.
company any untyped shared Company filter.
role any untyped shared Single role filter (independent of user).
user any untyped shared Single user filter (independent of role).
companyGroup any untyped shared Company group filter.
asOnDate string String mc_section_status "As of" date, start-of-month; selects the mc_status snapshot.
rolesToDisplay any untyped mc_section_status Role ids that each get their own assigned-user column — distinct from role.
includeNonPostingClients any untyped shared Include non-posting clients.
includeInactiveClients any untyped shared Include inactive clients.
reportDate string String billable_hours Single "as of" date; the trailing 1/4/13-week and YTD windows are computed backwards from it.
companyIdArray any untyped billable_hours Multi-select company ids.
isExclude any untyped billable_hours When true, companyIdArray becomes a NOT IN filter instead of IN.
startDate string String fp_profitability Date-range start (distinct from the whole-month startMonth).
endDate string String fp_profitability Date-range end.
globalRole any untyped fp_profitability Role half of the collapsible Role/User pair; fp_profitability requires both or neither.
globalUser any untyped fp_profitability User half of that pair.

Example request

curl -X POST 'https://api.example.com/api/tat/reports/standard/report/preview' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
        "type": "monthly_budget",
        "filter": { "startMonth": "2026-01-01", "endMonth": "2026-03-01", "role": 3, "user": 88 },
        "isPivot": false
      }'

Success response — 200 OK

{
  "error": false,
  "data": [
    {
      "company": "Acme Ltd",
      "project": "Bookkeeping",
      "role": "Bookkeeper",
      "price": 1500,
      "task_user": "Doe, Jane",
      "task_name": "Bank Reconciliation",
      "task_actual_billable_hour": 12.5,
      "task_estimate_hour": 10,
      "task_over_under_hour": 2.5,
      "task_over_under_amount": 250,
      "task_over_under_percentage": 25,
      "budget_hours": 40,
      "actual_billable_hour": 38,
      "over_under_hour": -2,
      "budget": 6000,
      "actual_amount": 5700,
      "over_under_amount": -300,
      "over_under_percentage": -5,
      "haveExpandData": false
    }
  ]
}
Field Type Description
error boolean false on success.
data[] array Report rows. The example shows monthly_budget's row shape (the one the exporter enumerates). The row shapes for mc_section_status, billable_hours and fp_profitability are produced by their own engines and are not enumerated here.

Errors

Status Condition Message/shape
200 monthly_budget without both months { "error": true, "msg": "Please select a Start and End month." }
200 A recognised type that is not wired up in this environment { "error": true, "msg": "This standard report is not yet available in this environment." }
400 type missing or outside the allowed set validation error message array
401 No/expired session { "message": "These credentials do not match our records." }

Notes

  • fp_profitability requires globalRole and globalUser both or neither; role/user on the other
    reports are independent single filters.
  • monthly_budget months are parsed as UTC from the first 10 characters of the supplied value.
POST/api/tat/reports/standard/report/export

Generates an XLSX workbook for one of the standard reports and returns a one-time download link.

Module Tasks & Time > Reports (Reports Builder)
Authentication Required — authenticated session cookie
Authorization No additional role requirement; the firm is resolved from the session and the download token is bound to the calling user
Content type application/json (the workbook is fetched separately)

Request body — the same fields as the standard report preview (type, filter, isPivot, firmId),
plus:

Field Type Required Validation / allowed values Description
exportName string No String File name and the Report Name header cell. Defaults to Monthly Budget v Actual by Client in the header and report in the file name. Non-[\w-] characters become _ in the file name.
company string No String Display label written into the workbook's Client header cell (- when absent).
role string No String Display label for the Role header cell.
user string No String Display label for the User header cell.

Example request

curl -X POST 'https://api.example.com/api/tat/reports/standard/report/export' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
        "type": "monthly_budget",
        "filter": { "startMonth": "2026-01-01", "endMonth": "2026-03-01" },
        "exportName": "Monthly Budget Q1",
        "company": "Acme Ltd",
        "role": "Bookkeeper",
        "user": "Doe, Jane"
      }'

Success response — 200 OK

{
  "error": false,
  "redirect": "/api/tat/reports/export/generated/7b6a5c4d-3e2f-41a0-9b8c-7d6e5f4a3b21"
}
Field Type Description
error boolean false on success.
redirect string Relative URL of the one-time download — GET /api/tat/reports/export/generated/:token.

Errors

Status Condition Message/shape
200 The underlying preview failed (e.g. missing months) The preview's own envelope, e.g. { "error": true, "msg": "Please select a Start and End month." }
200 A recognised type that is not wired up in this environment { "error": true, "msg": "This standard report is not yet available in this environment." }
400 type missing or outside the allowed set validation error message array
401 No/expired session { "message": "These credentials do not match our records." }

Notes

  • mc_section_status, billable_hours and fp_profitability each have their own dedicated export builder;
    monthly_budget is assembled inline.
  • The monthly_budget workbook opens with Report Name / Start Month / End Month / Client / Role / User /
    Generated On / Generated By rows, then a blank row, then the column header row. A leading User column
    is inserted only when filter.role or filter.user was supplied. A row with haveExpandData renders
    its role as <role> (All).
  • The download reuses exactly the same token plumbing as POST /api/tat/reports/make/export — see
    GET /api/tat/reports/export/generated/:token for the one-time, session-bound retrieval semantics.

11.8 Invoicing

The Invoicing module turns approved timesheet hours and fixed-price contract lines into invoices and credit memos, routes them through an approver chain, and posts the approved batches into QuickBooks Online (QBO). It is organised into five areas — Create Invoice, Configure, Post to GL, Timesheet Details and Invoicing Audit Log. Every route in this section is served under the /api prefix, so every path below is /api/<method path>.

Invoice lifecycle. Billable work accumulates in the un-invoiced queue (time_history rows with billable time, plus fp_uninvoice_queue rows for fixed-price recurrences). Selecting queue rows creates batch_invoice rows in Preview status with a child invoice row and invoice_details lines. Routing a preview for approval moves it to Invoice and seeds invoice_approvers plus approver to-dos. Each approver records a disposition; when every approver has approved, the batch becomes Approve (or Non Posting when the invoice carries no billable lines) and is assigned a batch_no (a YmdHis timestamp string). A rejection sets Rejected; Unapprove / Re Submit reset the batch to Invoice. Approved batches are posted to QuickBooks, becoming Posted on success or Post Failed (with error_msg) on failure; unposting sets Unposted. Editing an invoice that is already Approve / Posted / Post Failed / Unposted / Non Posting unposts it in QBO, clears every approver's disposition and returns it to Invoice for a fresh approval pass.

Status values persisted on batch_invoice.status: Preview, Invoice, Re Submit, Approve, Rejected, Non Posting, Posted, Post Failed, Unposted. UnInvoice and New are request-only pseudo-statuses accepted by POST /api/invoicing/batch/list — they are not stored. The Post to GL grid additionally derives a per-batch display status with the precedence Post Failed > Approved > Posted > Unposted > No Approver(s) > Approval Pending.

Type enums. batch_invoice.type: 1 = Invoice, 2 = Credit Memo. Line-item item_type / time_history.invoicing: 1 = Hourly, 2 = Fixed Price (returned to clients as {id, name} pairs). Line-item item_detail / time_history.billing: 1 = Billable, 2 = No Charge, 3 = Non-Billable.

Approval model. Approvers are resolved per batch in tiers: an explicit company-scoped Invoice Approver assignment (role id 13) wins outright; otherwise the firm's default approvers-by-service-role (invoice_default_approver) apply, narrowed to the service roles that actually appear on the invoice's lines; a service-line approver overlay (invoice_default_sl_approver / company-scoped equivalents, role id 15 Invoice Approver By Service Line) is merged on top, gated by the firm's use_service_line_approver flag with a per-company override. Role ids used by this module: InvoiceApprover = 13, InvoiceManager = 14, InvoiceApproverByServiceLine = 15, FirmInternalAccountingManager = 17, GlobalMasterSystemAdministrator = 1.

Authentication and permissions. Every endpoint reads the session cookie and resolves the caller, returning 401 with {"message":"These credentials do not match our records."} when session.login.userId is missing or session.login.state !== 'authenticated'. Role enforcement is applied per endpoint and is documented with each endpoint below. Global request validation strips body properties that are not part of the documented request schema and applies type coercion, so a 400 Bad Request is returned for any validation violation.

QuickBooks Online side effects. The endpoints that reach QBO are: POST /api/invoicing/post-to-gl/post (create or delete invoices/credit memos), POST /api/invoicing/bulk/action (Post, UnPost and — via the delete cascade — Delete), POST /api/invoicing/preview/delete and POST /api/invoicing/preview/update (the latter deletes the QBO invoice when an already-posted invoice is edited). All other endpoints only read cached QBO metadata (qbo_items, qbo_customer, qbo_terms, class/location) or write local rows.


11.8.1 Create Invoice

GET/api/invoicing/fetch/dropdown

Returns every dropdown/reference list the Create Invoice and Ad Hoc Invoice screens need in one payload — companies, company groups, projects, service lines and roles, users, the firm's QBO customer/item/term/class/location lists, and the firm's invoice configuration.

Module Tasks & Time > Invoicing > Create Invoice
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Companies are scoped by role: GlobalMasterSystemAdministrator (1) sees every company in every firm, a firm user sees their firm's, a mapped user sees their user_company_mapping set. Inactive companies are kept.
Content type application/json

Example request

curl -X GET 'https://api.example.com/api/invoicing/fetch/dropdown' \
  -H 'Accept: application/json' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "companies": [{ "id": 41, "name": "Northwind Manufacturing", "deleted": 0 }],
  "companyGroups": [{ "id": 7, "group": "Midwest Group" }],
  "customerList": [{ "id": 12, "qbo_id": "58", "name": "Northwind Manufacturing", "term": 3 }],
  "firmAccountingMangers": [214],
  "invoiceBudgetReport": "eyJpdiI6...",
  "invoiceConfig": {
    "invoiceStartDate": "2026-01-01",
    "todoDueDate": 5,
    "ignoreTimesheetDate": null,
    "invoiceMessage": "Thank you for your business.",
    "invoiceStatement": "Please remit within 30 days.",
    "paymentTerms": null,
    "qboInvoiceDate": 0,
    "isInvoiceManager": false
  },
  "invoiceManagers": [188, 214],
  "itemList": [{ "id": 3, "qbo_id": "45", "name": "Consulting", "description": "Consulting services", "price": 150 }],
  "itemListFixed": [{ "id": 9, "qbo_id": "61", "name": "Monthly Retainer", "description": "", "price": 2500 }],
  "projects": [{ "id": 88, "name": "FY26 Audit", "company_id": 41, "billing": 1, "active_status": 1, "service_line": 4, "invoicing": 1 }],
  "qboCategoriesConfig": { "track_class": true, "assign_class": 1, "track_location": false },
  "qboClass": [{ "id": 2, "qbo_id": "200000000001445721", "cls_name": "Advisory" }],
  "qboLocation": [],
  "serviceLines": [{ "id": 4, "name": "Tax", "is_active": 0 }],
  "serviceRoles": [{ "id": 6, "role": "Senior Accountant", "price_per_hour": 150 }],
  "termList": [{ "id": 1, "qbo_id": "3", "name": "Net 30" }],
  "users": [{ "id": 188, "name": "Alvarez, Dana" }]
}
Field Type Description
companies array Companies in the caller's scope (id, name, deleted).
companyGroups array Company groups available for filtering.
customerList array Cached QBO customers for the firm's internal company.
firmAccountingMangers number[] User ids holding FirmInternalAccountingManager (17) in this firm.
invoiceBudgetReport string Encrypted id of the firm's invoice budget report.
invoiceConfig object The firm's invoice_configure row.
invoiceManagers number[] User ids holding InvoiceManager (14).
itemList / itemListFixed array Cached QBO items for hourly / fixed-price selection.
projects array Projects with billing / service-line / invoicing flags.
qboCategoriesConfig object track_class, assign_class, track_location from the QBO company preferences.
qboClass / qboLocation array Cached QBO classes and locations (qbo_id kept as a string — ids can be 18 digits).
serviceLines array Non-deleted service lines for the firm (or global), with is_active.
serviceRoles array Non-deleted service roles with price_per_hour.
termList array Cached QBO payment terms.
users array Firm users as {id, name}.

Errors

Status Condition Message/shape
401 No authenticated session, or the user row is missing/deleted {"message":"These credentials do not match our records."}

Notes

  • QBO ids (qbo_id, class ids) are returned as strings throughout; QuickBooks class/customer/location ids can be 18-digit values that lose precision if coerced to JavaScript numbers.
  • Reads cached QBO metadata only — no live QuickBooks call.

POST/api/invoicing/batch/list

The single list endpoint behind the Create Invoice screen. Depending on status it returns the un-invoiced queue, creates preview invoices from the queue, or lists existing batch_invoice rows.

Module Tasks & Time > Invoicing > Create Invoice
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Company scope is role-derived (inactive companies retained); any companies sent in the body are intersected with that scope.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
status string Yes String UnInvoice returns the grouped un-invoiced queue; New creates preview invoices from the queue and then lists them as Preview; Invoice lists the whole post-preview status set (Invoice, Re Submit, Approve, Posted, Post Failed, Unposted, Rejected, Non Posting); any other value is matched literally against batch_invoice.status. Empty defaults to Preview.
filter object No Optional — untyped Free-form filter bag (see Notes for the keys actually read).
page integer No Numeric (numeric strings are coerced), integer Omit or 0 for an unpaginated result; any > 0 value returns a 30-row window.
sortBy string No String Column to sort by.
orderBy string No String Sort direction.
companies object[] No Array Company selections; only each element's id is read, then intersected with the caller's scope.
batchInvoiceId string No String Encrypted batch_invoice.id used to narrow the list to a single invoice.
ignoreWarning integer No Numeric (numeric strings are coerced), integer 1 bypasses the not-approved-timesheet warning on the UnInvoice path. Forced on for New.

Example request

curl -X POST 'https://api.example.com/api/invoicing/batch/list' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
    "status": "Preview",
    "filter": { "invoiceByServiceLine": true, "date": "2026-08-31" },
    "companies": [{ "id": 41 }]
  }'

Success response — 201 Created

[
  {
    "invoice": {
      "id": 9012,
      "customer_qbo": "58",
      "customer_email": "ap@northwind.example",
      "customer_cc": "",
      "customer_bcc": "",
      "customer_address": "100 Mill Rd, Springfield",
      "terms": "3",
      "class_qbo": "200000000001445721",
      "location_qbo": "",
      "tax_rate": 0,
      "message_invoice": "Thank you for your business.",
      "message_statement": "",
      "details": [
        {
          "id": 55011, "item_detail": 1, "item_type": 1, "item_description": "August advisory",
          "item_name": "Consulting", "item_project": 88, "item_service_line": 4, "item_user": 188,
          "item_qbo": "45", "quantity": 12.5, "rate": 150, "amount": 1875, "is_taxable": 0,
          "from": "Timesheet", "invoice_date": null,
          "project_name": "FY26 Audit", "service_line_name": "Tax", "user_name": "Alvarez, Dana"
        }
      ]
    },
    "id": 4471,
    "invoice_id": 9012,
    "company_id": 41,
    "company": { "id": 41, "name": "Northwind Manufacturing", "deleted": 0 },
    "date": "2026-08-31",
    "due_date": "2026-09-30",
    "invoice_amount": 1875,
    "tax_amount": 0,
    "amount": 1875,
    "invoice_no": "INV-2096",
    "batch_no": null,
    "company_group": { "id": 7, "group": "Midwest Group" },
    "status": "Preview",
    "error_msg": null,
    "type": "Invoice",
    "encId": "eyJpdiI6...",
    "invoice_approvers": [],
    "resolve_pending": 0
  }
]
Field Type Description
invoice object Present only when the batch has an invoice row; carries the header plus details line items. QBO reference ids are strings.
id number batch_invoice.id.
invoice_id number invoice.id (omitted when 0).
company / company_group object Resolved company and company-group labels.
invoice_amount / tax_amount / amount number Falls back to the computed line total when the stored value is 0.
status string Persisted batch_invoice.status.
type string Credit Memo when batch_invoice.type = 2, otherwise Invoice.
encId string Encrypted batch_invoice.id.
invoice_approvers array Approvers with their latest disposition.
resolve_pending number Count of approvers who have not yet dispositioned.

The UnInvoice path returns a different, grouped shape (one row per company, optionally split by service line and/or project) with status: "UnInvoice", type: "Invoice", a details array of raw queue rows, and the resolved invoice header fields (customer_qbo, customer_email, customer_address, class_qbo, location_qbo, terms, message_invoice, message_statement).

When the not-approved-timesheet warning fires, the endpoint instead returns the warning payload:

{ "showWarning": true, "warningData": [{ "user_id": 188, "user": "Alvarez, Dana", "week": "2026-W34", "status": "Unsubmit" }] }

Errors

Status Condition Message/shape
400 status missing or not a string Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
201 Preview creation found a queue line with no QBO product mapping {"error":true,"message":"Some invoice(s) don't have Mapping with Products."}
201 One or more previews failed to create {"error":true,"message":"Failed to create N invoice(s): <company>: <reason> \| ..."}

Notes

  • filter is a free-form object. The keys read by this endpoint are: amount, approver, batchNo, company, companyGroup, companyGroupName, companyName, date, dueDate, excludeFilter, invoiceByFixedPrice, invoiceByProject, invoiceByServiceLine, invoiceDate, invoiceNo, invoiceStartDate, itemType, paymentTerms, project, serviceLine, startDate, status, throughDate, toDate.
  • status: "New" is a write operation: it creates Preview batch invoices from the un-invoiced queue (in chunks of 50 companies) before listing them. status: "UnInvoice" is read-only.
  • The not-approved-timesheet warning is evaluated only when the request includes hourly item types and ignoreWarning is not set; it is force-bypassed for status: "New".
  • invoiceByServiceLine / invoiceByProject change the grouping of the UnInvoice result as well as the shape of previews created from it.

POST/api/invoicing/todo/pending/approval-list

Lists the batch invoices awaiting approval that are attached to a given to-do, for the Home/To-do invoice modal.

Module Tasks & Time > Invoicing > Create Invoice
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; no company scope is applied — rows are reached only through the to-do's own invoice_todo_mapping rows.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
todoId integer No Numeric (numeric strings are coerced), integer tasks.id of the to-do. Omitted or 0 returns [].
offset integer No Numeric (numeric strings are coerced), integer Accepted by the request schema; not read by this endpoint.
limit integer No Numeric (numeric strings are coerced), integer Accepted by the request schema; not read by this endpoint.

Example request

curl -X POST 'https://api.example.com/api/invoicing/todo/pending/approval-list' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "todoId": 30712 }'

Success response — 201 Created

[
  {
    "id": 4471,
    "encId": "eyJpdiI6...",
    "invoice_no": "INV-2096",
    "company_id": 41,
    "company_name": "Northwind Manufacturing",
    "date": "2026-08-31",
    "due_date": "2026-09-30",
    "amount": 1875,
    "status": "Invoice",
    "invoice_approvers": [{ "user_id": 214, "name": "Brooks, Lee", "status": "Approve" }]
  }
]
Field Type Description
id / encId number / string batch_invoice.id and its encrypted form.
date / due_date string ISO YYYY-MM-DD, empty string when null.
invoice_approvers array Every non-deleted approver with the status of that user's latest invoice_disposition row ('' when none). Approve renders a green check, Reject a red cross.

Errors

Status Condition Message/shape
401 No authenticated session {"message":"These credentials do not match our records."}

Notes

  • Resolution is via invoice_todo_mapping.task_id → batch_invoice_id, not by scanning invoice_approvers for the current user.
  • Rows are ordered by batch_invoice.due_date ascending, then id descending.
  • Archived approver users (users.deleted = 1) are excluded from invoice_approvers.

POST/api/invoicing/preview/un-invoice-to-preview

Converts explicitly selected un-invoiced queue rows into Preview batch invoices.

Module Tasks & Time > Invoicing > Create Invoice
Authentication Required — authenticated session cookie
Authorization InvoiceManager (14) required.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
data object[] Yes Array Un-invoiced queue rows, in the shape returned by POST /api/invoicing/batch/list with status: "UnInvoice" (each details element).
filter object No Optional — untyped Same filter bag as the batch list; invoiceByServiceLine / invoiceByProject / invoiceByFixedPrice control how the queue rows are split into invoices.

Example request

curl -X POST 'https://api.example.com/api/invoicing/preview/un-invoice-to-preview' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
    "data": [
      { "company_id": 41, "item_detail": 1, "item_type": 1, "item_qbo": 45,
        "item_description": "August advisory", "quantity": 12.5, "rate": 150,
        "amount": 1875, "is_taxable": 0, "time_history_ids": [77120, 77121] }
    ],
    "filter": { "invoiceByServiceLine": true }
  }'

Success response — 201 Created

{ "error": false, "message": "Preview Invoice(s) created successfully." }
Field Type Description
error boolean false on success.
message string Human-readable result.

Errors

Status Condition Message/shape
400 data is not an array Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
403 Caller is not an InvoiceManager {"message":"You are not authorized to perform this invoice action."}
201 data normalised to an empty set {"error":true,"message":"No uninvoiced queue items selected."}

Notes

  • Creates batch_invoice (status Preview) + invoice + invoice_details rows and marks the consumed time_history rows as previewed / consumes the matching fp_uninvoice_queue rows.
  • No QuickBooks call is made.

POST/api/invoicing/queue/delete

Removes lines from the un-invoiced queue without invoicing them: hourly lines have their billable time moved to non-billable, fixed-price lines are marked excluded.

Module Tasks & Time > Invoicing > Create Invoice
Authentication Required — authenticated session cookie
Authorization InvoiceManager (14) required.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
data object[] Yes Array Queue rows to remove. Elements are not individually validated; this endpoint reads from, item_type, time_history_id, invoice_price_list_id (or invoicePriceListId) and invoice_date (or invoiceDate).
fromPreview boolean No Boolean Accepted by the request schema; not read by this endpoint.

Example request

curl -X POST 'https://api.example.com/api/invoicing/queue/delete' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "data": [ { "time_history_id": 77120, "item_type": 1, "from": "Timesheet" } ] }'

Success response — 201 Created

{ "error": false, "message": "Queue line updated successfully." }

Errors

Status Condition Message/shape
400 data is not an array Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
403 Caller is not an InvoiceManager {"message":"You are not authorized to perform this invoice action."}
201 data empty {"error":true,"message":"No queue line selected."}

Notes

  • A row whose from contains "fixed" (case-insensitive) or whose item_type is 2 inserts an fp_uninvoice_queue row with is_excluded = 1, removing that occurrence from the queue.
  • Any other row with a time_history_id has its non_billable_time increased by its billable_time, and billable_time set to 0 — the hours stay in the timesheet but leave the billable queue.

POST/api/invoicing/queue/not-approved/timesheet/list

Returns the users/weeks whose timesheets are not yet approved for the companies in scope — the blocking warning shown before previewing invoices.

Module Tasks & Time > Invoicing > Create Invoice
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Any companyIdArray sent in the body is intersected with the caller's role-derived company scope; an empty array falls back to the whole scope.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
filter object No Optional — untyped Filter bag; the date window and week filters are read from it.
pagination object No Optional — untyped Accepted by the request schema; not read by this endpoint.
companyIdArray integer[] No Array, numeric (numeric strings are coerced) Companies to check; intersected with the caller's scope.

Example request

curl -X POST 'https://api.example.com/api/invoicing/queue/not-approved/timesheet/list' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "companyIdArray": [41, 42], "filter": { "date": "2026-08-31" } }'

Success response — 201 Created

{
  "showWarning": true,
  "warningData": [
    { "user_id": 188, "user": "Alvarez, Dana", "week": "2026-W34", "status": "Unsubmit" }
  ]
}
Field Type Description
showWarning boolean true when warningData is non-empty.
warningData[].user_id number time_history.user_id.
warningData[].user string users.lf_name.
warningData[].week string YYYY-Www, the Monday-anchored work week.
warningData[].status string Latest timesheet_disposition_status.status for that user/week; defaults to Unsubmit. Weeks already Approve/Approved are excluded.

Errors

Status Condition Message/shape
401 No authenticated session {"message":"These credentials do not match our records."}

Notes

  • Returns { "showWarning": false, "warningData": [] } when the caller has no companies in scope or the time_history table is absent.
  • The week string is computed from time_history.created_at (falling back to date), matching the queue's own week derivation so the warning and the queue never disagree.

POST/api/invoicing/product/config

Resolves the description, QBO class and rate to apply to an invoice line when a product/item is selected.

Module Tasks & Time > Invoicing > Create Invoice
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the GL product mapping lookup is scoped to the caller's firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
productId integer No Numeric (numeric strings are coerced), integer Alternative field name for the QuickBooks item id, accepted for compatibility with earlier clients; used only when itemQbo is absent.
itemQbo integer No Numeric (numeric strings are coerced), integer QBO item id to look up.
serviceRoleId integer No Numeric (numeric strings are coerced), integer Service role to price from, when no GL mapping override supplies one.
companyId integer No Numeric (numeric strings are coerced), integer Company used for the service_role_users rate override.
description string No String Fallback description echoed back when no firm-level mapping exists.
classId string No String Fallback QBO class id (string — class ids can be 18 digits).
price number No Numeric (numeric strings are coerced), number Fallback rate echoed back when no rate can be resolved.

Example request

curl -X POST 'https://api.example.com/api/invoicing/product/config' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "itemQbo": 45, "companyId": 41, "serviceRoleId": 6, "description": "Consulting services", "classId": "200000000001445721", "price": 150 }'

Success response — 201 Created

{ "description": "Consulting services", "class": "200000000001445721", "rate": 165 }
Field Type Description
description string Mapping's product_description, else the cached qbo_items description, else the posted description.
class string Mapping's class, else the cached item's class id, else the posted classId. Exact string — never numerically coerced.
rate number service_role_users.price_per_hour for the company+role (only when the override's user still exists), else service_roles.price_per_hour, else the posted price.

Errors

Status Condition Message/shape
401 No authenticated session {"message":"These credentials do not match our records."}

Notes

  • The firm-level override lives in invoicing_product_mapping (keyed by firm_id + product); when no row exists, the endpoint echoes back the fields the client already sent rather than returning blanks.
  • Reads cached QBO item metadata only; no live QuickBooks call.

POST/api/invoicing/preview/delete

Deletes preview (or any) invoices with a full cascade: restores hours and fixed-price occurrences to the queue, removes approver to-dos, and voids the QuickBooks invoice when one exists.

Module Tasks & Time > Invoicing > Create Invoice
Authentication Required — authenticated session cookie
Authorization InvoiceManager (14) required.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
data object[] Yes Array Rows to delete. Elements are not individually validated; this endpoint reads batchInvoiceId and invoiceId. When invoiceId is missing, 0, or equal to batchInvoiceId, it is resolved from the batch.

Example request

curl -X POST 'https://api.example.com/api/invoicing/preview/delete' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "data": [ { "batchInvoiceId": 4471, "invoiceId": 9012 } ] }'

Success response — 201 Created

{ "error": false, "message": "Invoice(s)/Credit Memo(s) deleted successfully." }

Errors

Status Condition Message/shape
400 data is not an array Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
403 Caller is not an InvoiceManager {"message":"You are not authorized to perform this invoice action."}
201 Nothing was deleted {"error":true,"message":"Something went wrong."}

Notes

  • Reaches QuickBooks Online. For each row that carries a qbo_invoice_id, the invoice (or credit memo, when type = 2) is deleted or voided in the firm's QBO realm before the local rows are soft-deleted.
  • Cascade order: remove invoice_todo_mapping rows (and any orphaned parent task) → write a Deleted audit row with a readable snapshot → restore line items to the un-invoiced queue (invoice_time_history_mapping deleted and time_history.previewed reset; fixed-price lines re-queued into fp_uninvoice_queue) → delete in QBO → soft-delete batch_invoice, invoice and invoice_details.
  • The audit snapshot is captured before the soft-delete, because it reads the rows being removed.

POST/api/invoicing/preview/update

Saves an edited invoice: header fields, line items, dates and amounts. Editing an already-approved or posted invoice unposts it and returns it to the approval queue.

Module Tasks & Time > Invoicing > Create Invoice
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session — any authenticated user may save an invoice they can address by id.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
data object Yes Nested objects validated The invoice payload.

data — invoice payload

Field Type Required Validation / allowed values Description
id integer Yes Numeric (numeric strings are coerced), integer batch_invoice.id.
date string No String Invoice date. Rejected when present but blank.
due_date string No String Due date. Rejected when present but blank.
invoice_amount number No Numeric (numeric strings are coerced) Pre-tax total written to batch_invoice.
tax_amount number No Numeric (numeric strings are coerced) Tax total.
amount number No Numeric (numeric strings are coerced) Grand total.
lineItems object[] No Array, each item validated Full line-item set. Omitting the key leaves lines untouched; sending it rewrites the set line by line (see Notes).
invoice object No Nested objects validated Invoice header.
notMoveQueueItems integer[] No Array, numeric (numeric strings are coerced) invoice_details.id values the user chose not to return to the un-invoiced queue when removed.

data.invoice — invoice header (every field optional; only fields actually present are persisted)

Field Type Required Validation / allowed values Description
id integer No Numeric (numeric strings are coerced), integer invoice.id.
customer_qbo string No String QBO customer id, kept as a string. Rejected when present but blank or "0".
customer_email / customer_cc / customer_bcc / customer_address string No String Customer contact fields.
terms string No String QBO payment-term id, kept as a string. Rejected when present but blank or "0".
class_qbo / location_qbo string No String QBO class / location ids, kept as strings.
tax_rate number No Numeric (numeric strings are coerced), number Tax rate applied to the invoice.
message_invoice / message_statement string No String Invoice and statement messages.

data.lineItems[] — line item (all fields optional)

Field Type Required Validation / allowed values Description
id integer No Numeric (numeric strings are coerced), integer Existing invoice_details.id; absent/unknown means a new line.
item_detail integer No Numeric (numeric strings are coerced), integer 1 Billable, 2 No Charge, 3 Non-Billable.
item_type integer No Numeric (numeric strings are coerced), integer 1 Hourly, 2 Fixed Price.
item_qbo integer No Numeric (numeric strings are coerced), integer QBO item id, accepted as a number on this field.
item_description string No String Line description.
item_project / item_service_line / item_user integer No Numeric (numeric strings are coerced), integer Project / service line / user attribution.
quantity / rate / amount number No Numeric (numeric strings are coerced) Line quantity, unit rate and extended amount.
is_taxable integer No Numeric (numeric strings are coerced), integer 0 / 1.
class_qbo string No String Line-level QBO class id (string).
location_qbo integer No Numeric (numeric strings are coerced), integer Line-level QBO location id.
from string No String Provenance, e.g. Timesheet, Fixed Price. On an existing line, omitting it preserves the stored value.
invoice_date string No String Occurrence date for fixed-price lines.
invoice_price_list_id integer No Numeric (numeric strings are coerced), integer Source invoice_price_list.id for fixed-price lines.

Example request

curl -X POST 'https://api.example.com/api/invoicing/preview/update' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
    "data": {
      "id": 4471,
      "date": "2026-08-31",
      "due_date": "2026-09-30",
      "invoice_amount": 1875, "tax_amount": 0, "amount": 1875,
      "invoice": { "id": 9012, "customer_qbo": "58", "terms": "3", "class_qbo": "200000000001445721", "tax_rate": 0 },
      "lineItems": [
        { "id": 55011, "item_detail": 1, "item_type": 1, "item_qbo": 45,
          "item_description": "August advisory", "quantity": 12.5, "rate": 150,
          "amount": 1875, "is_taxable": 0 }
      ],
      "notMoveQueueItems": []
    }
  }'

Success response — 201 Created

{ "error": false, "message": "Invoice updated successfully.", "data": null }
Field Type Description
error boolean false on success.
message string Result message.
data null Always null; the field is retained on the response shape for compatibility with earlier clients. Callers re-fetch the list themselves.

Errors

Status Condition Message/shape
400 data.id missing or not an integer, or a nested field fails validation Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
201 data.id is 0 or the batch_invoice table is absent {"error":true,"message":"Invoice preview not found."}
201 invoice.customer_qbo present but blank / "0" {"error":true,"message":"The Customer Field is required."}
201 invoice.terms present but blank / "0" {"error":true,"message":"The Terms Field is required."}
201 date present but blank {"error":true,"message":"The Invoice Date Field is required."}
201 due_date present but blank {"error":true,"message":"The Due Date Field is required."}

Notes

  • Reaches QuickBooks Online. When the invoice's current status is Approve, Posted, Post Failed, Unposted or Non Posting, the edit unposts it: a Posted invoice is deleted or voided in QBO, every approver's disposition is cleared, and the batch is reset to Invoice for a fresh approval pass.
  • Header validation runs before any mutation, so a rejected payload has no side effects.
  • Line handling is per line: lines matched by id are updated in place (preserving their invoice_time_history_mapping), unknown ids are inserted, and removed lines are returned to the un-invoiced queue unless listed in notMoveQueueItems.
  • Approver to-do due dates are recomputed after the save.
  • Audit rows are written only for genuine changes — a header row when any header field changed, plus one row per changed/added/removed line; a true no-op edit writes nothing.
  • Blank QBO reference ids are persisted as NULL, never as the empty string (the underlying columns are integer).

POST/api/invoicing/preview/route/approval

Routes one or more preview invoices for approval: seeds approvers, generates approver to-dos and sets the batch status to Invoice.

Module Tasks & Time > Invoicing > Create Invoice
Authentication Required — authenticated session cookie
Authorization InvoiceManager (14) required.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
ids integer[] Yes Array, numeric (numeric strings are coerced) batch_invoice.id values to route. Non-positive values are dropped.

Example request

curl -X POST 'https://api.example.com/api/invoicing/preview/route/approval' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "ids": [4471, 4472] }'

Success response — 201 Created

{ "error": false, "message": "Invoiced successfully." }

Errors

Status Condition Message/shape
400 ids is not an array Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
403 Caller is not an InvoiceManager {"message":"You are not authorized to perform this invoice action."}
201 ids resolves to an empty set {"error":true,"message":"No invoices selected for approval routing."}

Notes

  • Approvers are seeded only for batches that have none yet; routing a batch that already has approvers leaves them unchanged.
  • Writes one Updated audit row per invoice recording the status transition {status: <old>} → {status: "Invoice"}.
  • Sets batch_invoice.status = 'Invoice' and invoiced_by = <caller>. batch_no is not assigned here; it is assigned later, at approval time.

POST/api/invoicing/preview/disposition

Records an approver's decision on an invoice (approve / reject / unapprove / re-submit) and recomputes the batch status.

Module Tasks & Time > Invoicing > Create Invoice
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. The role recorded on the audit row is resolved from the acting user's approver role on that invoice (InvoiceApprover / InvoiceApproverByServiceLine), except Re Submit, which is always recorded as InvoiceManager (14).
Content type application/json

Request body

Field Type Required Validation / allowed values Description
batchInvoiceId integer Yes Numeric (numeric strings are coerced), integer batch_invoice.id.
userId integer Yes Numeric (numeric strings are coerced), integer The approver the disposition is recorded against (may differ from the acting user, who is stored as action_by).
status string Yes One of Approve, Reject, Unapprove, Re Submit The disposition.
reason string No String Free-text reason; required by the UI for Reject / Unapprove / Re Submit, where it also becomes an invoice comment.

Example request

curl -X POST 'https://api.example.com/api/invoicing/preview/disposition' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "batchInvoiceId": 4471, "userId": 214, "status": "Approve" }'

Success response — 201 Created

{
  "error": false,
  "message": "Disposition updated successfully.",
  "status": "Approve",
  "batch_no": "20260901103012"
}
Field Type Description
status string The new batch_invoice.status.
batch_no string | null The assigned posting batch number — only non-null when the new status is Approve.

Errors

Status Condition Message/shape
400 status outside the allowed set, or batchInvoiceId / userId not integers Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
201 batchInvoiceId or userId is 0 {"error":true,"message":"Invalid disposition payload."}

Notes

  • Status transitions: Reject → Rejected. Approve → Approve once every approver has approved and the invoice has at least one billable line, Non Posting when every approver has approved but no billable lines remain, otherwise Invoice. Unapprove and Re Submit → Invoice, and both clear is_disposition for all approvers on the batch.
  • Approve and Reject remove the disposing approver's to-do; Unapprove regenerates the approver to-dos.
  • Reject, Unapprove and Re Submit open (or reuse) a comment thread and store the reason as an invoice comment.
  • Reaching Approve assigns batch_no (a YmdHis timestamp), which is what makes the batch visible to Post to GL.
  • Audit rows: one primary row with the action mapped to Approved / Unapproved / Rejected / Resubmitted, plus a secondary Updated row documenting the invoice entering the batch (on first batch_no assignment) or leaving it (on Reject / Unapprove when a batch_no existed).
  • No QuickBooks call is made.

POST/api/invoicing/preview/post/comment

Adds a quick comment to an invoice and records a Commented audit entry.

Module Tasks & Time > Invoicing > Create Invoice
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. The invoice's company must belong to the caller's firm. The audit role_id is resolved from the caller's role on that company.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
batchInvoiceId integer Yes Numeric (numeric strings are coerced), integer batch_invoice.id.
comment string Yes String Comment text; rejected when it trims to empty.
assignTo integer[] No Array, numeric (numeric strings are coerced) Accepted by the request schema; not read by this endpoint.

Example request

curl -X POST 'https://api.example.com/api/invoicing/preview/post/comment' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "batchInvoiceId": 4471, "comment": "Confirmed rate with the client." }'

Success response — 201 Created

{ "error": false, "message": "Comment created successfully." }

Errors

Status Condition Message/shape
400 batchInvoiceId not an integer or comment not a string Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
401 The invoice's company is outside the caller's firm {"message":"These credentials do not match our records."}
201 batchInvoiceId is 0 {"error":true,"message":"Batch invoice id is required."}
201 The invoice has no resolvable company {"error":true,"message":"Invoice not found."}
201 comment trims to empty {"error":true,"message":"Comment is required."}

Notes

  • The comment is written directly to the invoice comment table; it does not use the shared open-comment thread mechanism, which covers only the Monthly Close and Accounting modules.
  • Writes an audit row with action: "Commented" and original_data: { "Comment": "<text>" }.

POST/api/invoicing/fetch/custorcomp

Two-way lookup between a Genie company and its mapped QuickBooks customer — used by the Ad Hoc Invoice screen to fill the counterpart field when either side is picked.

Module Tasks & Time > Invoicing > Create Invoice
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. QBO lookups are scoped to the caller's firm-internal company.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id integer Yes Numeric (numeric strings are coerced), integer A company.id when field = "company", or a QBO customer id when field = "customer".
field string Yes One of company, customer Which side of the pair is being supplied.

Example request

curl -X POST 'https://api.example.com/api/invoicing/fetch/custorcomp' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "id": 41, "field": "company" }'

Success response — 201 Created

{
  "customer": { "company_id": 41, "customer": 58, "term": 3, "class": "200000000001445721", "location": 0 },
  "customerList": { "id": 12, "qbo_id": "58", "name": "Northwind Manufacturing", "email": "ap@northwind.example" }
}
Field Type Description
customer object | null Returned when field = "company" — the invoicing_customer_mapping row for that company.
company object | null Returned when field = "customer" — the GL customer mapping matched by QBO customer id within the caller's firm.
customerList object | null The cached qbo_customer row for the resolved customer id, or null when it cannot be resolved.

Errors

Status Condition Message/shape
400 field outside company / customer, or id not an integer Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}

Notes

  • The response key differs by direction: customer + customerList for field: "company", company + customerList for field: "customer".
  • Reads cached QBO customer data only; no live QuickBooks call.

POST/api/invoicing/adhocinvoice/save

Creates an ad-hoc invoice or credit memo directly, bypassing the un-invoiced queue. The new document is created in Invoice status with approvers and to-dos already seeded.

Module Tasks & Time > Invoicing > Create Invoice
Authentication Required — authenticated session cookie
Authorization InvoiceManager (14) required. companyId must belong to the caller's firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
type integer Yes Numeric (numeric strings are coerced), integer batch_invoice.type — 1 = Invoice, 2 = Credit Memo.
companyId integer Yes Numeric (numeric strings are coerced), integer Billed company; must belong to the caller's firm.
customerQbo string No String QBO customer id, kept as a string (18-digit safe). Blank is stored as 0.
customerAddress / customerEmail / customerCc / customerBcc string No String Customer contact fields.
terms string No String QBO payment-term id as a string; blank stored as NULL.
date string Yes String Invoice date.
dueDate string No String Due date; defaults to date when omitted.
classQbo string No String QBO class id as a string; blank stored as NULL.
locationQbo string No String QBO location id as a string; blank stored as 0.
messageInvoice string No String Message on invoice; falls back to the firm's configured invoice message when omitted.
messageStatement string No String Message on statement; falls back to the firm's configured statement message.
taxRate number No Numeric (numeric strings are coerced), number Tax rate; defaults to 0.
invoiceAmount number Yes Numeric (numeric strings are coerced), number Pre-tax total.
taxAmount number Yes Numeric (numeric strings are coerced), number Tax total.
amount number Yes Numeric (numeric strings are coerced), number Grand total.
lineItems object[] Yes Array, each item validated Line items (see below).

lineItems[] — line item

Field Type Required Validation / allowed values Description
item_detail integer Yes Numeric (numeric strings are coerced), integer 1 Billable, 2 No Charge, 3 Non-Billable.
item_type integer Yes Numeric (numeric strings are coerced), integer 1 Hourly, 2 Fixed Price.
item_qbo integer Yes Numeric (numeric strings are coerced), integer QBO item id.
project_id integer No Numeric (numeric strings are coerced), integer Project attribution; stored as 0 when omitted.
service_line_id integer No Numeric (numeric strings are coerced), integer Service-line attribution; stored as 0 when omitted.
item_description string Yes String Line description.
quantity number Yes Numeric (numeric strings are coerced), number Quantity.
rate number Yes Numeric (numeric strings are coerced), number Unit rate.
is_taxable integer Yes Numeric (numeric strings are coerced), integer 0 / 1.
class_qbo string No String Line-level QBO class id (string).
user_id integer No Numeric (numeric strings are coerced), integer User attribution; stored as 0 when omitted.

Example request

curl -X POST 'https://api.example.com/api/invoicing/adhocinvoice/save' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
    "type": 1,
    "companyId": 41,
    "customerQbo": "58",
    "terms": "3",
    "date": "2026-09-01",
    "dueDate": "2026-10-01",
    "classQbo": "200000000001445721",
    "taxRate": 0,
    "invoiceAmount": 500, "taxAmount": 0, "amount": 500,
    "lineItems": [
      { "item_detail": 1, "item_type": 1, "item_qbo": 45,
        "item_description": "Ad hoc advisory", "quantity": 2, "rate": 250, "is_taxable": 0 }
    ]
  }'

Success response — 201 Created

{ "error": false, "message": "Invoice Created/Updated Successfully.", "id": 4489 }
Field Type Description
id number The newly created batch_invoice.id.

Errors

Status Condition Message/shape
400 Any required field missing or of the wrong type Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
401 companyId outside the caller's firm {"message":"These credentials do not match our records."}
403 Caller is not an InvoiceManager {"message":"You are not authorized to perform this invoice action."}
201 batch_invoice / invoice / invoice_details tables unavailable {"error":true,"message":"Invoicing tables are unavailable."}
201 batch_invoice insert failed {"error":true,"message":"Failed to create ad-hoc invoice."}
201 invoice insert failed {"error":true,"message":"Failed to create invoice payload."}

Notes

  • A credit memo is only ever raised from this endpoint (type: 2); everything created from the un-invoiced queue is an invoice.
  • The invoice number is generated server-side; each line's amount is recomputed as quantity × rate rounded to 2 decimals rather than trusting a client-supplied total.
  • The company's company group is resolved and stored on the batch.
  • Because the document is created directly in Invoice status, approvers are seeded and approver to-dos generated immediately — there is no separate route-for-approval step.
  • Writes a Created audit row with a readable snapshot (customer / address / terms / dates / line-item count).
  • No QuickBooks call is made at save time; the document reaches QBO only via Post to GL.

11.8.2 Configure

POST/api/task/invoicing/config

Loads an Invoicing Configure screen's data set. The from discriminator selects which screen: the default (managers, users, messages, dates), the Invoice Approver screen, or the Service-Line-based Approver screen.

Module Tasks & Time > Invoicing > Configure > General
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. The body's firmId is ignored; the firm is resolved from the caller's own users.firm_id (falling back to 0 = the global configuration set), so a caller can never read another firm's configuration.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmId integer Yes Numeric (numeric strings are coerced), integer Required by the request schema but overridden server-side with the caller's own firm id.
from string No String invoiceApprover, slBasedInvoiceApprover, or omitted/other for the default configuration payload.

Example request

curl -X POST 'https://api.example.com/api/task/invoicing/config' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "firmId": 12, "from": "invoiceApprover" }'

Success response — 201 Created

Default payload (no from):

{
  "invoiceManagers": [{ "id": 188, "name": "Alvarez, Dana" }],
  "firmUsers": [{ "id": 214, "name": "Brooks, Lee" }],
  "firmAllUsers": [{ "id": 188, "name": "Alvarez, Dana" }, { "id": 214, "name": "Brooks, Lee" }],
  "serviceRoles": [{ "id": 6, "role": "Senior Accountant" }],
  "serviceLines": [{ "id": 4, "name": "Tax" }],
  "qboCategoriesConfig": { "track_class": true, "assign_class": 1, "track_location": false },
  "invoiceDate": "2026-01-01",
  "todoDueDate": 5,
  "ignoreTimesheetDate": null,
  "invoiceMessage": "Thank you for your business.",
  "invoiceStatement": "Please remit within 30 days."
}

from: "invoiceApprover" returns { serviceRoles, defaultApprover } where defaultApprover is { service_role_id, user_id }[] from invoice_default_approver.
from: "slBasedInvoiceApprover" returns { serviceLines, serviceRoles, defaultSLApprover } where defaultSLApprover is { service_line_id, service_role_id, user_id }[] from invoice_default_sl_approver.

Field Type Description
invoiceManagers array Firm users holding InvoiceManager (14).
firmUsers array Firm users not holding InvoiceManager.
firmAllUsers array All non-deleted firm users.
serviceRoles array Service roles with deleted IN (0, 2) for the firm or global.
serviceLines array Active (is_active = 0), non-deleted service lines for the firm or global.
invoiceDate string | null The firm's invoice start date.
todoDueDate number Relative day offset for approver to-do due dates.
ignoreTimesheetDate string | null Cut-off date below which the not-approved-timesheet warning is suppressed.

Errors

Status Condition Message/shape
400 firmId missing or not an integer Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}

Notes

  • A firm-less Global Master System Administrator resolves to firm 0 and therefore edits the global configuration row set — there is no firm picker on these screens.
  • Returns { invoiceManagers: [], firmUsers: [], firmAllUsers: [], serviceRoles: [], serviceLines: [] } when the users table is unavailable.

POST/api/task/invoicing/config/save-invoice-manager

Sets the firm's Invoice Managers (role 14) by assigning and de-assigning the role.

Module Tasks & Time > Invoicing > Configure > General
Authentication Required — authenticated session cookie
Authorization Global admin or firm admin required, and firmId must equal the caller's own resolved firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmId integer Yes Numeric (numeric strings are coerced), integer Must match the caller's own firm id.
managers object[] Yes Array Selected managers. Elements are not individually validated; this endpoint reads id (falling back to user_id).

Example request

curl -X POST 'https://api.example.com/api/task/invoicing/config/save-invoice-manager' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "firmId": 12, "managers": [{ "id": 188 }, { "id": 214 }] }'

Success response — 201 Created

{ "error": false, "message": "Invoice Managers Created/Updated Successfully." }

Errors

Status Condition Message/shape
400 firmId or managers missing/mistyped Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
403 Caller is neither global nor firm admin, or firmId ≠ the caller's firm {"message":"You do not have permission to update invoice managers."}

Notes

  • Performs a keyed upsert with revive: users no longer selected have their assign_user_roles row soft-deleted; selected users have an existing row revived (deleted_at = NULL) or a new row inserted with company_id = NULL and role_from = NULL.
  • Only users belonging to the caller's firm are considered for de-assignment.

POST/api/task/invoicing/config/save-invoice-approvers

Sets the firm's default invoice approvers per service role, then re-resolves approvers on every un-approved invoice in the firm.

Module Tasks & Time > Invoicing > Configure > Approvers
Authentication Required — authenticated session cookie
Authorization Global admin or firm admin required, and firmId must equal the caller's own resolved firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmId integer Yes Numeric (numeric strings are coerced), integer Must match the caller's own firm id.
defaultApprover object[] Yes Array, each item validated Approver assignments.
defaultApprover[].service_role_id integer Yes Numeric (numeric strings are coerced), integer Service role the approver covers.
defaultApprover[].user_id integer Yes Numeric (numeric strings are coerced), integer Approving user.

Example request

curl -X POST 'https://api.example.com/api/task/invoicing/config/save-invoice-approvers' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "firmId": 12, "defaultApprover": [ { "service_role_id": 6, "user_id": 214 } ] }'

Success response — 201 Created

{ "error": false, "message": "Invoice Approver Created/Updated Successfully." }

Errors

Status Condition Message/shape
400 Missing/mistyped firmId or a malformed defaultApprover row Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
403 Caller is neither global nor firm admin, or firmId ≠ the caller's firm {"message":"You do not have permission to update this invoicing configuration."}

Notes

  • After persisting, the change is propagated synchronously to existing un-approved invoices, wrapped in a queue_manager audit row that is marked complete (1) on success or failed (2) on error; an error is re-thrown after the audit row is closed.

POST/api/task/invoicing/config/save-sl-approvers

Sets the firm's default approvers per service line + service role, then re-resolves approvers on existing un-approved invoices.

Module Tasks & Time > Invoicing > Configure > Approvers
Authentication Required — authenticated session cookie
Authorization Global admin or firm admin required, and firmId must equal the caller's own resolved firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmId integer Yes Numeric (numeric strings are coerced), integer Must match the caller's own firm id.
defaultSLApprover object[] Yes Array, each item validated Service-line approver assignments.
defaultSLApprover[].service_line_id integer Yes Numeric (numeric strings are coerced), integer Service line covered.
defaultSLApprover[].service_role_id integer Yes Numeric (numeric strings are coerced), integer Service role covered.
defaultSLApprover[].user_id integer Yes Numeric (numeric strings are coerced), integer Approving user.

Example request

curl -X POST 'https://api.example.com/api/task/invoicing/config/save-sl-approvers' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "firmId": 12, "defaultSLApprover": [ { "service_line_id": 4, "service_role_id": 6, "user_id": 214 } ] }'

Success response — 201 Created

{ "error": false, "message": "Invoice ServiceLineBased Approver Created/Updated Successfully." }

Errors

Status Condition Message/shape
400 Missing/mistyped firmId or a malformed defaultSLApprover row Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
403 Caller is neither global nor firm admin, or firmId ≠ the caller's firm {"message":"You do not have permission to update this invoicing configuration."}

Notes

  • Service-line approvers are only applied to an invoice when the firm's use_service_line_approver flag is on (per-company override: cs_use_service_line_invoice_approver.invoice_approver).
  • Same synchronous re-resolve + queue_manager audit lifecycle as save-invoice-approvers.

POST/api/task/invoicing/config/save-date

One endpoint serving three mutually exclusive date settings: the invoice start date, the approver to-do due-date offset, and the ignore-not-approved-timesheet cut-off.

Module Tasks & Time > Invoicing > Configure > General
Authentication Required — authenticated session cookie
Authorization Global admin or firm admin required. The request body carries no firmId, so the firm is always the caller's own.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
date string Yes String The value being saved. A date string for the start-date and ignore-timesheet modes; a numeric string day offset when isTodoDate is true.
isTodoDate boolean No Boolean true saves date into invoice_configure.todo_due_date as an integer offset.
ignoreTimesheetDate boolean No Boolean Present (either value) selects the ignore-timesheet cut-off mode, writing invoice_configure.ignore_timesheet_date.

Example request

curl -X POST 'https://api.example.com/api/task/invoicing/config/save-date' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "date": "2026-01-01" }'

Success response — 201 Created

{ "error": false, "message": "Invoice Date Updated Successfully." }
Condition message
ignoreTimesheetDate present Ignore Not Approved Timesheet Date Updated Successfully.
isTodoDate present (and ignoreTimesheetDate absent) Todo Due Date Updated Successfully.
Neither present Invoice Date Updated Successfully.

Errors

Status Condition Message/shape
400 date missing or not a string Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
403 Caller is neither global nor firm admin {"message":"You do not have permission to update this invoicing configuration."}

Notes

  • The three modes are strictly exclusive — exactly one column is written per call, so saving the ignore-timesheet cut-off can never overwrite the invoice start date.
  • todo_due_date is coerced with truncation toward zero, falling back to 0 for a non-numeric value.
  • The row is created on first save (invoice_configure insert keyed on firm_id), updated thereafter.
  • Saving the invoice date does not push a live notification update; the notification badge refreshes on the user's next poll.

POST/api/task/invoicing/config/save-message

Saves the firm's default "Message on Invoice" and "Message on Statement" text.

Module Tasks & Time > Invoicing > Configure > General
Authentication Required — authenticated session cookie
Authorization Global admin or firm admin required. No firmId is accepted in the request body — the firm is always the caller's own.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
message string No String Default message on invoice. Only written when the key is present.
statement string No String Default message on statement. Only written when the key is present.

Example request

curl -X POST 'https://api.example.com/api/task/invoicing/config/save-message' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "message": "Thank you for your business.", "statement": "Please remit within 30 days." }'

Success response — 201 Created

{ "error": false, "message": "Invoice Message/Statement Updated Successfully." }

Errors

Status Condition Message/shape
400 message or statement present but not a string Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
403 Caller is neither global nor firm admin {"message":"You do not have permission to update this invoicing configuration."}

Notes

  • Omitting a field leaves the stored value untouched; sending an empty string clears it.
  • These values are the fallback used when an ad-hoc invoice is saved without explicit messages.

POST/api/task/invoicing/config/gl-product

Loads the mapping data for one of three Configure screens, selected by from: GL Product/Service mapping, Labor Items, or Service-Line Class Mapping.

Module Tasks & Time > Invoicing > Configure > Services / Labor / Service-Line Class Mapping
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. The body's firmId is ignored; the caller's own users.firm_id is used (falling back to 0).
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmId integer Yes Numeric (numeric strings are coerced), integer Required by the request schema but overridden server-side with the caller's firm.
from string Yes String glProductMapping, laborItem or slClassMapping. Any other value returns {}.

Example request

curl -X POST 'https://api.example.com/api/task/invoicing/config/gl-product' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "firmId": 12, "from": "glProductMapping" }'

Success response — 201 Created

{
  "serviceRoles": [
    { "id": 6, "role": "Senior Accountant", "product": "45", "product_description": "Consulting services", "class": "200000000001445721", "location": 0 }
  ],
  "qboProducts": [
    { "id": 3, "qbo_id": "45", "name": "Consulting", "description": "Consulting services", "price": 150, "class_id": "200000000001445721", "type": "Service",
      "qboClass": { "id": 2, "qbo_id": "200000000001445721", "cls_name": "Advisory" } }
  ],
  "qboClass": [{ "id": 2, "qbo_id": "200000000001445721", "cls_name": "Advisory" }],
  "qboLocation": []
}
from Response shape
glProductMapping { serviceRoles, qboProducts, qboClass, qboLocation } — each qboProducts entry is enriched with its resolved qboClass object (or null).
laborItem { qboProducts } — each entry enriched with qboClass plus laborData ({ id, item_id, company_id, labor }, {} when unmapped).
slClassMapping { slClassMapping, qboClass } — slClassMapping is { service_line_id, name, class_qbo }[], class_qbo defaulting to "0".
anything else {}

Errors

Status Condition Message/shape
400 firmId or from missing/mistyped Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}

Notes

  • When the firm has use_service_role disabled, service roles fall back to the global set (firm_id = 0); when use_service_line is disabled, service lines fall back to the global set (firm_id IS NULL).
  • QBO item and class ids are returned as exact strings.
  • Reads cached QBO metadata only; no live QuickBooks call.

POST/api/task/invoicing/config/gl-customer

Loads the GL Customer mapping grid — each of the firm's companies with its mapped QBO customer, payment term, class and location, plus the dropdown option lists.

Module Tasks & Time > Invoicing > Configure > Customers
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. The body's firmId is ignored; the caller's own users.firm_id is used.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmId integer Yes Numeric (numeric strings are coerced), integer Required by the request schema but overridden server-side.
filter object[] No Array, each item validated "Filter by Company" selections.
filter[].id integer No Numeric (numeric strings are coerced), integer company.id to keep in the grid.

Example request

curl -X POST 'https://api.example.com/api/task/invoicing/config/gl-customer' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "firmId": 12, "filter": [ { "id": 41 } ] }'

Success response — 201 Created

{
  "data": [
    { "id": 41, "name": "Northwind Manufacturing", "override_price": 0,
      "customer": 58, "term": 3, "class": "200000000001445721", "location": 0 }
  ],
  "dropdownOptions": {
    "companyOptions": [{ "id": 41, "name": "Northwind Manufacturing" }, { "id": 42, "name": "Contoso Ltd" }],
    "customerOption": [{ "id": 12, "qbo_id": "58", "name": "Northwind Manufacturing", "term": 3 }],
    "termsOptions": [{ "id": 1, "qbo_id": "3", "name": "Net 30" }],
    "classOptions": [{ "id": 0, "name": "--Select--", "cls_name": "--Select--", "qbo_id": "0" }],
    "locationOptions": [{ "id": 0, "name": "--Select--", "cls_name": "--Select--", "qbo_id": "0" }]
  }
}
Field Type Description
data array The grid rows — narrowed by filter. class is an exact string; customer, term, location are numeric ids (0 when unmapped).
dropdownOptions.companyOptions array Always the full company list, regardless of the applied filter, so more companies can be added to the filter.
dropdownOptions.customerOption / termsOptions array Cached QBO customers and payment terms for the firm's internal company.
dropdownOptions.classOptions / locationOptions array Cached QBO classes/locations, each prefixed with a --Select-- sentinel (id: 0, qbo_id: "0").

Errors

Status Condition Message/shape
400 firmId missing/mistyped, or a malformed filter element Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}

Notes

  • Returns empty data and empty option lists when the company table is unavailable.
  • Reads cached QBO metadata only; no live QuickBooks call.

POST/api/task/invoicing/config/save-gl-product

Saves the firm's GL Product/Service mapping — which QBO item, description, class and location each service role bills to.

Module Tasks & Time > Invoicing > Configure > Services
Authentication Required — authenticated session cookie
Authorization Global admin or firm admin required, and firmId must equal the caller's own resolved firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmId integer Yes Numeric (numeric strings are coerced), integer Must match the caller's own firm id.
data object[] Yes Array Mapping rows. Elements are not individually validated; this endpoint reads id (falling back to service_role_id, then role_id), product, product_description, class and location. Rows with no resolvable service role id are skipped.

Example request

curl -X POST 'https://api.example.com/api/task/invoicing/config/save-gl-product' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
    "firmId": 12,
    "data": [ { "id": 6, "product": "45", "product_description": "Consulting services", "class": "200000000001445721", "location": 0 } ]
  }'

Success response — 201 Created

{ "error": false, "message": "Invoice GL Product/Service Mapping Created/Updated Successfully." }

Errors

Status Condition Message/shape
400 firmId or data missing/mistyped Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
403 Caller is neither global nor firm admin, or firmId ≠ the caller's firm {"message":"You do not have permission to update this invoicing configuration."}

Notes

  • Upsert keyed on (firm_id, service_role_id) in invoicing_product_mapping; an existing soft-deleted row is revived (deleted_at = NULL).
  • product and class are persisted as exact strings — QBO item and class ids can exceed JavaScript's safe integer range, and coercing them produces ids QuickBooks rejects with "Invalid Reference Id".

POST/api/task/invoicing/config/save-labor-items

Flags which QBO items count as labor for the firm's internal company.

Module Tasks & Time > Invoicing > Configure > Labor
Authentication Required — authenticated session cookie
Authorization Global admin or firm admin required, and firmId must equal the caller's own resolved firm. Rows are additionally filtered to companies belonging to that firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmId integer Yes Numeric (numeric strings are coerced), integer Must match the caller's own firm id.
data object[] Yes Array Labor-item rows. Elements are not individually validated; this endpoint reads company_id (used for the firm-scope check), qbo_id (falling back to item_id), and the labor flag from laborData.labor (falling back to labor). Rows with no item_id or no company_id are skipped.

Example request

curl -X POST 'https://api.example.com/api/task/invoicing/config/save-labor-items' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "firmId": 12, "data": [ { "company_id": 41, "qbo_id": 45, "laborData": { "labor": 1 } } ] }'

Success response — 201 Created

{ "error": false, "message": "Labor Items Created/Updated Successfully." }

Errors

Status Condition Message/shape
400 firmId or data missing/mistyped Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
403 Caller is neither global nor firm admin, or firmId ≠ the caller's firm {"message":"You do not have permission to update this invoicing configuration."}

Notes

  • Rows whose company_id is not one of the caller's firm's companies are silently dropped before persisting — labor_items has no firm column, so this check is what prevents cross-firm writes.
  • Upsert keyed on (item_id, company_id); labor is normalised to 0 / 1.

POST/api/task/invoicing/config/save-gl-customer

Saves the GL Customer mapping — which QBO customer, payment term, class and location each company invoices to.

Module Tasks & Time > Invoicing > Configure > Customers
Authentication Required — authenticated session cookie
Authorization Global admin or firm admin required. The request body carries no firmId, so the firm is always the caller's own; rows are additionally filtered to that firm's companies.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
data object[] Yes Array Mapping rows. Elements are not individually validated; this endpoint reads id (falling back to company_id), customer, term, class and location. Rows with no resolvable company id are skipped.

Example request

curl -X POST 'https://api.example.com/api/task/invoicing/config/save-gl-customer' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "data": [ { "id": 41, "customer": 58, "term": 3, "class": "200000000001445721", "location": 0 } ] }'

Success response — 201 Created

{ "error": false, "message": "Invoice GL Customer Mapping Created/Updated Successfully." }

Errors

Status Condition Message/shape
400 data is not an array Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
403 Caller is neither global nor firm admin {"message":"You do not have permission to update this invoicing configuration."}

Notes

  • Rows whose company lies outside the caller's firm are silently dropped — invoicing_customer_mapping keys purely on company_id and has no firm column.
  • Upsert keyed on company_id; class is stored as an exact string, customer / term / location as integers.

POST/api/task/invoicing/config/save-sl-class

Saves the Service-Line → QBO Class mapping for the firm.

Module Tasks & Time > Invoicing > Configure > Service-Line Class Mapping
Authentication Required — authenticated session cookie
Authorization Global admin or firm admin required, and firmId must equal the caller's own resolved firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmId integer Yes Numeric (numeric strings are coerced), integer Must match the caller's own firm id.
mappingData object[] Yes Array Mapping rows. Elements are not individually validated; this endpoint reads service_line_id (falling back to id) and class_qbo. Rows with no service line id are skipped.

Example request

curl -X POST 'https://api.example.com/api/task/invoicing/config/save-sl-class' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "firmId": 12, "mappingData": [ { "service_line_id": 4, "class_qbo": "200000000001445721" } ] }'

Success response — 201 Created

{ "error": false, "message": "Invoice ServiceLine Mapping Updated Successfully." }

Errors

Status Condition Message/shape
400 firmId or mappingData missing/mistyped Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
403 Caller is neither global nor firm admin, or firmId ≠ the caller's firm {"message":"You do not have permission to update this invoicing configuration."}

Notes

  • Upsert keyed on (service_line_id, firm_id) in the resolved service-line-class mapping table; class_qbo is stored as an exact string.

POST/api/task/invoicing/config/export/file

Generates the GL Customer Mapping Excel template (a blank mapping sheet plus an "Available Data" reference sheet) and returns a one-shot download link.

Module Tasks & Time > Invoicing > Configure > Customers
Authentication Required — authenticated session cookie
Authorization Global admin or firm admin required, and firmId must equal the caller's own resolved firm.
Content type application/json (the link it returns serves .xlsx)

Request body

Field Type Required Validation / allowed values Description
firmId integer Yes Numeric (numeric strings are coerced), integer Must match the caller's own firm id.

Example request

curl -X POST 'https://api.example.com/api/task/invoicing/config/export/file' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "firmId": 12 }'

Success response — 201 Created

{ "redirect": "/api/task/invoicing/config/export/file/generated/3f7c1a90-6a2b-4d55-9c0e-2f1b8a44e211" }
Field Type Description
redirect string Relative URL of the generated file; the token is single-use.

Errors

Status Condition Message/shape
400 firmId missing or not an integer Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
403 Caller is neither global nor firm admin, or firmId ≠ the caller's firm {"message":"You do not have permission to update this invoicing configuration."}

Notes

  • Workbook contains two sheets: GL Customer Mapping (header row only: Company, GL Customer Name, Class, Location) and Available Data (four parallel value columns of valid names).
  • Payment Terms is absent from the template; it is derived from the chosen customer on import.
  • The generated workbook is held as temporary server-side storage and referenced by a single-use token. The download request must be served by the same application instance that produced the export, so multi-instance deployments must preserve instance affinity for these routes; see Deployment & Operational Considerations. The file is deleted after the first successful download.

POST/api/task/invoicing/config/import/file

Imports a completed GL Customer Mapping workbook. Valid rows are saved even when other rows fail.

Module Tasks & Time > Invoicing > Configure > Customers
Authentication Required — authenticated session cookie
Authorization Global admin or firm admin required, and firmId must equal the caller's own resolved firm.
Content type multipart/form-data (the upload is sent in the file part)

Request body — multipart form

Field Type Required Validation / allowed values Description
file file Yes Extension must be .xlsx or .xlx The completed workbook.
firmId integer Yes Numeric (numeric strings are coerced), integer Must match the caller's own firm id.

Example request

curl -X POST 'https://api.example.com/api/task/invoicing/config/import/file' \
  -b 'genie.sid=<session-cookie>' \
  -F 'firmId=12' \
  -F 'file=@GL Customer Mapping Template.xlsx'

Success response — 201 Created

{
  "error": false,
  "message": "GL mapping has been imported Successfully",
  "headArray": ["Company", "GL Customer Name", "Class", "Location", "Reason"],
  "errorArray": [],
  "downloadLink": "#"
}

Partial failure:

{
  "error": true,
  "message": "There are some failure to import GL mapping data",
  "headArray": ["Company", "GL Customer Name", "Class", "Location", "Reason"],
  "errorArray": [["Acme Widgets", "Unknown Customer", "", "", "Invalid Values for customer"]],
  "downloadLink": "/api/task/invoicing/config/export/file/generated/9a3e..."
}
Field Type Description
headArray string[] Column headers for the on-screen error grid.
errorArray array[] One row per hard-failed import row, with a trailing reason cell.
downloadLink string Link to a regenerated workbook pre-filled with the failed rows, or "#" when everything imported.

Errors

Status Condition Message/shape
400 firmId missing or not an integer Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
403 Caller is neither global nor firm admin, or firmId ≠ the caller's firm {"message":"You do not have permission to update this invoicing configuration."}
201 No file uploaded {"error":true,"message":"Please upload an Excel file."}
201 Extension not .xlsx / .xlx {"error":true,"message":"Please provide Excel sheet format supported file."}
201 Sheet empty or header-only {"error":true,"message":"Empty Excel Sheet.Please put on some data to import."}
201 Header row does not match {"error":true,"message":"GL Customer Mapping import format is wrong."}

Notes

  • Company and GL Customer Name are hard validations — an unrecognised value fails the row. Class and Location are soft: an unknown name is flagged in the downloadable error workbook but the row still imports with that field unset.
  • Payment Terms is derived from the matched customer's QBO term, never read from the sheet.
  • A trailing Reason column (present on a re-downloaded error sheet) is stripped before header validation, so an error workbook can be corrected and re-uploaded directly.

GET/api/task/invoicing/config/export/file/generated/:token

Streams a previously generated Excel template (GL Customer Mapping or Fixed Price Invoicing) and deletes it.

Module Tasks & Time > Invoicing > Configure > Customers
Authentication Not required — possession of the single-use token is the only credential
Authorization No role requirement
Content type Binary download (.xlsx attachment)

Path parameters

Name Type Required Description
token string Yes The UUID returned in the redirect of an export endpoint.

Example request

curl -X GET 'https://api.example.com/api/task/invoicing/config/export/file/generated/3f7c1a90-6a2b-4d55-9c0e-2f1b8a44e211' \
  -o 'GL Customer Mapping Template.xlsx'

Success response — 200 OK

Binary .xlsx body with a Content-Disposition attachment filename of either GL Customer Mapping Template.xlsx or Fixed Price Invoicing Template.xlsx, depending on which export created it.

Errors

Status Condition Message/shape
404 Token unknown or already consumed {"error":true,"message":"File not found."}

Notes

  • The token is invalidated before streaming begins, so each link works exactly once; the temporary file is removed once the download completes.
  • Tokens are held in temporary server-side storage: they do not survive an application restart, and the download must be served by the same application instance that produced the export. Multi-instance deployments must preserve instance affinity for these routes; see Deployment & Operational Considerations.

POST/api/task/invoicing/config/fixed-price

Loads the fixed-price item list for a project, together with the QBO product list needed to edit it.

Module Tasks & Time > Invoicing > Configure > Fixed Price
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. The decrypted clientId company must belong to the caller's firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
clientId string Yes String AES-encrypted company.id (a plain numeric string is also accepted); decrypted server-side.
projectId string Yes String AES-encrypted project.id, same handling.

Example request

curl -X POST 'https://api.example.com/api/task/invoicing/config/fixed-price' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "clientId": "eyJpdiI6...", "projectId": "eyJpdiI6..." }'

Success response — 201 Created

{
  "company": { "id": 5, "name": "Expert Solutions LLP", "qbo_connection": 1, "encId": "eyJpdiI6..." },
  "project": { "id": 88, "name": "FY26 Audit", "company_id": 41 },
  "projectCompany": { "id": 41, "name": "Northwind Manufacturing" },
  "qboInvoiceData": [
    { "id": 9, "qbo_id": "61", "name": "Monthly Retainer", "description": "Retainer", "price": 2500 }
  ],
  "invoiceList": [
    { "id": 301, "client_id": 41, "project_id": 88, "item_qbo": "61", "item_name": "Monthly Retainer",
      "description": "Retainer", "amount": 2500, "quantity": 1,
      "start_date": "2026-01-01", "end_date": "2026-12-31",
      "frequency": "monthly", "comments": "", "order": 0, "is_manual_desc": 0 }
  ]
}
Field Type Description
company object | null The firm-internal company (the QBO-connected one), with qbo_connection and its encrypted id — used for the "reconnect QuickBooks" banner. Not the project's client.
project object | null The project, looked up by id alone.
projectCompany object | null The project's actual client company — what the UI labels "Company".
qboInvoiceData array Cached QBO products, excluding type = "group" items; qbo_id is an exact string.
invoiceList array The project's invoice_price_list rows. description is refreshed from the current QBO item description unless is_manual_desc = 1.

Errors

Status Condition Message/shape
400 clientId or projectId missing / not strings Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
401 The decrypted client company is outside the caller's firm {"message":"These credentials do not match our records."}

Notes

  • Returns { company: null, project: null, projectCompany: null, qboInvoiceData: [], invoiceList: [] } when invoice_price_list is unavailable.
  • Reads cached QBO product data only; no live QuickBooks call.

POST/api/task/invoicing/config/fixed-price/save

Inserts or updates the fixed-price item lines for a project.

Module Tasks & Time > Invoicing > Configure > Fixed Price
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. The decrypted clientId company must belong to the caller's firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
clientId string Yes String AES-encrypted (or plain) company.id.
projectId string Yes String AES-encrypted (or plain) project.id.
firmId integer Yes Numeric (numeric strings are coerced), integer Written to invoice_price_list.firm_id when that column exists.
invoicePriceList object[] Yes Array Line rows. Elements are not individually validated; this endpoint reads id (0/absent = insert), item_qbo, item_name, description, amount, quantity, start_date, end_date, frequency, comments, order, is_manual_desc.

Example request

curl -X POST 'https://api.example.com/api/task/invoicing/config/fixed-price/save' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
    "clientId": "eyJpdiI6...", "projectId": "eyJpdiI6...", "firmId": 12,
    "invoicePriceList": [
      { "id": 0, "item_qbo": 61, "item_name": "Monthly Retainer", "description": "Retainer",
        "amount": 2500, "quantity": 1, "start_date": "2026-01-01", "end_date": "2026-12-31",
        "frequency": "monthly", "comments": "", "order": 0, "is_manual_desc": 0 }
    ]
  }'

Success response — 201 Created

{
  "error": false,
  "message": "Fixed price items saved successfully.",
  "invoice_price_list": [{ "id": 301, "order": 0 }]
}
Field Type Description
invoice_price_list array The project's saved rows as {id, order}, so the client can patch newly assigned ids in place by order without a full reload.

Errors

Status Condition Message/shape
400 Any required field missing or mistyped Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
401 The decrypted client company is outside the caller's firm {"message":"These credentials do not match our records."}
201 invoice_price_list table unavailable {"error":true,"message":"Fixed price table is unavailable."}

Notes

  • The extended total column, where present, is written as amount × quantity.
  • Rows with id > 0 are updated; everything else is inserted.
  • This configures the recurrence source; the actual invoice lines are produced later by the un-invoiced queue.

DELETE/api/task/invoicing/config/fixed-price/:id

Soft-deletes a single fixed-price item line.

Module Tasks & Time > Invoicing > Configure > Fixed Price
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. The row's client_id company must belong to the caller's firm; this is checked before the row is deleted.
Content type application/json

Path parameters

Name Type Required Description
id string Yes invoice_price_list.id, either plain numeric or AES-encrypted.

Example request

curl -X DELETE 'https://api.example.com/api/task/invoicing/config/fixed-price/301' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{ "error": false, "message": "Fixed price item deleted successfully." }

Errors

Status Condition Message/shape
401 No authenticated session {"message":"These credentials do not match our records."}
401 The row's client company is outside the caller's firm {"message":"These credentials do not match our records."}
200 id does not decode to a positive integer {"error":true,"message":"Invalid fixed price id."}
200 The row does not exist or has no client_id {"error":true,"message":"Fixed price item not found."}

Notes

  • Soft delete only (deleted_at is set); historical invoices referencing the price-list row are unaffected.
  • The firm-ownership check applies to every request: a row whose client company lies outside the caller's firm cannot be deleted by supplying its id.

POST/api/task/project/fixed/download/file

Generates the Fixed Price Invoicing Excel template for the firm — a blank entry sheet plus "Existing Data" and "Available Data" reference sheets — and returns a one-shot download link.

Module Tasks & Time > Invoicing > Configure > Fixed Price
Authentication Required — authenticated session cookie
Authorization Firm-ownership check only — firmId must equal the caller's own resolved firm. No admin requirement, because this route is reached from the Project Fixed Price Items page, which is open to any Tasks & Time user.
Content type application/json (the link it returns serves .xlsx)

Request body

Field Type Required Validation / allowed values Description
firmId integer Yes Numeric (numeric strings are coerced), integer Must match the caller's own firm id.

Example request

curl -X POST 'https://api.example.com/api/task/project/fixed/download/file' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "firmId": 12 }'

Success response — 201 Created

{ "redirect": "/api/task/invoicing/config/export/file/generated/8b1d4c22-0f6e-4a71-b3d2-77c9e51a6f04" }

Errors

Status Condition Message/shape
400 firmId missing or not an integer Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
403 firmId ≠ the caller's own firm {"message":"Invalid firm context."}

Notes

  • Template header (sheet Fixed Price Invoicing): Company Name, Project, Item Name, Item Description, Price (EA), Quantity, Recurrence Start date (mm/dd/yyyy), Recurrence type, Repeat every, Repeat until, Weekend rule, Rules, Comments.
  • The Existing Data sheet appends an extra Internel Id (Do not edit/delete) column; the blank entry sheet does not carry it.
  • Weekend rule values map to the labels Leave As Is, Prior Business day, Next Business day, Skip day.
  • The download is served by GET /api/task/invoicing/config/export/file/generated/:token.

POST/api/task/project/fixed/import/file

Imports a completed Fixed Price Invoicing workbook, inserting new price-list rows and updating existing ones by their internal id.

Module Tasks & Time > Invoicing > Configure > Fixed Price
Authentication Required — authenticated session cookie
Authorization Firm-ownership check only — firmId must equal the caller's own resolved firm. No admin requirement (same reasoning as the download route).
Content type multipart/form-data (the upload is sent in the file part)

Request body — multipart form

Field Type Required Validation / allowed values Description
file file Yes Extension must be .xlsx, .xls or .xlx The completed workbook.
firmId integer Yes Numeric (numeric strings are coerced), integer Must match the caller's own firm id.

Example request

curl -X POST 'https://api.example.com/api/task/project/fixed/import/file' \
  -b 'genie.sid=<session-cookie>' \
  -F 'firmId=12' \
  -F 'file=@Fixed Price Invoicing Template.xlsx'

Success response — 201 Created

{ "error": false, "message": "Fixed price template imported successfully (14 row(s))." }

Partial failure:

{
  "error": true,
  "message": "The following values are failed to import due to missing fields",
  "header": ["Company Name", "Project", "Item Name", "Item Description", "Price (EA)", "Quantity",
             "Recurrence Start date (mm/dd/yyyy)", "Recurrence type", "Repeat every", "Repeat until",
             "Weekend rule", "Rules", "Comments", "Reason"],
  "errorData": [["Acme Widgets", "Unknown Project", "Retainer", "", "2500", "1", "", "", "", "", "", "", "", "Project not found"]],
  "downloadLink": "/api/task/invoicing/config/export/file/generated/1c9f..."
}
Field Type Description
header string[] Column headers for the error grid; Internel Id (Do not edit/delete) is included only for an update-mode import, and Reason is always last.
errorData array[] The failed rows with a trailing reason cell.
downloadLink string Regenerated three-sheet workbook with the failed rows pre-filled for correction and re-upload.

Errors

Status Condition Message/shape
400 firmId missing or not an integer Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
403 firmId ≠ the caller's own firm {"message":"Invalid firm context."}
201 No file uploaded {"error":true,"message":"Please upload an Excel file."}
201 Extension not .xlsx / .xls / .xlx {"error":true,"message":"Please provide Excel sheet format supported file."}
201 Sheet empty or header-only {"error":true,"message":"Empty Excel Sheet.Please put on some data to import."}
201 Header row has missing or extra columns {"error":true,"message":"Fixed Price Invoicing import Format is wrong."}

Notes

  • Company and project names are resolved against the caller's firm only.
  • A row carrying an Internel Id value updates that invoice_price_list row (and revives it if soft-deleted); a row without one is appended after the project's current maximum order.
  • is_manual_desc is set to 1 when the sheet's Item Description differs from the current QBO item description, so a later reload will not overwrite the manual text.
  • Partial import: every valid row is saved even when other rows fail.

11.8.3 Post to GL

GET/api/invoicing/post-to-gl/get

Returns the Post to GL grid — one row per posting batch, with its derived status, invoice counts and any QuickBooks error messages.

Module Tasks & Time > Invoicing > Post to GL
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Company scope is role-derived: GlobalMasterSystemAdministrator (1) sees every company in every firm, a firm user their firm's, a mapped user their user_company_mapping set.
Content type application/json

Example request

curl -X GET 'https://api.example.com/api/invoicing/post-to-gl/get' \
  -H 'Accept: application/json' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

[
  {
    "batch_no": "20260901103012",
    "no_of_invoice": 3,
    "status": "Approved",
    "posted": "0/3",
    "allStatus": ["Approve"],
    "error_msg": "",
    "noApprovers": 0,
    "type": "Invoice",
    "credit_memo_count": 0
  }
]
Field Type Description
batch_no string Posting batch number (YmdHis). Grid is ordered by this, descending.
no_of_invoice number Invoices in the batch.
status string Derived display status: Post Failed > Approved > Posted > Unposted > No Approver(s) > Approval Pending.
posted string "<posted count>/<total>".
allStatus string[] The distinct persisted batch_invoice.status values in the batch.
error_msg string Distinct error_msg values joined by ,\n — one per line for the popover.
noApprovers number 1 when the batch has no approvers configured (never 1 when any invoice is Post Failed).
type string Credit Memo only when every invoice in the batch is a credit memo; otherwise Invoice.
credit_memo_count number Count of credit memos in the batch.

Errors

Status Condition Message/shape
401 No authenticated session {"message":"These credentials do not match our records."}

Notes

  • Every load first re-derives the persisted status of batches in Approve / Invoice from the live approver dispositions: a batch whose approvers have all approved becomes Approve, otherwise Invoice. Batches with no approvers are left unchanged — that is the case the grid surfaces as No Approver(s).
  • Returns [] when batch_invoice is unavailable or the caller has no companies in scope.
  • The derived status is computed strictly from the persisted statuses, never from a leftover error_msg (an error_msg can survive on a still-Posted invoice whose unpost attempt failed).

POST/api/invoicing/post-to-gl/post

Posts an entire batch to QuickBooks Online, or unposts it. This is the primary QuickBooks write path.

Module Tasks & Time > Invoicing > Post to GL
Authentication Required — authenticated session cookie
Authorization FirmInternalAccountingManager (17) for the caller's firm required. Additionally, every invoice carrying the given batch_no must be inside the caller's company scope.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
batchNo string Yes String, must not be empty The posting batch number. Whitespace-only values are rejected at the boundary.
fromUnPost boolean No Boolean true unposts the batch (deletes/voids each posted invoice in QBO and sets Unposted) instead of posting it.

Example request

curl -X POST 'https://api.example.com/api/invoicing/post-to-gl/post' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "batchNo": "20260901103012" }'

Success response — 201 Created

{ "error": false, "message": "Invoice(s) posted successfully." }

Unpost returns { "error": false, "message": "Invoice(s) unposted successfully." }.

Field Type Description
error boolean true when any invoice in the batch failed.
message string Success text, or the de-duplicated failure messages joined by ,.

Errors

Status Condition Message/shape
400 batchNo missing or empty Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
403 Caller is not a FirmInternalAccountingManager for their firm {"message":"You are not authorized to post or unpost invoices."}
403 Any invoice in the batch belongs to a company outside the caller's scope {"message":"This batch is outside your access scope."}
201 batchNo trims to empty {"error":true,"message":"Batch number is required."}
201 The firm has no QuickBooks connection {"error":true,"message":"Please connect your QuickBooks account before posting."}
201 The batch is already locked by another post/unpost {"error":true,"message":"This batch is already being posted. Please wait for it to finish."}
201 No invoice in the batch is in a postable state {"error":true,"message":"This batch has invoice(s) pending approval and cannot be posted yet."}
201 A line's QBO item id is not in the connected company's item list {"error":true,"message":"Invoice <no>: item id(s) <ids> not found in your connected QuickBooks company's item list. Please re-select the item on this line and try again."}
201 The invoice's payment term is not in the connected company {"error":true,"message":"Invoice <no>: the payment term on this invoice was not found in your connected QuickBooks company (it may have been renamed, deleted, or belong to a different QuickBooks connection). Please re-select the Terms on this invoice and try again."}
201 The invoice contains only non-billable items {"error":true,"message":"Unable to Post the invoice with only Non billable items."}
201 QuickBooks rejected the create/delete call {"error":true,"message":"<QBO error> [diag: ...]"}

Notes

  • Reaches QuickBooks Online — always live. There is no dry-run mode; every call has a real QBO effect, which is why the higher FirmInternalAccountingManager permission is always required.
  • Postable statuses: Approve, Unposted, Post Failed (repost), plus Invoice only when the batch has no approvers. An Invoice batch with pending approvers is never postable.
  • Both post and unpost take the same batch-level lock (invoice_lock, action batchPosting), so a second click, a concurrent unpost, or an unpost racing an in-flight post is rejected rather than duplicated in QuickBooks.
  • Before calling QBO, item ids and payment-term ids are pre-flight verified live against QuickBooks, falling back to the local qbo_items / qbo_terms cache only when the live check cannot run — so a stale cache surfaces a named, actionable message instead of QuickBooks' generic "Invalid Reference Id" fault.
  • On success each invoice is updated to Posted with its qbo_invoice_id, invoice_no (QBO DocNumber) and invoiced_by, and a Posted audit row is written. On failure the invoice becomes Post Failed with error_msg and no audit row is written.
  • Unpost only touches invoices currently in Posted status and carrying a qbo_invoice_id; a row that was never posted is skipped entirely (no status flip, no audit row). Successful unposts clear qbo_invoice_id and error_msg, set Unposted, and write an UnPosted audit row.
  • Credit memos (type = 2) are created and deleted through the QBO credit-memo endpoints rather than the invoice endpoints.

POST/api/invoicing/bulk/action

Applies Post, UnPost or Delete to an explicit set of invoice ids from the invoice list.

Module Tasks & Time > Invoicing > Post to GL
Authentication Required — authenticated session cookie
Authorization Post / UnPost require FirmInternalAccountingManager (17); Delete requires InvoiceManager (14). All three additionally require every id to be inside the caller's company scope.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
status string Yes One of Post, UnPost, Delete The action to apply.
invoiceIds integer[] Yes Array, numeric (numeric strings are coerced), each item must be an integer batch_invoice.id values. Empty arrays and non-integer entries are rejected at the boundary.

Example request

curl -X POST 'https://api.example.com/api/invoicing/bulk/action' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "status": "Post", "invoiceIds": [4471, 4472] }'

Success response — 201 Created

{ "error": false, "message": "Invoice(s) posted successfully." }
status Success message
Post Invoice(s) posted successfully.
UnPost Invoice(s) unposted successfully.
Delete Invoice(s)/Credit Memo(s) deleted successfully.

Errors

Status Condition Message/shape
400 status outside the allowed set, or invoiceIds empty / containing non-integers Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
403 Post / UnPost by a non-FirmInternalAccountingManager {"message":"You are not authorized to post or unpost invoices."}
403 Delete by a non-InvoiceManager {"message":"You are not authorized to perform this invoice action."}
403 Any id belongs to a company outside the caller's scope {"message":"One or more of the selected invoices is outside your access scope."}
201 invoiceIds filtered down to nothing {"error":true,"message":"No invoices selected."}
201 Post / UnPost with no QuickBooks connection {"error":true,"message":"Please connect your QuickBooks account before posting."} / ... before unposting.
201 Post with no invoice in a postable state {"error":true,"message":"No invoices found for Approved Status."}
201 UnPost with no invoice currently Posted {"error":true,"message":"No invoices found for Posted Status."}
201 Delete deleted nothing {"error":true,"message":"Something went wrong."}
201 QuickBooks rejected a create/delete Same per-invoice messages as POST /api/invoicing/post-to-gl/post

Notes

  • Reaches QuickBooks Online for all three actions: Post creates the invoices/credit memos, UnPost deletes/voids them, and Delete runs the full delete cascade which also voids any posted QBO document.
  • Post filters the supplied ids down to postable ones using the same rule as batch posting (Approve / Unposted / Post Failed, or Invoice with no approvers) before building QBO payloads.
  • Unlike batch posting, this endpoint takes no batch lock — it operates on explicit invoice ids rather than a batch number.
  • The company-scope check fails closed: the scope set always contains at least a 0 sentinel, so an unscoped caller can never match rows.

11.8.4 Timesheet Details

POST/api/invoicing/timesheet/details

Returns the timesheet entries behind a set of invoice line items, for the Timesheet Details modal on an invoice.

Module Tasks & Time > Invoicing > Timesheet Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. The returned serviceRoles list is scoped to the caller's firm; the timesheet rows themselves are reached through the supplied invoice_details ids.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
invoiceDetails integer[] Yes Array, numeric (numeric strings are coerced) invoice_details.id values whose mapped timesheet entries should be returned. Non-positive values are dropped.

Example request

curl -X POST 'https://api.example.com/api/invoicing/timesheet/details' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "invoiceDetails": [55011, 55012] }'

Success response — 201 Created

{
  "mappingData": [
    {
      "invoice_info": [
        { "item_name": "Consulting", "item_detail": 1, "item_description": "August advisory",
          "quantity": 6.25, "price": 150, "priceRate": 937.5, "taxable": "No", "taxRate": 0, "tax": 0 }
      ],
      "time_history_id": 77120,
      "user_id": 188,
      "week": "2026-W34",
      "task_id": 30712,
      "company_id": 41,
      "name": "Quarterly review",
      "project": 88, "project_id": 88,
      "serviceLine": 4, "service_line_id": 4,
      "role": 6, "role_id": 6,
      "invoiceDetails": 1, "invoice_detail_id": 1,
      "invoicing": "Hourly", "invoicing_id": 1,
      "user_name": "Alvarez, Dana",
      "date": "2026-08-20",
      "time": "09:15:00",
      "reportedTime": 22500, "billableTime": 22500, "noChargeTime": 0, "nonBillableTime": 0,
      "timeNotes": "Quarterly review", "internalNotes": "",
      "validationError": false, "changeTaskData": false, "moveToUnInvoice": false,
      "haveBillableTime": true, "throughDate": null,
      "invoice_id": 9012, "batch_invoice_id": 4471
    }
  ],
  "serviceRoles": [{ "id": 6, "role": "Senior Accountant", "deleted": 0 }]
}
Field Type Description
mappingData array One row per unique time_history_id.
mappingData[].invoice_info array One entry per associated invoice_details line: item_name (resolved via qbo_items), item_detail, item_description, quantity, price, priceRate, taxable ("Yes"/"No"), taxRate, tax.
mappingData[].invoicing string Hourly or Fixed Price, derived from invoicing_id (1 / 2).
mappingData[].reportedTime / billableTime / noChargeTime / nonBillableTime number Seconds.
mappingData[].week string ISO week (YYYY-Www, Monday-start).
mappingData[].invoice_id / batch_invoice_id number invoice.id and batch_invoice.id the entry belongs to.
serviceRoles array Firm service roles for the role picker, excluding archived roles (deleted NOT IN (0,2)).

Errors

Status Condition Message/shape
400 invoiceDetails is not an array Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}

Notes

  • Line quantity is computed with the shared rule: a Fixed Price line bills billable + no-charge + non-billable time; an Hourly line bills only the bucket matching its item_detail. Seconds are rounded to whole minutes (a non-zero value ≤ 30 s becomes a 1-minute floor) before conversion to hours.
  • item_name is resolved from the cached qbo_items data, not from a stored column on invoice_details.

POST/api/invoicing/timesheet/details/edit

Saves edits made in the Timesheet Details modal: writes the grid back to time_history, optionally returns entries to the un-invoiced queue, rebuilds the invoice's timesheet lines and recomputes the batch totals.

Module Tasks & Time > Invoicing > Timesheet Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session — any authenticated user may save. The firm used for line rebuilding is the caller's own users.firm_id.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
timesheetDetails object[] Yes Array Edited rows. Elements are not individually validated; the keys read are those the read endpoint returns (see below).
invoiceId integer Yes Numeric (numeric strings are coerced), integer batch_invoice.id (not invoice.id, which is supplied per row as invoice_id).
oldTimesheetData object[] No Array Accepted by the request schema; not read by this endpoint.

Keys read from each timesheetDetails[] element: time_history_id (required — rows without it are skipped), invoice_id, project_id, name, service_line_id, role_id, invoice_detail_id, invoicing_id, reportedTime, billableTime, noChargeTime, nonBillableTime, timeNotes, internalNotes, date, time, throughDate, changeTaskData, moveToUnInvoice.

Example request

curl -X POST 'https://api.example.com/api/invoicing/timesheet/details/edit' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
    "invoiceId": 4471,
    "timesheetDetails": [
      { "time_history_id": 77120, "invoice_id": 9012, "project_id": 88, "name": "Quarterly review",
        "service_line_id": 4, "role_id": 6, "invoice_detail_id": 1, "invoicing_id": 1,
        "reportedTime": 22500, "billableTime": 18000, "noChargeTime": 4500, "nonBillableTime": 0,
        "timeNotes": "Quarterly review", "internalNotes": "",
        "date": "2026-08-20", "time": "09:15:00", "moveToUnInvoice": false }
    ]
  }'

Success response — 201 Created

{
  "error": false,
  "message": "Timesheet and Invoice Updated Successfully.",
  "invoiceData": { "id": 4471, "invoice_no": "INV-2096", "status": "Preview", "amount": 1800 }
}
Field Type Description
invoiceData object | null The refreshed batch invoice, so the caller can re-render without a second round trip. null when invoiceId is 0.

Errors

Status Condition Message/shape
400 timesheetDetails not an array or invoiceId not an integer Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
201 time_history table unavailable {"error":true,"message":"Timesheet table is unavailable."}

Notes

  • Rows are keyed by time_history_id, not by id — id is a 1-based display index supplied by the client, not a stored key.
  • The edited task label is written to time_history.name only; the underlying tasks.name is left unchanged, so an invoice edit cannot rename the task elsewhere in the application.
  • changeTaskData: true marks the entry as invoice-sourced (time_history.timesheet_entry = 2).
  • created_at is rewritten as <date> <time> from the row's own values, so a date edit cannot silently shift the time of day.
  • Through-date eviction: when the edited date moves past the row's throughDate, the entry is detached from the invoice entirely (mappings removed, sole line item soft-deleted) and the move-to-queue step is skipped.
  • moveToUnInvoice: true detaches the mappings and sets time_history.previewed = 0, returning the hours to the un-invoiced queue.
  • After the row loop, every from = 'Timesheet' line on the invoice is torn down and rebuilt from the new time_history state, then batch_invoice.invoice_amount / tax_amount / amount are recomputed.
  • No QuickBooks call is made.

POST/api/invoicing/timesheet/details/export

Turns the rows currently shown in the Timesheet Details grid into a CSV payload for client-side download.

Module Tasks & Time > Invoicing > Timesheet Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the payload is formatted from the request body only.
Content type application/json (CSV is returned as a string field, not as a file)

Request body

Field Type Required Validation / allowed values Description
exportName string No String Explicit file name; when blank a name is derived.
invoiceNo string No String Used in the derived file name.
data object[] Yes Array Rows to export. Elements are not individually validated — the union of all keys across all rows becomes the CSV header, in first-seen order.

Example request

curl -X POST 'https://api.example.com/api/invoicing/timesheet/details/export' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
    "invoiceNo": "INV-2096",
    "data": [ { "User": "Alvarez, Dana", "Date": "2026-08-20", "Billable": "05:00" } ]
  }'

Success response — 201 Created

{
  "error": false,
  "fileName": "INV-2096-timesheet-details-2026-09-01.csv",
  "mimeType": "text/csv",
  "content": "\"User\",\"Date\",\"Billable\"\n\"Alvarez, Dana\",\"2026-08-20\",\"05:00\""
}
Field Type Description
fileName string exportName when supplied, otherwise <invoiceNo or "invoice">-timesheet-details-<YYYY-MM-DD>.csv.
mimeType string Always text/csv.
content string The full CSV document; every value is double-quoted with " escaped as "".

Errors

Status Condition Message/shape
400 data is not an array Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}
201 data is empty {"error":true,"message":"No data available for export."}

Notes

  • Purely a formatter — no database read. The exported content is exactly what the client sends, so the CSV always matches what the user sees on screen.
  • The response is JSON containing the CSV as a string; the client is responsible for triggering the download.

11.8.5 Invoicing Audit Log

POST/api/invoicing/audit/list

Returns the Invoicing Audit Log — every recorded invoice action with the user, role, invoice number and before/after values — with filtering, sorting and paging.

Module Tasks & Time > Invoicing > Invoicing Audit Log
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Rows are restricted to invoices whose company is in the caller's role-derived scope.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
offset integer No Numeric (numeric strings are coerced), integer Row offset; values ≤ 0 are treated as 0.
limit integer No Numeric (numeric strings are coerced), integer Page size; values ≤ 0 fall back to 30.
sort_field string No String One of Date, Time, User, User Role, Invoice No, Action. Anything else sorts by date.
sort_order string No String ASC (case-insensitive) or anything else for DESC.
filters object[] No Array, each item validated Column filters.
filters[].column string Yes (within a filter) String User, User Role, Invoice No, Action, Date, Original Value, New Value. Unknown columns are ignored.
filters[].filter any No Optional — untyped The filter value; see Notes for per-column semantics.
fetchDropDown boolean No Boolean true wraps the list in an object that also carries the filter dropdown data.

Example request

curl -X POST 'https://api.example.com/api/invoicing/audit/list' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
    "offset": 0, "limit": 30,
    "sort_field": "Date", "sort_order": "DESC",
    "filters": [
      { "column": "Action", "filter": "Posted" },
      { "column": "Date", "filter": "Custom/2026-08-01/2026-08-31" }
    ]
  }'

Success response — 201 Created

[
  {
    "action": "Posted",
    "original_data": {
      "customer": "Northwind Manufacturing",
      "customer_email": "ap@northwind.example",
      "customer_cc": "-", "customer_bcc": "-",
      "address": "100 Mill Rd, Springfield",
      "terms": "Net 30",
      "invoiceDate": "08/31/2026",
      "dueDate": "09/30/2026",
      "LineItem": "1 Line items Posted.",
      "class": "Advisory"
    },
    "new_data": { "qbo_invoice_id": "1042", "invoice_no": "1042", "response": { } },
    "created_at": "2026-09-01T10:31:12.000Z",
    "user_name": "Brooks, Lee",
    "role_name": "Firm Internal Accounting Manager",
    "invoice_no": "INV-2096",
    "audit_date": "09/01/2026",
    "audit_time": "10:31 AM"
  }
]

With fetchDropDown: true the response becomes { "dropDownData": { "users": [...], "userRoles": [...] }, "listData": [...] }.

Field Type Description
action string The audit verb: Created, Updated, Commented, Approved, Unapproved, Rejected, Resubmitted, Posted, UnPosted, Deleted.
original_data / new_data object | null Parsed JSON blobs. Unparseable text is wrapped as {"value": "<text>"}.
created_at string | null Raw timestamp.
audit_date / audit_time string Formatted date and time for display.
user_name / role_name string Acting user and their role at the time; empty string when unresolved.
invoice_no string The invoice's number.

Errors

Status Condition Message/shape
400 A malformed filters element, or a mistyped scalar Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}

Notes

  • Returns [] (or {dropDownData:{users:[],userRoles:[]}, listData:[]} when fetchDropDown is set) if the audit tables are unavailable or the caller has no companies in scope.
  • Filter semantics: User and User Role match on numeric id (values ≤ 0 ignored). Invoice No is normalised on both sides — # and whitespace are stripped — so a value typed as displayed (#INV - 2096) still matches the stored INV-2096. Action matches exactly, except unposted/UnPosted, which is matched case-insensitively. Date accepts a named preset, an object {startDate, endDate}, or the string form Custom/<from>/<to>. Original Value and New Value are LIKE substring matches on the raw JSON text.
  • Sorting by Time always yields created_at DESC, regardless of sort_order.
  • The per-row id is not included in the response.
  • Audit writes are non-fatal: a failed audit insert is logged but never blocks the financial action it accompanies.

POST/api/invoicing/audit/dropdownData

Returns just the filter dropdown data for the Invoicing Audit Log — the firm's users and the roles actually assigned within it.

Module Tasks & Time > Invoicing > Invoicing Audit Log
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Requires at least one company in the caller's scope; the lists themselves are scoped by the caller's raw users.firm_id (a firm-less Global Master System Administrator gets every user).
Content type application/json

Request body (the same schema as the list endpoint; no field is read by this endpoint)

Field Type Required Validation / allowed values Description
offset integer No Numeric (numeric strings are coerced), integer Accepted; not read.
limit integer No Numeric (numeric strings are coerced), integer Accepted; not read.
sort_field string No String Accepted; not read.
sort_order string No String Accepted; not read.
filters object[] No Array, each item validated Accepted; not read.
fetchDropDown boolean No Boolean Accepted; not read.

Example request

curl -X POST 'https://api.example.com/api/invoicing/audit/dropdownData' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{}'

Success response — 201 Created

{
  "users": [
    { "id": 188, "name": "Alvarez, Dana" },
    { "id": 214, "name": "Brooks, Lee" }
  ],
  "userRoles": [
    { "id": 14, "name": "Invoice Manager", "role": { "id": 14, "role_name": "Invoice Manager" } }
  ]
}
Field Type Description
users array Non-deleted users in the caller's firm, ordered by last name.
userRoles array Distinct roles actually assigned to those users, each duplicated into a nested role object for compatibility with earlier clients.

Errors

Status Condition Message/shape
400 A malformed filters element Standard request validation error
401 No authenticated session {"message":"These credentials do not match our records."}

Notes

  • Returns { "users": [], "userRoles": [] } when the audit tables are unavailable or the caller has no companies in scope.
  • The user list is scoped by firm, not by the company scope — a firm-less Global Master System Administrator receives every user.
  • Roles are read from assign_user_roles, so only roles actually in use appear in the filter.

11.9 Timesheet

The Timesheet module is the time-capture and time-approval half of Tasks & Time. It covers four screens:

  • Timesheet Entry — the Daily/Weekly grid where a user records time against a company, project and task, splits that time into Billable / No Charge / Non Billable buckets, and drives the disposition (submit / approve / reject / void) workflow.
  • Timesheet Reports — the per-week roster of users and their disposition status, plus the Excel export in four different shapes.
  • Timesheet Configure — the per-firm grid that assigns each user an approver, a cost rate, a full/part-time flag and the report-visibility permission flags, plus the firm-level "use timesheet" master switch.
  • Timesheet Audit Log — the read-only trail of Created / Updated / Deleted timesheet rows.

Every route in this section is served under /api, so each path below begins with /api/.

11.9.1 Timesheet Conventions

Week string. Every timesheet week is identified by an ISO-8601-style week string YYYY-Www (e.g. 2026-W36), normalised server-side:

  • The accepted input pattern is ^(\d{4})-W(\d{1,2})$, case-insensitive. The week number is clamped to 1..53 and zero-padded to two digits on output.
  • Any missing, empty or unparseable value silently falls back to the current week — no validation error is raised. A client that sends a malformed week gets data for today's week, not an error.

Week range (Saturday → Friday). A week string is converted into a date range by first locating the ISO Monday of that week (anchored on Jan 4), then shifting back two days to Saturday and forward six days to Friday. A timesheet week therefore runs Saturday to Friday, and "Week Ending" everywhere in this module means that Friday. Both ends are returned as YYYY-MM-DD strings in UTC terms.

Date → week. A date is snapped to a week string as follows: weekend dates shift forward to the following Monday first (Saturday +2 days, Sunday +1 day) before the ISO week is taken, so a weekend entry belongs to the Sat–Fri window that starts on that weekend, not the one that ends on it.

Second/hour rounding. All stored times are integer seconds. The following rounding is applied on read and before allocation:

Input Result
0, non-finite 0
1 … 30 seconds 60 (rounds up to one minute — never rounds a non-zero positive to zero)
> 30 seconds Rounded to the nearest whole minute
negative Rounded to the nearest whole minute, keeping the sign; 0 if the magnitude rounds to zero minutes

Hours are derived as seconds / 3600 rounded to 2 decimals for the utilisation strip, and as HH:mm (with the rounding above re-applied first) in Excel exports.

Allocation invariant. Allocation rounds capture_time, reported_time, billable_time, no_charge_time and non_billable_time, then forces billable + no_charge + non_billable === reported by pushing the difference into the bucket named by invoice_detail_id (1 → billable, 2 → no charge, 3 → non billable; anything else → billable). Rows that still fail the equality are flagged validationError: 1. On the read path this is persisted back to time_history (a self-healing UPDATE); on the export path persistence is skipped.

Date formats in payloads.

Context Format Example
Entry row date, request date, week ranges YYYY-MM-DD 2026-09-04
Audit Log week_ending, timesheet_date, audit_date; Reports week label MM-DD-YYYY 09-04-2026
Audit Log audit_time hh:mm AM/PM 02:35 PM
Excel export "Week Ending" / row Date cells MM/DD/YYYY 09/04/2026
Excel export "Generated On" MM/DD/YYYY hh:mm:AM in America/New_York 09/01/2026 10:15:AM
Audit Log filter values (Week Ending / Timesheet Date) MM-DD-YYYY or YYYY-MM-DD (both accepted) 09-04-2026

Approval / disposition flow. A week's disposition is a row in the disposition table keyed by (user_id, week); the latest row (highest id) wins. Valid statuses are SUBMIT, UNSUBMIT, APPROVE, UNAPPROVE, REJECT, VOID, UNVOID; a user with no row at all reads as UNSUBMIT. Actor rules are enforced server-side by POST /api/task/timesheet/weekly/status:

Transition Who may perform it
SUBMIT / UNSUBMIT The sheet's owner, or a "special user"
APPROVE / UNAPPROVE / REJECT The target's configured approver, or a special user
VOID / UNVOID Special users only

A special user is a Global Master System Administrator (role id 1, resolved from assigned roles — not only the users.global_admin column), a firm admin (users.firm_admin = 1), or both.

Approver relationship. The approver link is not driven by the TimesheetApprover role (role id 16), which is not used anywhere in this module. The relationship is a row in timesheet_config where user_id = <target> and approver_id IN (<caller>, <target>) (the second value covers the self-approver edge case). The check is one-way: being approved by someone does not grant rights over them.

Utilisation targets. The Entry endpoints return a utilTableData strip built from a single per-day billable target: the user's utilization_config_user.override_target_bill_day if > 0, else utilization_config_role.target_bill_day for that row's role_id scoped to the firm, else 0. The five row targets are that per-day figure multiplied by [1, 5, 5, 20, 65] for ['Today', 'This Wk', 'Last Wk', 'Last 4 Wks', 'Last 13 Wks']. Rolling windows are derived as follows: This Wk is the Sat–Fri window containing the anchor date; Last 4 Wks and Last 13 Wks end at last week's Friday and exclude the current, in-progress week.

Authentication. Every endpoint reads the session identified by the genie.sid cookie (session state is stored in MySQL) and resolves the authenticated user from it. The request is rejected with 401 unless session.login.userId is set and session.login.state === 'authenticated' and a users row with that id and deleted = 0 exists. No endpoint in this module carries a route-level role requirement; all authorisation is applied within the endpoint logic.

Validation. Global request validation is applied to every endpoint. Unknown body properties are silently stripped; a failing validation rule yields 400 with the standard validation error array.

Save to Google Sheet / Drive. POST /api/send/drive does not belong to this module; it is served by the Google Drive integration and backed by the drive_config table. Its contract: not linked → { error: true, message, url: <consent url> }; linked → { error: false, sendType, accessToken, picker }. The in-module export endpoint (POST /api/task/timesheet/weekly/export) is what produces the workbook that is subsequently handed to Drive.


11.9.2 Timesheet Entry

POST/api/task/timesheet/weekly

Loads the dropdown catalogues for the Daily/Weekly timesheet grid — selectable users, companies, projects (grouped by company), billing types, service lines and service roles. It returns no time entries; the grid rows come from POST /api/task/timesheet/filter.

Module Tasks & Time > Timesheet > Timesheet Entry
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session. The user dropdown is scoped to self + approvees unless the caller is a Global Master System Administrator (role id 1), a firm admin, or has timesheet_config.view_others = 1. The company/project dropdowns are scoped separately (Global Company Administrator roles 1/3/4 → all companies; firm admin → own firm; everyone else → companies with non-NoAccess Task & Time access).
Content type application/json

Request body

Field Type Required Validation / allowed values Description
week string No Optional, string Week string YYYY-Www. Anything unparseable or omitted falls back to the current week — no error.

Example request

curl -X POST https://api.example.com/api/task/timesheet/weekly \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"week":"2026-W36"}'

Success response — 200 OK

{
  "loginUserId": 237,
  "dropdownValues": {
    "userOptions": [
      {
        "id": 237,
        "name": "Byfield, Kadian",
        "full_name": "Byfield, Kadian",
        "firm_admin": 0,
        "first_name": "Kadian",
        "last_name": "Byfield",
        "firm_id": 32,
        "active": 1,
        "deleted": 0,
        "enc_id": "eyJpdiI6..."
      }
    ],
    "companyOptions": [
      { "id": 415, "name": "Northwind Foods LLC", "deleted": 0 }
    ],
    "projectOptions": {
      "415": [
        {
          "id": 9022,
          "name": "Monthly Close",
          "company_id": 415,
          "company_name": "Northwind Foods LLC",
          "service_line": 4,
          "invoicing": 1,
          "billing": 1
        }
      ]
    },
    "billingOptions": [
      { "id": 1, "name": "Billable" },
      { "id": 2, "name": "No Charge" },
      { "id": 3, "name": "Non Billable" }
    ],
    "serviceLineOptions": [
      { "id": 4, "name": "Accounting", "firm_id": 32, "is_active": 1, "deleted_at": null }
    ],
    "serviceRoleOptions": [
      { "id": 11, "name": "Bookkeeper", "role": "Bookkeeper", "order": 2, "price": 0, "icon": "", "deleted": 2 }
    ]
  }
}
Field Type Description
loginUserId number The authenticated caller's user id.
dropdownValues.userOptions[] object[] Selectable users. name/full_name are users.lf_name trimmed, falling back to a formatted first/last name. enc_id is the encrypted user id. Sorted by name.
dropdownValues.companyOptions[] object[] id, name, deleted (company.deleted is a 3-value enum: 0 active, 1 inactive, 2 deleted). Only deleted = 0/NULL, active = 1/NULL, deleted_at IS NULL rows are returned.
dropdownValues.projectOptions object Map of company_id → array of project objects (id, name, company_id, company_name, service_line, invoicing, billing).
dropdownValues.billingOptions[] object[] Fixed list: 1 Billable, 2 No Charge, 3 Non Billable. Maps to invoice_detail_id.
dropdownValues.serviceLineOptions[] object[] Scoped to the firm's own service_line rows when firms.use_service_line = 1, else to the global set (firm_id = 0). Soft-deleted lines are kept and their name is suffixed " (deleted)" with is_active forced to 0, so history referencing them still renders. Ordered by order ASC.
dropdownValues.serviceRoleOptions[] object[] Scoped exclusively (never additively): the firm's own roles when use_service_role = 1, the global catalogue otherwise. deleted is a 4-value enum where 0 and 2 are both live states.

Errors

Status Condition Message/shape
400 week present but not a string Standard validation error array
401 No session, state !== 'authenticated', or no matching non-deleted users row { "message": "These credentials do not match our records." }

Notes

  • Daily and Weekly Timesheet share this single endpoint; one call feeds both screens' dropdowns.
  • A firm-less Global Master System Administrator gets every user across all firms in userOptions, because firm scoping would key them to a firm id that matches nobody.
  • Users who are deleted or inactive are dropped from userOptions unless they have actual time_history rows in the selected week, so a week containing a since-deactivated user's history does not silently lose that user.
  • view_others = 1 widens the user dropdown but never grants edit rights — see POST /api/task/timesheet/filter's viewOnly.

POST/api/task/timesheet/filter

Returns one user's timesheet rows for a week (or a single day), together with the edit-permission flags, the week's disposition status, and the utilisation strip. This is the main read for the Daily/Weekly grid.

Module Tasks & Time > Timesheet > Timesheet Entry
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session. A Global Master System Administrator (role id 1) may load any firm's user; everyone else is restricted to their own firm. Edit rights (viewOnly) require self, a special user (Global Master System Administrator or firm admin), or an approver-of-target timesheet_config row. Row visibility is additionally company-scoped.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
user number Yes Numeric (numeric strings are coerced), integer Target user id whose sheet is being read.
week string No Optional, string Week string YYYY-Www. If omitted, the week is derived from date; if both are absent, the current week is used.
date string No Optional, string A single YYYY-MM-DD date. When present the query narrows to that one day (Daily view) and the utilisation strip switches to daily mode.

Example request

curl -X POST https://api.example.com/api/task/timesheet/filter \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"user":237,"week":"2026-W36"}'

Success response — 200 OK

{
  "configData": {
    "approver_id": 32,
    "authorized": 0,
    "submit": 1,
    "user_id": 237,
    "view_others": 0,
    "cost_per_hour": "85.00",
    "time_status": 1
  },
  "specialUser": false,
  "viewOnly": false,
  "allowAddEntry": true,
  "timeSheetData": [
    {
      "id": 884213,
      "user_id": 237,
      "task_id": 55120,
      "company_id": 415,
      "project_id": 9022,
      "name": "Reconcile bank feed",
      "service_line_id": 4,
      "assigned_role_id": 11,
      "invoice_detail_id": 1,
      "invoicing_id": 1,
      "date": "2026-09-02",
      "capture_time": 5400,
      "reported_time": 5400,
      "billable_time": 5400,
      "no_charge_time": 0,
      "non_billable_time": 0,
      "time_notes": "Cleared September items",
      "internal_notes": "",
      "company": "Northwind Foods LLC",
      "project": "Monthly Close",
      "service_line": "Accounting",
      "assigned_role": "Bookkeeper",
      "invoice_detail": "Billable",
      "invoicing": "Hourly",
      "estimated_time": "02:00",
      "manualEntry": 1,
      "invoiceEntry": 0,
      "is_email": 0,
      "email_id": 0,
      "validationError": 0,
      "previewed": 0,
      "isCompleted": 0,
      "encTaskId": "eyJpdiI6...",
      "task_id_copy": "copy_55120"
    }
  ],
  "allowedTimesheetConfig": 1,
  "status": "UNSUBMIT",
  "reason": "",
  "fromApproved": false,
  "userSelect": { "id": 237, "name": "Byfield, Kadian" },
  "utilTableData": {
    "resData": [
      {
        "name": "This Wk",
        "start_date": "2026-08-29",
        "end_date": "2026-09-04",
        "billable_hours": "18.50",
        "target": "40.00"
      }
    ],
    "horizantalData": {
      "head": ["", "Today", "This Wk", "Last Wk", "Last 4 Wks", "Last 13 Wks"],
      "body": [
        ["Billable Hours", "1.50", "18.50", "36.00", "142.00", "451.25"],
        ["Target", "8.00", "40.00", "40.00", "160.00", "520.00"]
      ]
    },
    "totals": {
      "capture_time": 5400,
      "reported_time": 5400,
      "no_charge_time": 0,
      "non_billable_time": 0
    }
  },
  "weekEnding": "2026-09-04"
}
Field Type Description
configData object The target user's timesheet_config row, defaulted when absent: approver_id (0), authorized (0), submit (1), view_others (0), cost_per_hour ("0.00", a string), time_status (1 = Full Time, 2 = Part Time).
specialUser boolean Caller is a Global Master System Administrator or a firm admin of the resolved firm.
viewOnly boolean true when the caller may not edit this sheet. Inverse of the self / special-user / approver-of-target rule.
allowAddEntry boolean true when the caller is a special user, or the target user's firm has timesheets enabled, and the sheet is not view-only. Note this follows the target's firm setting, not the viewer's.
timeSheetData[] object[] The week's (or day's) rows, ordered created_at DESC, id DESC. Times are integer seconds, already rounded and re-allocated. capture_time is abs(end_time - start_time); reported_time is override_time when set, else capture_time. invoice_detail / invoicing are derived labels. manualEntry is 1 when timesheet_entry = 1; invoiceEntry is 1 when timesheet_entry = 2. validationError is 1 when the buckets do not sum to reported_time. task_id_copy is "copy_" + task_id.
allowedTimesheetConfig number Always 1. No per-user check narrows this value.
status string Latest disposition for the week, uppercased; "UNSUBMIT" when no row exists.
reason string Rejection reason from the latest disposition row, "" if none.
fromApproved boolean status === "APPROVE". When true, the approved snapshot rows (is_approved = 1) are returned instead of the working rows, and no company allow-list is applied.
userSelect object { id, name } of the resolved target user.
utilTableData object resData[] (per-window name, start_date, end_date, billable_hours, target — the last two are 2-dp strings), horizantalData (a head row plus Billable Hours / Target body rows — note the spelling), and totals (raw second sums over the returned rows).
weekEnding string The week's Friday, YYYY-MM-DD.

Errors

Status Condition Message/shape
200 Target user id resolves to no accessible user { "error": true, "message": "User not found." } (a 200, not a 404)
400 user missing or not coercible to an integer Standard validation error array
401 No session / not authenticated / user row missing { "message": "These credentials do not match our records." }

Notes

  • Side effect on a read. Allocation runs before the response is built and writes rounded/reallocated billable_time, no_charge_time, non_billable_time (and a re-synced service_line) back to time_history for any row whose values changed. A failure of that UPDATE is suppressed and never fails the read.
  • The "Today" row is included in utilTableData only when the viewed week contains the real current date; in Daily mode it is always included.
  • Last 4 Wks and Last 13 Wks billable sums are computed per row with second rounding applied before summing, not as a raw SQL SUM().
  • Non-approved rows are filtered by the caller's company allow-list, which includes soft-deleted companies (deleted IN (0, 1)) for firm users, so entries against a soft-deleted company remain visible in the grid as well as on invoices.
  • view_others = 1 widens which users the caller may load but never sets viewOnly to false.

POST/api/task/timesheet/save

Persists a batch of timesheet rows for one user — creating manual entries, updating existing rows, and soft-deleting removed ones — and writes an audit record for each change. Returns the recalculated utilisation strip.

Module Tasks & Time > Timesheet > Timesheet Entry
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session. The caller must be the target user, a special user (Global Master System Administrator, role id 1 or firm admin), or the target's configured approver. Failure returns a 200 body with permission: true, not a 401.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
data object[] Yes Array, each item validated The rows to persist. Processed in reverse array order.
userId number Yes Numeric (numeric strings are coerced), integer The user whose sheet is being written. Also the permission target.
week string No Optional, string Week string YYYY-Www. If absent, derived from date.
date string No Optional, string YYYY-MM-DD; presence switches the returned utilisation strip to daily mode.
initialData Record<string, unknown>[] No Optional, array The pre-edit snapshot of the same rows, keyed by id. Used only to compute the Updated audit diff — a row absent here produces no Updated audit entry.

Each element of data — every field is optional:

Field Type Required Validation / allowed values Description
id number No Optional, numeric (numeric strings are coerced), integer time_history.id. Absent/0 ⇒ a new manual task + history row is created.
user_id number No Optional int Row owner.
task_id number No Optional int tasks.id. On a delete this is the task that is soft-deleted; on a create it is echoed back as manual_id.
task_id_copy string No Optional, string Client-side row key ("copy_" + task_id). Not persisted.
company_id number No Optional int
project_id number No Optional int
name string No Optional, string Task name.
service_line_id number No Optional int
assigned_role_id number No Optional int
invoice_detail_id number No Optional int Billing type: 1 Billable, 2 No Charge, 3 Non Billable. Drives which bucket absorbs an allocation difference.
invoicing_id number No Optional int 1 Hourly, 2 Fixed Price.
capture_time number No Optional int Seconds.
reported_time number No Optional int Seconds.
billable_time number No Optional int Seconds.
no_charge_time number No Optional int Seconds.
non_billable_time number No Optional int Seconds.
date string No Optional, string YYYY-MM-DD for the row.
time_notes string No Optional, string
internal_notes string No Optional, string
manualEntry number No Optional int
invoiceEntry number No Optional int
deleted_at unknown No Optional (no type validator) Truthy ⇒ delete. Any truthy value combined with task_id > 0 soft-deletes the task and writes a Deleted audit row; the entry is then skipped.

Example request

curl -X POST https://api.example.com/api/task/timesheet/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
        "userId": 237,
        "week": "2026-W36",
        "data": [
          {
            "id": 884213,
            "task_id": 55120,
            "company_id": 415,
            "project_id": 9022,
            "name": "Reconcile bank feed",
            "service_line_id": 4,
            "assigned_role_id": 11,
            "invoice_detail_id": 1,
            "invoicing_id": 1,
            "date": "2026-09-02",
            "capture_time": 5400,
            "reported_time": 5400,
            "billable_time": 5400,
            "no_charge_time": 0,
            "non_billable_time": 0,
            "time_notes": "Cleared September items"
          }
        ],
        "initialData": [
          { "id": 884213, "reported_time": 3600, "billable_time": 3600 }
        ]
      }'

Success response — 200 OK

{
  "error": false,
  "message": "Timesheet updated successfully.",
  "data": [
    { "manual_id": -3, "id": 884590, "task_id": 55344 }
  ],
  "utilTableData": {
    "resData": [],
    "horizantalData": { "head": [], "body": [] },
    "totals": {
      "capture_time": 5400,
      "reported_time": 5400,
      "no_charge_time": 0,
      "non_billable_time": 0
    }
  }
}
Field Type Description
error boolean false on success.
message string "Timesheet updated successfully."
data[] object[] One entry per newly created row only: manual_id echoes the client's provisional task_id, id is the new time_history.id, task_id the new tasks.id. Empty when nothing was created.
utilTableData object Same shape as in POST /api/task/timesheet/filter, recomputed from the rows just written.

Errors

Status Condition Message/shape
200 Caller may not edit the userId user's sheet { "error": true, "permission": true, "message": "You don't have permission to update this user's timesheet." }
200 Any exception while writing (task/history/audit) { "error": true, "message": "<original error message>", "data": [ ...rows created before the failure... ] }
400 data not an array, userId not an integer, or a nested row fails validation Standard validation error array
401 No session / not authenticated / user row missing { "message": "These credentials do not match our records." }

Notes

  • Not atomic. Task, history and audit writes are separate statements — there is no enclosing transaction. A mid-batch failure leaves earlier rows committed; the rows created before the failure are returned in data so the client can reconcile.
  • Per-entry branching, in order: deleted_at truthy and task_id > 0 → soft-delete + Deleted audit; no id → create task + history + Created audit; otherwise → update history + Updated audit (only when the diff against initialData is non-empty).
  • Audit payloads carry a fixed 14-column display snapshot: company, project, name, service_line, assigned_role, invoice_detail, invoicing, date, capture_time, reported_time, billable_time, no_charge_time, non_billable_time, time_notes. Created and Deleted store the full snapshot in original_data with new_data null; Updated stores only the changed keys, symmetrically in both.
  • The diff only compares keys present on both the previous and current payloads, so a field merely missing from one side is not reported as a change.
  • week/date are used only to compute week_ending for the audit rows and to anchor the returned utilisation strip; they do not filter what is written.

POST/api/task/timesheet/weekly/status

Records a disposition transition (submit / unsubmit / approve / unapprove / reject / void / unvoid) for one user's week, applies the approval flags to that week's time_history rows, and fires the notification email.

Module Tasks & Time > Timesheet > Timesheet Entry
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session. Special users (Global Master System Administrator role id 1, or firm admin) may perform any transition. Otherwise — SUBMIT/UNSUBMIT require user_id to be the caller's own id; APPROVE/UNAPPROVE/REJECT require the caller to be the target's timesheet_config.approver_id; VOID/UNVOID are refused.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
user_id number Yes Numeric (numeric strings are coerced), integer The user whose week is being dispositioned.
week string Yes String Week string YYYY-Www. Normalised (falls back to the current week if unparseable).
status string Yes String; uppercased then checked against SUBMIT, UNSUBMIT, APPROVE, UNAPPROVE, REJECT, VOID, UNVOID The transition. Case-insensitive on input.
reason string No Optional, string Required in practice when status = REJECT — a blank/whitespace reason throws 400.
data Record<string, unknown>[] No Optional, array The grid rows. When present and status is SUBMIT or APPROVE, each row is validated (see Errors). Ignored for all other statuses.

Row keys read from data during SUBMIT/APPROVE validation: company_id, project_id, name, service_line_id, assigned_role_id, invoice_detail_id, invoicing_id, date, reported_time, billable_time, no_charge_time, non_billable_time.

Example request

curl -X POST https://api.example.com/api/task/timesheet/weekly/status \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
        "user_id": 237,
        "week": "2026-W36",
        "status": "SUBMIT",
        "data": [
          {
            "company_id": 415,
            "project_id": 9022,
            "name": "Reconcile bank feed",
            "service_line_id": 4,
            "assigned_role_id": 11,
            "invoice_detail_id": 1,
            "invoicing_id": 1,
            "date": "2026-09-02",
            "reported_time": 5400,
            "billable_time": 5400,
            "no_charge_time": 0,
            "non_billable_time": 0
          }
        ]
      }'

Success response — 200 OK

{
  "error": false,
  "message": "Time Sheet status created successfully."
}
Field Type Description
error boolean false on success.
message string "Time Sheet status created successfully."

Errors

Status Condition Message/shape
200 Caller is not an allowed actor for this transition { "error": true, "permission": true, "message": "You don't have permission to change this timesheet's status." }
400 status not in the allowed set Message: "Invalid disposition status."
400 status = REJECT with a blank/whitespace reason Message: "The Reason field is required."
400 SUBMIT/APPROVE and any data row is missing a required field Message: "Please fill required fields."
400 SUBMIT/APPROVE and any data row has reported_time !== billable_time + no_charge_time + non_billable_time Message: "Allocated Time is not equal to the Reported time for some Tasks."
400 user_id, week or status missing / wrong type Standard validation error array
401 No session / not authenticated / user row missing { "message": "These credentials do not match our records." }

Notes

  • Validation order matters: permission is checked before the REJECT reason and the data row checks, so an unauthorised caller always gets the permission body rather than a 400.
  • On APPROVE, every time_history row in the week's Sat–Fri range for that user gets awaiting_approval = 0, is_approved = 1, approved_billable_time = 1 (each applied only if the column exists).
  • On UNAPPROVE, is_approved = 0 and approved_billable_time / approved_no_charge_time / approved_non_billable_time are set to NULL.
  • VOID, UNVOID, SUBMIT, UNSUBMIT and REJECT write a disposition row only — they do not touch time_history.
  • A new disposition row is appended (never updated in place); reads take the highest id for (user_id, week). A failure to store the row is suppressed to keep the endpoint resilient across schema variants, so a status call can report success even when the row was not stored.
  • Email side effect, fire-and-forget. SUBMIT emails the target's approver ("Timesheet approval required"); REJECT emails the submitter ("Timesheet Approval - Rejected", including the reason). The email is dispatched asynchronously, so mail-server latency or failure never delays or fails the disposition save; failures are logged as warnings only. No email is sent for the other five statuses. No real-time websocket notification is emitted.

11.9.3 Timesheet Reports

POST/api/task/timesheet/user/list

Returns the user dropdown for the Timesheet Reports screen for a given week.

Module Tasks & Time > Timesheet > Timesheet Reports
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session. The list is narrowed to self + approvees unless the caller is a Global Master System Administrator (role id 1), a firm admin, or has timesheet_config.view_others = 1. A Global Master System Administrator with no firm of their own receives every user across all firms.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
week string Yes String Week string YYYY-Www. Normalised; an unparseable value silently becomes the current week.

Example request

curl -X POST https://api.example.com/api/task/timesheet/user/list \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"week":"2026-W36"}'

Success response — 200 OK

[
  {
    "id": 237,
    "name": "Byfield, Kadian",
    "full_name": "Byfield, Kadian",
    "firm_admin": 0,
    "first_name": "Kadian",
    "last_name": "Byfield",
    "firm_id": 32,
    "active": 1,
    "deleted": 0,
    "enc_id": "eyJpdiI6..."
  }
]
Field Type Description
(root) object[] A bare array — not wrapped in an envelope. Sorted by name.
id number User id.
name / full_name string users.lf_name trimmed, falling back to a formatted first/last name.
firm_admin number 1 when the user is their firm's admin.
first_name / last_name string | null Raw name columns.
firm_id number The user's firm.
active number 1 active, 0 inactive.
deleted number Hard-delete flag; 0 = present.
enc_id string Encrypted user id, for URLs that must not expose the raw id.

Errors

Status Condition Message/shape
400 week missing or not a string Standard validation error array
401 No session / not authenticated / user row missing { "message": "These credentials do not match our records." }

Notes

  • Unlike POST /api/task/timesheet/weekly, this call always includes history users: users with time_history rows in the week are added to the list even if they were not in the base firm query, so a week's history never lacks its author.
  • Deleted/inactive users are still dropped unless they have history in the selected week.
  • lf_name is trimmed before it is returned, because some stored records carry leading or trailing whitespace.

POST/api/task/timesheet/weekly/list

Returns the Timesheet Reports roster: one row per candidate user for the week, with their disposition status and whether the caller may open their sheet.

Module Tasks & Time > Timesheet > Timesheet Reports
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session. Row candidates are every entitled user intersected with users who have a live timesheet_config row; the per-row viewTimesheet flag is narrowed to self + approvees unless the caller is a Global Master System Administrator (role id 1), a firm admin, or has view_others = 1. The viewHierarchy flag requires users.firm_admin = 1 or timesheet_config.authorized = 1.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
week string Yes String Week string YYYY-Www. Not clamped — future weeks are accepted, so the list and the export agree.
status string Yes String; matched case-insensitively (see mapping below) Disposition filter.
hierarchy boolean No Optional, boolean true restricts rows to the users the caller directly approves ("Reporting Hierarchy" mode).
fromMounted boolean No Optional, boolean true adds viewHierarchy to the response (the screen's initial load).

status mapping, matched on the lowercased, trimmed value:

Accepted values Statuses included
"", all, all timesheets UNSUBMIT, SUBMIT, APPROVE, UNAPPROVE, REJECT, VOID, UNVOID
submitted, submitted timesheets SUBMIT
open, unsubmitted/unapproved/unvoided timesheets, unsubmit UNSUBMIT, UNAPPROVE, UNVOID
rejected, rejected timesheets REJECT
approved, approved timesheets APPROVE
pending, timesheets pending approval SUBMIT
voided, voided timesheets VOID
anything else the uppercased input, matched literally

Example request

curl -X POST https://api.example.com/api/task/timesheet/weekly/list \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"week":"2026-W36","status":"All Timesheets","hierarchy":false,"fromMounted":true}'

Success response — 200 OK

{
  "time_sheet_list": [
    {
      "user": {
        "id": 237,
        "name": "Byfield, Kadian",
        "full_name": "Byfield, Kadian",
        "firm_admin": 0,
        "first_name": "Kadian",
        "last_name": "Byfield",
        "firm_id": 32,
        "active": 1,
        "deleted": 0,
        "enc_id": "eyJpdiI6...",
        "disposition_status": "SUBMIT"
      },
      "week": "09-04-2026",
      "status": "SUBMIT",
      "viewTimesheet": true
    }
  ],
  "viewHierarchy": true
}
Field Type Description
time_sheet_list[] object[] One row per included user.
time_sheet_list[].user object The POST /api/task/timesheet/user/list user object plus disposition_status (a duplicate of the sibling status).
time_sheet_list[].week string Week-ending label, MM-DD-YYYY.
time_sheet_list[].status string The week's disposition, uppercased; "UNSUBMIT" when synthesised.
time_sheet_list[].viewTimesheet boolean Whether the caller may open ("eye" icon) that user's sheet. Computed independently of hierarchy mode.
viewHierarchy boolean Present only when fromMounted is true. Whether the caller may use the Reporting Hierarchy toggle.

Errors

Status Condition Message/shape
400 week or status missing / not a string, or hierarchy / fromMounted not booleans Standard validation error array
401 No session / not authenticated / user row missing { "message": "These credentials do not match our records." }

Notes

  • Row inclusion is a two-stage gate. A user with a real disposition row for the week is included whenever the filter matches, regardless of active/deleted status. A user with no disposition row is synthesised as UNSUBMIT only when all three hold: their timesheet_config.submit is truthy, the filter includes UNSUBMIT, and they are either active or have time_history rows in that week.
  • Candidates are intersected with users who have a live (non soft-deleted) timesheet_config row: a user never added via Timesheet Configure never appears here at all, however active they are.
  • Hierarchy mode swaps the candidate set to the caller's direct reports (themselves drawn from non-deleted timesheet_config rows) and swaps the submit map to the approver-scoped one.
  • Hierarchy is a display filter, not a narrower security boundary — the export endpoint re-derives the wider non-hierarchy ceiling.
  • A firm-less Global Master System Administrator is exempted from the firm clause on both the candidate list and the timesheet_config intersection, since firm resolution would otherwise substitute their own user id and empty the report.

POST/api/task/timesheet/weekly/export

Builds a styled .xlsx workbook of timesheet data and returns it as a base64 data: URI in the JSON body. Four different payload shapes select four different workbook layouts.

Module Tasks & Time > Timesheet > Timesheet Reports
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session. For Shapes 2 and 3 only, userArray is filtered against the caller's entitlement ceiling (every firm user when the caller is a Global Master System Administrator / firm admin / has view_others; otherwise self + direct reports). Unauthorised ids are silently dropped and a warning is logged. Shapes 1 and 4 render client-supplied rows without a server-side re-check.
Content type application/json — the workbook is embedded as a data: URI, not streamed as a binary download.

Request body. Every field is optional; the combination present selects the shape.

Field Type Required Validation / allowed values Description
exportName string No Optional, string Report title. Defaults to "Timesheet Report".
week string No Optional, string Week string YYYY-Www, used only for the "Week Ending" header line and (Shapes 2/3) the server-side entry fetch. Unparseable values are echoed verbatim; an empty value yields "".
fromReport unknown No Optional (no type validator) Truthy ⇒ Shape 1 (report list).
data Record<string, unknown>[] No Optional, array Shape 1: the report rows ({ week, status, user: { full_name \| name } }). Shape 4: the grid rows; nested arrays are flattened one level.
userArray Record<string, unknown>[] No Optional, array Non-empty ⇒ Shape 2 or 3. Each element must carry an id; entries are then fetched server-side for those users.
withSummary unknown No Optional (no type validator) With userArray: truthy ⇒ Shape 3 (summary), falsy ⇒ Shape 2 (details).
user Record<string, unknown> No Optional (no type validator) Shape 4: only user.name is read, for the "User" header line.
status unknown No Optional (no type validator) Shape 4: stringified into the "Disposition Status" header line.
capture unknown No Optional (no type validator) Shape 4: "Total Time Capture" header value, in seconds, rendered as HH:mm.
reported unknown No Optional (no type validator) Shape 4: "Total Time Reported" header value, in seconds, rendered as HH:mm.

Shape selection, evaluated in order: fromReport truthy → Shape 1; else userArray non-empty → Shape 3 if withSummary truthy, otherwise Shape 2; else → Shape 4.

Shape 4 reads these keys from each row of data: company, project, name, service_line, assigned_role, invoice_detail, invoicing, date, capture_time, reported_time, estimated_time, billable_time, no_charge_time, non_billable_time, time_notes.

Example request (Shape 2 — details for selected users)

curl -X POST https://api.example.com/api/task/timesheet/weekly/export \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
        "exportName": "Weekly Timesheet Report",
        "week": "2026-W36",
        "userArray": [{ "id": 237 }, { "id": 32 }],
        "withSummary": false
      }'

Success response — 200 OK

{
  "error": false,
  "redirect": "data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,UEsDBBQABgAIAAAAIQ...",
  "exportName": "Weekly Timesheet Report",
  "total": 42
}
Field Type Description
error boolean Always false on this path.
redirect string The complete workbook as a base64 data: URI. The client turns this into a download.
exportName string Echo of the resolved report name.
total number Number of rows in the sheet including the header/meta rows.

Workbook layouts (the sheet tab is always named Worksheet):

Shape Trigger Meta rows Column headers
1 — Report list fromReport truthy Report Name, Week Ending, Generated On, Generated By Week Ending, User, Status
2 — Details userArray non-empty, withSummary falsy Report Name, Week Ending, Generated On, Generated By User, Company, Project, Task Name, Service Line, Role, Task Type, Invoicing Details, Invoicing, Date, Time Capture, Time Reported, Difference, Billable Time, No Charge Time, Non Billable Time, Time Notes
3 — Summary userArray non-empty, withSummary truthy Report Name, Week Ending, Generated On, Generated By, Total Time Capture, Total Time Reported User, Company, Project, Time Capture, Time Reported, Billable Time, No Charge Time, Non Billable Time
4 — Single-user weekly neither of the above Report Name, User, Week Ending, Generated On, Generated By, Disposition Status, Total Time Capture, Total Time Reported Company, Project, Task Name, Service Line, Role, Invoicing Details, Invoicing, Date, Time Capture, Time Reported, Estimated Time, Billable Time, No Charge Time, Non Billable Time, Time Notes

Shape 1 maps raw statuses to past-tense labels: SUBMIT→Submitted, UNSUBMIT→Unsubmitted, APPROVE→Approved, UNAPPROVE→Unapproved, REJECT→Rejected, VOID→Voided, UNVOID→Unvoided. An unrecognised status is written through unchanged.

Errors

Status Condition Message/shape
400 exportName / week not strings, or data / userArray not arrays Standard validation error array
401 No session / not authenticated / user row missing { "message": "These credentials do not match our records." }

No other error path exists — an empty result set produces a workbook containing a merged, centred --No Record Found-- placeholder row rather than an error.

Notes

  • Shapes 2 and 3 are authorised server-side. Ids outside the caller's entitlement ceiling are dropped from the export silently (the caller still gets a 200), with a server-side warning recording how many were removed.
  • All time cells are seconds converted to HH:mm (with second rounding re-applied first). Negative values are prefixed - and coloured red (FF0000). Conversion is anchored on a per-shape "anchor column" — a row whose anchor cell is not a finite number is left untouched.
  • Header rows are bold 12pt; Shape 3 additionally shades group rows E3E3E3, bolds total rows, and applies hairline top borders. Column widths are auto-approximated to min(longest cell + 2, 60) characters.
  • "Generated On" is always formatted in America/New_York, independent of the server's own timezone.
  • Exports never write to time_history: on the export path allocation runs with persistence disabled, so downloading a report cannot mutate stored time.
  • To push the result to Google Drive/Sheets the client calls POST /api/send/drive (see Conventions) — that route does not belong to this module.

11.9.4 Timesheet Configure

POST/api/task/timesheet/config

Loads the Timesheet Configuration grid for one firm: every visible firm user with their approver, permission flags, cost rate and full/part-time status, plus the dropdown option sets and the firm's "use timesheet" master switch.

Module Tasks & Time > Timesheet > Timesheet Configure
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session. Reading the caller's own firm is always allowed; reading another firm requires Global Master System Administrator (role id 1) or Global User Administrator (role id 2, resolved by name as well as id).
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmId number Yes Numeric (numeric strings are coerced), integer The firm to load. Note timesheet_config.firm_id keys on firms.user_id, not firms.id; the firm lookup accepts either (WHERE user_id = ? OR id = ?).

Example request

curl -X POST https://api.example.com/api/task/timesheet/config \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"firmId":32}'

Success response — 200 OK

{
  "allowUser": true,
  "userOptions": [
    { "id": 237, "name": "Byfield, Kadian" },
    { "id": 32, "name": "Alvarez, Mia" }
  ],
  "configData": [
    {
      "id": 4471,
      "firm_id": 32,
      "user_id": 237,
      "approver_id": 32,
      "submit": 1,
      "authorized": 0,
      "view_others": 0,
      "view_invoice_reports": 1,
      "view_profitablity_reports": 2,
      "cost_per_hour": 85,
      "time_status": 1,
      "created_at": "2026-02-11T09:14:02.000Z",
      "updated_at": "2026-08-30T16:02:55.000Z",
      "deleted_at": null,
      "user_name": "Byfield, Kadian"
    },
    {
      "user_id": 32,
      "user_name": "Alvarez, Mia",
      "approver_id": 0,
      "submit": 1,
      "authorized": 0,
      "view_others": 0,
      "view_invoice_reports": 0,
      "view_profitablity_reports": 0,
      "cost_per_hour": "0.00",
      "time_status": 1
    }
  ],
  "firmAdmin": 32,
  "dropdownValues": {
    "viewInvoiceReportsOptions": [
      { "id": 1, "name": "All" },
      { "id": 2, "name": "My Company Groups" }
    ],
    "viewProfitabilityReportsOptions": [
      { "id": 0, "name": "Select" },
      { "id": 2, "name": "No" },
      { "id": 1, "name": "Yes" }
    ],
    "timeStatusOptions": [
      { "id": 1, "name": "Full Time" },
      { "id": 2, "name": "Part Time" }
    ]
  }
}
Field Type Description
allowUser boolean firms.use_timesheet === 1 — the firm-level master switch.
userOptions[] object[] { id, name } for every visible firm user. This is also the pool the Approver dropdown draws from — the same source as configData, so a user can never appear in one and not the other.
configData[] object[] One entry per visible user, in the same order as userOptions. A user with a timesheet_config row gets that row returned with all of its stored columns (so cost_per_hour arrives as a number) plus user_name. A user without one gets a synthetic default: approver_id: 0, submit: 1, authorized: 0, view_others: 0, view_invoice_reports: 0, view_profitablity_reports: 0, cost_per_hour: "0.00" (a string), time_status: 1 — and no id.
firmAdmin number The id of the first visible user with firm_admin = 1, or 0.
dropdownValues.viewInvoiceReportsOptions[] object[] 1 All, 2 My Company Groups.
dropdownValues.viewProfitabilityReportsOptions[] object[] 0 Select (placeholder), 2 No, 1 Yes. Note that 2 means No, not Yes.
dropdownValues.timeStatusOptions[] object[] 1 Full Time, 2 Part Time.

Note the DB column is spelled view_profitablity_reports (missing the second i) and the response preserves that spelling; the dropdown key is spelled correctly (viewProfitabilityReportsOptions).

Errors

Status Condition Message/shape
400 firmId missing or not coercible to an integer Standard validation error array
401 Not authenticated, or firmId is another firm and the caller is neither a Global Master System Administrator nor a Global User Administrator { "message": "These credentials do not match our records." }

Notes

  • Visible users are users rows with firm_id = firmId, COALESCE(deleted,0) = 0, deleted_at IS NULL and COALESCE(active,1) = 1, ordered lf_name, first_name, last_name. Inactive users are therefore excluded from both the grid and the Approver dropdown. Both deleted = 0 and deleted_at IS NULL are applied because the two flags are independent in this schema and either alone lets rows through.
  • cost_per_hour is typed differently between the two configData branches (a number for a stored row, a string for a default). The save endpoint normalises the value to a string on receipt — see below.

GET/api/task/timesheet/firm/dropdown

Returns every non-deleted firm as { id, name }, for the Global Timesheet Configuration screen's firm selector.

Module Tasks & Time > Timesheet > Timesheet Configure
Authentication Required — fully authenticated session (two-factor completed)
Authorization Restricted. Requires Global Master System Administrator (role id 1) or Global User Administrator (role id 2) — the same gate as the screen it feeds. Everyone else gets 401.
Content type application/json

This endpoint takes no path parameters, no query parameters and no request body.

Example request

curl -X GET https://api.example.com/api/task/timesheet/firm/dropdown \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

[
  { "id": 32, "name": "Acme Accounting Partners" },
  { "id": 118, "name": "Beacon CPA Group" }
]
Field Type Description
(root) object[] A bare array, ordered by firm name ascending.
id number firms.user_id, not firms.id — this is the value timesheet_config.firm_id holds and the value POST /api/task/timesheet/config expects as firmId.
name string firms.account_name (falling back to firms.name if the column is absent).

Errors

Status Condition Message/shape
401 Not authenticated, or the caller is neither a Global Master System Administrator nor a Global User Administrator { "message": "These credentials do not match our records." }

Notes

  • The id semantics are load-bearing: the value returned here is firms.user_id and must be sent back as firmId.
  • The deleted filter adapts to the schema: COALESCE(deleted,0) = 0 if the column exists, else deleted_at IS NULL, else no filter.
  • This is the only GET route in the Timesheet module.

POST/api/task/timesheet/config/save

Saves the Timesheet Configuration grid. Two mutually exclusive modes: toggling the firm's use_timesheet master switch, or rewriting the per-user configuration rows.

Module Tasks & Time > Timesheet > Timesheet Configure
Authentication Required — fully authenticated session (two-factor completed)
Authorization Restricted. A Global Master System Administrator (role id 1) may save any firm. Anyone else must be that firm's firm admin (users.firm_admin = 1) and be saving their own firm. Everyone else gets 401. Note this is stricter than the read gate — a Global User Administrator may read but not save.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmId number No Optional, numeric (numeric strings are coerced), integer Target firm (keyed on firms.user_id). Defaults to the caller's own resolved firm id.
colName string No Optional, string Presence selects toggle mode. The only supported value is "use_timesheet"; anything else is rejected.
colValue number No Optional, numeric (numeric strings are coerced), integer The new firms.use_timesheet value. Defaults to 0 when omitted in toggle mode.
data object[] No Optional, array, each item validated Grid rows. Used only when colName is absent.

Each element of data:

Field Type Required Validation / allowed values Description
id number No Optional, numeric (numeric strings are coerced), integer Existing timesheet_config.id. Not used for matching — the upsert re-looks-up by (firm_id, user_id).
user_id number Yes Numeric (numeric strings are coerced), integer The configured user.
approver_id number Yes Numeric (numeric strings are coerced), integer Who approves this user's timesheets. 0 means none.
submit number Yes Numeric (numeric strings are coerced), integer Whether the user is offered in the Reports roster when they have no disposition row. 0 suppresses the synthesised UNSUBMIT row.
authorized number Yes Numeric (numeric strings are coerced), integer Grants the Reporting Hierarchy toggle on Timesheet Reports.
view_others number Yes Numeric (numeric strings are coerced), integer Widens the user dropdowns to every firm user. Does not grant edit rights.
view_invoice_reports number Yes Numeric (numeric strings are coerced), integer 1 All, 2 My Company Groups.
view_profitablity_reports number Yes Numeric (numeric strings are coerced), integer 0 unset/Select, 2 No, 1 Yes. Note the column spelling.
cost_per_hour string Yes Normalised to a string server-side, then validated as a string Cost rate. A numeric value is accepted and converted to a string, so a row loaded from POST /api/task/timesheet/config can be posted back unchanged. null and undefined are not converted and are rejected with 400.
time_status number Yes Numeric (numeric strings are coerced), integer 1 Full Time, 2 Part Time.

Example request (grid save)

curl -X POST https://api.example.com/api/task/timesheet/config/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
        "firmId": 32,
        "data": [
          {
            "id": 4471,
            "user_id": 237,
            "approver_id": 32,
            "submit": 1,
            "authorized": 0,
            "view_others": 0,
            "view_invoice_reports": 1,
            "view_profitablity_reports": 2,
            "cost_per_hour": "85.00",
            "time_status": 1
          }
        ]
      }'

Example request (master-switch toggle)

curl -X POST https://api.example.com/api/task/timesheet/config/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"firmId":32,"colName":"use_timesheet","colValue":1}'

Success response — 200 OK

{
  "error": false,
  "message": "Timesheet Configuration updated successfully."
}
Field Type Description
error boolean false on success.
message string "Timesheet Configuration updated successfully." — identical for both modes.

Errors

Status Condition Message/shape
200 colName present but not "use_timesheet" { "error": true, "message": "Unsupported timesheet setting." }
200 Toggle mode and no firms row matches firmId by user_id or id { "error": true, "message": "Firm not found." }
400 A data row is missing a required field, or cost_per_hour is null/undefined Standard validation error array
401 Not authenticated, or the caller is not a Global Master System Administrator and is not the firm admin of firmId { "message": "These credentials do not match our records." }

Notes

  • Authorisation is enforced server-side, independently of whether the client renders the grid as editable. A caller who is neither a Global Master System Administrator nor the firm admin of firmId cannot rewrite approvers, permission flags or cost rates by calling this endpoint directly.
  • Toggle mode resolves the firm row by user_id first, then id as a fallback, then updates by primary key. A combined user_id = ? OR id = ? lookup is not used, because it can resolve to the wrong firm.
  • Grid mode is a scoped wipe-and-resave: every timesheet_config row for the firm whose user_id is in the currently visible user set is soft-deleted (deleted_at = now()), then each posted row is upserted by (firm_id, user_id) with deleted_at = NULL. The scoping matters because the grid hides inactive users: an unscoped wipe would discard their cost rate, approver and permissions with nothing left to restore on reactivation.
  • Rows are written one statement at a time with no enclosing transaction.
  • Sending colName alongside data means data is ignored — the toggle branch returns before the grid branch runs.
  • Sending neither colName nor data still performs the scoped wipe (soft-deleting every visible user's config) and then returns success with nothing re-inserted.

11.9.5 Timesheet Audit Log

POST/api/task/timesheet/audit/list

Returns a paged, filtered, sorted list of timesheet audit records — the Created / Updated / Deleted trail written by POST /api/task/timesheet/save. Optionally returns the filter dropdown data in the same call.

Module Tasks & Time > Timesheet > Timesheet Audit Log
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session. Rows are scoped to the caller's company allow-list — a Global Master System Administrator (role id 1) gets no company restriction; a mapped (non-firm) user is limited to their user_company_mapping companies; a firm user/admin is limited to company.firm_id = <their firm>.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
offset number No Optional, numeric (numeric strings are coerced), integer SQL OFFSET. Defaults to 0.
limit number No Optional, numeric (numeric strings are coerced), integer SQL LIMIT. Defaults to 30. No upper bound is enforced.
sort_field string No Optional, string One of Date, User, Week Ending, Timesheet Date, Action. Anything else (including Time) falls back to tad.created_at.
sort_order string No Optional, string asc or desc (case-insensitive); anything else is treated as desc. Ignored — forced to DESC — whenever sort_field is not one of the five recognised values.
filters object[] No Optional, array, each item validated Column filters, AND-combined.
fetchDropDown boolean No Optional, boolean true wraps the result and includes the dropdown data (see Success response).
includeInactiveOptions number No Optional, numeric (numeric strings are coerced), integer 1 includes rows authored by inactive users (drops the u.active = 1 clause). Any other value keeps them hidden.

Each element of filters:

Field Type Required Validation / allowed values Description
column string Yes String Which column to filter. Recognised: Date, User, Week Ending, Timesheet Date, Action, Original Value, New Value. An empty or unrecognised value is skipped.
filter unknown No Optional (no type validator) The filter value; its accepted form depends on column.

Per-column filter semantics:

column Accepted filter Applied as
Date A preset string: Today, Yesterday, This Week, Last Week, Next Week, This Month, Last Month, Next Month — all resolved in America/New_York (weeks run Mon–Sun here, unlike the Sat–Fri timesheet week). Or "Custom/<start>/<end>". Or an object { startDate, endDate }. DATE(tad.created_at) BETWEEN ? AND ?
User A numeric user id (not a display name). Non-numeric or <= 0 is skipped. tad.user_id = ? (or tad.created_by = ?)
Week Ending MM-DD-YYYY or YYYY-MM-DD DATE(tad.week_ending) = ?
Timesheet Date MM-DD-YYYY or YYYY-MM-DD DATE(th.created_at) = ?
Action A non-empty string, e.g. Created, Updated, Deleted tad.action = ? (exact)
Original Value A non-empty string tad.original_data LIKE '%…%'
New Value A non-empty string tad.new_data LIKE '%…%'

Example request

curl -X POST https://api.example.com/api/task/timesheet/audit/list \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
        "offset": 0,
        "limit": 30,
        "sort_field": "Date",
        "sort_order": "desc",
        "includeInactiveOptions": 0,
        "filters": [
          { "column": "Date", "filter": "This Month" },
          { "column": "User", "filter": 237 },
          { "column": "Action", "filter": "Updated" }
        ]
      }'

Success response — 200 OK

When fetchDropDown is falsy or absent, the body is a bare array:

[
  {
    "id": 90211,
    "user_id": 237,
    "user_name": "Byfield, Kadian",
    "task_name": "Reconcile bank feed",
    "week_ending": "09-04-2026",
    "timesheet_date": "09-02-2026",
    "action": "Updated",
    "original_data": { "reported_time": 3600, "billable_time": 3600 },
    "new_data": { "reported_time": 5400, "billable_time": 5400 },
    "audit_date": "09-02-2026",
    "audit_time": "02:35 PM",
    "created_at": "2026-09-02T18:35:11.000Z"
  }
]

When fetchDropDown is true, the same array is wrapped:

{
  "dropDownData": { "users": [ { "id": 237, "name": "Byfield, Kadian", "deleted": 0, "active": 1 } ] },
  "listData": [ { "id": 90211, "...": "..." } ]
}
Field Type Description
id number timesheet_audit_details.id.
user_id number Author of the change (0 when the join found nobody).
user_name string users.lf_name, falling back to first_name + " " + last_name. "" when unresolved.
task_name string time_history.name — the task the audited row belongs to. Needed because an Updated payload carries only changed columns.
week_ending string MM-DD-YYYY, or "".
timesheet_date string MM-DD-YYYY derived from time_history.created_at, or "".
action string Created, Updated or Deleted as written by the save endpoint.
original_data object | string The stored JSON, parsed; left as the raw string if it does not parse. For Created/Deleted this is the full 14-column snapshot; for Updated, only the changed keys' previous values.
new_data object | string | null Same treatment. null for Created/Deleted.
audit_date string MM-DD-YYYY of tad.created_at.
audit_time string hh:mm AM/PM of tad.created_at.
created_at string The raw tad.created_at value as returned by the driver.
dropDownData.users[] object[] Only when fetchDropDown is true — identical to POST /api/task/timesheet/audit/dropdownData's payload.

Errors

Status Condition Message/shape
400 offset/limit/includeInactiveOptions not integers, sort_field/sort_order not strings, filters not an array, or a filter element lacking column Standard validation error array
401 No session / not authenticated / user row missing { "message": "These credentials do not match our records." }

Notes

  • The response has two different top-level shapes depending on fetchDropDown — a bare array, or { dropDownData, listData }. Clients must branch on the flag they sent.
  • No total-count field is returned; paging is offset/limit only.
  • The audit table is joined to time_history via time_history_id (with a timesheet_id variant tolerated) using an INNER JOIN, so an audit row whose time_history row no longer exists does not appear at all.
  • The schema is probed at runtime: a missing user_id, created_by, week_ending, deleted_at or link column degrades the query gracefully rather than erroring. If the timesheet_audit_details table does not exist at all, an empty array is returned.
  • The User sort orders by users.user_name, while the column displays and filters on lf_name.
  • The company allow-list is applied only when the time_history link column exists; without it no company restriction is emitted.

POST/api/task/timesheet/audit/dropdownData

Returns the user list that populates the Audit Log's "User" filter dropdown.

Module Tasks & Time > Timesheet > Timesheet Audit Log
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session. The firm clause is applied only when the caller's own users.firm_id > 0 — a firm-less Global Master System Administrator therefore lists every user in the system.
Content type application/json

Request body, the same body schema as POST /api/task/timesheet/audit/list. Only one field is read by this endpoint:

Field Type Required Validation / allowed values Description
includeInactiveOptions number No Optional, numeric (numeric strings are coerced), integer 1 includes soft-deleted users (drops the deleted_at IS NULL clause). Any other value keeps them hidden.

offset, limit, sort_field, sort_order, filters and fetchDropDown are accepted by the request schema (and so pass validation) but do not affect this endpoint's result.

Example request

curl -X POST https://api.example.com/api/task/timesheet/audit/dropdownData \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"includeInactiveOptions":0}'

Success response — 200 OK

{
  "users": [
    { "id": 32,  "name": "Alvarez, Mia",    "deleted": 0, "active": 1 },
    { "id": 237, "name": "Byfield, Kadian", "deleted": 0, "active": 1 }
  ]
}
Field Type Description
users[] object[] Ordered by lf_name, then first_name, then last_name.
users[].id number User id — this is the value the Audit Log's User filter expects.
users[].name string users.lf_name, falling back to a formatted first/last name.
users[].deleted number The hard-delete int flag. Returned as data, not filtered on.
users[].active number 1 active, 0 inactive. Returned as data, not filtered on.

Errors

Status Condition Message/shape
400 includeInactiveOptions not coercible to an integer (or another body field has the wrong type) Standard validation error array
401 No session / not authenticated / user row missing { "message": "These credentials do not match our records." }

Notes

  • The visibility gate is users.deleted_at only; the deleted int flag is not used here, because the two columns can disagree for a given record. Both flags are still returned as data so the client can render an "(inactive)" suffix.
  • The deleted_at column's existence is probed at runtime; if absent, no inactive filter is applied at all.
  • The same payload is reachable inline by calling POST /api/task/timesheet/audit/list with fetchDropDown: true, which saves a round trip on the screen's initial load.

↑ Back to Table of Contents


12. Monthly Close

The Monthly Close module drives four screen families: Close Status (a manager-facing roll-up of where every company stands in the current close), Close Details (the nine per-company review sections and their drill-down lists), Firm Configuration (firm-wide period definitions plus the per-section Configure screens), and the supporting Review Assignments, QuickBooks Close Dates and Open Comments surfaces.

Every route in this section is mounted under an /api prefix, so the full path is /api/<route path>. Every route reads the caller's identity from the session (session.login), which requires session.login.userId and session.login.state === 'authenticated'. Permission decisions are not made at the route level; they are evaluated server-side per request. The module's permission menu id is MONTHLY_CLOSE_MENU_ID = 3, which is the menu that per-company user_company_access / user_group_access grants are looked up against; access levels are 1 NoAccess, 2 ViewOnly, 3 Standard, 4 Admin.

Selecting a period and a company scope. Almost every endpoint here takes the period as a month token + year pair in the request body — monthVal/yearVal on most routes, month/year on the Close Status manager tabs and Review Assignments. The pair is collapsed server-side into a YYYY-MM-01 month-start string: the month token is matched case-insensitively by 3-letter prefix (jan…dec), an unmatched token falls back to the current month, and a zero/absent year falls back to the current year. That month-start is the key used against mc_date / as_on_date / comment_date. The company (or company-group) scope is likewise passed per request — companyId on the Close Details and Configure routes, cg_id/cgId (a c_<id> / g_<id> token) plus id and type ('company' | 'group' | '') on the scoped/roll-up routes. Separately, the browser's sticky selection lives in the session and is set through the Session module's POST /api/company/session/period (month/year only, company preserved) and POST /api/company/session (company); see the Session section. Monthly Close's month/year selects post to POST /api/company/session/period so the period carries across screens, but the Monthly Close endpoints themselves never read it — they always use the values in their own request body.

Request validation runs on every route. Unknown body properties are stripped silently (no 400), and numeric fields accept numeric strings, which are coerced before validation.

Errors common to the whole module:

Status Condition Message/shape
401 No authenticated session, or the user row is missing/deleted { "message": "These credentials do not match our records." }
401 The requested companyId does not belong to the caller's firm { "message": "These credentials do not match our records." }
400 Request validation rejects the body { statusCode, message: [...], error: "Bad Request" }

12.1 Close Status

The Close Status screen has two halves: the review-status grid (one node per company group, lazily walked by the client) and the three manager roll-up tabs — Calendar (closeDeadline), Relative (unClosedDeadline) and Completion (completionResults) — each with a drill-down modal and an Excel export.

POST/api/mc/peer/review/status/filter

Returns one company-group node's Close Status rows — that group's own direct companies (paged) plus its direct child-group ids, which the client walks recursively.

Module Monthly Close > Close Status
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Results are scoped to the caller's firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
monthVal string Yes Must not be empty String Month token (e.g. Apr), resolved by prefix.
yearVal number Yes Must not be empty Integer Four-digit year.
cg_id string No String Scope token, e.g. g_12 / c_45. Empty falls back to "All Companies" in the firm.
id number No Integer Numeric id of the selected company/group.
type string No One of group, company, "" Which kind of id id is.
primary number No Integer Filter by primary-accountant user id.
reviewer number No Integer Filter by reviewer user id.
status number No Integer Filter by status code (see the status table under Close Details). 0 = no filter.
closeDead number No Integer Filter by close-deadline day. 0 = no filter.
sortColumn string No One of company, primary, reviewer Defaults to company.
sortBy string No One of asc, desc Defaults to asc.
page number No Integer Page index into the group's own companies.
includeInactive boolean No Boolean Include inactive companies in the picker set.

Example request

curl -X POST https://api.example.com/api/mc/peer/review/status/filter \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"monthVal":"Apr","yearVal":2026,"cg_id":"g_12","id":12,"type":"group","page":0}'

Success response — 201 Created

{
  "start_time": "2026-09-01T10:12:33.001Z",
  "end_time": "2026-09-01T10:12:33.412Z",
  "error": false,
  "peerStatusArr": {
    "corg": "group",
    "data": [
      {
        "group": "West Region",
        "groupCompanyLength": 57,
        "isLoopAgain": true,
        "currentIndex": 50,
        "groupId": 12,
        "groupChild": ["g_18", "g_19"],
        "company": [
          {
            "status": "Completed",
            "statusCode": 5,
            "statusClass": "fa-check text-success animated tada",
            "company_name": "Acme Holdings",
            "company_id": 401,
            "date": "04/30/26",
            "close": "15",
            "primary": { "id": 88, "name": "Dana Reed", "email": "dana@firm.com" },
            "reviewer": [{ "id": 91, "name": "Sam Ortiz", "email": "sam@firm.com" }]
          }
        ]
      }
    ],
    "commentCount": { "401": "2/7" },
    "commentType": 104,
    "monthVal": "Apr",
    "yearVal": 2026
  }
}
Field Type Description
start_time / end_time string ISO timestamps stamped at response build.
peerStatusArr.corg string group, company, or the fallback scope type.
peerStatusArr.data[] object[] One node. isLoopAgain/currentIndex drive the client's paging of this group's own companies; groupChild holds g_<id> tokens for the child groups to request next.
peerStatusArr.data[].company[] object[] date is the completion date (m/d/yy) or -; close is the close-deadline day as a string; primary is an object ({} when unassigned); reviewer is an array (a single object for company scope).
peerStatusArr.commentCount object companyId → "<unresolved>/<total>".
peerStatusArr.commentType number Always 104 (the Close Status comment type).

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • For a group scope this returns exactly one node, not the flattened subtree; the client recursively re-calls this endpoint once per child group to walk the tree.
  • An empty scope (cg_id blank / id 0) falls back to every company in the firm.
  • Close Status data is never filtered by the company's active/deleted state; includeInactive only affects the picker option list.
POST/api/mc/status/manager/closeDeadline

The Calendar tab: per-manager completed/total counts bucketed by the firm's configured calendar periods (day-of-month ranges), plus the drill-down detail rows behind each bucket.

Module Monthly Close > Close Status
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Firm-scoped server-side.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
month string Yes Must not be empty String Month token.
year number Yes Must not be empty Integer Four-digit year.
cgId string No String Scope token (g_<id> / c_<id>).
id number No Integer Selected company/group id.
type string No One of group, company, "" Kind of id.
includeInactive boolean No Boolean (string forms are coerced) Boolean Include inactive companies.

Example request

curl -X POST https://api.example.com/api/mc/status/manager/closeDeadline \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"month":"Apr","year":2026,"cgId":"g_12","id":12,"type":"group"}'

Success response — 201 Created

{
  "data": [
    {
      "manager_id": 88,
      "manager_name": "Dana Reed",
      "close_status": [
        { "completed": 4, "total": 6, "percent": 66.67 },
        { "completed": 9, "total": 9, "percent": 100 },
        { "completed": 13, "total": 15, "percent": 86.67 },
        { "completed": 0, "total": 2, "percent": 0 }
      ]
    },
    {
      "manager_id": "all_manager",
      "manager_name": "All Managers",
      "close_status": [{ "completed": 26, "total": 32, "percent": 81.25 }]
    }
  ],
  "columns": [
    { "order": 1, "type": "period", "text": "1 - 10", "original": "1 - 10" },
    { "order": 2, "type": "total", "text": "Total", "original": "total" },
    { "order": 3, "type": "noDeadline", "text": "Non Closing", "original": "noDeadline" }
  ],
  "report": {
    "88": { "1 - 10": [{ "company": "Acme Holdings", "deadline": "10", "primary": "Dana Reed", "reviewer": "Sam Ortiz" }], "total": [], "noDeadline": [] },
    "all_manager": { "1 - 10": [], "total": [], "noDeadline": [] }
  }
}
Field Type Description
data[] object[] One row per manager, sorted by manager_name, with a trailing all_manager aggregate row. close_status is positionally aligned with columns.
columns[] object[] Bucket definitions: one per configured calendar period, then total, then noDeadline ("Non Closing").
report object managerId → { <column.original>: detailRow[] }, plus an all_manager union. These rows feed the drill-down modal and are what the modal's export posts back.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • Deadline days are weekend-adjusted using the firm's close_deadline_config before bucketing.
  • Each column is evaluated independently, so overlapping configured periods each count the same company.
POST/api/mc/status/manager/unClosedDeadline

The Relative tab: per-manager counts of unclosed companies bucketed by how many days past/due the deadline is.

Module Monthly Close > Close Status
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Firm-scoped server-side.
Content type application/json

Request body (identical to closeDeadline above).

Example request

curl -X POST https://api.example.com/api/mc/status/manager/unClosedDeadline \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"month":"Apr","year":2026,"cgId":"g_12","id":12,"type":"group"}'

Success response — 201 Created

{
  "data": [
    { "manager_id": 88, "manager_name": "Dana Reed", "un_close": [{ "total": 3 }, { "total": 1 }, { "total": 4 }, { "total": 0 }] },
    { "manager_id": "all_manager", "manager_name": "All Managers", "un_close": [{ "total": 7 }] }
  ],
  "columns": [
    { "order": 1, "type": "period", "text": "1 - 5 Days Past", "original": "past_1_5" },
    { "order": 2, "type": "total", "text": "Total", "original": "total" },
    { "order": 3, "type": "noDeadline", "text": "Non Closing", "original": "noDeadline" }
  ],
  "details": {
    "88": { "past_1_5": [{ "company": "Acme Holdings", "deadline": "10", "primary": "Dana Reed", "reviewer": "Sam Ortiz" }], "total": [], "noDeadline": [] },
    "all_manager": { "past_1_5": [], "total": [], "noDeadline": [] }
  }
}
Field Type Description
data[].un_close[] object[] { total } per column, positionally aligned with columns (buckets + Total + Non Closing).
details object Same drill-down shape as report on the Calendar tab.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/mc/status/manager/completionResults

The Completion tab: per-manager closed-on-time / closed-late / unclosed / no-deadline counts with percentages.

Module Monthly Close > Close Status
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Firm-scoped server-side.
Content type application/json

Request body (identical to closeDeadline above).

Example request

curl -X POST https://api.example.com/api/mc/status/manager/completionResults \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"month":"Apr","year":2026,"cgId":"g_12","id":12,"type":"group"}'

Success response — 201 Created

{
  "data": [
    {
      "manager_id": 88,
      "manager_name": "Dana Reed",
      "closedOnTime": { "val": 12, "percent": 75 },
      "closedLate": { "val": 2, "percent": 12.5 },
      "unClosed": { "val": 2, "percent": 12.5 },
      "noDeadline": { "val": 1, "percent": 6.25 },
      "total": 16
    },
    {
      "manager_id": "all_manager",
      "manager_name": "All Managers",
      "closedOnTime": { "val": 20, "percent": 71.43 },
      "closedLate": { "val": 4, "percent": 14.29 },
      "unClosed": { "val": 4, "percent": 14.29 },
      "noDeadline": { "val": 2, "percent": 7.14 },
      "total": 28
    }
  ],
  "details": {
    "88": { "closedOnTime": [{ "company": "Acme Holdings", "deadline": "10", "closed": "04/28/26", "primary": "Dana Reed", "reviewer": "Sam Ortiz" }] },
    "all_manager": { "closedOnTime": [] }
  }
}
Field Type Description
data[] object[] Sorted by manager_name, trailing all_manager aggregate. total excludes noDeadline (it is closedOnTime + closedLate + unClosed).
details object Drill-down rows keyed by manager then bucket; the Completion tab's rows carry an extra closed (date closed) column.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/mc/status/manager/makeExport

Generates the drill-down modal export workbook from rows the client posts back, and returns a one-shot download link.

Module Monthly Close > Close Status
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session.
Content type application/json

Request body (the scope fields of the tab endpoints above, plus the export fields)

Field Type Required Validation / allowed values Description
month string Yes Must not be empty String Month token (rendered into the export's "As On Date" header).
year number Yes Must not be empty Integer Year.
cgId / id / type / includeInactive — No As on the tab endpoints above Scope fields.
tab string No One of calendar, relative, completion Defaults to calendar; picks the report name and sheet layout.
title any No none The modal's bucket title, rendered into the meta header.
exportName string No String User-chosen file name; \ / # ? " % are stripped server-side. Defaults to the report name.
managerName string No String The drill-down modal's manager, rendered into the meta header.
rows object[] No none The modal's visible rows, exported as-is. Non-array values yield an empty sheet body.

Example request

curl -X POST https://api.example.com/api/mc/status/manager/makeExport \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"month":"Apr","year":2026,"tab":"completion","title":"Closed On Time","managerName":"Dana Reed","exportName":"April completion","rows":[{"company":"Acme Holdings","deadline":"10","closed":"04/28/26","primary":"Dana Reed","reviewer":"Sam Ortiz"}]}'

Success response — 201 Created

{ "error": false, "redirect": "/api/mc/status/manager/download/generated/1f0c2b6e-2b1c-4a2f-9b8e-7f2e2a1d55c3" }
Field Type Description
error boolean Always false on success.
redirect string Path to GET /api/mc/status/manager/download/generated/:token. Single-use.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • Report name is derived from tab: Closed Status Report (calendar), Unclosed Status Report (relative), Completion Status Report (completion).
  • The Calendar tab export carries five parallel status columns; the relative and completion tabs export the modal's Company/Deadline/(Date Closed)/Primary/Reviewer table. Internal fields such as statusCode are never written to the sheet.
  • Note the route naming: the drill-down modal export posts to makeExport, while the grid export posts to makeIndivudualExport.
POST/api/mc/status/manager/makeIndivudualExport

Generates the grid export workbook by re-running the requested tab's own query server-side, and returns a one-shot download link. (The route path is spelled exactly as shown and must be requested that way.)

Module Monthly Close > Close Status
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session.
Content type application/json

Request body (same fields as makeExport; rows / title / managerName are ignored on this path because the grid is rebuilt server-side).

Example request

curl -X POST https://api.example.com/api/mc/status/manager/makeIndivudualExport \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"month":"Apr","year":2026,"cgId":"g_12","id":12,"type":"group","tab":"calendar","exportName":"April calendar grid"}'

Success response — 201 Created

{ "error": false, "redirect": "/api/mc/status/manager/download/generated/9c4a5b12-7d33-4c0a-b2ad-0f4e6a1b8e77" }
Field Type Description
error boolean Always false on success.
redirect string Path to the token download route. Single-use.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • The requested tab's own query (closeDeadline, unClosedDeadline or completionResults) is re-run server-side, so the export always matches what the grid would render for the same filters.
  • The sheet opens with a Report Name / Report Generated On / As On Date / Generated By meta block, then the grid including its merged title row and blank manager-column header.
GET/api/mc/status/manager/download/generated/:token

Streams a previously generated Close Status export workbook and deletes it.

Module Monthly Close > Close Status
Authentication Not required — possession of the single-use token is the only credential (unlike the QuickBooks Close Dates download, which additionally requires a matching session user)
Authorization No role requirement
Content type Binary .xlsx download

Path parameters

Name Type Required Description
token string Yes The UUID returned in the redirect of makeExport / makeIndivudualExport.

Example request

curl -OJ https://api.example.com/api/mc/status/manager/download/generated/9c4a5b12-7d33-4c0a-b2ad-0f4e6a1b8e77

Success response — 200 OK, Content-Disposition: attachment; filename="<exportName>.xlsx", body is the XLSX binary.

Errors

Status Condition Message/shape
404 Unknown or already-consumed token { "error": true, "message": "File not found." }

Notes

  • Single-use: the token is invalidated before streaming begins, and the temporary file is removed once the response finishes.
  • The generated workbook is held as temporary server-side storage and referenced by a single-use token. The download request must be served by the same application instance that produced the export, so multi-instance deployments must preserve instance affinity for these routes. This is the export mechanism used throughout the module. See Deployment & Operational Considerations.

12.2 Close Details

Close Details is the per-company close checklist. It comprises exactly nine sections, each with its own review screen, its own comment-module id, and — except Cash Flow Statement — a Configure screen:

section Section name Comment subModule id Review route Configure route
1 Bank and Card Reconciliations 59 /monthly-close/close-details/bank-card/review /monthly-close/close-details/bank-card/configure
2 Accounts Receivable 53 /monthly-close/close-details/accounts-receivable/review /monthly-close/close-details/accounts-receivable/configure
3 Accounts Payable 54 /monthly-close/close-details/accounts-payable/review /monthly-close/close-details/accounts-payable/configure
4 Balance Sheet General 62 /monthly-close/close-details/balance-sheet/review /monthly-close/close-details/balance-sheet/configure
5 Uncharacterized Revenue and Expense 56 /monthly-close/close-details/uncharacterized/review /monthly-close/close-details/uncharacterized/configure
6 Payroll 58 /monthly-close/close-details/payroll/review /monthly-close/close-details/payroll/configure
7 Income Statement General 55 /monthly-close/close-details/income-statement/review /monthly-close/close-details/income-statement/configure
8 Cash Flow Statement 57 /monthly-close/close-details/cash-flow/review # (no Configure workflow exists)
9 Other 60 /monthly-close/close-details/other/review /monthly-close/close-details/other/configure

The list is rendered in the order 1, 2, 3, 4, 5, 6, 9, 7, 8 — that is, Other sits between Payroll and Income Statement General.

Each section's Comments badge counts a fixed set of mc_comments.type ids, not the section id: 1 → [44,45,46,47]; 2 → [1,2,3,7,8,48,49]; 3 → [4,5,6,9,10,50,51]; 4 → [11,12,13,14,15,16,17,18,19,105,106,107]; 5 → [27,28,29,30,36,37,38,39]; 6 → [40,41,42,43]; 7 → [20,21,22,23,24,25,26]; 8 → [31,32,33,34,35]; 9 → [100].

Persisted mc_status.mc_status values are 1 Skip, 2 Submit, 3 Approve. The display status the API returns per section is:

statusId statusName Derived from
1 Not started no mc_status row and no comments
2 Skipped mc_status = 1
3 In Review mc_status = 2
4 Started no status row but the section has comments
5 Completed / Skip & Completed mc_status = 3 (the latter when is_skipped = 1)

Every write endpoint in this subsection except mc/status/update additionally requires Monthly Close edit rights: the caller's Monthly Close (menu 3) access for the company must be Admin (4) or Standard (3), with a blanket pass for a Global Master System Administrator or a firm admin. ViewOnly (2) and NoAccess (1) are rejected with 403.

POST/api/mc/monthly/close/filter

The Close Details landing grid: the nine sections with their status, comment counts and the whole-close QA action flags for the selected company and month.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the backend asserts the company belongs to the caller's firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Numeric (numeric strings are coerced) Integer Company to load.
monthVal string Yes String Month token.
yearVal number Yes Numeric (numeric strings are coerced) Integer Year.
status number No Integer Filter the returned sections by display statusId (1–5). 0/absent = all.

Example request

curl -X POST https://api.example.com/api/mc/monthly/close/filter \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"monthVal":"Apr","yearVal":2026}'

Success response — 201 Created

{
  "error": false,
  "approveError": "",
  "data": {
    "viewContent": [
      {
        "section": 1,
        "subModule": 59,
        "sectionName": "Bank and Card Reconciliations",
        "reviewRoute": "/monthly-close/close-details/bank-card/review",
        "configRoute": "/monthly-close/close-details/bank-card/configure",
        "statusId": 5,
        "statusName": "Completed",
        "statusCls": "fa-check text-success animated tada",
        "unresolvedCount": 1,
        "totalCount": 4,
        "module": 103
      }
    ],
    "qaStatusArray": {
      "canReview": true,
      "canApprove": false,
      "canSkip": false,
      "canUnReview": false,
      "canUnApprove": false,
      "canUnSkip": false,
      "hover_status": "",
      "canSubmit": true,
      "hasCompleted": true
    },
    "totalCommentCount": { "unresolvedCount": 3, "totalCount": 12 },
    "connection_qbo": 1
  }
}
Field Type Description
approveError string Same value as qaStatusArray.hover_status; rendered as the Approve button's tooltip.
data.viewContent[] object[] One entry per section (filtered by status when given), in MC_CLOSE_DETAILS_ORDER. module: 103 is the Monthly Close comment module id.
data.qaStatusArray object Whole-close action flags. canReview is the SUBMIT permission (primary accountant); canSkip/canApprove belong to the month's assigned reviewer. canSubmit is an alias of canReview. hasCompleted is true when any section is at statusId 5.
data.totalCommentCount object Count of every mc_comments row for the company/month — not the sum of the per-section badges.
data.connection_qbo number 1 when the company has a live QuickBooks connection, else 0.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • The QA flags are computed over all nine sections before the status filter is applied.
  • statusCls carries the icon and colour class tokens the client renders for each status.
POST/api/mc/status/update

Skip / Submit / Approve a single section, or run the corresponding "… All" action across the whole close; also supports reverting each of those.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Firm ownership of companyId is asserted; Monthly Close edit rights are not separately required — eligibility is expressed through the returned can* flags.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Integer Company.
monthVal string Yes String Month token.
yearVal number Yes Integer Year.
mode number Yes Integer 1 = Skip, 2 = Submit; any other value is treated as 3 (Approve).
section number Yes Integer Target section (1–9). Ignored when overall is true; values ≤ 0 select nothing.
overall boolean No Boolean Run the whole-close ("… All") variant.
isRevert number No Integer 1 = revert the action (Un-Skip / Un-Submit / Un-Approve).

Example request

curl -X POST https://api.example.com/api/mc/status/update \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"monthVal":"Apr","yearVal":2026,"mode":2,"section":4}'

Success response — 201 Created

{
  "error": false,
  "errorMsg": "",
  "canSkip": false,
  "canReview": false,
  "canApprove": true,
  "canUnSkip": false,
  "canUnReview": true,
  "canUnApprove": false,
  "hoverStatus": "",
  "canSubmit": false,
  "hover_status": ""
}
Field Type Description
error / errorMsg boolean / string false / "" on success.
can* / hoverStatus boolean / string The recomputed whole-close QA flags, spread into the top level of the response.
hover_status string Snake_case alias of hoverStatus, carrying the same value.

Errors

Status Condition Message/shape
200/201 mc_status table is absent in this deployment { "error": true, "message": "mc_status table not found." }

Notes

  • The "… All" variants do not apply the mode to all nine sections. Submit All / Skip All target only sections with no mc_status row yet; Approve All targets only sections currently at Skip or Submit; Un-Skip/Un-Submit/Un-Approve All target only the sections currently in that state.
  • Reverting never hard-deletes: Un-Skip and Un-Submit soft-delete (deleted = 1), and Un-Approve steps the row back to Skip (when is_skipped) or Submit, so a re-approved section still remembers it was skipped.
POST/api/other/section/details

The "Other Monthly Close Items" panel header: task counts, combined comment counts and the two saved checkbox flags for the company and date.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Numeric (numeric strings are coerced) Integer Company.
asOnDate string Yes String The close period date.

Example request

curl -X POST https://api.example.com/api/other/section/details \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"asOnDate":"2026-04-01"}'

Success response — 201 Created

{
  "count": {
    "totalTask": 14,
    "incompleteTask": 3,
    "unresolvedCount": 5,
    "totalCount": 21,
    "moduleId": 60,
    "mcUnResolve": 2,
    "mcTotal": 9
  },
  "check": { "other_section_incomplete": 0, "other_section_complete": 1 }
}
Field Type Description
count.totalTask / incompleteTask number Task counts for the company/period.
count.unresolvedCount / totalCount number Monthly Close plus task comment counts combined.
count.moduleId number The "Other" comment module id (60 by default).
count.mcUnResolve / mcTotal number The Monthly Close half of those counts, broken out.
check object mc_checkbox_data flags for this company/period; both keys always present, defaulting to 0. Written by POST /api/other/section/check.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/mc/bankcard/review/filter

Loads the Bank & Card Reconciliations review screen: bank accounts, credit cards, undeposited-funds data, balances and the section's QA status.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Returns canEdit so the UI can disable inputs.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Integer Company.
monthVal string Yes String Month token.
yearVal number Yes Integer Year.
active number No Integer Active/inactive account filter.

Example request

curl -X POST https://api.example.com/api/mc/bankcard/review/filter \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"monthVal":"Apr","yearVal":2026,"active":1}'

Success response — 201 Created

{
  "title": "Bank and Card Reconciliations",
  "moduleId": 59,
  "totalCommentCount": { "unresolvedCount": 1, "totalCount": 4 },
  "connection_qbo": 1,
  "filterTable": {
    "filterBankCard": [
      {
        "id": 900,
        "account_name": "Operating Checking",
        "account_num": "1000",
        "account_type": "Bank",
        "qbo_id": "35",
        "company_id": 401,
        "active": 1,
        "q_id": "35",
        "end_date": "2026-04-30",
        "is_date_posted": "35",
        "encrypt_data": "PVdKa0...",
        "comment_count": "0/0"
      }
    ],
    "filterCreditCard": [],
    "undepositData": [],
    "balanceArray": [],
    "asOnDate": "2026-04-01",
    "monthVal": "Apr",
    "yearVal": 2026
  },
  "qaStatus": {
    "canSkip": false, "canReview": true, "canApprove": false,
    "canUnSkip": false, "canUnReview": false, "canUnApprove": false,
    "hoverStatus": "", "canSubmit": true
  },
  "canEdit": true
}
Field Type Description
title string Section heading.
moduleId number comment_module.module_id for this section (falls back to 59).
connection_qbo number 1 when QuickBooks is connected.
filterTable.filterBankCard[] / filterCreditCard[] object[] One row per account. q_id echoes qbo_id when the reconciliation checkbox is ticked ('' when not); is_date_posted encodes the "all transactions posted" checkbox the same way; encrypt_data is the route param for the flagged-list drill-down. Credit-card rows carry an extra statement_date.
qaStatus object Section-level QA flags (section 1).
canEdit boolean Whether this user may edit Monthly Close for this company.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • Comment counts per row are populated separately; the initial payload ships "0/0".
  • The flagged-transaction counts are a second call — see POST /api/mc/bankcard/flag/allcount.
POST/api/mc/bankcard/flag/allcount

Phase-2 call for the Bank & Card screen: given every account's qbo_id, returns the flagged-transaction count per account.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Integer Company.
monthVal string Yes String Month token.
yearVal number Yes Integer Year.
qboIds (string|number)[] Yes Array Every account qbo_id on the screen. De-duplicated and stringified server-side.

Example request

curl -X POST https://api.example.com/api/mc/bankcard/flag/allcount \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"monthVal":"Apr","yearVal":2026,"qboIds":["35","36","91"]}'

Success response — 201 Created

{ "count": { "35": 4, "91": 1 } }
Field Type Description
count object qbo_id → number of flagged transactions. Accounts with zero flags are omitted. {} when qboIds is empty, monthVal/yearVal are missing, or the qbo_bank_cc_data table is absent.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • A transaction is flagged when its age at the month end exceeds the per-transaction-type day threshold. Thresholds resolve per company override → firm-wide global → a built-in default of 10.
  • Reconciled/cleared rows (is_clr in R, C) and soft-deleted rows are excluded.
GET/api/mc/bank/view/reconciliations/:id

The "View flagged List" drill-down for one bank or credit-card account.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the decoded company is firm-checked.
Content type application/json

Path parameters

Name Type Required Description
id string Yes The encrypt_data value from a Bank & Card review row — the encrypted triple "<qboId>/<companyId>/<asOnDate>". Both URL-encoded and raw forms are accepted.

Example request

curl -b 'genie.sid=<session-cookie>' \
  'https://api.example.com/api/mc/bank/view/reconciliations/PVdKa0hDVGZ3...'

Success response — 200 OK

{
  "error": false,
  "company_id": 401,
  "company_name": "Acme Holdings",
  "accountName": "1000 Operating Checking",
  "accountType": "Bank",
  "is_creditCard": false,
  "qbo_id": "35",
  "month": "Apr",
  "year": 2026,
  "monthVal": "Apr",
  "yearVal": 2026,
  "commentType": 44,
  "acceptType": 44,
  "subModuleId": 59,
  "bankCcData": []
}
Field Type Description
is_creditCard boolean True when the account type is Credit Card.
commentType / acceptType number 45 for a credit card, 44 for a bank account.
subModuleId number The Bank and Card Reconciliations comment-module id (default 59).
bankCcData[] object[] The flagged transactions for this account and month.

Errors

Status Condition Message/shape
200 The id cannot be decrypted, or decrypts to fewer than 3 parts, or has an empty qboId/companyId { "error": true, "message": "Invalid list id.", "bankCcData": [] }
POST/api/mc/save/bank/reconciliations

Per-row autosave for the Bank & Card screen: the reconciliation checkbox, the "all transactions posted" checkbox, statement dates, and the undeposited-funds row.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization Requires Monthly Close edit rights on the company: Admin/Standard access, or global/firm admin. Otherwise 403.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Integer Company.
type string Yes String 'bank' | 'credit' | 'undep'; not enum-validated.
month string No String Month token.
year number No Integer Year.
monthVal string No String Alias of month.
yearVal number No Integer Alias of year.
qboId string No String The account being saved.
chkBc string No String Holds the account's qbo_id when the reconciliation checkbox is ticked, '' when not.
chkDate string No String Same encoding for the "all transactions posted" checkbox.
stDate string No String Statement start date.
endDate string No String Statement end date.
undepDate string No String Undeposited-funds date.
undepCheck number No Integer Undeposited-funds checkbox value.

Example request

curl -X POST https://api.example.com/api/mc/save/bank/reconciliations \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"monthVal":"Apr","yearVal":2026,"type":"bank","qboId":"35","chkBc":"35","chkDate":"","endDate":"2026-04-30"}'

Success response — 201 Created

{ "error": false }

Errors

Status Condition Message/shape
403 Caller lacks Monthly Close edit rights on the company { "error": true, "message": "You do not have permission to edit Monthly Close for this company." }

Notes

  • Both key spellings (month/year and monthVal/yearVal) are accepted; the backend coalesces them.
POST/api/mc/ar/review/filter

Loads the Accounts Receivable review screen.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. canEdit is returned for UI gating.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Numeric (numeric strings are coerced) Integer Company.
monthVal string Yes String Month token.
yearVal number Yes Numeric (numeric strings are coerced) Integer Year.

Example request

curl -X POST https://api.example.com/api/mc/ar/review/filter \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"monthVal":"Apr","yearVal":2026}'

Success response — 201 Created

{
  "title": "Account Receivable",
  "moduleId": 53,
  "connection_qbo": 1,
  "totalCommentCount": { "unresolvedCount": 2, "totalCount": 6 },
  "qaStatus": {
    "canSkip": false, "canReview": true, "canApprove": false,
    "canUnSkip": false, "canUnReview": false, "canUnApprove": false,
    "hoverStatus": "", "canSubmit": true
  },
  "canEdit": true,
  "approveError": "",
  "report_date": "2026-04-30",
  "filterTable": {}
}
Field Type Description
title string Account Receivable.
moduleId number Resolved from comment_module by submodule name; falls back to 53.
qaStatus object Section 2 QA flags.
canEdit boolean Monthly Close edit rights for this company.
report_date string The AR aging report date.
filterTable object The AR review buckets (delinquent invoices, credit memos, unapplied payments), each carrying its own encrypted "View List" id.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/mc/ap/review/filter

Loads the Accounts Payable review screen.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. canEdit is returned for UI gating.
Content type application/json

Request body (structurally identical to the AR review filter: companyId, monthVal, yearVal, all required).

Example request

curl -X POST https://api.example.com/api/mc/ap/review/filter \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"monthVal":"Apr","yearVal":2026}'

Success response — 201 Created

{
  "title": "Account Payable",
  "moduleId": 54,
  "connection_qbo": 1,
  "totalCommentCount": { "unresolvedCount": 0, "totalCount": 3 },
  "qaStatus": {
    "canSkip": false, "canReview": true, "canApprove": false,
    "canUnSkip": false, "canUnReview": false, "canUnApprove": false,
    "hoverStatus": "", "canSubmit": true
  },
  "canEdit": true,
  "approveError": "",
  "report_date": "2026-04-30",
  "filterTable": {}
}

Field meanings are the same as the AR filter above, with title Account Payable, moduleId falling back to 54, and qaStatus covering section 3.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/mc/checkbox/save

Generic save for the per-section reconciliation attestation checkboxes (e.g. "does AR Aging tie to the Balance Sheet AR balance?").

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization Requires Monthly Close edit rights on the company; otherwise 403.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Integer Company.
monthVal string Yes String Month token.
yearVal number Yes Integer Year.
colName string Yes String The mc_checkbox_data.col_name discriminator, e.g. ar_balance. Not enum-constrained — each section uses its own name.
value number Yes Integer The stored value (0/1).

Example request

curl -X POST https://api.example.com/api/mc/checkbox/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"monthVal":"Apr","yearVal":2026,"colName":"ar_balance","value":1}'

Success response — 201 Created

{ "error": false }

Errors

Status Condition Message/shape
403 Caller lacks Monthly Close edit rights { "error": true, "message": "You do not have permission to edit Monthly Close for this company." }
200/201 mc_checkbox_data table missing { "error": true, "message": "mc_checkbox_data table not found." }
200/201 mc_checkbox_data lacks the expected columns { "error": true, "message": "mc_checkbox_data has unsupported schema." }

Notes

  • Keyed by company_id + as_on_date (the resolved month start) + col_name; upserts in place. Shared by AR, AP, Balance Sheet, Income Statement and Payroll.
GET/api/mc/ar/review/list/:id

Paginated transaction list behind one Accounts Receivable review bucket.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the decoded company is firm-checked.
Content type application/json

Path parameters

Name Type Required Description
id string Yes The bucket's encrypted list id, minted by the AR review filter as "<companyId>/<monthEnd>/<type>/<days>".

Query parameters

Name Type Required Default Description
page string No 1 Page number, coerced to a number. An absent value yields page 1; a non-numeric value yields NaN.
commonId string No '' Pins a deep-linked transaction onto page 1 (excluded from every other page).

Example request

curl -b 'genie.sid=<session-cookie>' \
  'https://api.example.com/api/mc/ar/review/list/PVdKa0hDVGZ3...?page=2&commonId=9931'

Success response — 200 OK

{
  "error": false,
  "companyId": 401,
  "companyName": "Acme Holdings",
  "encryptData": "PVdKa0hDVGZ3...",
  "type": 1,
  "daysOld": 90,
  "typeName": "Seriously Delinquent Invoices",
  "asOnDate": "2026-04-30",
  "monthVal": "Apr",
  "yearVal": 2026,
  "acceptType": 7,
  "commentType": 1,
  "subModuleId": 53,
  "commonId": "9931",
  "invoiceData": [],
  "noMoreData": false
}
Field Type Description
type / typeName number / string The bucket. AR uses 1 Seriously Delinquent Invoices, 2 Seriously Delinquent Credit Memo, 5 Delinquent Unapplied Customer Payments.
acceptType / commentType number Accept/revoke type_id and comment thread type for this bucket — AR: 1→7/1, 2→8/2, 5→49/48.
daysOld number The age cutoff encoded in the list id.
commonId string Echo of the pinned transaction id (or '').
invoiceData[] object[] The page's transactions.
noMoreData boolean True when the page returned fewer rows than the page size (end of list).

Errors

Status Condition Message/shape
200 The encrypted id cannot be parsed { "error": true, "message": "Invalid list id.", "invoiceData": [] }

Notes

  • The AR and AP list endpoints return the same response shape; only the bucket metadata differs.
GET/api/mc/ap/review/list/:id

Paginated transaction list behind one Accounts Payable review bucket.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the decoded company is firm-checked.
Content type application/json

Path parameters

Name Type Required Description
id string Yes The bucket's encrypted list id (same shape as AR).

Query parameters

Name Type Required Default Description
page string No 1 Page number.
commonId string No '' Pins a deep-linked transaction onto page 1.

Example request

curl -b 'genie.sid=<session-cookie>' \
  'https://api.example.com/api/mc/ap/review/list/PVdKa0hDVGZ3...?page=1'

Success response — 200 OK; identical shape to the AR list, with subModuleId falling back to 54 and AP's own bucket metadata:

{
  "error": false,
  "companyId": 401,
  "companyName": "Acme Holdings",
  "encryptData": "PVdKa0hDVGZ3...",
  "type": 3,
  "daysOld": 90,
  "typeName": "Seriously Delinquent Bills",
  "asOnDate": "2026-04-30",
  "monthVal": "Apr",
  "yearVal": 2026,
  "acceptType": 9,
  "commentType": 4,
  "subModuleId": 54,
  "commonId": "",
  "invoiceData": [],
  "noMoreData": true
}
Field Type Description
type / typeName number / string AP uses 3 Seriously Delinquent Bills, 4 Seriously Delinquent Vendor Credits, 6 Delinquent Unapplied Vendor Payments.
acceptType / commentType number AP: 3→9/4, 4→10/5, 6→51/50.

Errors

Status Condition Message/shape
200 The encrypted id cannot be parsed { "error": true, "message": "Invalid list id.", "invoiceData": [] }
POST/api/mc/common/accept

Accept or revoke a single flagged transaction on any section's list screen.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization Requires Monthly Close edit rights on the company identified by cgId; otherwise 403.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
cgId number Yes Numeric (numeric strings are coerced) Integer The company id (despite the name) whose row is being accepted.
monthVal string Yes String Month token.
yearVal number Yes Integer Year.
value number Yes Integer The accept/revoke value to store.
commonId string | number No No type constraint The row key — a numeric qbo_id for AR/AP, the base64 enc_bank_id for Bank & Card.
typeId number No Integer The bucket's accept type.
commonIds (string|number)[] No Array Batch form of commonId.
typeIds number[] No Array Batch form of typeId.
isAllcheckBox boolean No Boolean "Select all" mode — the server expands the bucket itself instead of enumerating rows.
encryptId string No String The encrypted list id, required when isAllcheckBox is set.
isFrom string No String Expansion branch for select-all. Supported values are 'AR', 'AP', 'revenue', 'IncomeStatement' and 'BalanceSheet'; the last two load their whole set in one response, so their clients never set this flag.
commonIdsUnSelected (string|number)[] No Array Rows the user un-ticked while select-all was active.

Example request

curl -X POST https://api.example.com/api/mc/common/accept \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"cgId":401,"monthVal":"Apr","yearVal":2026,"value":1,"commonId":"9931","typeId":7}'

Success response — 201 Created

{ "error": false }

Errors

Status Condition Message/shape
403 Caller lacks Monthly Close edit rights on the company { "error": true, "message": "You do not have permission to edit Monthly Close for this company." }

Notes

  • This route and mc/common/accept/all share one request contract; the batch route is the single-row route with the scalar ids replaced by arrays.
POST/api/mc/common/accept/all

Batch accept/revoke — the header "select all" action on a list screen.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization Requires Monthly Close edit rights on the company identified by cgId; otherwise 403.
Content type application/json

Request body (identical to mc/common/accept; this route is where commonIds/typeIds or the isAllcheckBox + encryptId + isFrom + commonIdsUnSelected combination is used).

Example request

curl -X POST https://api.example.com/api/mc/common/accept/all \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"cgId":401,"monthVal":"Apr","yearVal":2026,"value":1,"isAllcheckBox":true,"encryptId":"PVdKa0hDVGZ3...","isFrom":"AR","commonIdsUnSelected":["9940"]}'

Success response — 201 Created

{ "error": false }

Errors

Status Condition Message/shape
403 Caller lacks Monthly Close edit rights on the company { "error": true, "message": "You do not have permission to edit Monthly Close for this company." }

Notes

  • Both routes perform the same upsert; the batch variant is exposed on its own path.
POST/api/mc/balancesheet/review/filter

Loads the Balance Sheet General review screen.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. canEdit is returned for UI gating.
Content type application/json

Request body (companyId, monthVal, yearVal, all required — the shared review-filter contract).

Example request

curl -X POST https://api.example.com/api/mc/balancesheet/review/filter \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"monthVal":"Apr","yearVal":2026}'

Success response — 201 Created

{
  "title": "Balance Sheet General",
  "moduleId": 62,
  "connection_qbo": 1,
  "totalCommentCount": { "unresolvedCount": 4, "totalCount": 11 },
  "qaStatus": {
    "canSkip": false, "canReview": true, "canApprove": false,
    "canUnSkip": false, "canUnReview": false, "canUnApprove": false,
    "hoverStatus": "", "canSubmit": true
  },
  "canEdit": true,
  "approveError": "",
  "filterTable": {
    "flaggedAccounts": 7,
    "gl_account_schedules": {}
  }
}
Field Type Description
moduleId number Resolved from comment_module; falls back to 62.
qaStatus object Section 4 QA flags.
filterTable.flaggedAccounts number Count of flagged GL accounts.
filterTable.gl_account_schedules object The GL Account Validation summary that links to the GL Validation screen.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • The screen's six drill-downs each carry an encrypted "View List" id minted as "<companyId>/<monthStart>/<type>/" — note it uses the month start and ends with a trailing slash with no days component, unlike the AR/AP ids.
GET/api/mc/balancesheet/review/list/:id

One of the six Balance Sheet General "View List" drill-downs.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the decoded company is firm-checked.
Content type application/json

Path parameters

Name Type Required Description
id string Yes Encrypted "<companyId>/<monthStart>/<type>/". type is 1–6.

Example request

curl -b 'genie.sid=<session-cookie>' \
  'https://api.example.com/api/mc/balancesheet/review/list/RG5jNXdIT...'

Success response — 200 OK

{
  "error": false,
  "companyId": 401,
  "companyName": "Acme Holdings",
  "encryptData": "RG5jNXdIT...",
  "type": 1,
  "typeName": "GL Account Variances List",
  "asOnDate": "2026-04-01",
  "monthVal": "Apr",
  "yearVal": 2026,
  "commentType": 11,
  "acceptType": 17,
  "threshold": 0,
  "accounts": [],
  "accSixMonth": {},
  "accSixMonthHeader": [],
  "priorVal": 0
}
Field Type Description
type / typeName number / string 1 GL Account Variances List, 2 Accumulated Depreciation and Amortization, 3 Balance Sheet Debit and Credit Balances, 4 Prepaid Expense, 5 Fixed Assets, 6 Deferred Revenue.
commentType number 1→11, 2→12, 3→14, 4→105, 5→106, 6→107.
acceptType number 1→17, 2→18, 3→19, 4→105, 5→106, 6→107.
accounts[] object[] The list rows, each decorated with accepted and checkCommentsCount ("<unresolved>/<total>").
accSixMonth / accSixMonthHeader object / object[] The trailing six-month balance buckets and their column headers.
priorVal number Prior-month / average-of-prior-N-months value.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • priorVal is returned for information only.
POST/api/mc/incomestatement/review/filter

Loads the Income Statement General review screen.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. canEdit is returned for UI gating.
Content type application/json

Request body (companyId, monthVal, yearVal, all required).

Example request

curl -X POST https://api.example.com/api/mc/incomestatement/review/filter \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"monthVal":"Apr","yearVal":2026}'

Success response — 201 Created

{
  "title": "Income Statement General",
  "moduleId": 55,
  "connection_qbo": 1,
  "totalCommentCount": { "unresolvedCount": 0, "totalCount": 2 },
  "qaStatus": {
    "canSkip": false, "canReview": true, "canApprove": false,
    "canUnSkip": false, "canUnReview": false, "canUnApprove": false,
    "hoverStatus": "", "canSubmit": true
  },
  "canEdit": true,
  "approveError": "",
  "filterTable": {
    "varianceRows": 5,
    "chart_of_accounts": {},
    "integrity": {}
  }
}
Field Type Description
moduleId number Falls back to 55.
qaStatus object Section 7 QA flags.
filterTable.varianceRows number Count of flagged GL variance rows.
filterTable.chart_of_accounts / integrity object The screen's supporting panels (including the gross-margin actual-vs-expected comparison).

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

GET/api/mc/incomestatement/review/list/:id

One of the two Income Statement General "View List" drill-downs.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the decoded company is firm-checked.
Content type application/json

Path parameters

Name Type Required Description
id string Yes Encrypted "<companyId>/<monthStart>/<type>/". type is 1 or 2.

Example request

curl -b 'genie.sid=<session-cookie>' \
  'https://api.example.com/api/mc/incomestatement/review/list/RG5jNXdIT...'

Success response — 200 OK

{
  "error": false,
  "companyId": 401,
  "companyName": "Acme Holdings",
  "encryptData": "RG5jNXdIT...",
  "type": 1,
  "typeName": "GL Account Variances List",
  "asOnDate": "2026-04-01",
  "monthVal": "Apr",
  "yearVal": 2026,
  "commentType": 20,
  "acceptType": 25,
  "accounts": [],
  "accSixMonth": {},
  "accSixMonthHeader": [],
  "priorVal": 0
}
Field Type Description
type / typeName number / string 1 GL Account Variances List, 2 Income Statement Debit and Credit Balances Flagged.
commentType number 1→20, 2→22.
acceptType number 1→25, 2→26.
accounts[] object[] Rows decorated with accepted and checkCommentsCount.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/mc/pr/review/filter

Loads the Payroll review screen.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. canEdit is returned for UI gating.
Content type application/json

Request body (companyId, monthVal, yearVal, all required).

Example request

curl -X POST https://api.example.com/api/mc/pr/review/filter \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"monthVal":"Apr","yearVal":2026}'

Success response — 201 Created

{
  "title": "Payroll",
  "moduleId": 58,
  "connection_qbo": 1,
  "totalCommentCount": { "unresolvedCount": 1, "totalCount": 1 },
  "qaStatus": {
    "canSkip": false, "canReview": true, "canApprove": false,
    "canUnSkip": false, "canUnReview": false, "canUnApprove": false,
    "hoverStatus": "", "canSubmit": true
  },
  "canEdit": true,
  "approveError": "",
  "filterTable": {
    "w2Employees": 24,
    "payrollDepartment": 1,
    "taxTarget": 12
  }
}
Field Type Description
moduleId number Falls back to 58.
qaStatus object Section 6 QA flags.
filterTable.w2Employees / payrollDepartment / taxTarget number The configured Payroll expectations this month is checked against (set on the Payroll Configure screen).

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

GET/api/mc/pr/review/list/:id

The Payroll review's single "View List" drill-down (wages plus payroll-tax GL accounts).

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the decoded company is firm-checked.
Content type application/json

Path parameters

Name Type Required Description
id string Yes Encrypted "<companyId>/<monthStart>/<type>/".

Example request

curl -b 'genie.sid=<session-cookie>' \
  'https://api.example.com/api/mc/pr/review/list/RG5jNXdIT...'

Success response — 200 OK

{
  "error": false,
  "company_id": 401,
  "companyId": 401,
  "companyName": "Acme Holdings",
  "encryptData": "RG5jNXdIT...",
  "asOnDate": "2026-04-01",
  "monthVal": "Apr",
  "yearVal": 2026,
  "wagesAccountData": [],
  "glAccountData": []
}
Field Type Description
wagesAccountData[] object[] The configured wages accounts and their balances.
glAccountData[] object[] The configured payroll-tax GL accounts and their balances.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • The account sets come from mc_payroll_config → mc_payroll_config_accounts, i.e. what the Payroll Configure screen wrote.
POST/api/mc/revenue/review/filter

Loads the Uncharacterized Revenue and Expense review screen.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. canEdit is returned for UI gating.
Content type application/json

Request body (companyId, monthVal, yearVal, all required).

Example request

curl -X POST https://api.example.com/api/mc/revenue/review/filter \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"monthVal":"Apr","yearVal":2026}'

Success response — 201 Created

{
  "title": "Uncharacterized revenue and expense",
  "moduleId": 56,
  "connection_qbo": 1,
  "totalCommentCount": { "unresolvedCount": 2, "totalCount": 5 },
  "qaStatus": {
    "canSkip": false, "canReview": true, "canApprove": false,
    "canUnSkip": false, "canUnReview": false, "canUnApprove": false,
    "hoverStatus": "", "canSubmit": true
  },
  "canEdit": true,
  "approveError": "",
  "filterTable": { "classRevenue": 1, "classExpense": 0 }
}
Field Type Description
moduleId number Falls back to 56.
qaStatus object Section 5 QA flags.
filterTable.classRevenue / classExpense number The two class-checking toggles from Uncharacterized Configure, plus the four review buckets and their encrypted list ids.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

GET/api/mc/revenue/review/list/:id

One of the four Uncharacterized "View List" drill-downs, paginated.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the decoded company is firm-checked.
Content type application/json

Path parameters

Name Type Required Description
id string Yes Encrypted "<companyId>/<monthStart>/<type>/"; type must be 1–4.

Query parameters

Name Type Required Default Description
page string No 1 Page number; values ≤ 0 or non-numeric fall back to 1.
commonId string No '' The row's enc_bank_id; pins that transaction onto page 1 when the window did not already contain it.

Example request

curl -b 'genie.sid=<session-cookie>' \
  'https://api.example.com/api/mc/revenue/review/list/RG5jNXdIT...?page=1&commonId=ZW5jXzk5Mw=='

Success response — 200 OK

{
  "error": false,
  "company_id": 401,
  "companyId": 401,
  "companyName": "Acme Holdings",
  "encryptData": "RG5jNXdIT...",
  "type": 1,
  "typeName": "Uncharacterized and Miscellaneous Revenue",
  "asOnDate": "2026-04-01",
  "commentDate": "2026-04-01",
  "monthVal": "Apr",
  "yearVal": 2026,
  "commentType": 27,
  "acceptType": 36,
  "subModuleId": 56,
  "bankAccounts": [],
  "noMoreData": true
}
Field Type Description
type / typeName number / string 1 Uncharacterized and Miscellaneous Revenue, 2 Uncharacterized and Miscellaneous Expense, 3 Revenue Un-classed, 4 Expense Un-classed.
commentType number 1→27, 2→28, 3→29, 4→30.
acceptType number 1→36, 2→37, 3→38, 4→39.
bankAccounts[] object[] The page's transactions, with the customer-or-vendor name resolved from qbo_customer by cus_or_ven (1 customer, 2 vendor).
noMoreData boolean True when fewer than 25 rows came back (page size is 25).

Errors

Status Condition Message/shape
200 Id cannot be parsed, or type is outside 1–4 { "error": true, "message": "Invalid list id.", "bankAccounts": [] }
POST/api/mc/cash/flow/statement/filter

Loads the Cash Flow Statement review screen — five checkbox options plus the section QA status.

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. canEdit is returned for UI gating.
Content type application/json

Request body (companyId, monthVal, yearVal, all required).

Example request

curl -X POST https://api.example.com/api/mc/cash/flow/statement/filter \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"monthVal":"Apr","yearVal":2026}'

Success response — 201 Created

{
  "title": "Cash Flow Statement",
  "moduleId": 57,
  "connection_qbo": 1,
  "totalCommentCount": { "unresolvedCount": 0, "totalCount": 0 },
  "qaStatus": {
    "canSkip": false, "canReview": true, "canApprove": false,
    "canUnSkip": false, "canUnReview": false, "canUnApprove": false,
    "hoverStatus": "", "canSubmit": true
  },
  "canEdit": true,
  "approveError": "",
  "filterTable": { "option_1": 1, "option_2": 0, "option_3": 1, "option_4": 0, "option_5": 0 }
}
Field Type Description
moduleId number Falls back to 57.
qaStatus object Section 8 QA flags.
filterTable.option_1 … option_5 number The five saved checkbox values (0/1) for this company and month. Written by POST /api/mc/cash/flow/statement/save (documented under Firm Configuration).

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • Cash Flow Statement has no Configure screen; its configRoute is '#'.
POST/api/mc/filter/other/reivew

Loads the "Other" section's review checklist for the company and month. (The path segment is spelled reivew, exactly as shown, and must be requested that way.)

Module Monthly Close > Close Details
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. canEdit is returned for UI gating.
Content type application/json

Request body (companyId, monthVal, yearVal, all required).

Example request

curl -X POST https://api.example.com/api/mc/filter/other/reivew \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"monthVal":"Apr","yearVal":2026}'

Success response — 201 Created

{
  "error": false,
  "connection_qbo": 1,
  "moduleId": 60,
  "totalCommentCount": { "unresolvedCount": 0, "totalCount": 1 },
  "qaStatus": {
    "canSkip": false, "canReview": true, "canApprove": false,
    "canUnSkip": false, "canUnReview": false, "canUnApprove": false,
    "hoverStatus": "", "canSubmit": true
  },
  "canEdit": true,
  "approveError": "",
  "getOtherReview": [
    { "id": 77, "item_name": "Bank fees reviewed", "type_of_field": 2 }
  ]
}
Field Type Description
moduleId number Falls back to 60.
qaStatus object Section 9 QA flags.
getOtherReview[] object[] The checklist items configured for this company (from mc_other_config) together with this month's saved answers. type_of_field defaults to 2.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • Items are defined on the Other Configure screen (POST /api/mc/other/config/save) and answered through POST /api/mc/filter/other/save.

12.3 Open Comments

Two overlapping comment surfaces are served here:

  • Monthly Close comments (mc_comments + mc_comments_assignee) — the per-transaction / per-section threads read and written from a section review screen (mc/comment/*).
  • Open Comments — the cross-module grid that merges Monthly Close, Accounting, Task and Invoice threads (comment/module, mc/thread/sort*, thread/*, comment/*, open-comment/edit). Its source discriminator is 'mc' | 'accounting' | 'task' | 'invoice', and thread creation branches on module: 103 = Monthly Close, 102 = Accounting, 101 = Task (requires a valid taskId), 106 = Invoice (requires a valid batchInvoiceId).
POST/api/mc/comment/view

Reads the Monthly Close comment threads for one company, month and comment type (optionally narrowed to a single transaction).

Module Monthly Close > Open Comments
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
month number Yes Numeric (numeric strings are coerced) Integer Month as a number.
year number Yes Integer Year.
companyId number Yes Integer Company.
type number No Integer The mc_comments.type to read.
commonId string | number No No type constraint Row key — numeric qbo_id for AR/AP, base64 enc_bank_id for Bank & Card.

Example request

curl -X POST https://api.example.com/api/mc/comment/view \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"month":4,"year":2026,"companyId":401,"type":1,"commonId":"9931"}'

Success response — 201 Created

{
  "error": false,
  "finalCommentData": {
    "getComment": [
      {
        "id": 5521,
        "thread_id": 880,
        "comment": "Confirmed with the client.",
        "resolve": 0,
        "user_id": 88,
        "user_name": "Dana Reed",
        "created_at": "2026-04-28T14:02:11.000Z",
        "assign_to": [91]
      }
    ],
    "assignUsers": [],
    "unResolveCount": 1,
    "type": 1,
    "loginUser": 88
  }
}
Field Type Description
finalCommentData.getComment[] object[] Flat comment rows; assign_to is the comma-separated assignee id list parsed into numbers.
finalCommentData.assignUsers[] object[] Users assignable on this company.
finalCommentData.unResolveCount number Unresolved comment count.
finalCommentData.type number The comment type read.
finalCommentData.loginUser number The caller's user id, so the UI can gate the edit affordance.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • This endpoint reads mc_comments only. The GL Validation sidebar has its own read: POST /api/mc/glvalidation/comments/view.
POST/api/comment/module

Builds the Open Comments filter tree — the Module › Submodule › Section hierarchy from comment_module, plus the assignable-user list for the selected company.

Module Monthly Close > Open Comments
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. When companyId > 0 the company must belong to the caller's firm; when 0/absent the tree is built firm-wide.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Numeric (numeric strings are coerced) Integer Company whose assignable users are returned. 0 = firm-wide.
noAccounting boolean No Boolean Omit the Accounting module from the tree.

Example request

curl -X POST https://api.example.com/api/comment/module \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"noAccounting":false}'

Success response — 201 Created

[
  {
    "name": "Monthly Close",
    "value": 103,
    "route": "monthly_close",
    "assignedUsers": [{ "id": 91, "name": "Sam Ortiz", "email": "sam@firm.com" }],
    "child": [
      {
        "name": "Accounts Receivable",
        "value": 53,
        "route": "mc_ar_review",
        "child": [{ "name": "Seriously Delinquent Invoices", "value": 1, "route": "mc_ar_list" }]
      }
    ]
  }
]
Field Type Description
(root array) object[] One entry per module, sorted by name.
value / route number / string The module row's module_id and deep-link route name.
assignedUsers[] object[] Users assignable for the requested company (or firm-wide).
child[] object[] Submodules; each submodule's own child[] holds its sections.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/mc/thread/sort

The Open Comments grid: every comment thread visible to the caller across Monthly Close, Accounting, Task and Invoice, filtered and sorted.

Module Monthly Close > Open Comments
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Per-thread write access is computed and returned as haveAccess (menu 81 Standard/Admin, or global/firm admin; always false for an inactive company).
Content type application/json

Request body (every field optional)

Field Type Required Validation / allowed values Description
companyId number | number[] No none (untyped) One company or several.
module number No Integer comment_module.module_id of the module filter.
subModule number No Integer Submodule module id.
section number No Integer Section module id.
startDate / endDate string No String Date-range filter.
monthlyPeriod string No String Close-period filter.
status string No String 'resolved' or 'open' filter the list; other values are ignored.
hideResolved boolean No Boolean Drop resolved threads.
includeInactive boolean No Boolean Include inactive companies.
assignedToMe boolean No Boolean Only threads assigning the caller.

Example request

curl -X POST https://api.example.com/api/mc/thread/sort \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":[401,402],"module":103,"hideResolved":true,"assignedToMe":false}'

Success response — 201 Created

{
  "threads": [
    {
      "key": "mc:880",
      "source": "mc",
      "thread_id": 880,
      "company_id": 401,
      "company_name": "Acme Holdings",
      "thread_title": "Confirmed with the client.",
      "attributes": "Monthly Close › Accounts Receivable › Seriously Delinquent Invoices",
      "route_data": {
        "type": "mc",
        "id": 9931,
        "enc_id": "PVdKa0hEVGZ3...",
        "invoice_no": "",
        "route": "mc_ar_review"
      },
      "assignee": "Sam Ortiz",
      "assignee_names": ["Sam Ortiz"],
      "assignee_ids": [91],
      "close_period": "2026-04-01",
      "last_comment": "Confirmed with the client.",
      "last_comment_date": "4/28/26",
      "last_comment_at": "2026-04-28T14:02:11.000Z",
      "date_resolved": "",
      "resolved": false,
      "module_name": "Monthly Close",
      "submodule_name": "Accounts Receivable",
      "section_name": "Seriously Delinquent Invoices",
      "initiator_comments": [],
      "responder_comments": [],
      "comments": [],
      "haveAccess": true
    }
  ],
  "counters": { "total": 1, "resolved": 0 },
  "currentUserId": 88,
  "canWriteAll": false
}
Field Type Description
threads[].key string Stable row identity across the merged sources.
threads[].thread_title string The initiating comment's text, falling back to the source title or #<attributeId>.
threads[].route_data object Deep-link descriptor. For a Monthly Close thread tied to a single transaction it also carries the encrypted list-screen id so the grid can jump straight to the row (null for section-level and non-MC threads).
threads[].comments[] object[] Every comment in the thread; initiator_comments / responder_comments are the same rows split by role.
threads[].haveAccess boolean Whether this viewer may write on the thread.
counters object total and resolved counts after filtering.
currentUserId number Lets the grid gate the edit affordance to the viewer's own comments.
canWriteAll boolean True for a global/firm admin.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • Results are ordered by source group first (mc → accounting → task → invoice), then most recently commented first within each group. They are not grouped by company.
POST/api/mc/thread/sort/fetch-users

Returns the users who can be assigned on a new or existing thread for one company and module.

Module Monthly Close > Open Comments
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the company is firm-checked.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Numeric (numeric strings are coerced) Integer Company.
module number Yes Integer Module id (103 Monthly Close, 102 Accounting, 101 Task, 106 Invoice).
date string No String Close period, used to resolve the month's review assignees.

Example request

curl -X POST https://api.example.com/api/mc/thread/sort/fetch-users \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"module":103,"date":"2026-04-01"}'

Success response — 201 Created

{
  "assignUsers": [{ "id": 91, "name": "Sam Ortiz", "email": "sam@firm.com" }],
  "reviewAssignUser": [{ "id": 91, "name": "Sam Ortiz", "email": "sam@firm.com" }]
}
Field Type Description
assignUsers[] object[] Everyone assignable on this company/module.
reviewAssignUser[] object[] The subset that is this month's recorded primary accountant / reviewer.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/mc/comment/create

Posts a Monthly Close comment (starting a thread, or replying into an existing one).

Module Monthly Close > Open Comments
Authentication Required — authenticated session cookie
Authorization Requires Monthly Close edit rights on the company; otherwise 403.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
month number Yes Integer Month as a number.
year number Yes Integer Year.
companyId number Yes Integer Company.
typeId number Yes Integer The mc_comments.type for this thread.
commentText string Yes String The comment body.
commonListId string | number No No type constraint Row key — numeric qbo_id for AR/AP, base64 enc_bank_id for Bank & Card.
threadId number No Integer Reply into this thread; omit to start a new one.
assignTo number[] No Array Assignee user ids.

Example request

curl -X POST https://api.example.com/api/mc/comment/create \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"month":4,"year":2026,"companyId":401,"typeId":1,"commentText":"Chasing the client.","commonListId":"9931","assignTo":[91]}'

Success response — 201 Created

{ "error": false, "thread_id": 880 }
Field Type Description
thread_id number The thread the comment landed in (newly created when threadId was absent).

Errors

Status Condition Message/shape
403 Caller lacks Monthly Close edit rights { "error": true, "message": "You do not have permission to edit Monthly Close for this company." }
POST/api/mc/comment/resolve

Resolves or un-resolves one or more Monthly Close comments.

Module Monthly Close > Open Comments
Authentication Required — authenticated session cookie
Authorization Requires Monthly Close edit rights on the comment's own company; otherwise 403.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
commentId number[] Yes Array The comment ids to update.
resolveVal number Yes Numeric (numeric strings are coerced) Integer The resolve value to write.

Example request

curl -X POST https://api.example.com/api/mc/comment/resolve \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"commentId":[5521,5522],"resolveVal":1}'

Success response — 201 Created

{ "error": false, "resolveComment": 1 }
Field Type Description
resolveComment number The resolve value now stored.

Errors

Status Condition Message/shape
403 Caller lacks Monthly Close edit rights on the owning company { "error": true, "message": "You do not have permission to edit Monthly Close for this company." }

Notes

  • The company is derived from the comment row itself, then permission-checked — the caller does not pass a companyId.
POST/api/comment/edit

Edits the text of one Monthly Close comment.

Module Monthly Close > Open Comments
Authentication Required — authenticated session cookie
Authorization Requires Monthly Close edit rights on the comment's own company; otherwise 403.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
commentId number Yes Numeric (numeric strings are coerced) Integer The comment to edit.
comment string Yes String The replacement text.

Example request

curl -X POST https://api.example.com/api/comment/edit \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"commentId":5521,"comment":"Chasing the client (updated)."}'

Success response — 201 Created

{ "error": false, "message": "Comment Updated Successfully" }

Errors

Status Condition Message/shape
403 Caller lacks Monthly Close edit rights on the owning company { "error": true, "message": "You do not have permission to edit Monthly Close for this company." }
POST/api/comment/editAssignUser

Replaces the assignee set on one Monthly Close comment.

Module Monthly Close > Open Comments
Authentication Required — authenticated session cookie
Authorization Requires Monthly Close edit rights on the comment's own company; otherwise 403.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
commentId number Yes Numeric (numeric strings are coerced) Integer The comment to reassign.
assignTo number[] Yes Array The new assignee user ids (replaces the existing set).

Example request

curl -X POST https://api.example.com/api/comment/editAssignUser \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"commentId":5521,"assignTo":[91,88]}'

Success response — 201 Created

{
  "error": false,
  "message": "Comment Assignee Updated Successfully",
  "assignee": [
    {
      "id": 3301,
      "mc_comments_id": 5521,
      "assign_to": 91,
      "user": { "id": 91, "name": "Sam Ortiz", "email": "sam@firm.com" }
    }
  ]
}
Field Type Description
assignee[] object[] The resulting mc_comments_assignee rows; user is null when the referenced user cannot be resolved.

Errors

Status Condition Message/shape
403 Caller lacks Monthly Close edit rights on the owning company { "error": true, "message": "You do not have permission to edit Monthly Close for this company." }
POST/api/thread/create

Starts a new comment thread from the Open Comments screen (or from the Invoicing/Task comment panels).

Module Monthly Close > Open Comments
Authentication Required — authenticated session cookie
Authorization Monthly Close edit rights are not required on this path; the company is firm-checked, and unauthenticated or foreign-firm requests fail with 401.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Numeric (numeric strings are coerced) Integer Company.
module number Yes Integer 103 Monthly Close, 102 Accounting, 101 Task, 106 Invoice.
comment string Yes String The initiating comment (trimmed before storage).
submodule number No Integer Submodule module id.
section number No Integer Section module id.
subSection string | number No No type constraint Becomes common_list_id; for Bank & Card sections this is the non-numeric enc_bank_id.
taskId number No Integer Required (and > 0) for module: 101.
batchInvoiceId number No Integer Required (and > 0) for module: 106.
asOnDate string No String Close period for the comment.
assignTo number[] No Array Assignee user ids.

Example request

curl -X POST https://api.example.com/api/thread/create \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"module":103,"submodule":53,"section":1,"subSection":"9931","comment":"New thread","asOnDate":"2026-04-01","assignTo":[91]}'

Success response — 201 Created

{ "error": false, "thread_id": 881, "source": "mc" }
Field Type Description
thread_id number The new thread id.
source string mc for module 103, accounting for 102, task for 101, invoice for 106.

Errors

Status Condition Message/shape
201 (body-level) module is anything other than 103/102, or is 101/106 without a valid parent id { "error": true, "message": "Threads can only be started for Monthly Close or Accounting from Open Comments." }

Notes

  • The rejection above prevents the Open Comments screen from creating orphan Task/Invoice comment rows; the parented Task/Invoice paths remain reachable from their own modules' comment panels.
POST/api/thread/comment/create

Replies into an existing thread on any of the four sources.

Module Monthly Close > Open Comments
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the company is firm-checked (401 otherwise).
Content type application/json

Request body

Field Type Required Validation / allowed values Description
source string Yes String; one of 'mc', 'accounting', 'task', 'invoice' (not enum-validated at runtime) Which comment table to write.
threadId number Yes Numeric (numeric strings are coerced) Integer Thread to reply into.
companyId number Yes Integer Company.
comment string Yes String The reply text.
assignTo number[] No Array Assignee user ids for the reply.

Example request

curl -X POST https://api.example.com/api/thread/comment/create \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"source":"mc","threadId":880,"companyId":401,"comment":"Client responded.","assignTo":[88]}'

Success response — 201 Created

{ "error": false }

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/comment/resolve

Resolves or un-resolves an Open Comments thread (or a single comment within one).

Module Monthly Close > Open Comments
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; access is validated against the caller's task-access scope (401 on failure).
Content type application/json

Request body (every field optional)

Field Type Required Validation / allowed values Description
module number No Integer Module id.
thread number No Integer Thread id to resolve.
commentId number No Integer A single comment id instead of a whole thread.
companyId number No Integer Company.
section number No Integer Section id.
subsection number No Integer Subsection id.
resolve number No Integer Resolve flag.
resolveVal number No Integer Resolve value to write.
source string No String; one of 'mc', 'accounting', 'task', 'invoice' Which comment table.

Example request

curl -X POST https://api.example.com/api/comment/resolve \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"source":"mc","thread":880,"companyId":401,"resolveVal":1}'

Success response — 201 Created

{ "error": false, "resolveComment": 1 }
Field Type Description
resolveComment number The resolve value now stored on the thread.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • Resolve is stored per comment row but written per thread: every comment in the thread is resolved at once, and a thread counts as open if any of its rows is open.
POST/api/open-comment/edit

Edits one comment (and optionally its assignees) from the Open Comments grid, on any source.

Module Monthly Close > Open Comments
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the caller's access to the owning company is validated (401 otherwise).
Content type application/json

Request body

Field Type Required Validation / allowed values Description
source string Yes String; one of 'mc', 'accounting', 'task', 'invoice' Which comment table.
commentId number Yes Numeric (numeric strings are coerced) Integer The comment to edit.
comment string Yes String The replacement text.
assignTo number[] No Array Replacement assignee ids.

Example request

curl -X POST https://api.example.com/api/open-comment/edit \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"source":"mc","commentId":5521,"comment":"Updated from Open Comments.","assignTo":[91]}'

Success response — 201 Created

{ "error": false }

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.


12.4 GL Validation

The GL Account Validation screen hangs off Monthly Close › Balance Sheet General › GL Account Validation. For one company and one close month it lists every balance-sheet GL account across three tabs and, per account, shows the Source (where its supporting schedule lives), the Source Balance scraped from that schedule, the QuickBooks GL Balance, and the variance — plus a per-account comment thread count.

Route prefix. These routes sit under mc/ with the rest of Close Details. The GL Validation dashboard and configuration screens are not part of this screen and are not exposed by this API.

Constants that form part of the contract:

Constant Values
Tabs (activeTab) asset (account types Other Current Asset, Fixed Asset, Other Asset), liabilities (Other Current Liability, Long Term Liability), equity (Equity). Defaults to asset.
GL_SOURCE_TYPE (source) genie = 1, excel = 2, google = 3, exclude = 4
GL_SOURCE_LABEL 1 Genie, 2 Excel, 3 Google Sheets, 4 Exclude from Validation
GL_TAG_TYPE (g_tag / tag_type) '' = 0, above = 1, Left = 2 (spelled exactly as shown, with a capital "L")
SHEET_ERROR_CODE / SHEET_ERROR_MESSAGE 0 Invalid Url, 1 Success, 2 Sheet Missing, 3 Tag Missing, 4 Value Missing, 5 Invalid Tag, 6 Invalid Value
GL_VALIDATION_COMMENT_TYPE 50 (accounting_comments.type)
ACCOUNTING_TYPE_GL_VALIDATION 6 (accounting_default.type)
POST/api/mc/glvalidation/work/filter

Lists the GL accounts for one tab with their source configuration, scraped source balance, GL balance, variance and comment counts.

Module Monthly Close > GL Validation
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the company must belong to the caller's firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Numeric (numeric strings are coerced) Integer Company.
monthVal string No String Month token; defaults to the current month when absent or unrecognised.
yearVal number No Integer Year.
activeTab string No String asset | liabilities | equity; unknown values fall back to asset. Not enum-validated.
qboId string No String Narrow the result to a single account (used by work/save to swap one row in place).

Example request

curl -X POST https://api.example.com/api/mc/glvalidation/work/filter \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"monthVal":"Apr","yearVal":2026,"activeTab":"asset"}'

Success response — 201 Created. The body is a bare array of account rows ([] when the company has no accounts of the tab's types).

[
  {
    "id": 3312,
    "company_id": 401,
    "acc_id": "77",
    "account_num": "1400",
    "account_name": "Prepaid Insurance",
    "account_type": "Other Current Asset",
    "active": 1,
    "source_id": 3,
    "source_label": "Google Sheets",
    "source_path": "https://docs.google.com/spreadsheets/d/…",
    "g_tag_type": 1,
    "g_tag_value": "12500.00",
    "g_tag_date": "2026-04-30",
    "error_log": "",
    "org_source_bal": 12500,
    "source_bal": "12,500.00",
    "org_gl_bal": 12500,
    "gl_bal": "12,500.00",
    "org_variance": 0,
    "variance": "0.00",
    "commentCount": 2,
    "unResolvedCount": 1,
    "sectionThreadCount": { "unresolvedCount": 3, "totalCount": 9 }
  }
]
Field Type Description
id number | null accounting_gl_validation.id, null when the account has never been configured.
acc_id string accounts.qbo_id.
source_id / source_label number / string See GL_SOURCE_TYPE / GL_SOURCE_LABEL; label falls back to -.
g_tag_type number 0 unset, 1 above, 2 Left.
g_tag_value / g_tag_date string The value scraped off the sheet's #g: tag and the date it applies to.
error_log string Last scrape error for this account (see SHEET_ERROR_MESSAGE).
org_* / formatted twins number / string Raw numeric and display-formatted source balance, GL balance and variance (variance = source − GL).
commentCount / unResolvedCount number This account's own thread counts.
sectionThreadCount object The same unresolved/total pair for the whole type-50 set for the month — identical on every row, and drives the section-level badge.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • Deleted accounts are excluded; inactive ones are not — the screen's "Show deleted accounts" checkbox filters on active client-side.
POST/api/mc/glvalidation/work/modal

Returns what the "Source Selection" modal should pre-fill for one account.

Module Monthly Close > GL Validation
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Both companyId and the company on the resolved row are firm-checked, so an id from another firm cannot be probed.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Numeric (numeric strings are coerced) Integer Company.
id number No Integer accounting_gl_validation.id, or 0/absent when the account has never been configured.
monthVal string No String Month token.
yearVal number No Integer Year.

Example request

curl -X POST https://api.example.com/api/mc/glvalidation/work/modal \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"id":3312,"monthVal":"Apr","yearVal":2026}'

Success response — 201 Created

Configured account:

{ "source_id": 3, "tag_type": "above", "url": "https://docs.google.com/spreadsheets/d/…" }

Never-configured account:

{ "tag_type": "Left", "source_id": 3, "url": "" }
Field Type Description
source_id number The saved source. A stored genie (1) source opens as Google Sheets (3), because Genie is not offered in the modal.
tag_type string '', above or Left. For a never-configured account it comes from the company's accounting_gl_validation_config.location_of_gl_balance, falling back to the firm-wide accounting_default row.
url string The sheet URL, preferring the one on this month's source-balance row over the one on the validation row (a bulk update rewrites the balance rows first).

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/mc/glvalidation/work/save

Saves one account's source configuration, immediately re-reads the sheet, and returns the freshly recomputed row.

Module Monthly Close > GL Validation
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the company is firm-checked.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
source string Yes String One of genie | excel | google | exclude. Not enum-validated.
accId string Yes String accounts.qbo_id of the row being configured.
id number No Integer Existing accounting_gl_validation.id.
compId number No Integer Company id.
companyId number No Integer Alias of compId; compId wins when both are present.
g_tag string No String above | Left — where the balance sits relative to the #g: tag.
gdrive_g_tag string No String Google Drive tag field.
url string No String The sheet URL.
account_name string No String Account name, used when resolving the sheet tab.
monthVal string No String Month token.
yearVal number No Integer Year.
activeTab string No String Tab, so the re-read row matches the visible list.

Example request

curl -X POST https://api.example.com/api/mc/glvalidation/work/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"source":"google","accId":"77","compId":401,"g_tag":"above","url":"https://docs.google.com/spreadsheets/d/…","monthVal":"Apr","yearVal":2026,"activeTab":"asset"}'

Success response — 201 Created

{
  "error": false,
  "accountData": { "acc_id": "77", "source_id": 3, "g_tag_value": "12500.00", "variance": "0.00" },
  "source_type": "Google Sheets",
  "data_id": 3312,
  "val": "12500.00"
}
Field Type Description
accountData object | null The re-read account row (same shape as one work/filter element), null if nothing came back.
source_type string The GL_SOURCE_LABEL for the saved source ('' when unknown).
data_id number The accounting_gl_validation row id.
val string The scraped g_tag_value, defaulting to "0.00".

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/mc/glvalidation/work/refresh

Re-scrapes every Google-sourced account listed in the payload.

Module Monthly Close > GL Validation
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; each item's company is firm-checked.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
data object[] Yes Array; each item validated against the item schema below One entry per account to refresh.

Each data[] item:

Field Type Required Validation / allowed values Description
data object Yes Nested object; validated { company_id: number (integer, required), qbo_id: string (string, required) }.
monthVal string No String Month token.
yearVal number No Integer Year.
position number No Integer accounting_gl_validation.g_tag_type — 1 above, 2 Left.
url string No String Sheet URL override.

Example request

curl -X POST https://api.example.com/api/mc/glvalidation/work/refresh \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"data":[{"monthVal":"Apr","yearVal":2026,"position":1,"url":"https://docs.google.com/spreadsheets/d/…","data":{"company_id":401,"qbo_id":"77"}}]}'

Success response — 201 Created

{ "error": false, "refreshed": 1, "failed": 0 }
Field Type Description
error boolean Always false — per-sheet problems are recorded in each row's error_log instead.
refreshed number How many items were attempted.
failed number How many failed.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/mc/glvalidation/comments/view

The GL Validation comment sidebar's read: every accounting_comments thread for one GL account in one close month.

Module Monthly Close > GL Validation
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the company is firm-checked.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Numeric (numeric strings are coerced) Integer Company.
accId string Yes String accounts.qbo_id — what accounting_comments.common_list_id holds for this type.
monthVal string No String Month token.
yearVal number No Integer Year.

Example request

curl -X POST https://api.example.com/api/mc/glvalidation/comments/view \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"accId":"77","monthVal":"Apr","yearVal":2026}'

Success response — 201 Created

{
  "error": false,
  "loginUser": 88,
  "threads": [
    {
      "threadId": 1201,
      "resolve": 0,
      "comments": [
        {
          "id": 7701,
          "comment": "Schedule updated.",
          "resolve": 0,
          "userId": 88,
          "userName": "Dana Reed",
          "createdAt": "2026-04-29T09:41:02.000Z",
          "assignTo": [91],
          "assignNames": ["Sam Ortiz"]
        }
      ]
    }
  ],
  "unResolveCount": 1,
  "totalCount": 1
}
Field Type Description
loginUser number Caller's user id.
threads[] object[] Grouped by thread_id. A thread's resolve is 0 if any of its comments is unresolved (resolve is stored per row but written per thread).
unResolveCount / totalCount number Counted over threads, not individual comments — the same definition the table's per-row pill uses.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • Read-only. Every mutation the sidebar performs uses the existing Open Comments endpoints, which write accounting_comments for module: 102 / source: 'accounting'.
POST/api/mc/glvalidation/work/bulkupdate

"Bulk Update Google Sheet": points every Google-sourced account on the visible tab at one workbook, then re-scrapes.

Module Monthly Close > GL Validation
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the company is firm-checked.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
url string Yes String The workbook every listed account should point at.
qbo_id (string|number)[] Yes Array The accounts.qbo_id list of the Google-sourced rows on the visible tab.
company_id number No Numeric (numeric strings are coerced) Integer Company id.
companyId number No Integer Alias of company_id; company_id wins when both are present.
monthVal string No String Month token.
yearVal number No Integer Year.

Example request

curl -X POST https://api.example.com/api/mc/glvalidation/work/bulkupdate \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"company_id":401,"monthVal":"Apr","yearVal":2026,"url":"https://docs.google.com/spreadsheets/d/…","qbo_id":["77","78","91"]}'

Success response — 201 Created

{ "error": false, "updated": 3, "refreshed": 11, "failed": 1 }
Field Type Description
updated number How many accounts had the URL written (the caller's list, i.e. the visible tab).
refreshed number How many accounts were re-scraped.
failed number How many scrapes failed.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • The URL is written only to the accounts the caller listed, but the subsequent refresh runs over every Google-sourced account of the company, each using its own stored tag position — hence refreshed can exceed updated.

12.5 Firm Configuration

Two kinds of screen live here: the firm-level configuration (calendar / relative period definitions and the weekend close-deadline rule) and the per-section Configure screens (Bank & Card, AR, AP, Balance Sheet, Income Statement, Payroll, Uncharacterized, Cash Flow, Other) that set the thresholds each review section is measured against.

Every Configure screen is scoped either to one company or to one company group. The shared scope resolution reads type ('group' selects group scope; anything else, including absent, means company scope) and then:

  • company scope → companyId | company_id | id (some routes also accept compId / onlyCompanyId), and refuses a zero/absent id;
  • group scope → companyGroupId, writing against the table's company_group_id column.

Configure filter responses carry a canEdit / accessLevel pair: each Configure table is gated on Admin access, so a user without Admin access to this company's Monthly Close menu — or looking at an inactive company — sees the whole table disabled.

GET/api/mc/firm/config/filter

Loads the firm-level Monthly Close configuration: the calendar and relative period definitions effective for the current month, plus the firm's weekend close-deadline rule.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Scoped to the caller's firm.
Content type application/json

Query parameters — none; any query string sent is ignored.

Example request

curl -b 'genie.sid=<session-cookie>' https://api.example.com/api/mc/firm/config/filter

Success response — 200 OK

{
  "routeArr": { "saveFirmConfig": "mc_save_firm_config" },
  "calFirmPeriods": [
    { "id": 71, "user_id": 9, "type": 1, "periods": "1-10", "period_type": 1, "past_or_due": 0, "saved_date": "2026-04-01", "deleted": 0, "created_at": null, "updated_at": null }
  ],
  "relFirmPeriods": [
    { "id": 84, "user_id": 9, "type": 2, "periods": "1-5", "period_type": 1, "past_or_due": 1, "saved_date": "2026-04-01", "deleted": 0, "created_at": null, "updated_at": null }
  ],
  "calenderDate": "2026-04-01",
  "relativeDate": "2026-04-01",
  "closeDeadlineConfig": 2
}
Field Type Description
routeArr object Retained for compatibility with earlier clients; it may be ignored.
calFirmPeriods[] object[] Calendar-period rows (type = 1) for the snapshot effective this month, ordered by period_type.
relFirmPeriods[] object[] Relative-period rows (type = 2) for their snapshot, ordered by past_or_due.
calenderDate / relativeDate string The saved_date snapshot each list was resolved to.
closeDeadlineConfig number | null The firm's weekend-adjustment rule (null when never configured).

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • The effective saved_date per type is resolved against the current month, and only non-deleted rows take part in resolving it. The rows actually returned are not filtered on deletion, so a soft-deleted row carrying the effective date is still returned.
POST/api/mc/firm/config/save

Replaces the firm's calendar and/or relative period definitions.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization Global Master System Administrator role or firm admin required. Otherwise 403.
Content type application/json

Request body (all fields optional)

Field Type Required Validation / allowed values Description
calenderPeriods string[] No Array Calendar period range labels.
calenderPeriodsType number[] No Array Numeric (numeric strings are coerced) Their period types, positionally aligned.
relativePeriods string[] No Array Relative period range labels.
relativePeriodsType number[] No Array Numeric (numeric strings are coerced) Their period types.
relativePastOrDue number[] No Array Numeric (numeric strings are coerced) Past/due discriminator per relative period.
calenderDate string No String The snapshot saved_date for the calendar set.
relativeDate string No String The snapshot saved_date for the relative set.

Example request

curl -X POST https://api.example.com/api/mc/firm/config/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"calenderPeriods":["1-10","11-20","21-31"],"calenderPeriodsType":[1,2,3],"calenderDate":"2026-05-01"}'

Success response — 201 Created

{ "error": false }

Errors

Status Condition Message/shape
403 Caller is neither a Global Master System Administrator nor a firm admin Unauthorized.
POST/api/mc/firm/config/close-deadline/save

Sets the firm's weekend close-deadline adjustment rule.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization Global Master System Administrator role or firm admin required, and firmId must be the session user's own firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
firmId number Yes Numeric (numeric strings are coerced) Integer Must match the caller's own firm.
value number Yes Numeric (numeric strings are coerced) Integer The rule; the backend whitelists 1, 2, 3.

Example request

curl -X POST https://api.example.com/api/mc/firm/config/close-deadline/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"firmId":9,"value":2}'

Success response — 201 Created

{ "error": false, "message": "Updated Successfully." }

Errors

Status Condition Message/shape
200/201 firms table lacks the expected columns { "error": true, "message": "Firm table has unsupported schema." }
200/201 No firms row for that id { "error": true, "message": "Firm row not found." }

Notes

  • Three controls apply to this route: the caller must be a global or firm administrator, firmId must be the caller's own firm, and value must be 1, 2 or 3.
POST/api/mc/configure/update

Kicks off a background refresh of the stored QuickBooks book-close date for the selected companies.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. The requested ids are intersected with the companies in the caller's firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companies number[] Yes Array Numeric (numeric strings are coerced) Company ids to refresh. De-duplicated; non-positive ids dropped.
type string No String Accepted; not read by this endpoint.

Example request

curl -X POST https://api.example.com/api/mc/configure/update \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companies":[401,402,403]}'

Success response — 201 Created

{ "error": false, "message": "Refresh started successfully." }

Errors

Status Condition Message/shape
200/201 No usable company ids, or none survive the firm intersection { "error": true, "message": "No companies selected." }
200/201 company table missing { "error": true, "message": "Company table not found." }

Notes

  • Fire-and-forget: the POST returns immediately and the QuickBooks calls continue server-side, one company at a time. A failure on one company does not stop the run.
  • A null result from QuickBooks ("not connected / call failed") leaves the stored value untouched; an empty result clears company.qbo_close_date.
POST/api/mc/ar/config/filter/bank

Loads the Bank & Card Configure screen: the per-transaction-type day thresholds plus the undeposited-funds GL account picker.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; canEdit / accessLevel are returned for UI gating.
Content type application/json

Request body (all fields optional)

Field Type Required Validation / allowed values Description
companyId number No Numeric (numeric strings are coerced) Integer Company scope.
id number No Integer Alternative id.
type string No String 'group' for group scope; otherwise company scope.
companyGroupId number No Numeric (numeric strings are coerced) Integer Group scope id.

Example request

curl -X POST https://api.example.com/api/mc/ar/config/filter/bank \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"type":"company"}'

Success response — 201 Created

{
  "error": false,
  "data": {
    "companyId": 401,
    "companies": [{ "id": 401, "name": "Acme Holdings" }],
    "companyDrop": [],
    "rows": [
      { "transactionType": "Expense", "threshold": 5, "globalThreshold": 5 },
      { "transactionType": "Check", "threshold": 10, "globalThreshold": 10 }
    ],
    "glAccounts": { "account": [], "filter": [] }
  },
  "canEdit": true,
  "accessLevel": 4
}
Field Type Description
data.companies[] object[] Companies in scope.
data.companyDrop[] object[] In-table company picker options; populated only for group scope, empty for company scope.
data.rows[] object[] One row per transaction type with the effective threshold and the firm-wide globalThreshold. Defaults when unconfigured: Expense 5, Journal 5, Transfer 5, Check 10, Deposit 3, Bill payment 10, Payment 10 (global 5), Refund Receipt 10 (global 5), Sales Receipt 5, Purchase 5.
data.glAccounts object | null { account, filter } — the company's chart of accounts plus the saved undeposited-funds selection. null for group scope or before a company is chosen.
canEdit / accessLevel boolean / number Admin gate and the raw access level (1 NoAccess … 4 Admin).

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/mc/ar/config/save/bank

Autosave for the Bank & Card Configure screen — thresholds and/or the undeposited-funds GL account.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; company scope is firm-checked.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
rows object[] Yes Array; each item validated Each row: transactionType (string, required) and threshold (int, required).
companyId number No Numeric (numeric strings are coerced) Integer Company scope. Optional because a group-scope save carries companyGroupId instead.
type string No String 'group' for group scope.
companyGroupId number No Integer Group scope id.
glAccount string[] No Array Each item must be a string The undeposited-funds GL account picker (0 or 1 element — it is single-select). Omitting the key means "this save didn't touch the GL account".

Example request

curl -X POST https://api.example.com/api/mc/ar/config/save/bank \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"type":"company","rows":[{"transactionType":"Check","threshold":12}],"glAccount":["1200"]}'

Success response — 201 Created

{ "error": false }

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/mc/default/save-global

Writes a firm-wide default value (mc_default.global_default).

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization Global Master System Administrator role required. A non-holder gets a body-level error, not an HTTP error.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
defaultId number Yes Numeric (numeric strings are coerced) Integer The mc_default.id to update.
value number Yes Numeric (numeric strings are coerced) Integer The new global default.

Example request

curl -X POST https://api.example.com/api/mc/default/save-global \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"defaultId":16,"value":7}'

Success response — 201 Created

{ "error": false, "message": "Updated Successfully." }

Errors

Status Condition Message/shape
200/201 Caller does not hold the Global Master System Administrator role { "error": true, "message": "Unauthorized." }
200/201 mc_default table missing { "error": true, "message": "mc_default table not found." }
200/201 mc_default lacks a global_default column { "error": true, "message": "mc_default has unsupported schema." }

Notes

  • The Configure page's "Global Defaults" column is display-only, so no client currently calls this route.
POST/api/mc/ar/config/filter

Loads the AR or AP Configure screen — one shared endpoint, discriminated by configTypeId.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; canEdit / accessLevel / checkUserDisable / canResetToGlobal are returned for UI gating.
Content type application/json

Request body (all fields optional)

Field Type Required Validation / allowed values Description
companyId number No Numeric (numeric strings are coerced) Integer Company scope.
typeId number[] No Array Explicit override of the three mc_setting type ids to load.
configTypeId number No Integer Screen discriminator: 3 = AR Configure, 4 = AP Configure. 4 selects types [3, 12, 9]; anything else selects AR's [2, 10, 11].
type string No String 'group' for group scope.
companyGroupId number No Integer Group scope id (falls back to companyId when group scope is selected).

Example request

curl -X POST https://api.example.com/api/mc/ar/config/filter \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"type":"company","configTypeId":3}'

Success response — 201 Created

{
  "error": false,
  "type": { "type_id": 2, "defaultId": 16, "label": "Days Past Due to Flag Open Invoices", "customValue": 90, "globalValue": 60 },
  "type_custom_payment": { "type_id": 10, "defaultId": 21, "label": "Days Past to Flag Unapplied Customer Payments", "customValue": 30, "globalValue": 30 },
  "type_vendor_credit": { "type_id": 11, "defaultId": 22, "label": "Days Past to Flag Unapplied Customer Credit Memos", "customValue": 30, "globalValue": 30 },
  "checkUserDisable": true,
  "canEdit": true,
  "accessLevel": 4,
  "canResetToGlobal": false
}
Field Type Description
type / type_custom_payment / type_vendor_credit object The three configured slots, positionally from the resolved type triple. label comes from mc_setting, falling back to a built-in map (2 Days Past Due to Flag Open Invoices, 10 Days Past to Flag Unapplied Customer Payments, 11 Days Past to Flag Unapplied Customer Credit Memos, 3 Days Past Due to Flag Open Bills, 12 Days Past to Flag Unapplied Vendor Payments, 9 Days Past to Flag Unapplied Vendor Credits).
checkUserDisable boolean true disables the Global Default input — it is true for anyone who is not a Global Master System Administrator.
canEdit / accessLevel boolean / number The per-company Admin gate.
canResetToGlobal boolean Global admin or firm admin only — a distinct rule from checkUserDisable.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • Without configTypeId (and without an explicit typeId) the backend defaults to the AR triple, so an AP request that omits it returns AR's settings.
POST/api/mc/ar/config/save

Saves one AR/AP threshold value.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; company scope is firm-checked.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
typeId number Yes Numeric (numeric strings are coerced) Integer Which mc_setting slot to write (2/10/11 for AR, 3/12/9 for AP).
customValue number Yes Numeric (numeric strings are coerced) Integer The company/group-specific value.
companyId number No Integer Company scope (optional — a group-scope save carries companyGroupId).
type string No String 'group' for group scope.
companyGroupId number No Integer Group scope id.

Example request

curl -X POST https://api.example.com/api/mc/ar/config/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"type":"company","typeId":2,"customValue":120}'

Success response — 201 Created

{ "error": false }

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/mc/balancesheet/config/filter

Loads the Balance Sheet General Configure screen.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; canEdit / accessLevel are returned for UI gating.
Content type application/json

Request body (the shared Configure-filter contract; companyId / type / companyGroupId are the fields this screen uses).

Example request

curl -X POST https://api.example.com/api/mc/balancesheet/config/filter \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"type":"company"}'

Success response — 201 Created

{
  "error": false,
  "row_id": 512,
  "filterType": "company",
  "defaultSetting": { "type": {} },
  "glVariance": 10,
  "ignoreVariance": 500,
  "variationPeriod": 3,
  "glVarianceDefault": 10,
  "ignoreVarianceDefault": 500,
  "prepaidThreshold": 1000,
  "prepaidThresholdDefault": 1000,
  "fixedThreshold": 2500,
  "fixedThresholdDefault": 2500,
  "deferredThreshold": 1000,
  "deferredThresholdDefault": 1000,
  "needPrepaid": 1,
  "needFixed": 1,
  "needDeferred": 0,
  "canEdit": true,
  "accessLevel": 4
}
Field Type Description
row_id number | null The mc_balance_sheet row id, null when none exists yet.
filterType string company or group.
defaultSetting.type object The raw firm-wide defaults.
glVariance / ignoreVariance / variationPeriod number Effective values (company/group value, else the default).
*Default fields number The firm-wide defaults, shown beside each input.
prepaidThreshold / fixedThreshold / deferredThreshold number Section thresholds.
needPrepaid / needFixed / needDeferred number Section toggles. Forced to 0 for group scope.
canEdit / accessLevel boolean / number Admin gate.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/mc/balancesheet/config/save

Autosave for Balance Sheet General Configure: variance trio, thresholds, section toggles, and the GL-account checkboxes.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; company scope is firm-checked.
Content type application/json

Request body — this endpoint accepts a free-form JSON object; no schema validation is applied and unknown properties are passed through. The keys it reads are:

Field Type Required Validation / allowed values Description
onlyCompanyId | companyId | company_id | compId | id number Yes (company scope) none Company scope, resolved in that precedence order. A zero/absent scope is refused.
type string No none 'group' selects group scope.
companyGroupId number Conditional none Required for group scope.
onlyCompany any No none Truthy (with account_type) selects the GL-account checkbox branch.
account_type | accountType string Conditional none The account bucket: prepaid, fixed, deferred, contra, amortization.
account_id | accountId string Conditional none The account being ticked.
checked boolean | 1 | '1' No none Tick state; anything else is treated as unticked.
row_id | rowId number No none Existing mc_balance_sheet row; created on demand when absent.
glVariance, ignoreVariance, variationPeriod number No none The variance trio. Present-ness selects that branch; variationPeriod defaults to 0.
prepaidThreshold, fixedThreshold, deferredThreshold number No none The thresholds branch.
needPrepaid, needFixed, needDeferred boolean-ish No none Section toggles; written as 1/0 only when present.
isReset boolean | 1 | '1' No none Writes the variance trio and the three thresholds in one statement.

Example request

curl -X POST https://api.example.com/api/mc/balancesheet/config/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"onlyCompanyId":401,"glVariance":12,"ignoreVariance":500,"variationPeriod":3,"needPrepaid":1}'

Success response — 201 Created

{ "error": false, "row_id": 512 }

Errors

Status Condition Message/shape
200/201 No company selected (company scope with a zero/absent id) { "error": true, "message": "No company selected." }
200/201 mc_balance_sheet table missing { "error": true, "message": "mc_balance_sheet missing." }

Notes

  • Ticking an amortization account also writes a contra row; that one bucket expands to both, and the client mirrors it optimistically.
  • Unknown keys are not stripped on this route; they are simply not read.
POST/api/mc/incomestatement/config/filter

Loads the Income Statement General Configure screen.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; canEdit / accessLevel are returned.
Content type application/json

Request body (companyId / type / companyGroupId).

Example request

curl -X POST https://api.example.com/api/mc/incomestatement/config/filter \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"type":"company"}'

Success response — 201 Created

{
  "error": false,
  "row_id": 318,
  "filterType": "company",
  "defaultSetting": { "type": {} },
  "gl_variance": 10,
  "period": 3,
  "ignore_variance": 500,
  "ignore_ck_box": 2,
  "margin_ck_box": 2,
  "gross_margin_variance": 5,
  "gross_margin_period": 3,
  "glVarianceDefault": 10,
  "ignoreVariance": 500,
  "grossMarginVariance": 5,
  "count_contra": 4,
  "canEdit": true,
  "accessLevel": 4
}
Field Type Description
row_id number | null The mc_income_statement row id.
gl_variance / period / ignore_variance number Effective GL-variance settings.
ignore_ck_box / margin_ck_box number The two checkbox columns; both default to 2.
gross_margin_variance / gross_margin_period number Gross-margin comparison settings.
glVarianceDefault / ignoreVariance / grossMarginVariance number Raw firm-wide defaults; the remaining defaults are read from defaultSetting.type.
count_contra number How many contra accounts are ticked.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/mc/incomestatement/config/save

Autosave for Income Statement General Configure.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; company scope is firm-checked.
Content type application/json

Request body — this endpoint accepts a free-form JSON object; no schema validation is applied and unknown properties are passed through. The keys it reads are:

Field Type Required Validation / allowed values Description
companyId | company_id | cgId | compId | id number Yes (company scope) none Scope, in that precedence order.
type string No none 'group' selects group scope.
companyGroupId number Conditional none Group scope id.
saveModalData any No none Truthy (with an account id) selects the account-checkbox branch and skips the settings write.
accountId | account_id string Conditional none Account being ticked.
accountType | account_type string No none Bucket; defaults to contra.
checked boolean | 1 | '1' No none Tick state.
glVariance number No none → i_gl_variance.
period number No none → i_period_variance.
chkIgnoreC number No none → i_ignore_ch_box; defaults to 2 when absent.
ignoreVariance number No none → i_ignore_variance.
chkValidateC number No none → gross_margin_chbox; defaults to 2 when absent.
grossMargin number No none → gross_margin_variance.
gmperiod number No none → gross_margin_period.

Example request

curl -X POST https://api.example.com/api/mc/incomestatement/config/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"glVariance":10,"period":3,"ignoreVariance":500,"grossMargin":5,"gmperiod":3}'

Success response — 201 Created

{ "error": false, "id": 318 }

Errors

Status Condition Message/shape
200/201 No company selected { "error": true, "message": "No company selected." }
200/201 mc_income_statement table missing { "error": true, "message": "mc_income_statement missing." }

Notes

  • The settings branch always writes all seven columns together, so a partial payload blanks the omitted ones to null.
POST/api/mc/payroll/filter

Loads the Payroll Configure screen.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; canEdit / accessLevel are returned.
Content type application/json

Request body (companyId / type / companyGroupId).

Example request

curl -X POST https://api.example.com/api/mc/payroll/filter \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"type":"company"}'

Success response — 201 Created

{
  "error": false,
  "row_id": 204,
  "filterType": "company",
  "w2": 24,
  "payrollDept": 1,
  "taxTarget": 12,
  "taxTolerance": 2,
  "taxTargetDefault": 10,
  "taxToleranceDefault": 2,
  "pr_w2": { "primary": 61, "value": 20 },
  "pr_department": { "primary": 62, "value": 1 },
  "pr_target": { "primary": 63, "value": 10 },
  "pr_tolerance": { "primary": 64, "value": 2 },
  "countWages": 3,
  "countPayroll": 5,
  "canEdit": true,
  "accessLevel": 4
}
Field Type Description
w2 / payrollDept / taxTarget / taxTolerance number Effective values (company/group value, else the firm default).
pr_w2 / pr_department / pr_target / pr_tolerance object { primary, value } pairs — primary is the mc_default.id the Global Defaults column posts to.
countWages / countPayroll number How many wages / payroll-tax accounts are ticked.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/mc/payroll/config/save

Autosave for Payroll Configure: the four scalars and the account checkboxes.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; company scope is firm-checked.
Content type application/json

Request body — this endpoint accepts a free-form JSON object; no schema validation is applied and unknown properties are passed through. The keys it reads are:

Field Type Required Validation / allowed values Description
companyId | company_id | compId | id number Yes (company scope) none Scope, in that precedence order.
type string No none 'group' selects group scope.
companyGroupId number Conditional none Group scope id.
w3Emp | w2Employees | w2_employees number No none → w2_employees.
payrollDept | payrollDepartment | payroll_department number No none → payroll_department.
taxTarget | tax_target number No none → tax_target.
taxTolerance | tax_tolerance number No none → tax_tolerance.
saveModalData any No none Truthy (with an account id) also writes an account checkbox.
accountId | account_id string Conditional none Account being ticked.
accountType | account_type string No none Bucket (wages / payroll tax).
checked boolean | 1 | '1' No none Tick state.

Example request

curl -X POST https://api.example.com/api/mc/payroll/config/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"w3Emp":24,"taxTarget":12,"taxTolerance":2}'

Success response — 201 Created

{ "error": false, "row_id": 204 }

Errors

Status Condition Message/shape
200/201 No company selected { "error": true, "message": "No company selected." }
200/201 mc_payroll_config table missing { "error": true, "message": "mc_payroll_config missing." }

Notes

  • Unlike Income Statement, only the scalars actually present are written — a null value is skipped rather than blanking the column.
  • The account sets written here (mc_payroll_config_accounts) are what GET /api/mc/pr/review/list/:id reads back.
POST/api/mc/revenue/config/filter

Loads the Uncharacterized Revenue and Expense Configure screen.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; canEdit / accessLevel / checkUserDisable are returned.
Content type application/json

Request body (companyId / type / companyGroupId).

Example request

curl -X POST https://api.example.com/api/mc/revenue/config/filter \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"type":"company"}'

Success response — 201 Created

{
  "error": false,
  "row_id": 145,
  "revenue": { "revenue_expense": [], "filter": [], "class": "revenue" },
  "expense": { "revenue_expense": [], "filter": [], "class": "expense" },
  "classRevenue": 1,
  "classExpense": 0,
  "revenueCount": 0,
  "expenseCount": 0,
  "checkUserDisable": true,
  "canEdit": true,
  "accessLevel": 4
}
Field Type Description
row_id number | null The mc_revenue_expense row id.
revenue / expense object Each carries revenue_expense (the selectable GL accounts) and filter (the saved selection) plus a literal class tag. Account-type whitelists: revenue = Income, Other Income; expense = Cost of Goods Sold, Other Expense, Expense.
classRevenue / classExpense number The two class-check toggles (0/1).
revenueCount / expenseCount number Size of each saved selection.
checkUserDisable boolean true for anyone who is not a Global Master System Administrator.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/mc/revenue/config/save

Autosave for Uncharacterized Configure: the two class flags, or one account checkbox.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the company is firm-checked. This route is company-scoped only — it has no group branch.
Content type application/json

Request body — this endpoint accepts a free-form JSON object; no schema validation is applied and unknown properties are passed through. The keys it reads are:

Field Type Required Validation / allowed values Description
companyId | company_id | compId number Yes none Company; firm-checked.
accountType | account_type string Conditional none revenue or expense — any other value falls through to the class-flag branch.
accountId | account_id string Conditional none Account being ticked; selects the account branch when present alongside a valid accountType.
checked boolean | 1 | '1' No none Tick state.
classRevenue | class_revenue number No none Written as 1/0 only when the key is present.
classExpense | class_expense number No none Written as 1/0 only when the key is present.

Example request

curl -X POST https://api.example.com/api/mc/revenue/config/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"classRevenue":1,"classExpense":0}'

Success response — 201 Created

{ "error": false, "row_id": 145 }

Errors

Status Condition Message/shape
200/201 mc_revenue_expense table missing { "error": true, "message": "mc_revenue_expense missing." }

Notes

  • The class flags are written only when present, so an account-only payload cannot blank them.
POST/api/mc/cash/flow/statement/save

Saves the Cash Flow Statement review's five checkboxes for one company and month.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization Requires Monthly Close (menu 3) Admin or Standard access on the company, or global/firm admin. Otherwise 403.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Numeric (numeric strings are coerced) Integer Company.
monthVal string Yes String Month token.
yearVal number Yes Numeric (numeric strings are coerced) Integer Year.
option_1 … option_5 number No Integer The five checkboxes as 0/1. Missing options default to 0.
option1 … option5 number No Integer Accepted as aliases of option_1 … option_5; the backend coalesces each pair.

Example request

curl -X POST https://api.example.com/api/mc/cash/flow/statement/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"monthVal":"Apr","yearVal":2026,"option1":1,"option2":0,"option3":1,"option4":0,"option5":0}'

Success response — 201 Created

{ "error": false }

Errors

Status Condition Message/shape
403 Caller's Monthly Close access on the company is ViewOnly or NoAccess Standard 403 error response
200/201 mc_cash_flow table missing { "error": true, "message": "mc_cash_flow missing." }

Notes

  • Both key spellings are accepted, so a client posting option1…option5 has its values persisted correctly.
  • Despite appearing under Firm Configuration, this route writes review state (per month), not configuration.
POST/api/mc/other/config/filter

Loads the "Other" checklist items configured for a company or group.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; canEdit / accessLevel are returned.
Content type application/json

Request body — this endpoint accepts a free-form JSON object; no schema validation is applied and unknown properties are passed through. The keys it reads, resolved through the shared Configure scope rules, are:

Field Type Required Validation / allowed values Description
companyId | company_id | id number Yes (company scope) none Company.
type string No none 'group' selects group scope.
companyGroupId number Conditional none Group scope id.

Example request

curl -X POST https://api.example.com/api/mc/other/config/filter \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"type":"company"}'

Success response — 201 Created

{
  "error": false,
  "content": [
    { "id": 77, "item_name": "Bank fees reviewed", "type_of_field": 2, "mc_option": null },
    { "id": 78, "item_name": "Owner draws confirmed", "type_of_field": 3, "mc_option": "Yes|No|N/A" }
  ],
  "canEdit": true,
  "accessLevel": 4
}
Field Type Description
content[] object[] The configured items, ordered by id, excluding soft-deleted rows. type_of_field is the field kind (1 Checkbox, 2 the default text/other, 3 PickList); mc_option holds the pipe-delimited option set for Checkbox/PickList items.
canEdit / accessLevel boolean / number Admin gate.

Errors

Status Condition Message/shape
200/201 No company selected { "error": true, "message": "No company selected." }
POST/api/mc/other/config/save

Creates, renames, retypes, sets the options of, or soft-deletes one "Other" checklist item.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization Requires Monthly Close Admin or Standard access on the company/group, or global/firm admin. Otherwise 403.
Content type application/json

Request body — this endpoint accepts a free-form JSON object; no schema validation is applied and unknown properties are passed through. The keys it reads are:

Field Type Required Validation / allowed values Description
companyId | company_id | id number Yes (company scope) none Scope (note id is also the item id below — the scope resolver reads it only when the company keys are absent).
type string No none 'group' selects group scope.
companyGroupId number Conditional none Group scope id.
id number No none The item id. 0/absent means create.
field string No none 'delete' soft-deletes; 'mc_option' writes the option set; 'type_of_field' writes the field kind; anything else (default 'item_name') writes the name.
datavalue string Conditional none The value for the chosen field. Coerced to a number for type_of_field.
mc_option string No none Alias of datavalue on the mc_option branch.

Example request

curl -X POST https://api.example.com/api/mc/other/config/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"id":0,"field":"item_name","datavalue":"Bank fees reviewed"}'

Success response — 201 Created

Create (or recycle of a soft-deleted row):

{ "error": false, "id": 79, "createOrUpdate": "create" }

Update:

{ "error": false, "id": 77, "createOrUpdate": "update" }

Delete / option write:

{ "error": false }
Field Type Description
id number The row id — the client stores it so the next edit is an update, not another insert.
createOrUpdate string create or update.

Errors

Status Condition Message/shape
403 Caller's Monthly Close access is ViewOnly or NoAccess { "message": "You do not have permission to edit this section." }
200/201 No company selected { "error": true, "message": "No company selected." }
200/201 mc_other_config table missing { "error": true, "message": "mc_other_config missing." }

Notes

  • Delete blanks the row as it soft-deletes it (item_name = NULL, type_of_field = 2, mc_option = NULL), because create recycles this company's oldest soft-deleted row before inserting — leaving stale values would resurrect them under the next item added.
  • Create writes only the one column it was given, so adding a row and then setting its type does not blank the name (and vice versa).
POST/api/other/section/check

Persists the two "Other Monthly Close Items" panel checkboxes for a company and period.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the company is firm-checked.
Content type application/json

Request body — this endpoint accepts a free-form JSON object; no schema validation is applied and unknown properties are passed through. The keys it reads are:

Field Type Required Validation / allowed values Description
companyId | company_id number Yes none Company; firm-checked.
asOnDate string No none Sent as "MMM-YYYY" (e.g. Apr-2022); it is split on - and resolved to the month start.
monthVal string No none Takes precedence over the month half of asOnDate.
yearVal number No none Takes precedence over the year half of asOnDate.
otherSectionComplete number No none Written as 1/0 to col_name = 'other_section_complete'.
otherSectionIncomplete number No none Written as 1/0 to col_name = 'other_section_incomplete'.

Example request

curl -X POST https://api.example.com/api/other/section/check \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"asOnDate":"Apr-2026","otherSectionComplete":1,"otherSectionIncomplete":0}'

Success response — 201 Created

{ "error": false }

Errors

Status Condition Message/shape
200/201 mc_checkbox_data table missing { "error": true, "message": "mc_checkbox_data table not found." }
200/201 mc_checkbox_data lacks the expected columns { "error": true, "message": "mc_checkbox_data has unsupported schema." }

Notes

  • Both col_name rows are written on every call, regardless of which checkbox changed.
  • The saved values are read back by POST /api/other/section/details in its check object.
POST/api/mc/filter/other/save

Saves one answer on the "Other" review checklist.

Module Monthly Close > Firm Configuration
Authentication Required — authenticated session cookie
Authorization No module-permission gate. The company is firm-checked and nothing more; unlike the neighbouring routes, this one does not require Monthly Close Admin or Standard access.
Content type application/json

Request body — this endpoint accepts a free-form JSON object; no schema validation is applied and unknown properties are passed through. The keys it reads are:

Field Type Required Validation / allowed values Description
companyId | company_id number Yes none Company; firm-checked.
answer_id | id number Yes none The mc_other_config item being answered.
mc_answer | value | answer string | string[] No none The answer. An array (checkbox items, type_of_field = 1) is joined with a \| separator before storage.
monthVal | month string No none Month token.
yearVal | year number No none Year.

Example request

curl -X POST https://api.example.com/api/mc/filter/other/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"id":78,"answer":["Yes","N/A"],"month":"Apr","year":2026}'

Success response — 201 Created

{ "error": false }

Errors

Status Condition Message/shape
200/201 mc_other_answer table missing { "error": true, "message": "mc_other_answer missing." }

Notes

  • Two payload shapes are accepted: {id, answer, type, month, year, companyId} and {answer_id, mc_answer, monthVal, yearVal}.
  • Upserts on company_id + answer_id + the resolved month start.

12.6 Review Assignments

The Review Assignments grid lists companies down the side and 24 months across the top, with the assigned reviewer in each cell plus per-company Primary Accountant, Monthly Close Manager and Close Deadline columns. The month header band is computed independently of the current filter: 24 columns spanning today − 11 months through today + 12 months, with a six-month visible band (today − 1 month … today + 4 months) whose first column is tagged startPos: 'start-here'.

The request contract used on these routes is not the Close Status one: here every field, including monthVal/yearVal, is optional, and an extra isReviewAssignment flag is accepted.

POST/api/get/primary_accountant

Returns the distinct primary accountants recorded across the companies in scope, for the grid's Primary Accountant filter.

Module Monthly Close > Review Assignments
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; scope is resolved within the caller's firm.
Content type application/json

Request body (Review Assignments variant; all fields optional)

Field Type Required Validation / allowed values Description
cg_id string No String Scope token (g_<id> / c_<id>).
id number No Integer Selected company/group id.
type string No String 'group' / 'company' (not enum-validated here).
primary / reviewer / status / closeDead / page number No Integer Accepted; not used by this endpoint.
monthVal string No String Accepted; not used by this endpoint.
yearVal number No Integer Accepted; not used by this endpoint.
sortColumn / sortBy string No String Accepted; not used by this endpoint.
isReviewAssignment number No Integer Accepted; only meaningful on /get/user_reviewers.

Example request

curl -X POST https://api.example.com/api/get/primary_accountant \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"cg_id":"g_12","id":12,"type":"group"}'

Success response — 201 Created. The body is a bare array.

[
  { "user_id": 88, "user_name": "Dana Reed", "email": "dana@firm.com", "profile_image": null }
]
Field Type Description
user_id number The user id.
user_name string Last-first display name (falls back to first+last).
email string Email address.
profile_image null Always null on this route.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • The primary-accountant source table has no month column, so this list is month-agnostic by nature.
POST/api/get/user_reviewers

Returns the reviewer option list — either the eligibility roster or the recorded reviewers, depending on isReviewAssignment.

Module Monthly Close > Review Assignments
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; scope is resolved within the caller's firm.
Content type application/json

Request body (Review Assignments variant; all fields optional). The fields this endpoint reads:

Field Type Required Validation / allowed values Description
cg_id string No String Scope token.
id number No Integer Company/group id.
type string No String Scope kind.
isReviewAssignment number No Numeric (numeric strings are coerced) Integer Truthy → users mapped to the selected company/group (the Review Assignments page always sends 1). Falsy/absent → users already recorded as a reviewer, restricted to the requested month (the Close Status page's behaviour).
monthVal string No String Month token; used only on the falsy-isReviewAssignment branch, and only when paired with yearVal.
yearVal number No Integer Year; same.

Example request

curl -X POST https://api.example.com/api/get/user_reviewers \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"cg_id":"g_12","id":12,"type":"group","isReviewAssignment":1}'

Success response — 201 Created. A bare array.

Eligibility-roster branch (isReviewAssignment truthy) — includes name parts:

[
  { "user_id": 91, "first_name": "Sam", "last_name": "Ortiz", "user_name": "Ortiz, Sam", "email": "sam@firm.com", "profile_image": null }
]

Recorded-reviewer branch (isReviewAssignment falsy):

[
  { "user_id": 91, "user_name": "Ortiz, Sam", "email": "sam@firm.com", "profile_image": null }
]
Field Type Description
user_id / user_name / email number / string / string The reviewer.
first_name / last_name string Present only on the eligibility-roster branch.
profile_image null Always null.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • When monthVal/yearVal are absent on the recorded-reviewer branch, the month restriction is dropped entirely (monthStart is null).
POST/api/mc/peer/review/assignment/filter

Loads the Review Assignments grid — one group block per call, plus the month headers, reviewer values, completion flags and manager list.

Module Monthly Close > Review Assignments
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Cell editability is returned per row: haveAccess is global admin / firm admin / holder of the Monthly Close Manager role (role id 11); checkCloseDeadLineAccess is firm-or-global admin, or this company's own Monthly Close Manager.
Content type application/json

Request body (all fields optional)

Field Type Required Validation / allowed values Description
cgId string No String Scope token.
compId number No Numeric (numeric strings are coerced) Integer Selected company id.
type string No String 'group' / 'company'.
primary number No Integer Filter by primary accountant.
reviewer number No Integer Filter by reviewer.
compName string No String Substring filter on company name (case-insensitive).
filterManager string No String Manager filter — a numeric id matches exactly, any other string is a case-insensitive substring match on the manager name.
startMonth string No String Start month for the grid.
month string No String Month token.
year number No Integer Year.
includeInactive number No Integer Include inactive companies.
reBalance number No Integer Truthy = lazy-load one subgroup's direct companies only (identified by groupId), skipping the full filter pipeline.
groupId number No Integer The subgroup to load when reBalance is set.

Example request

curl -X POST https://api.example.com/api/mc/peer/review/assignment/filter \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"cgId":"g_12","compId":0,"type":"group","month":"Apr","year":2026}'

Success response — 201 Created

{
  "error": false,
  "corg": "group",
  "corgId": 12,
  "headerArr": [
    { "monthKey": "may_2025", "month": "May 25", "startPos": "", "cellDisplay": "none" },
    { "monthKey": "apr_2026", "month": "Apr 26", "startPos": "start-here", "cellDisplay": "" }
  ],
  "peerReviewerData": [
    {
      "group": "West Region",
      "group_id": 12,
      "company": [
        {
          "companyId": 401,
          "name": "Acme Holdings",
          "accountantName": "Dana Reed",
          "primaryAccountantId": 88,
          "deleteOrInactive": 0,
          "deadline": 15,
          "manager": { "user_id": 88, "user": "Dana Reed" },
          "user": [],
          "haveAccess": true,
          "checkCloseDeadLineAccess": true
        }
      ]
    }
  ],
  "value": [{ "401": { "apr_2026": { "id": 91, "name": "Sam Ortiz", "email": "sam@firm.com" } } }],
  "completed": [{ "401": { "apr_2026": 1 } }],
  "managers": [{ "user_id": 88, "user": "Dana Reed" }],
  "companyLength": 1,
  "bal_peerData": [18, 19]
}
Field Type Description
corg / corgId string / number Resolved scope kind and id.
headerArr[] object[] The 24 month columns. cellDisplay is 'none' outside the visible six-month band; startPos is 'start-here' on the first visible month.
peerReviewerData[] object[] Always an array of group blocks; this response carries just the first one, and reBalance calls append more. Each block is { group, group_id, company[] }.
peerReviewerData[].company[].manager object | string { user_id, user }, or the literal string "Unassigned".
peerReviewerData[].company[].haveAccess boolean Whether reviewer cells are editable (forced false for a deleted company).
peerReviewerData[].company[].checkCloseDeadLineAccess boolean Whether the Close Deadline input is editable.
value[] object[] [ { companyId: { monthKey: reviewer } } ] — the assigned reviewer per cell.
completed[] object[] [ { companyId: { monthKey: 0\|1 } } ] — completion flag per cell.
managers[] object[] Manager filter options.
companyLength number Rows in this block after filtering.
bal_peerData number[] Group scope only — the remaining subgroup ids for the client to lazy-load.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/mc/peer/review/save

Saves one grid cell (a month's reviewer, the primary accountant, or the close deadline), or copies a month's reviewers forward.

Module Monthly Close > Review Assignments
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. On the ordinary save path the company is firm-checked; the copy path is checked before that (see notes).
Content type application/json

Request body (all fields optional by design)

Field Type Required Validation / allowed values Description
fromCopy number No Numeric (numeric strings are coerced) Truthy = run the copy-reviewer-for-month operation and ignore every field below except cgId / copyMonth / copyYear / month / year / primary / reviewer. Checked first.
companyId number No Numeric (numeric strings are coerced) Integer Company (required in practice on the ordinary path — a copy request carries none).
head string No String What is being saved: close_deadline, primary_accountant, or a <month>_<year> cell key (e.g. apr_2026). Lower-cased and trimmed.
value unknown No none The new value. ''/falsy clears the field.
month string No String Overrides the month parsed out of head.
year string No String Overrides the year parsed out of head.
cgId string No String Scope token for the copy operation.
copyMonth string No String Source month for the copy operation.
copyYear number No Integer Source year for the copy operation.
primary number No Integer Filter carried into the copy operation.
reviewer number No Integer Filter carried into the copy operation.

Example request

curl -X POST https://api.example.com/api/mc/peer/review/save \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401,"head":"apr_2026","value":91}'

Success response — 201 Created

Ordinary save:

{ "error": false }

Copy operation (fromCopy truthy):

{ "error": false, "message": "Reviewer Updated Successfully." }

Errors

Status Condition Message/shape
200/201 head is empty on the ordinary path { "error": true, "message": "Invalid assignment head." }
200/201 A month cell save with no resolvable month or year { "error": true, "message": "Invalid month assignment." }

Notes

  • companyId and head are optional by design: fromCopy is evaluated before companyId is validated, and a copy request carries neither. Requiring them would cause request validation to reject copy requests.
  • The close-deadline range is not validated server-side; whatever arrives is stored, including a blank value, which clears the field.
  • Saving a truthy close_deadline triggers a reviewer to-do refresh against last month's data.
POST/api/mc/peer/review/user-list

Returns the users assignable as a reviewer for one company.

Module Monthly Close > Review Assignments
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; the company is firm-checked and the roster is additionally scoped to the caller's firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Numeric (numeric strings are coerced) Integer Company.

Example request

curl -X POST https://api.example.com/api/mc/peer/review/user-list \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyId":401}'

Success response — 201 Created

{
  "error": false,
  "data": [
    { "id": 91, "name": "Ortiz, Sam", "email": "sam@firm.com", "profile": "" }
  ]
}
Field Type Description
data[] object[] Users with a direct user_company_access grant on this company, or a user_group_access grant anywhere in the company's full group tree, scoped to Monthly Close (menu 3) with access_type != 1.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

POST/api/mc/peer/review/assignment/download/file

Generates the blank Review Assignments import template and returns a one-shot download link.

Module Monthly Close > Review Assignments
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session.
Content type application/json

Request body (all fields optional)

Field Type Required Validation / allowed values Description
compId number No Numeric (numeric strings are coerced) Integer The selected company/group id; falls back to companyId.
companyId number No Integer Alias of compId.
companyType string No String 'group' selects group resolution; anything else means company. Falls back to type.
type string No String Alias of companyType.
cgId string No String Accepted; not read on this path.
month string No String Accepted; not read on this path.
year number No Integer Accepted; not read on this path.

Example request

curl -X POST https://api.example.com/api/mc/peer/review/assignment/download/file \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"compId":12,"companyType":"group"}'

Success response — 201 Created

{ "error": false, "redirect": "/api/mc/peer/review/assignment/download/generated/6d0f9a11-38ff-4b62-9df3-c2b7a4d10e55" }
Field Type Description
redirect string Path to the token download route. Single-use.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • This is a blank fill-in template (header rows only) plus a reference "Available Data" sheet; it is not pre-populated with current assignments.
  • Three sheets: Close Deadline (Company, Close Deadline), Review Assignments (Company, Month Year(MM/YYYY), Reviewer) and the reference sheet. The same builder produces the error-annotated re-download after a failed import, adding a Reason column.
POST/api/mc/peer/review/assignment/import/file

Imports a filled-in Review Assignments template.

Module Monthly Close > Review Assignments
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Session validity only — this endpoint derives nothing else from the authenticated user.
Content type multipart/form-data

Request body

Field Type Required Validation / allowed values Description
file file part Yes extension must be xlsx or xlx The workbook. Both a file name and file content are required.
compId | companyId number No Integer The selected id.
companyType / type / cgId / month / year — No As on the template download route Accepted; only compId/companyId is read on this path.

Example request

curl -X POST https://api.example.com/api/mc/peer/review/assignment/import/file \
  -b 'genie.sid=<session-cookie>' \
  -F 'file=@Review Assignments Template.xlsx' \
  -F 'compId=401'

Success response — 201 Created

{ "error": false, "message": "Data are Imported Successfully." }

Row-level failures (still 201, error in the body):

{
  "error": true,
  "errorLog": {
    "head": ["Company", "Close Deadline", "Month Year(MM/YYYY)", "Reviewer", "Reason"],
    "body": [{ "company": "Acme Holdings", "monthYear": "13/2026", "reviewer": "Sam Ortiz", "error": "Invalid Month Year." }]
  },
  "errorExcelLink": "/api/mc/peer/review/assignment/download/generated/2f7c9d40-1a6b-4f8e-9c21-77b0e3a5d612"
}
Field Type Description
errorLog.head string[] Column headings for the error table.
errorLog.body[] object[] The rejected rows with their reason.
errorExcelLink string Download path for an error-annotated copy of the workbook (same 3-sheet shape with Reason populated).

Errors (all returned in the body, HTTP 201)

Status Condition Message/shape
201 No file, or no buffer { "error": true, "message": "Please upload an Excel file." }
201 Extension is not xlsx/xlx { "error": true, "message": "Please provide Excel sheet format supported file." }
201 Workbook unreadable, or a header row contains an unexpected column { "error": true, "message": "Invalid Excel File. Columns are not matched with the template." }
201 Both sheets entirely empty { "error": true, "message": "Empty Excel Sheet.Please put on some data to import." }
201 Review Assignments sheet has data rows but the wrong header triple { "error": true, "message": "Review Assignments Excel Formation is Wrong." }
201 Headers present but no data rows survived { "error": true, "message": "No Data to Import." }

Notes

  • The accepted extensions are exactly xlsx and xlx; .xls files are rejected. Header validation rejects unexpected columns only — it does not require every expected column to be present.
  • On this route compId is always resolved as a company id, never as a group id. A group-scoped import therefore resolves no firm, and every row fails with "Company Not Found in Genie." Import with a company-scoped id.
  • Close-deadline updates and reviewer inserts are applied best-effort: valid rows are written even when other rows in the workbook error.
GET/api/mc/peer/review/assignment/download/generated/:token

Streams a generated Review Assignments template (blank or error-annotated) and deletes it.

Module Monthly Close > Review Assignments
Authentication Not required — possession of the single-use token is the only credential
Authorization No role requirement
Content type Binary .xlsx download

Path parameters

Name Type Required Description
token string Yes The UUID from a redirect / errorExcelLink.

Example request

curl -OJ https://api.example.com/api/mc/peer/review/assignment/download/generated/6d0f9a11-38ff-4b62-9df3-c2b7a4d10e55

Success response — 200 OK, Content-Disposition: attachment; filename="Review Assignments Template.xlsx" (or "Error of Review Assignments Template.xlsx"), body is the XLSX binary.

Errors

Status Condition Message/shape
404 Unknown or already-consumed token { "error": true, "message": "File not found." }

Notes

  • The generated file is held as temporary server-side storage and referenced by a single-use token. The download must be served by the same application instance that produced it, so multi-instance deployments must preserve instance affinity for these routes. See Deployment & Operational Considerations.

12.7 QuickBooks Close Dates

A read-only report of the QuickBooks book close date stored against each company in a group, with its primary accountant and reviewer, plus an Excel export. The stored dates themselves are refreshed by POST /api/mc/configure/update (Firm Configuration).

POST/api/mc/report/closedate

Returns the close-date report rows for a company group (or a single company) plus the manager filter options.

Module Monthly Close > QuickBooks Close Dates
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session; group membership is intersected with the caller's firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyGroup number Yes Numeric (numeric strings are coerced) Integer The selected group (or company) id. A value ≤ 0 returns an empty report.
type string Yes String One of group, company Which kind of id companyGroup is.
filterManager number[] No Array Numeric (numeric strings are coerced) Manager user ids to narrow the client list to.

Example request

curl -X POST https://api.example.com/api/mc/report/closedate \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyGroup":12,"type":"group","filterManager":[88]}'

Success response — 200 OK (this read-only report returns 200, not 201)

{
  "error": false,
  "clients": [
    {
      "id": 401,
      "name": "Acme Holdings",
      "qbo_close_date": "04/30/2026",
      "primary_user": "Dana Reed",
      "reviewer_user": "Sam Ortiz",
      "user_id": 88,
      "assign_to": 91
    }
  ],
  "managers": [{ "user_id": 88, "user": "Dana Reed", "company_id": 401 }],
  "connectionAvailable": true
}
Field Type Description
clients[] object[] One row per company, ordered by company name. qbo_close_date is formatted MM/DD/YYYY, or - when unset. user_id / assign_to are the primary and reviewer user ids (null when unset).
managers[] object[] Manager filter options, sorted by name.
connectionAvailable boolean True when at least one company in scope has qbo_connection = 1.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • With no companies in scope the response is { "error": false, "clients": [], "managers": [], "connectionAvailable": false }.
  • Where a company yields multiple primary/reviewer rows, the last row wins outright, including when its primary/reviewer values are NULL. When filterManager is set, each selected manager contributes exactly one company (the first it was found on) and the list is ordered by manager rather than by company name.
POST/api/mc/report/closedate/export

Generates the close-date report workbook and returns a one-shot download link.

Module Monthly Close > QuickBooks Close Dates
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyGroup number Yes Numeric (numeric strings are coerced) Integer The group id. This route accepts no type field — the export always resolves the scope as 'group'.
filterManager number[] No Array Numeric (numeric strings are coerced) Manager ids to filter by.
exportName string No String User-chosen file name; \ / # ? " % are stripped and an empty result falls back to QuickBooks Close Date Report.

Example request

curl -X POST https://api.example.com/api/mc/report/closedate/export \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"companyGroup":12,"filterManager":[88],"exportName":"April close dates"}'

Success response — 200 OK (this route returns 200, not 201)

{ "error": false, "redirect": "/api/mc/report/closedate/export/generated/44c1f0a9-6b6e-4d3f-8b23-5c9b1f0f9a2e" }
Field Type Description
redirect string Path to the token download route. Single-use, and bound to the requesting user.

Errors

Status Condition Message/shape
200 The report has no rows { "error": true, "msg": "There is no data to Export" }

Notes

  • Workbook styling: every column width 20, the spacer and header rows bold, size 12 and centred, and all data cells except the Client column right-aligned.
GET/api/mc/report/closedate/export/generated/:token

Streams a generated close-date report workbook to the user who requested it, and deletes it.

Module Monthly Close > QuickBooks Close Dates
Authentication Required — authenticated session cookie; unlike the other download routes in this section, this one also requires the caller to be the user who generated the export
Authorization Owner-only.
Content type Binary .xlsx download

Path parameters

Name Type Required Description
token string Yes The UUID from the export's redirect.

Example request

curl -OJ -b 'genie.sid=<session-cookie>' \
  https://api.example.com/api/mc/report/closedate/export/generated/44c1f0a9-6b6e-4d3f-8b23-5c9b1f0f9a2e

Success response — 200 OK, Content-Disposition: attachment; filename="<exportName>.xlsx", body is the XLSX binary.

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }
404 Unknown or already-consumed token { "error": true, "message": "File not found." }
404 The token belongs to another user { "error": true, "message": "File not found." } — the same 404 as an unknown token, so someone else's export is never confirmed to exist. The entry is left in place so its real owner can still fetch it.

Notes

  • Unlike the other download routes in this module, this one requires an authenticated session. The redirect is followed by a same-origin top-level navigation, so the session cookie is sent automatically and the check is transparent to the user.
  • The workbook is an .xlsx file held as temporary server-side storage and referenced by a single-use token. The download must be served by the same application instance that produced it, so multi-instance deployments must preserve instance affinity for these routes. See Deployment & Operational Considerations.

12.8 Company Group Tree

POST/api/get/treeview

Returns the company-group tree and flat group/company option lists the Monthly Close scope picker renders.

Module Monthly Close > Company Group Tree
Authentication Required — authenticated session cookie
Authorization Three tiers resolved server-side: (1) a Global Company Administrator sees every group and company in every firm, with no firm/company filter at all; (2) a firm admin sees the whole firm unconditionally; (3) any other user sees only the companies and groups they hold access to for the requested menu (default MONTHLY_CLOSE_MENU_ID = 3).
Content type application/json

Request body — this endpoint accepts a loosely typed request body; no schema validation is applied.

Field Type Required Validation / allowed values Description
module number No none The permission menu to scope tier-3 visibility to. Coerced to a number; a value ≤ 0 (or absent) falls back to the Monthly Close menu id (3).

Example request

curl -X POST https://api.example.com/api/get/treeview \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{"module":3}'

Success response — 201 Created

{
  "tree": [
    {
      "id": 12,
      "name": "West Region",
      "clickable": true,
      "child": {
        "group": [
          { "id": 18, "name": "California", "clickable": true, "child": { "group": [], "company": [{ "id": 401, "name": "Acme Holdings" }] } }
        ],
        "company": []
      }
    }
  ],
  "group": [{ "id": 12, "name": "West Region" }, { "id": 18, "name": "California" }],
  "company": [{ "id": 401, "name": "Acme Holdings", "deleted": 0 }]
}
Field Type Description
tree[] object[] The nested group tree. Each node is { id, name, clickable, child: { group[], company[] } }; clickable marks whether the group itself may be selected.
group[] object[] Flat { id, name } list of every visible group.
company[] object[] Flat { id, name, deleted } list of every visible company; deleted lets the picker mark inactive companies.

Errors

No endpoint-specific error conditions are documented for this endpoint; the standard responses in §5.3 Status codes apply.

Notes

  • This route is not under the mc/ prefix even though it belongs to this module; request it at the path shown.
  • Tier-3 visibility uses the shared, plan_type-matched menu-access rules, resolved for menu 3 (Monthly Close) rather than menu 81 (Tasks & Time).
  • Tier 1 is resolved from the caller's assigned roles, not from a users.global_admin column, which does not exist in this database.

↑ Back to Table of Contents


13. Comments Center

Comments Center holds the threaded comment panels attached to Tasks and to Invoicing batches. Both are built on the shared comment_thread table with per-domain comment and assignee tables.

Conventions used throughout this section:

  • Paths in this section are served under /api.
  • Global request validation runs on every request. Any body property not declared in the endpoint's request body schema is silently deleted before the endpoint sees it.
  • Authentication is enforced per endpoint rather than by a route-level gate: every request resolves the caller from the session, which must carry a user id and be in the authenticated state; otherwise the request fails with 401 { "message": "These credentials do not match our records." }. The session cookie is genie.sid (httpOnly, SameSite=Lax).
  • No CSRF token is required on any endpoint in this section; CSRF protection applies only to the authentication endpoints.
  • Several :id path parameters and body ids accept either a plain numeric id or an encrypted (base64) id. List responses hand back the encrypted form as encryptId.

13.1 Task Comments

Threaded comments on a Task Detail record.

POST/api/task/view/comment

Loads every comment thread on a task, with each comment's author, assignees and per-thread resolve state, plus the list of users assignable on the task's company.

Module Comments Center > Task Comments
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session. The task's company must satisfy company.firm_id = <caller's firm> and company.deleted = 0; a task outside the caller's firm returns the empty payload rather than an error.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Numeric (numeric strings are coerced), integer Company context. Used only as a fallback — the company actually used is the task's own company_id.
taskId string Yes String Task id, plain numeric or encrypted.

Example request

curl -X POST 'https://api.example.com/api/task/view/comment' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "companyId": 412, "taskId": "9081" }'

Success response — 200 OK

{
  "getComment": [
    {
      "id": 5521,
      "thread_id": 1183,
      "comment": "Bank rec is missing the November statement.",
      "resolve": 0,
      "user_id": 77,
      "createdUser": { "id": 77, "name": "Nguyen, Alex" },
      "createdDateTime": "12/03/2025 09:14 AM",
      "assignee": [
        {
          "assign_to": 91,
          "user": { "id": 91, "lf_name": "Patel, Riya", "email": "riya@example.com", "name": "Patel, Riya" }
        }
      ]
    }
  ],
  "getCommentThread": [{ "id": 1183, "task_id": 9081, "resolve": 0 }],
  "assignUsers": [
    { "id": 91, "name": "Patel, Riya", "company_id": 412, "user_id": 91, "user_name": "Patel, Riya", "email": "riya@example.com", "profile_image": "" }
  ],
  "viewComments": [
    { "caption": "Resolve Thread", "none": "", "resolveLabelShow": "d-none", "allCommentValue": 0 }
  ],
  "threadCount": 1,
  "loginUser": 77
}
Field Type Description
getComment[] array One entry per non-deleted task_comments row, ascending by id.
getComment[].createdUser object { id, name } — users.lf_name ("Last, First"), falling back to first+last.
getComment[].createdDateTime string Formatted timestamp.
getComment[].assignee[] array { assign_to, user: { id, lf_name, email, name } } per task_comments_assignee row.
getCommentThread[] array comment_thread rows for the task (id, task_id, resolve). When no thread rows exist, threads are synthesised from the comments' distinct thread_ids.
assignUsers[] array Users mapped to the company who hold non-revoked Task & Time (menu 81) access on the company or its company group.
viewComments[] array Per-thread UI labels, index-aligned with getCommentThread.
threadCount number Length of getCommentThread.
loginUser number Caller's user id.

Errors

Status Condition Message/shape
400 companyId missing/non-integer, or taskId missing/non-string Standard request-validation error array
401 No authenticated session, or user row missing/deleted { "message": "These credentials do not match our records." }

Notes

  • An unresolvable taskId, or a task outside the caller's firm, returns the empty payload (getComment: [], threadCount: 0, loginUser) — not a 404 — but still with assignUsers: [].
  • Assignable-user resolution first requires a user_company_mapping row; if that yields nothing it falls back to a company/group-access-only query so the picker is never empty for group-granted companies.

POST/api/task/view/comment/save

Adds a comment to a task, creating a new comment_thread when no threadId is supplied, and optionally assigning users to that comment.

Module Comments Center > Task Comments
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session; the task must belong to the caller's firm and to a non-deleted company.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number Yes Numeric (numeric strings are coerced), integer Fallback company; the task's own company_id wins.
taskId string Yes String Plain or encrypted task id.
comment string Yes String Comment body. Trimmed; an empty result is rejected.
assignUser array No Optional, array — elements typed number \| string User ids to assign. Coerced to a number; values <= 0 are dropped.
threadId number No Optional, Numeric (numeric strings are coerced), integer Reply into an existing thread. Omit to start a new thread.

Example request

curl -X POST 'https://api.example.com/api/task/view/comment/save' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "companyId": 412, "taskId": "9081", "comment": "Statement uploaded.", "assignUser": [91], "threadId": 1183 }'

Success response — 200 OK

{ "error": false, "id": 5522, "thread_id": 1183 }
Field Type Description
error boolean false on success.
id number New task_comments.id.
thread_id number Thread the comment landed in (new or supplied).

Errors

Status Condition Message/shape
200 taskId unresolvable, or task not in the caller's firm { "error": true, "message": "Task not found." }
200 comment blank after trim { "error": true, "message": "Comment Field is required." }
400 Missing/invalid companyId, taskId or comment Standard request-validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Failure cases return HTTP 200 with error: true; only validation and auth produce non-2xx.
  • New threads are written with in_company = 0 and resolve = 0 explicitly (the column is NOT NULL with no DB default under strict sql_mode).
  • task_comments.comment_date is stamped with the first day of the current month.

POST/api/task/view/comment/edit

Edits one task comment: either replaces its text, or replaces its full assignee set.

Module Comments Center > Task Comments
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session; the owning task must belong to the caller's firm. No author-only restriction — any user with access to the task may edit any comment on it.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
commentId number Yes Numeric (numeric strings are coerced), integer task_comments.id.
colName string Yes String 'assign_to' switches to assignee-replacement mode; any other value is treated as a comment-text update.
colValue unknown No Optional only — no type validation For assign_to: an array of user ids. Otherwise: the new comment text (coerced with String(colValue ?? '')).

Example request

curl -X POST 'https://api.example.com/api/task/view/comment/edit' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "commentId": 5521, "colName": "assign_to", "colValue": [91, 104] }'

Success response — 200 OK

{
  "error": false,
  "message": "Comment Updated Successfully",
  "assignee": [
    { "task_comments_id": 5521, "assign_to": 91, "id": 91, "lf_name": "Patel, Riya", "first_name": "Riya", "last_name": "Patel", "email": "riya@example.com" }
  ]
}
Field Type Description
error boolean false on success.
message string Always "Comment Updated Successfully".
assignee[] array Refreshed assignee rows joined to users. Empty array on a text update.

Errors

Status Condition Message/shape
200 commentId not found / soft-deleted, or its task is outside the caller's firm { "error": true, "message": "Comment not found." }
400 Missing/invalid commentId or colName Standard request-validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Assignee mode is destructive: all existing task_comments_assignee rows for the comment are deleted, then re-inserted from colValue. Sending colValue: [] clears every assignee.
  • On the text path a missing colValue writes an empty string — unlike the Invoicing Comments equivalent, this endpoint has no blank-value guard.

POST/api/task/comment/resolve

Toggles a comment thread between resolved and re-opened, cascading the new state to every comment in the thread.

Module Comments Center > Task Comments
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session; the thread's task must belong to the caller's firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
commentId number Yes Numeric (numeric strings are coerced), integer Despite the name, this is a comment_thread.id, not a comment id.
resolveVal number Yes Numeric (numeric strings are coerced), integer The current state. 0 resolves the thread (new value 1); any other value re-opens it (new value 0).

Example request

curl -X POST 'https://api.example.com/api/task/comment/resolve' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "commentId": 1183, "resolveVal": 0 }'

Success response — 200 OK

{ "error": false, "resolveComment": 1 }
Field Type Description
error boolean false on success.
resolveComment number The state just written: 1 = resolved, 0 = re-opened.

Errors

Status Condition Message/shape
200 Thread id not found, or its task is outside the caller's firm { "error": true, "message": "Thread not found." }
400 Missing/invalid commentId or resolveVal Standard request-validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Two writes occur: comment_thread.resolve for the thread, and task_comments.resolve for every non-deleted comment with that thread_id.

13.2 Invoicing Comments

Threaded comments on an invoicing batch (batch_invoice). Save and resolve share the same open-comment-thread mechanics that back the Monthly Close / Accounting comment sources.

POST/api/invoicing/view/comment

Loads the comment threads on an invoice batch plus the assignable-user list for the comment composer.

Module Comments Center > Invoicing Comments
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
batchInvoiceId number No Optional, Numeric (numeric strings are coerced), integer Invoice batch id. Omitted or 0 returns the empty payload.
companyId number No Optional, Numeric (numeric strings are coerced), integer Fallback company for the assignable-user lookup; the batch's own company takes precedence when it can be resolved.
threadId number No Optional, Numeric (numeric strings are coerced), integer Accepted by the request body schema; it does not affect the result.

Example request

curl -X POST 'https://api.example.com/api/invoicing/view/comment' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "batchInvoiceId": 3320, "companyId": 412 }'

Success response — 200 OK

{
  "getComment": [
    {
      "id": 812,
      "thread_id": 2044,
      "comment": "Hold this batch until the rate change is approved.",
      "resolve": 0,
      "user_id": 77,
      "createdUser": { "id": 77, "name": "Nguyen, Alex" },
      "createdDateTime": "12/03/2025 09:14 AM",
      "assignee": []
    }
  ],
  "getCommentThread": [{ "id": 2044, "resolve": 0 }],
  "assignUsers": [{ "id": 91, "name": "Patel, Riya" }],
  "viewComments": [
    { "caption": "Resolve Thread", "none": "", "resolveLabelShow": "d-none", "allCommentValue": 0 }
  ],
  "threadCount": 1,
  "loginUser": 77
}
Field Type Description
getComment[] array Non-deleted invoice_comments rows for the batch, ascending by id, shaped exactly as the Task Comments equivalent.
getCommentThread[] array comment_thread rows (id, resolve) referenced by those comments.
assignUsers[] array Assignable users for the batch's company.
viewComments[] array Per-thread UI labels, index-aligned with getCommentThread.
threadCount number Length of getCommentThread.
loginUser number Caller's user id.

Errors

Status Condition Message/shape
400 Non-integer values supplied for the optional fields Standard request-validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • When batchInvoiceId is absent/0, or the invoice_comments table is not present, the empty payload is returned but assignUsers is still populated, so the composer opens with a working picker on a batch that has no comments yet.

POST/api/invoicing/view/comment/save

Posts a comment on an invoice batch — a reply when threadId is supplied, otherwise a new thread.

Module Comments Center > Invoicing Comments
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement, but the resolved company must belong to the caller's firm; a mismatch raises 401.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
batchInvoiceId number Yes Numeric (numeric strings are coerced), integer Invoice batch id.
companyId number Yes Numeric (numeric strings are coerced), integer Fallback company; batch_invoice.company_id wins when the batch resolves.
comment string Yes String Comment body; trimmed before insert.
assignTo number[] No Optional, array, Numeric (numeric strings are coerced) User ids to assign; values <= 0 are dropped.
threadId number No Optional, Numeric (numeric strings are coerced), integer Reply into an existing thread. Omit to create one.

Example request

curl -X POST 'https://api.example.com/api/invoicing/view/comment/save' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "batchInvoiceId": 3320, "companyId": 412, "comment": "Approved.", "assignTo": [91] }'

Success response — 200 OK

New thread:

{ "error": false, "thread_id": 2045, "source": "invoice" }

Reply into an existing thread:

{ "error": false }
Field Type Description
error boolean false on success.
thread_id number Present on the new-thread path only.
source string Present on the new-thread path only; always "invoice" for this route.

Errors

Status Condition Message/shape
200 New-thread path where the resolved batchInvoiceId is <= 0 { "error": true, "message": "Threads can only be started for Monthly Close or Accounting from Open Comments." }
400 Missing/invalid batchInvoiceId, companyId or comment Standard request-validation error array
401 No authenticated session, or the resolved company is not in the caller's firm { "message": "These credentials do not match our records." }

Notes

  • Threads and replies created here are recorded under module 106 (Invoice).
  • New invoice threads write comment_thread.task_id = NULL (it is an FK to tasks), in_company = 0, resolve = 0.
  • The reply path carries the batch id forward from the thread's first comment; it does not re-check the supplied batchInvoiceId against the thread.

POST/api/invoicing/view/comment/edit

Edits one invoice comment: replaces its text, or replaces its full assignee set.

Module Comments Center > Invoicing Comments
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session, and no firm-scope assertion on this path — the update is keyed on commentId alone.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
commentId number Yes Numeric (numeric strings are coerced), integer invoice_comments.id.
colName string Yes String 'assign_to' switches to assignee-replacement mode; any other value updates the comment text.
colValue unknown Yes Accepted as-is — no type constraint, so both shapes are preserved Array of user ids for assign_to; the new comment text otherwise.

Example request

curl -X POST 'https://api.example.com/api/invoicing/view/comment/edit' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "commentId": 812, "colName": "comment", "colValue": "Approved after rate review." }'

Success response — 200 OK

{ "error": false, "message": "Comment Updated Successfully" }
Field Type Description
error boolean false on success.
message string Always "Comment Updated Successfully".

Errors

Status Condition Message/shape
200 invoice_comments table not present { "error": true, "message": "Comments table unavailable." }
200 Text path with a blank/missing colValue after trim { "error": true, "message": "Comment Field is required." }
400 Missing/invalid commentId or colName, or colValue absent entirely Standard request-validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • colValue carries no declared type because it is polymorphic; it is nevertheless part of the request body schema, so the global unknown-property stripping does not remove it.
  • Unlike Task Comments, a blank text value is rejected instead of being written.
  • Assignee mode deletes all invoice_comments_assignee rows for the comment before re-inserting; it does not return the refreshed assignee list.

POST/api/invoicing/comment/resolve

Toggles an invoice comment thread between resolved and re-opened.

Module Comments Center > Invoicing Comments
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement. Firm scoping is applied inside the SQL: the comment_thread update only matches when the thread's task or batch invoice belongs to a company in the caller's firm. A global admin with no firm_id bypasses that predicate.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
commentId number Yes Numeric (numeric strings are coerced), integer Despite the name, a comment_thread.id.
resolveVal number Yes Numeric (numeric strings are coerced), integer Current state. 0 resolves (new value 1); anything else re-opens (new value 0).
companyId number No Optional, Numeric (numeric strings are coerced), integer Passed through; used only on the Monthly Close / Accounting sources, not on the invoice source.

Example request

curl -X POST 'https://api.example.com/api/invoicing/comment/resolve' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "commentId": 2044, "resolveVal": 0, "companyId": 412 }'

Success response — 200 OK

{ "error": false, "resolveComment": 1 }
Field Type Description
error boolean false on success.
resolveComment number The state just written: 1 = resolved, 0 = re-opened.

Errors

Status Condition Message/shape
400 Missing/invalid commentId or resolveVal Standard request-validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Two writes: comment_thread.resolve (firm-scoped) and invoice_comments.resolve for every non-deleted comment on the thread.
  • The invoice_comments update is not firm-scoped; only the comment_thread update is. A thread outside the caller's firm therefore leaves the thread row untouched while still returning error: false.

↑ Back to Table of Contents


14. Report Center

The Report Center is the reporting half of Genie: it lets a firm build, preview, package and export financial reports over its QuickBooks-synced ledger. It is organised into five areas — Report List (browse/add/clone/delete report definitions), Report Preview (run a report definition and export it), Report Writer (author a report's rows, columns, period settings and axis), Report Packages (group reports into a package and run/export them together) and Configure (per-company accounting method, fiscal month, QuickBooks refresh). All report generation runs through a single shared engine, so the preview screen, a package card and an Excel export can never disagree about a report's numbers.

Every route in this section is published under the /api prefix, so the public path is /api/<method path>. Authentication is enforced per endpoint and requires a session whose login.userId is set and whose login.state is authenticated. Request validation is applied globally: it strips unknown body and query keys and coerces numeric string values, so any field not listed in the request schema is silently discarded before the endpoint runs.

Two conventions recur throughout:

  • Encrypted ids. report_id, package_id, company_id and id path/body values accept either a plain numeric id or the application's encrypted (base64) id. Both forms are resolved server-side; when neither decodes, the id resolves to 0 and is treated as "not found".
  • error is not an HTTP status. Several endpoints return 200 with { error: true, msg: ... } for business rejections (duplicate name, package already exists) because the front end shows a toast rather than treating them as request failures.

14.1 Report List

GET/api/reports/list

Loads the Report List screen: the company scope picker, the Global and Custom report columns for one report group, the GL-validation badge and the Chart-of-Accounts change counter.

Module Report Center > Report List
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Capability flags are computed rather than blocking the request: canManage = global admin, firm admin, or role 1 (GlobalMasterSystemAdministrator) / role 5 (GlobalReportandDashboardAdministrator); is_global_user = global admin or role 1/5; show_custom_reports = session company ≠ 0 or role 9 (CompanyGroupReportandDashboardAdministrator)
Content type application/json

Query parameters

Name Type Required Default Description
group_id integer No 1 Report group tab (global_report_list.group_type). Falsy values fall back to 1.
searchKey string No '' Case-insensitive substring match on report name.
orderBy string No ASC Only the literal desc (case-insensitive) selects DESC; anything else is ASC.
searchLetter string No '' A–Z jump filter. Only a single [a-z] character is honoured; anything else is ignored.
companyId integer No session company Company scope override. 0/absent falls back to the session company, then the firm-internal company, then the first visible company.
isValidation integer No 0 1 switches to the deferred, expensive GL-validation response (see Notes).

Example request

curl -sS 'https://api.example.com/api/reports/list?group_id=1&orderBy=asc' \
  -H 'Cookie: genie.sid=<session-cookie>'

Success response — 200 OK

{
  "error": false,
  "group_id": 1,
  "selectedCompany": { "id": 12, "encId": "aVs2...", "name": "Acme Holdings", "qbo_connection": 1, "active": 1, "deleted": 0 },
  "companies": [
    { "id": 12, "encId": "aVs2...", "name": "Acme Holdings", "qbo_connection": 1, "active": 1, "deleted": 0 }
  ],
  "includeInactiveDefault": false,
  "account_changes_unseen": 3,
  "changes_redirect": "/company/balanceSheet?changes=1",
  "glValidation": { "status": "Pass", "color": "green", "canRefresh": false, "qboError": false },
  "data": {
    "global": [
      {
        "id": 20, "encryptId": "Qk9...", "report_name": "Balance Sheet (QuickBooks Mirror)",
        "type": 1, "is_removable": 1, "mirror_report": 2, "group_type": 1, "company_id": 0,
        "desc": "", "preview_url": "/reports/preview/Qk9..."
      }
    ],
    "custom": []
  },
  "canManage": true,
  "is_global_user": true,
  "global_can_edit": true,
  "show_custom_reports": true
}
Field Type Description
error boolean Always false on success.
group_id integer Echo of the resolved group tab.
selectedCompany object | null { id, encId, name, qbo_connection, active, deleted }.
companies array Companies visible to the user (role-scoped to the companies visible to the caller), including deleted = 1 rows so the "Include Inactive Companies" toggle has something to reveal.
includeInactiveDefault boolean Always false.
account_changes_unseen integer Unseen Chart-of-Accounts changes for the selected company.
changes_redirect string Target of the changes strip's "Click here" link.
glValidation object Cheap probe: { status: 'Pass'\|'Fail'\|'N/A', color, canRefresh, qboError, reason? }.
data.global / data.custom array Report rows: id, encryptId, report_name, type, is_removable, mirror_report, group_type, company_id, desc (tooltip, joined from report_writer.desc), preview_url.
canManage, is_global_user, global_can_edit, show_custom_reports boolean UI capability gates (see Roles above).

With isValidation=1 the shape is instead:

{
  "error": false,
  "glValidation": { "status": "Fail", "color": "red", "canRefresh": true, "qboError": false, "reason": "View variances" },
  "glVariance": [ { "label": "Total Income", "genie": 120500.0, "quickbooks": 120400.0, "variance": 100.0 } ]
}

Errors

Status Condition Message/shape
400 group_id, companyId or isValidation is not an integer Request validation error array
401 No authenticated session, or the user row is missing/deleted { "message": "These credentials do not match our records." }

Notes

  • type = 1 rows are the Global column (company-less), type = 3/4 the Custom column (scoped to the selected company). Rows with deleted = 1 are excluded.
  • desc does not live on global_report_list; it is LEFT JOINed from report_writer (company 0 for the Global list, the selected company for the Custom list).
  • isValidation=1 is the expensive path: two live Intuit Reports API calls (P&L + Balance Sheet over the running fiscal year through end of last month) reconciled against Genie's own seeded Balance Sheet mirror (report id 20). It is intended to be fired once, after the page paints — never on the initial list load.
  • The badge returned on the plain list load is only the cheap connection/unseen-changes probe.
GET/api/reports/searchReport

Search alias for the Report List. Identical behaviour to GET /api/reports/list.

Module Report Center > Report List
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session (same capability flags as /api/reports/list)
Content type application/json

Query parameters — identical to GET /api/reports/list.

Example request

curl -sS 'https://api.example.com/api/reports/searchReport?searchKey=income&group_id=1' \
  -H 'Cookie: genie.sid=<session-cookie>'

Success response — 200 OK. Identical shape to GET /api/reports/list.

Errors — identical to GET /api/reports/list.

Notes

  • Compatibility route: retained for compatibility with earlier clients. It accepts the same parameters and produces the same result as GET /api/reports/list, which is the canonical route.
POST/api/reports/addReport

Creates a new, empty report definition in global_report_list and returns the URLs for previewing and editing it.

Module Report Center > Report List
Authentication Required — authenticated session cookie
Authorization Global admin, firm admin, or any of role 1 (GlobalMasterSystemAdministrator), 5 (GlobalReportandDashboardAdministrator), 9 (CompanyGroupReportandDashboardAdministrator). Otherwise 401.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
reportName string Yes String; additionally rejected by this endpoint when blank after trim Report display name.
type integer Yes Numeric (numeric strings are coerced), integer global_report_list.type: 1 GlobalReport, 2 GlobalCalulated, 3 CustomReport, 4 CustomCalulated.
groupType integer Yes Numeric (numeric strings are coerced), integer Report group tab the report belongs to.

Example request

curl -sS -X POST 'https://api.example.com/api/reports/addReport' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: genie.sid=<session-cookie>' \
  -d '{"reportName":"Monthly Income Statement","type":3,"groupType":1}'

Success response — 200 OK

{
  "error": false,
  "report_id": 481,
  "report_name": "Monthly Income Statement",
  "previewURL": "/reports/preview/S0xt...",
  "editURL": "/reports/writer/S0xt..."
}
Field Type Description
error boolean false on success; true with msg on a duplicate name.
report_id integer New global_report_list.id. For the special-user path, the id of the last row created.
report_name string Trimmed name as stored.
specialUser boolean Present and true only when the role-9 multi-company path fired.
previewURL / editURL string Front-end routes carrying the encrypted report id.

Duplicate name (still 200):

{ "error": true, "msg": "Report name already exist." }

Errors

Status Condition Message/shape
400 reportName blank after trim Report name field is required
400 global_report_list table missing Report list table is unavailable.
400 type / groupType not integers, or reportName not a string Request validation error array
401 Not authenticated { "message": "These credentials do not match our records." }
401 Caller holds none of the allowed roles Not allowed to add report.

Notes

  • Company scoping: type 3 or 4 stores the session company (falling back to the firm's internal company); type 1/2 stores NULL/0.
  • Duplicate check is name + type, additionally company-scoped for type 3/4, and ignores soft-deleted rows.
  • Special-user fan-out: a role-9 user working at company scope 0 who creates a Custom report gets one global_report_list row per mapped company (from their user-company map), each stamped added_by. The response then carries specialUser: true and the id of the last row inserted.
  • Inserted columns are chosen by schema introspection, so a deployment missing group_type, mirror_report, added_by etc. simply omits them rather than failing.
GET/api/reports/deleteReport

Removes a report definition (soft delete when the deleted column exists).

Module Report Center > Report List
Authentication Required — authenticated session cookie
Authorization Global admin or firm admin only. Otherwise 401.
Content type application/json

Query parameters

Name Type Required Default Description
report_id string Yes — String. Plain numeric or encrypted report id.

Example request

curl -sS 'https://api.example.com/api/reports/deleteReport?report_id=481' \
  -H 'Cookie: genie.sid=<session-cookie>'

Success response — 200 OK

{ "error": false }
Field Type Description
error boolean Always false.

Errors

Status Condition Message/shape
400 report_id absent or not a string Request validation error array
401 Not authenticated { "message": "These credentials do not match our records." }
401 Caller is neither global nor firm admin Not allowed to delete report.

Notes

  • This route uses GET with a query parameter although it performs a mutation.
  • When global_report_list.deleted exists the row is soft-deleted (deleted = 1, updated_at touched); otherwise it is hard-deleted.
  • A missing global_report_list table returns { error: false } rather than an error.
  • Soft-deleted reports still generate: preview and package runs do not filter on deleted.
POST/api/reports/clone

Clones a report definition under a new name, including a deep copy of its writer (header, columns and detail rows).

Module Report Center > Report List
Authentication Required — authenticated session cookie
Authorization Global admin or firm admin only. Otherwise 401.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
report_id string Yes String Source report id (plain or encrypted).
report_name string Yes String; rejected by this endpoint when blank after trim Name for the clone.
type integer Yes Numeric (numeric strings are coerced), integer Target type: 1 Global, 2 GlobalCalulated, 3 Custom, 4 CustomCalulated. Falls back to the source report's type when 0.
groupType integer Yes Numeric (numeric strings are coerced), integer Accepted but not used for the insert — the clone inherits group_type from the source row.

Example request

curl -sS -X POST 'https://api.example.com/api/reports/clone' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: genie.sid=<session-cookie>' \
  -d '{"report_id":"481","report_name":"Monthly Income Statement (Copy)","type":3,"groupType":1}'

Success response — 200 OK

{
  "error": false,
  "report_id": 482,
  "url": "/reports/writer/T0Ru..."
}
Field Type Description
error boolean false on success; true with msg on a duplicate name.
report_id integer New report id.
url string Report Writer route for the clone (encrypted id).

Duplicate name (still 200): { "error": true, "msg": "Report name already exist." }

Errors

Status Condition Message/shape
400 report_name blank after trim Report name field is required
400 global_report_list table missing Report list table is unavailable.
400 Source report id does not resolve to a row Report not found
401 Not authenticated { "message": "These credentials do not match our records." }
401 Caller is neither global nor firm admin Not allowed to clone report.

Notes

  • Target company: a clone saved as Global (type = 1) is stored with company_id = 0; any other type lands on the session company.
  • group_type, based_on and reporting_group are copied from the source report, not from the request — so a clone of a Customer/Project/Class/Location/Reporting-Group writer keeps its axis.
  • Deep copy: the source's report_writer row is duplicated (a Global target reads the company_id = 0 writer; any other target reads the report's first writer), then every report_writer_column and report_writer_details row is copied onto the new writer.
  • Only live (deleted = 0) column and detail rows are copied; soft-deleted columns and rows are not carried into the clone.

14.2 Report Preview

Preview generation is delegated to the shared report engine, which is also what Report Packages uses for each card.

GET/api/reports/preview/:id

Bootstraps the Report Preview screen for one report: the report row, the preview form's controls and state, any saved preview parameters, and — as a first-paint convenience — an already-generated report payload.

Module Report Center > Report Preview
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. The response returns meta.global_can_edit (global admin, firm admin, or role 1/5, and the selected company's name must not match /inactive/i) and meta.isGlobalAdmin (global admin or role 1) as UI gates.
Content type application/json

Path parameters

Name Type Required Description
id string Yes Report id — plain numeric or encrypted. Not schema-validated.

Example request

curl -sS 'https://api.example.com/api/reports/preview/Qk9tRTA4' \
  -H 'Cookie: genie.sid=<session-cookie>'

Success response — 200 OK

{
  "error": false,
  "report": {
    "id": 20, "encryptId": "Qk9tRTA4", "report_name": "Balance Sheet (QuickBooks Mirror)",
    "type": 1, "group_type": 1, "mirror_report": 2, "based_on": 1, "reporting_group": 0
  },
  "meta": {
    "is_fixed": 0, "as_month": "", "as_year": "", "mirror_id": 2, "based_on": 1,
    "checkAccExist": false, "company_id": 12, "connection_qbo": 1,
    "sync_date": "08/21/25 04:15PM", "global_can_edit": true, "isGlobalAdmin": true,
    "report_for": "Acme Holdings",
    "reportingGroups": [ { "id": 4, "name": "West Region", "type": 1 } ],
    "classAndLocation": [ { "qbo_id": "3", "cls_name": "Retail", "cls_or_dept": 1 } ],
    "previewData": { "monthVal": "Aug", "yearVal": "2025", "main_period": 1 }
  },
  "generated": {
    "error": false, "report_id": 20, "report_name": "Balance Sheet (QuickBooks Mirror)",
    "mirror_report": 2, "based_on": 1, "reporting_group": 0, "is_fixed": 0, "is_mirror": 1, "mirror_id": 2,
    "headers": ["Aug 25"],
    "headerGroups": { "first": [], "second": [] },
    "columns": [ { "key": "c1", "order": "A", "label": "Aug 25", "format": 1, "rw_precision": 0, "is_override": 0 } ],
    "rows": [ { "account": "TOTAL ASSETS", "account_num": "-", "row_type": 2, "row_class": "rw-format-bold", "rw_precision": 0, "format": 1, "class_indent": 0, "empty_formula": false, "values": { "c1": 1284300.55 } } ],
    "allowReportWithRG": true, "isSameFiscal": true, "isSameReportMethod": true, "accountingMethods": []
  }
}
Field Type Description
report object id, encryptId, report_name, type, group_type, mirror_report, based_on, reporting_group.
meta.is_fixed 0 | 1 1 hides Period / Period Sequence / No. of Periods / Compare-another-period. Forced to 0 for mirror reports 1, 2 and 5.
meta.as_month / meta.as_year string Writer's saved as-on date, only populated when is_fixed.
meta.mirror_id integer 0 none, 1 Profit & Loss, 2 Balance Sheet, 3 A/R Aging Summary, 4 A/P Aging Summary, 5 Cash Flow.
meta.based_on integer 1 GLAccount, 2 Customer, 3 Project, 4 Class, 5 Location, 6 ReportingGroup.
meta.checkAccExist boolean Whether a custom (mirror_report = 0) report has direct account rows.
meta.connection_qbo 0 | 1 Selected company's QuickBooks connection state.
meta.sync_date string Formatted last QBO sync timestamp.
meta.report_for string Company name, or the reporting group's name for a based_on = 6 report.
meta.reportingGroups array { id, name, type } — type 1 Firm, 2 Cross Firm, 3 ME Group.
meta.classAndLocation array { qbo_id, cls_name, cls_or_dept } — cls_or_dept 1 Class, 2 Location. Sourced from the report's reporting group when based_on = 6, otherwise from the session company.
meta.previewData object | null Previously saved preview parameters for (report, company, user), or null.
generated object A pre-generated run; same shape as POST /api/reports/view/generate_income_statement.

Errors

Status Condition Message/shape
400 global_report_list missing, or the id does not resolve to a row Report not found
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • Mirror reports 3 and 4 (A/R and A/P Aging Summary) are not pre-generated — generated comes back as { error: false, report_id, headers: [], columns: [], rows: [] }, because those two are a live Intuit Reports API call the client immediately re-runs with the form's real aging period.
  • A report with no report_writer row is treated as fixed — except for mirror 1/2/5, which are always non-fixed.
  • The pre-generated generated block is a first-paint convenience; clients may ignore it and call the generate endpoint themselves.
POST/api/reports/view/generate_income_statement

Runs a report definition against the session company's ledger and returns the full grid (headers, column metadata and valued rows). This is the core report-generation endpoint.

Module Report Center > Report Preview
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. Reporting-group access is checked inside the engine for a based_on = 6 report.
Content type application/json

Request body — this endpoint accepts a free-form JSON object; no schema validation is applied, so unknown properties are neither rejected nor stripped. The keys read are:

Field Type Required Validation / allowed values Description
report_id (alias reportId) string | number Yes Plain or encrypted id; unresolvable ids yield Report not found The report to run.
monthVal (alias month) string No Short month name (Jan…Dec); the literal Month is treated as unset As-on month.
yearVal (alias year) number | string No > 0 As-on year. Falls back to the writer's as_on_date, then to today.
main_period (alias mainPeriod) number No 3 = Months YTD Period mode.
period number No — Period sequence.
trailing_period (aliases trailingPeriod, num_of_period) number No 12 Number of trailing periods.
include_total (alias includeTotal) boolean-ish No true when absent Whether the Total column is built.
is_period (alias isPeriod) number No 1 enables Compare-another-period mode.
periodColArr string[] No — The compare-period column keys.
selectedClass string[] No — Class filter.
selectedLoc string[] No — Location filter.
expandone / expandtwo number No 0 None, 1 Class, 2 Location Expand 1st By / Expand 2nd By.
nonZero / nonZero2 number No 1 enables Non-zero-only variants of the two expand axes.
acc_rep_group number No > 0 Run against this reporting group.
expandCompany number No 1 enables Expand a reporting-group run by company.
agingPeriod (alias aging_period) number No — A/R & A/P Aging Summary only: days per aging bucket.
numPeriod (alias num_period) number No — A/R & A/P Aging Summary only: number of buckets.

Example request

curl -sS -X POST 'https://api.example.com/api/reports/view/generate_income_statement' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: genie.sid=<session-cookie>' \
  -d '{
        "report_id": "481",
        "monthVal": "Aug", "yearVal": 2025,
        "main_period": 1, "period": 2, "trailing_period": 12,
        "include_total": 1, "expandone": 1, "nonZero": 1,
        "selectedClass": ["3"], "selectedLoc": []
      }'

Success response — 200 OK

{
  "error": false,
  "report_id": 481,
  "report_name": "Monthly Income Statement",
  "mirror_report": 0,
  "based_on": 1,
  "reporting_group": 0,
  "is_fixed": 0,
  "is_mirror": 0,
  "mirror_id": 0,
  "headers": ["Jul 25", "Aug 25", "Total"],
  "headerGroups": { "first": [], "second": [] },
  "columns": [
    { "key": "c1", "order": "A", "label": "Jul 25", "type": 1, "format": 1, "style": "rw-format-bold",
      "rw_precision": 0, "is_override": 0, "group_key": null, "group_label": null,
      "second_key": null, "second_label": null, "leaf_label": "Jul 25",
      "axis_cls": null, "axis_loc": null, "axis_com": null, "detail_order": 1 }
  ],
  "rows": [
    { "account": "Total Income", "account_num": "-", "row_type": 2, "row_class": "rw-format-bold",
      "rw_precision": 0, "format": 1, "class_indent": 0, "empty_formula": false,
      "values": { "c1": 82500.0, "c2": 91300.25, "total": 173800.25 } }
  ],
  "allowReportWithRG": true,
  "isSameFiscal": true,
  "isSameReportMethod": true,
  "accountingMethods": []
}
Field Type Description
headers string[] Column labels in order.
headerGroups object { first: [], second: [] } — grouped header rows for an expanded or multi-axis run; both empty when nothing is expanded.
columns array Per-column metadata. key is what each row's values map is keyed by; format, rw_precision and is_override decide the cell's suffix and decimals.
rows array account, account_num ('-' when absent), row_type, row_class (rw-format-* style classes), rw_precision, format, class_indent, empty_formula, values.
multiaxis 1 Present only on a multi-axis (based_on 2–6, non-mirror) run.
allowReportWithRG boolean false when the report cannot be run against the requested reporting group; the grid comes back empty.
isSameFiscal, isSameReportMethod, accountingMethods boolean / array Reporting-group consistency strip. accountingMethods is { company, method }[].

A/R and A/P Aging Summary (mirror_report 3 or 4) return a different, flat table alongside empty headers/columns/rows:

{
  "error": false, "report_id": 22, "report_name": "A/R Aging Summary",
  "mirror_report": 3, "is_mirror": 1, "mirror_id": 3, "is_fixed": 0,
  "type": "aging", "header": ["Customer", "Current", "1 - 30", "31 - 60", "Total"],
  "body": [["Acme Corp", "1200.00", "0.00", "340.00", "1540.00"]],
  "total_rows": [1],
  "headers": [], "columns": [], "rows": []
}

Errors

Status Condition Message/shape
400 report_id does not resolve to a global_report_list row Report not found
401 Not authenticated { "message": "These credentials do not match our records." }
500 An engine failure (invalid formula, QBO error, unserialisable value) Logged server-side; the response is JSON

Notes

  • include_total defaults to true when the field is absent — a caller that wants the Total column dropped must send include_total: 0 explicitly.
  • Soft-deleted reports still generate: this endpoint applies no deleted filter, so a report removed from the list still runs inside a package.
  • Mirror reports have no report_writer rows; their rows are synthesised per run from the company's chart of accounts (mirror 1 P&L, 2 Balance Sheet, 5 Cash Flow). On a reporting-group run the chart of accounts is the name-clubbed union of the group's companies.
  • Reporting-group refusals (allowReportWithRG: false) happen when: the report's based_on is not GLAccount; the report is mirror 3, 4 or 5; a custom report contains a direct Income-Statement/Balance-Sheet account row; or the user cannot access the group of a based_on = 6 report. An empty group likewise returns an empty grid.
  • All /reports/ requests are traced in the application log (arrival, completion with duration, and a "connection closed with NO response" entry when a proxy gives up). Large integer values are converted before serialisation, so a large ledger total cannot break the JSON response.
POST/api/reports/rgCompany/classLoc

Returns the merged class/location list for a reporting group's companies, plus the fiscal-month and accounting-method consistency flags the preview strip shows.

Module Report Center > Report Preview
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Request body — this endpoint accepts a free-form JSON object; no schema validation is applied, so unknown properties are neither rejected nor stripped. The keys read are:

Field Type Required Validation / allowed values Description
reportingGroup (alias group_id) number No Defaults to 0 Reporting group id, or (when modeRg = 'com') a company id. 0 returns the empty payload.
modeRg string No 'rg' (default) or 'com' 'rg' expands the id into the group's companies; 'com' treats it as a single company id and forces both consistency flags to true.

Example request

curl -sS -X POST 'https://api.example.com/api/reports/rgCompany/classLoc' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: genie.sid=<session-cookie>' \
  -d '{"reportingGroup":4,"modeRg":"rg"}'

Success response — 200 OK

{
  "error": false,
  "classAndLocation": [
    { "qbo_id": "3", "cls_name": "Retail", "cls_or_dept": 1 },
    { "qbo_id": "9", "cls_name": "Portland", "cls_or_dept": 2 }
  ],
  "isSameFiscal": true,
  "isSameReportMethod": false,
  "accountingMethods": [
    { "company": "Acme Holdings", "method": "Accrual" },
    { "company": "Acme West", "method": "Cash" }
  ]
}
Field Type Description
classAndLocation array { qbo_id, cls_name, cls_or_dept } — cls_or_dept 1 Class, 2 Location. Merged across the group's companies and de-duplicated by name when more than one company is in play.
isSameFiscal boolean Whether every company in the group shares a fiscal month. Always true when modeRg = 'com'.
isSameReportMethod boolean Whether every company shares an accounting method. Always true when modeRg = 'com'.
accountingMethods array { company, method } per company.

Errors

Status Condition Message/shape
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • A missing or 0 group id short-circuits to { error: false, classAndLocation: [], isSameFiscal: true, isSameReportMethod: true, accountingMethods: [] } — it is not an error.
  • The same consistency computation is embedded in every reporting-group report run, so this endpoint exists purely to refresh the strip without re-running the report.
POST/api/reports/view/save_preview

Persists the preview form's parameters for the current (report, company, user) triple so the screen reopens with the same selections.

Module Report Center > Report Preview
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Request body

Field Type Required Validation / allowed values Description
report_id string Yes String Plain or encrypted report id.
parameters object No Optional, object; stored verbatim as JSON The preview form state. Defaults to {} when omitted.

Example request

curl -sS -X POST 'https://api.example.com/api/reports/view/save_preview' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: genie.sid=<session-cookie>' \
  -d '{
        "report_id": "481",
        "parameters": { "monthVal": "Aug", "yearVal": "2025", "main_period": 1,
                        "trailing_period": 12, "include_total": 1,
                        "selectedClass": ["3"], "showType": "Non Zero" }
      }'

Success response — 200 OK

{ "error": false, "message": "Preview saved successfully." }
Field Type Description
error boolean Always false.
message string Preview saved successfully., or Preview saved (storage table not available). when report_writer_preview does not exist.

Errors

Status Condition Message/shape
400 report_id missing/not a string, or parameters not an object Request validation error array
400 report_id does not resolve, or the session has no company Report not found
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • Upsert keyed on (report_id, company_id, user_id) in report_writer_preview, with each key included only when the column exists.
  • Saved parameters are read back by GET /api/reports/preview/:id as meta.previewData.
  • parameters is a free-form object; its contents are not validated beyond being an object. Report saves accept a request body of up to 20 MB, which accommodates a large payload (many per-value conditional-formatting rules, hundreds of class/location selections). A body above that limit returns 413 request entity too large.
POST/api/reports/view/download

Regenerates the report from the supplied preview parameters and returns a styled .xlsx workbook, base64-encoded in the JSON envelope.

Module Report Center > Report Preview
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session
Content type application/json envelope carrying a base64 XLSX payload (contentType: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) — not a binary HTTP download

Request body

Field Type Required Validation / allowed values Description
report_id string Yes String Plain or encrypted report id.
parameters object No Optional, object The same parameter bag generate_income_statement accepts (spread into the generator call), plus two export-only keys read here: include_total (default 1), showType ('Non Zero' drops all-zero rows), acc_rep_group, monthVal/yearVal (used for the as-on banner) and export_name (the file name).

Example request

curl -sS -X POST 'https://api.example.com/api/reports/view/download' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: genie.sid=<session-cookie>' \
  -d '{
        "report_id": "481",
        "parameters": { "monthVal": "Aug", "yearVal": "2025", "main_period": 1,
                        "trailing_period": 12, "include_total": 1,
                        "showType": "Non Zero", "export_name": "Aug 2025 IS" }
      }'

Success response — 200 OK

{
  "error": false,
  "fileName": "Aug 2025 IS.xlsx",
  "contentType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  "contentBase64": "UEsDBBQABgAIAAAAIQ..."
}
Field Type Description
error boolean Always false on success.
fileName string <export_name or report name>.xlsx, with \ / # % stripped; falls back to report.xlsx.
contentType string Always the XLSX MIME type.
contentBase64 string The workbook. The client is responsible for decoding and saving it.

Errors

Status Condition Message/shape
400 report_id missing/not a string, or parameters not an object Request validation error array
400 report_id does not resolve Report not found
400 Underlying generation cannot find the report Report not found
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • The workbook is rebuilt from the generator payload that backs the on-screen table, so the export and the screen cannot drift apart. This means a download re-runs the report.
  • Layout: two merged bold grey size-14 info rows (report name + generated-on; company or reporting group + as-on date + generated-by), then bold size-12 centred header rows honouring colspan merges, then the body — column A left-aligned and 45 characters wide, all other columns right-aligned and 16 wide, negatives in red, and rw-format-bold / -underline / -underline-double / -up-underline / -box row classes rendered as bold and thin/double/top borders.
  • The on-screen visibility rules are honoured: include_total off drops the Total column(s), and showType = 'Non Zero' drops all-zero rows.
  • The worksheet is named after the report with \ / : * ? " < > | [ ] stripped, truncated to 30 characters.
  • A/R and A/P Aging Summary export their own flat QuickBooks table instead of the writer grid.
  • Because parameters can be large, this route also accepts a request body of up to 20 MB.

14.3 Report Writer

GET/api/reports/writer

Loads everything the Report Writer screen needs on open: the report row, its writer header, the company scope, class/location and reporting-group pick lists, edit locks and dashboard-card warnings.

Module Report Center > Report Writer
Authentication Required — authenticated session cookie
Authorization No hard role check. global_can_edit is false for a Global report (type 1/2) unless the caller is a global admin or holds role 1 (GlobalMasterSystemAdministrator) or role 5 (GlobalReportandDashboardAdministrator); it is also forced false when the selected company's name matches /inactive/i, and forced true for a global or firm admin.
Content type application/json

Query parameters

Name Type Required Default Description
report_id string Yes — String. Plain or encrypted report id.
company_id integer No session company Optional, numeric (numeric strings are coerced), integer. Report List's company picker; 0/absent uses the session company. Ignored for a Global report, which always reads at company 0.

Example request

curl -sS 'https://api.example.com/api/reports/writer?report_id=481&company_id=12' \
  -H 'Cookie: genie.sid=<session-cookie>'

Success response — 200 OK

{
  "error": false,
  "report": {
    "id": 481, "encryptId": "S0xt...", "report_name": "Monthly Income Statement",
    "type": 3, "group_type": 1, "mirror_report": 0, "based_on": 1, "reporting_group": 0
  },
  "writer": {
    "id": 903, "company_id": 12, "type": 3, "desc": "Board pack version",
    "as_on_date": "2025-08-01", "as_on_month": 8, "as_on_year": 2025, "is_fixed": 0
  },
  "fisical_month": "Jan",
  "prior_year": 2021,
  "months": ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],
  "global_can_edit": true,
  "globalUser": false,
  "locked": false,
  "add_locked": false,
  "company": { "id": 12, "encId": "aVs2...", "name": "Acme Holdings", "qbo_connection": 1, "active": 1, "deleted": 0 },
  "companies": [],
  "clsAndLoc": [ { "qbo_id": "3", "cls_name": "Retail", "cls_or_dept": 1 } ],
  "reportingGroups": [ { "id": 4, "name": "West Region", "type": 1 } ],
  "isGlobalAdmin": true,
  "desiger": false,
  "designer_msg": "",
  "designer_msg_html": ""
}
Field Type Description
report object id, encryptId, report_name, type (1 GlobalReport / 2 GlobalCalulated / 3 CustomReport / 4 CustomCalulated), group_type, mirror_report, based_on, reporting_group.
writer object | null report_writer header: id, company_id (nullable), type, desc, as_on_date (YYYY-MM-DD or ''), as_on_month, as_on_year, is_fixed.
fisical_month string Short fiscal-month name from report_configure.fiscal_month, default Jan.
prior_year integer Current year − 4; the lower bound of the Sample As-on-date year list.
months string[] The 12 short month names.
global_can_edit boolean Master edit gate for the screen.
globalUser boolean true for a Global report — disables the Add-row sidebar's GL Accounts tab and hides the Custom groups block, because a global report has no company and therefore no chart of accounts.
locked boolean Report id 2 or 20: no sort handle, no delete, no reference, read-only names, dead row-style dropdown.
add_locked boolean Report id 2 only: hides Add row, the Enable-Fixed-Period button and the fixed-period switch.
company / companies object / array Company scope, role-filtered.
clsAndLoc array { qbo_id, cls_name, cls_or_dept } — 1 Class, 2 Location.
reportingGroups array { id, name, type } — a global admin or role 1 sees every firm's groups; everyone else only their own firm's.
isGlobalAdmin boolean Global admin or role 1.
desiger, designer_msg, designer_msg_html boolean / string Set when the report feeds a dashboard card; the writer then refuses row deletion and locks the fixed-period switch.

A mirror report returns a redirect instruction instead (still 200, still error: false):

{ "error": false, "msg": "This report cannot be edited in the Report Writer.", "redirect": "/reports/list" }

Errors

Status Condition Message/shape
400 report_id absent/unresolvable, or no matching global_report_list row Report not found
400 company_id not an integer, report_id not a string Request validation error array
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • A Global report's writer is the row with company_id IS NULL / 0; the session company's own writer row for the same report is not used.
  • The mirror-report refusal is returned with error: false because it is a redirect instruction rather than a request failure.
POST/api/reports/writer/based-on

Returns the rows and columns for the selected "Based on" axis. GL-Account reports get the editable row/column grid; the multi-axis modes get a read-only preview plus the GL Account Editor rows.

Module Report Center > Report Writer
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Request body

Field Type Required Validation / allowed values Description
report_id string Yes String Plain or encrypted report id.
based_on integer No Optional, numeric (numeric strings are coerced), integer. 1 GLAccount, 2 Customer, 3 Project, 4 Class, 5 Location, 6 ReportingGroup. Defaults to 1. The axis being previewed.
company_id integer No Optional, numeric (numeric strings are coerced), integer Overrides the session company for the writer and account lookups. Ignored (forced to 0) for a Global report.

Example request

curl -sS -X POST 'https://api.example.com/api/reports/writer/based-on' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: genie.sid=<session-cookie>' \
  -d '{"report_id":"481","based_on":1,"company_id":12}'

Success response — 200 OK. Two shapes, discriminated by mode.

GL-Account mode (based_on = 1):

{
  "error": false,
  "mode": "gl",
  "based_on": 1,
  "writer": { "id": 903, "company_id": 12, "type": 3, "desc": "", "as_on_date": "", "as_on_month": 0, "as_on_year": 0, "is_fixed": 0 },
  "rows": [
    { "id": 5501, "unique_id": 1, "row_type": 1, "account_id": 84, "group_type": 3, "operator": 0,
      "style": "bold", "format": 1, "rw_precision": 0, "display": 1, "order": 1,
      "result_name": "4000 Sales", "formula": "", "reference_id": 0, "class": "", "location": "" }
  ],
  "columns": [
    { "id": 771, "column_type": 1, "period_type": 0, "period": "0", "column_name": "Aug 25",
      "style": "bold", "format": 1, "rw_precision": 0, "is_override": 0, "order": "A",
      "operation": "0", "formula": "" }
  ],
  "totalRowCount": 1,
  "desiger": false, "designer_msg": "", "designer_msg_html": ""
}

Multi-axis mode (based_on 2–6):

{
  "error": false,
  "mode": "multi",
  "based_on": 4,
  "writer": { "id": 903, "company_id": 12, "type": 3, "desc": "", "as_on_date": "", "as_on_month": 0, "as_on_year": 0, "is_fixed": 0 },
  "isGlobal": false,
  "reportingGroup": 0,
  "reportingGroups": [ { "id": 4, "name": "West Region", "type": 1 } ],
  "isGlobalAdmin": true,
  "previewRows": [ { "name": "Retail" }, { "name": "Wholesale" } ],
  "glEditorRows": [],
  "columns": [],
  "desiger": false, "designer_msg": "", "designer_msg_html": "",
  "totalRowCount": 0
}
Field Type Description
mode 'gl' | 'multi' Which pane set the client should render.
writer object | null The report_writer header (same shape as GET /api/reports/writer).
rows array GL mode only. report_writer_details joined to accounts for display names.
columns array report_writer_column rows.
previewRows array Multi-axis only: { name }[], a read-only preview — top 10 customers/projects (leaf of a Parent:Child QBO name) or top 10 classes. Empty for based_on = 6. For a Global report it is the literal placeholder list Customer 1 … Customer 10 (or Project/Class/Location).
glEditorRows array Multi-axis only: the GL Account Editor's carried-over rows. Only row_type = 1 (account) rows carry over when the report's previous axis was GLAccount.
isGlobal boolean Whether this is a Global report.
reportingGroup integer The group id stored on the report row.
totalRowCount integer Row count in GL mode; always 0 in multi-axis mode.

Errors

Status Condition Message/shape
400 report_id absent/unresolvable, or no matching report row Report not found
400 based_on / company_id not integers Request validation error array
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • In multi-axis mode the Rows pane is read-only; the editable panes are the GL Account Editor and the Fixed Period Column Editor.
  • The row-name join applies (row_type = 1 AND group_type = 3) OR group_type = 4 when taking the account name — so a group_type = 4 row takes it regardless of row_type.
POST/api/reports/writer/search-rows

Backs the Add-row sidebar and the GL Account Editor pick lists: account groups (global and custom) on one tab, GL accounts on the other.

Module Report Center > Report Writer
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. The response's globalUser flag is 1 for a global admin or role 1/5.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
based_on integer No Optional, numeric (numeric strings are coerced), integer. Default 0 Current axis. Class (4) and Location (5) suppress balNeeded.
checkGroups integer No Optional, numeric (numeric strings are coerced), integer. 1 reporting groups tab (default), 2 GL accounts tab Which tab is being populated.
searchKey string No Optional, string Substring match on group name, or on account name/number.
report_type integer No Optional, numeric (numeric strings are coerced), integer. Default 3 global_report_list.type. 1 (Global) suppresses the Custom groups block.
checkActive boolean | string | number No Optional, no type constraint. Accepted as the string "true", boolean true, or numeric 1 "Show deleted accounts". When falsy the account query filters accounts.active = 1.
isGlEditor integer No Optional, numeric (numeric strings are coerced), integer 1 when the list feeds the GL Account Editor (checkbox rows instead of add-row buttons). Echoed back untouched.
company_id integer No Optional, numeric (numeric strings are coerced), integer Overrides the session company for the account/custom-group lookups.

Example request

curl -sS -X POST 'https://api.example.com/api/reports/writer/search-rows' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: genie.sid=<session-cookie>' \
  -d '{"checkGroups":2,"searchKey":"4000","company_id":12,"checkActive":"false","based_on":1}'

Success response — 200 OK. Two shapes, discriminated by checkGroups.

Groups tab (checkGroups = 1):

{
  "error": false, "checkGroups": 1, "globalUser": 1, "reportType": 3, "isGlEditor": 0,
  "globalGroups": [ { "id": 7, "name": "Total Income", "type": 1 } ],
  "customGroups": [ { "id": 31, "name": "Marketing Spend" } ]
}

Accounts tab (checkGroups = 2):

{
  "error": false, "checkGroups": 2, "globalUser": 1, "reportType": 3, "isGlEditor": 0,
  "incomeAccounts": [
    { "qbo_id": "84", "account_num": "4000", "account_name": "Sales", "label": "4000 Sales", "active": 1 }
  ],
  "balanceAccounts": [],
  "balNeeded": true
}
Field Type Description
globalUser 0 | 1 Whether the caller is a global report administrator.
globalGroups array global_account_groups rows of type 1 or 2: { id, name, type }.
customGroups array custom_account_group rows for the company: { id, name }. Always [] when report_type = 1 (Global).
incomeAccounts array Accounts of type Income, Other Income, Cost of Goods Sold, Expense, Other Expense.
balanceAccounts array Accounts of type Accounts Payable, Accounts Receivable, Bank, Credit Card, Equity, Fixed Asset, Long Term Liability, Other Current Liability, Other Current Asset, Other Asset.
account row object { qbo_id, account_num, account_name, label, active }; label is "<account_num> <account_name>" trimmed.
balNeeded boolean false when based_on is Class (4) or Location (5) — Balance Sheet accounts cannot be picked for those axes.

Errors

Status Condition Message/shape
400 Any numeric field is not an integer, or searchKey is not a string Request validation error array
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • checkActive is intentionally not type-constrained: the checkbox state may be sent as the string "true"/"false", as a boolean, or as 1, and all three are accepted.
  • active is returned on every account row because "Show deleted accounts" only toggles a CSS class on already-rendered rows client-side.
  • With no company id resolvable (company 0), the account and custom-group lists come back empty.
POST/api/reports/writer/reporting-group/companies

Returns the companies inside a reporting group plus the merged class/location list the Apply Filter sidebar shows.

Module Report Center > Report Writer
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Request body

Field Type Required Validation / allowed values Description
group_id integer No Optional, numeric (numeric strings are coerced), integer Reporting group id. 0/absent returns the empty payload.

Example request

curl -sS -X POST 'https://api.example.com/api/reports/writer/reporting-group/companies' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: genie.sid=<session-cookie>' \
  -d '{"group_id":4}'

Success response — 200 OK

{
  "error": false,
  "companies": [ { "id": 12, "name": "Acme Holdings" }, { "id": 18, "name": "Acme West" } ],
  "clsAndLoc": [
    { "qbo_id": "3", "cls_name": "Retail", "cls_or_dept": 1 },
    { "qbo_id": "9", "cls_name": "Portland", "cls_or_dept": 2 }
  ]
}
Field Type Description
companies array { id, name } for every live company mapped to the group via reporting_group_company.
clsAndLoc array { qbo_id, cls_name, cls_or_dept } across all the group's companies, de-duplicated case-insensitively on (cls_or_dept, cls_name). cls_or_dept 1 Class, 2 Location.

Errors

Status Condition Message/shape
400 group_id not an integer Request validation error array
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • A missing or 0 group_id returns { error: false, companies: [], clsAndLoc: [] } — not an error.
  • The class/location list is de-duplicated by name, so two companies with a "Retail" class contribute one entry (the first company's qbo_id wins).
POST/api/reports/writer/save

Persists the whole writer: the report row, the writer header, every row, every fixed-period column and (for multi-axis reports) the GL Account Editor columns.

Module Report Center > Report Writer
Authentication Required — authenticated session cookie
Authorization Any authenticated user may save a Custom report. Saving a Global report (type 1 or 2) requires global admin, role 1 (GlobalMasterSystemAdministrator) or role 5 (GlobalReportandDashboardAdministrator); otherwise 401.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
report_id string Yes String Plain or encrypted report id.
report_name string No Optional, string; a blank/trimmed-empty value is rejected in the body (200 with msg) New report name.
desc string No Optional, string Report description / tooltip, stored on report_writer.desc.
based_on integer No Optional, numeric (numeric strings are coerced), integer. 1 GLAccount, 2 Customer, 3 Project, 4 Class, 5 Location, 6 ReportingGroup. Defaults to 1. The report's axis.
type integer No Optional, numeric (numeric strings are coerced), integer. 1 Global, 2 GlobalCalulated, 3 Custom, 4 CustomCalulated Decides the writer's company_id and the global-report role gate. Defaults to the report's stored type.
reporting_group number | string | null No Optional, no type constraint Stored only when based_on = 6; otherwise written as NULL.
monthVal string | number No Optional, no type constraint. 0 means "not selected" Sample As-on-date month (1–12).
yearVal string | number No Optional, no type constraint. 0 means "not selected" Sample As-on-date year.
is_fixed integer No Optional, numeric (numeric strings are coerced), integer. 1 = fixed period Fixed-period switch.
company_id integer No Optional, numeric (numeric strings are coerced), integer Overrides the session company for scoping and the duplicate-name check.
details object[] No Optional, array; elements are free-form objects The report's rows — see the row-shape table below.
columnData object[] No Optional, array; elements are free-form objects Fixed-period columns — see the column-shape table below.
editorColumnData object[] No Optional, array; elements are free-form objects GL Account Editor columns (multi-axis reports only).

details[] keys read by this endpoint (each element is a loosely typed object; unknown keys are ignored):

Key Type Description
order number 1-based row position; the upsert key together with the writer id.
row_type number Row kind. 3 marks a reference row.
qbo_id number Stored as account_id.
operator number Arithmetic operator for the row.
style string | string[] Row style classes; an array is joined.
number string Stored as format.
display number Show/hide flag; default 1.
precision number Stored as rw_precision.
res_name string Stored as result_name (the row label).
refrence number For row_type = 3, the order of the referenced row; resolved server-side to that detail's primary key. The field name is spelled exactly as shown and must be sent that way.
group_id number Stored as group_type.
unique_id number Stable row identity; falls back to order when 0.
formula string Calculated-row formula.
class, location string | null Per-row class/location filter.

columnData[] keys read by this endpoint:

Key Type Description
order string Column letter (A, B, …); the upsert key together with the writer id.
column_type number Column kind; default 1.
period_type number Period type; default 0.
period string Period value; default '0'.
column_name string Header label.
columnStyle / style string | string[] Column style classes.
columnFormat / format string Number format.
precision / rw_precision number Decimal places.
is_override number Whether the column overrides row formatting.
operation string Column operation; default '0'.
formula string Calculated-column formula.

editorColumnData[] keys read by this endpoint: order (a column letter, converted to A→1, B→2, …), column_type, account_group (stored as account_id), editor_title (used as the result name when column_type = 1), column_name (used otherwise), columnStyle/style, columnFormat/format, display, precision/rw_precision, editor_type (stored as group_type), formula, class, location.

Example request

curl -sS -X POST 'https://api.example.com/api/reports/writer/save' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: genie.sid=<session-cookie>' \
  -d '{
        "report_id": "481",
        "report_name": "Monthly Income Statement",
        "desc": "Board pack version",
        "based_on": 1, "type": 3, "is_fixed": 0,
        "monthVal": 8, "yearVal": 2025, "company_id": 12,
        "details": [
          { "order": 1, "row_type": 1, "qbo_id": 84, "group_id": 3, "operator": 0,
            "style": ["bold"], "number": "1", "display": 1, "precision": 0,
            "res_name": "4000 Sales", "unique_id": 1, "formula": "", "class": null, "location": null },
          { "order": 2, "row_type": 3, "refrence": 1, "res_name": "Ref: Sales",
            "style": [], "number": "1", "display": 1, "precision": 0, "group_id": 0, "unique_id": 2 }
        ],
        "columnData": [
          { "order": "A", "column_type": 1, "period_type": 0, "period": "0",
            "column_name": "Aug 25", "columnStyle": ["bold"], "columnFormat": "1",
            "precision": 0, "is_override": 0, "operation": "0", "formula": "" }
        ],
        "editorColumnData": []
      }'

Success response — 200 OK

{ "error": false, "report_id": 481, "writer_id": 903 }
Field Type Description
error boolean false on success. Also false — with msg and no ids — on the two business rejections below.
report_id integer The saved report id.
writer_id integer The report_writer row id (created on first save).

Business rejections (both 200, both error: false):

{ "error": false, "msg": "Report name field is required" }
{ "error": false, "msg": "Report name already exist." }

Errors

Status Condition Message/shape
400 report_id absent/unresolvable, or no matching report row Report not found
400 The writer header could not be created or updated Unable to save the report writer.
400 A request body field fails validation (e.g. details not an array) Request validation error array
401 Not authenticated { "message": "These credentials do not match our records." }
401 Saving a Global report without global admin / role 1 / role 5 Not allowed to update report.
413 Payload exceeds the 20 MB JSON body limit request entity too large

Notes

  • This is the large-payload route. JSON and URL-encoded request bodies are accepted up to 20 MB, because a report save can carry a per-value conditional-formatting rule for every distinct value in a Company/Project column — hundreds of entries. A body above that limit fails with 413.
  • Persistence contract. report_writer holds one row per (report_id, type, company_id); company_id is NULL when a global admin saves a Global report. report_writer_details is soft-deleted wholesale, then every posted row is re-upserted on (report_writer_id, order) — so unique_id and each row's primary key survive a reorder. report_writer_column follows the same delete-then-re-upsert pattern keyed on (report_writer_id, order).
  • The name uniqueness check is scoped to the same type (and, for a Custom report, the same company) and excludes the report being saved.
  • monthVal/yearVal of 0 (the "Month"/"Year" placeholder options) store NULL as the writer's as_on_date; otherwise it is stored as YYYY-MM-01.
  • GL Account Editor columns are not stored as report_writer_column rows — they become report_writer_details rows whose order is the column letter's index + 1.
  • After the save, dashboard card-designer references are remapped so cards keep pointing at the same logical rows.

14.4 Report Packages

GET/api/reports/viewPackage

Lists report packages split into Global and Custom tabs, with the company scope, GL-validation badge and Chart-of-Accounts change counter the screen shows.

Module Report Center > Report Packages
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. canManageGlobal (the "+ New package" button on the Global tab) requires global admin or role 1 (GlobalMasterSystemAdministrator) / role 5 (GlobalReportandDashboardAdministrator); canManageCustom also allows firm admins.
Content type application/json

Query parameters

Name Type Required Default Description
searchKey string No '' Case-insensitive substring match on package name.
type integer No 0 (both) 1 Global packages only, 2 Custom packages only.
group_id integer No — Accepted but not read by this endpoint.
companyId integer No session company Company scope override, resolved the same way as the Report List.

Example request

curl -sS 'https://api.example.com/api/reports/viewPackage?type=2&searchKey=close' \
  -H 'Cookie: genie.sid=<session-cookie>'

Success response — 200 OK

{
  "error": false,
  "canManage": true,
  "canManageGlobal": true,
  "canManageCustom": true,
  "isGlobalAdmin": true,
  "selectedCompany": { "id": 12, "encId": "aVs2...", "name": "Acme Holdings", "qbo_connection": 1, "active": 1, "deleted": 0 },
  "companies": [],
  "glValidation": { "status": "Pass", "color": "green", "canRefresh": false, "qboError": false },
  "connection_qbo": 1,
  "account_changes_unseen": 0,
  "changes_redirect": "/company/balanceSheet?changes=1",
  "packages": [
    { "id": 66, "encryptId": "UGtn...", "package_name": "Monthly Close Pack", "package_desc": "",
      "type": 2, "reporting_group": 0, "expand_by": 0, "company_id": 12,
      "report_ids": [20, 481], "report_count": 2 }
  ],
  "data": { "global": [], "custom": [] },
  "reportingGroups": [ { "id": 4, "name": "West Region", "type": 1 } ]
}
Field Type Description
packages array All packages passing the filters. type 1 Global, 2 Custom; expand_by is the Expand-By-Company flag; report_ids and report_count come from live report_package_details rows.
data.global / data.custom array The same rows partitioned by type.
connection_qbo 0 | 1 Selected company's QuickBooks connection state.
glValidation object Same cheap probe shape as the Report List.
account_changes_unseen / changes_redirect integer / string Chart-of-Accounts change strip.
canManage, canManageGlobal, canManageCustom, isGlobalAdmin boolean UI capability gates.
reportingGroups array { id, name, type } groups available to the user.

Errors

Status Condition Message/shape
400 type, group_id or companyId not an integer Request validation error array
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • Visibility filtering happens after the query: a Custom package (type = 2) is dropped unless its company_id matches the selected company; a Global package (type = 1) is dropped if it carries any company_id.
  • If the report_package table is absent the endpoint still returns a well-formed payload with empty packages and data.
POST/api/reports/viewPackage/save

Creates, updates or clones a report package, and (conditionally) rewrites the list of reports inside it.

Module Report Center > Report Packages
Authentication Required — authenticated session cookie
Authorization Global admin, firm admin, role 1 (GlobalMasterSystemAdministrator) or role 5 (GlobalReportandDashboardAdministrator). Otherwise 401.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
package_id string No Optional, string Plain or encrypted id. Absent/0 creates a new package.
package_name string Yes String; rejected by this endpoint when blank after trim Package name.
package_desc string No Optional, string Description.
type integer No Optional, numeric (numeric strings are coerced), integer. Coerced to 1 unless it is 1 or 2 1 Global, 2 Custom. On an update, the stored type wins; on a clone, the parent's type wins.
reporting_group integer No Optional, numeric (numeric strings are coerced), integer Reporting group the package runs against. On a clone, inherited from the parent when omitted.
expanyByCompany integer No Optional, numeric (numeric strings are coerced), integer Expand-By-Company flag, stored as expand_by. On a clone, inherited from the parent when omitted. The field name is spelled exactly as shown and must be sent that way.
parent_package_id string No Optional, string Source package when cloning.
report_ids (string | number)[] No Optional, array Simple ordered list of report ids (plain or encrypted); order is the array index + 1. Used only when report_rows is empty.
report_rows object[] No Optional, array; elements are free-form objects Full per-report settings — see below.

report_rows[] keys read by this endpoint (each maps to a report_package_details column, written only when that column exists):

Key Type Default Description
report_id (or id) string | number — Plain or encrypted report id. Rows that do not resolve are skipped.
order number index + 1 Position in the package; also disambiguates the same report added twice.
period_id number 1 Period mode picker (1-based).
period_sequence number 1 Period sequence, or the aging-bucket size for A/R & A/P reports.
trailing_periods number 12 Number of trailing periods.
show_id number 2 1 = show zero rows, 2 = hide all-zero rows.
include_total number 0 Whether the Total column is generated.
another_period string '' Comma-separated compare-period keys. The literal '0' means "none".
expand_one / expand_two number 0 Expand axes: 0 None, 1 Class, 2 Location, 3 Class (non-zero only), 4 Location (non-zero only).
class_filter / location_filter string '' Comma-separated qbo ids; '0' or empty means no filter.

Example request

curl -sS -X POST 'https://api.example.com/api/reports/viewPackage/save' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: genie.sid=<session-cookie>' \
  -d '{
        "package_id": "66",
        "package_name": "Monthly Close Pack",
        "package_desc": "Board pack",
        "type": 2, "reporting_group": 0, "expanyByCompany": 0,
        "report_rows": [
          { "report_id": "20", "order": 1, "period_id": 1, "period_sequence": 1,
            "trailing_periods": 12, "show_id": 2, "include_total": 1,
            "another_period": "0", "expand_one": 0, "expand_two": 0,
            "class_filter": "", "location_filter": "" }
        ]
      }'

Success response — 200 OK

{ "error": false, "package_id": 66, "encryptId": "UGtn..." }
Field Type Description
error boolean false on success; true with msg on a duplicate name.
package_id integer Created or updated package id.
encryptId string Encrypted form of the same id.

Duplicate name (still 200): { "error": true, "msg": "Package already exist" }

Errors

Status Condition Message/shape
400 package_name blank after trim Package name field is required
400 report_package table missing Report package table is unavailable.
400 A request body field fails validation Request validation error array
401 Not authenticated { "message": "These credentials do not match our records." }
401 Caller lacks the manage roles Not allowed to update report package.
413 Payload exceeds the 20 MB JSON body limit request entity too large

Notes

  • Detail rows are only touched when the caller sends them. The destructive wipe-and-reinsert of report_package_details runs only if report_rows or report_ids is present (even as an empty array), or if this is a clone. A save that carries neither leaves the package's report list completely alone.
  • An explicitly empty report_rows: [] does clear the list, because removing the last report from a package is submitted through this endpoint.
  • Cloning (parent_package_id): the clone inherits the parent's type, and — when the request omits them — its reporting_group and expand_by. If neither report_rows nor report_ids is supplied, the parent's detail rows are copied.
  • Company scoping on insert: type = 2 stores the session company; type = 1 stores 0.
  • Duplicate check is name + type, company-scoped for Custom packages, excluding the package being updated and any soft-deleted rows.
  • This route accepts a request body of up to 20 MB, which accommodates a large report_rows array.
GET/api/reports/viewPackage/delete

Soft-deletes a package and all of its detail rows.

Module Report Center > Report Packages
Authentication Required — authenticated session cookie
Authorization Global admin, firm admin, role 1 (GlobalMasterSystemAdministrator) or role 5 (GlobalReportandDashboardAdministrator). Otherwise 401.
Content type application/json

Query parameters

Name Type Required Default Description
package_id string Yes — String. Plain or encrypted package id.

Example request

curl -sS 'https://api.example.com/api/reports/viewPackage/delete?package_id=66' \
  -H 'Cookie: genie.sid=<session-cookie>'

Success response — 200 OK

{ "error": false }
Field Type Description
error boolean Always false.

Errors

Status Condition Message/shape
400 package_id absent or not a string Request validation error array
401 Not authenticated { "message": "These credentials do not match our records." }
401 Caller lacks the manage roles Not allowed to delete report package.

Notes

  • This route uses GET with a query parameter although it performs a mutation.
  • When a deleted column exists both report_package and its report_package_details rows are soft-deleted; otherwise both are hard-deleted.
  • An unresolvable id or a missing report_package table returns { error: false } rather than an error.
GET/api/reports/viewPackage/detail

Loads one package's edit screen: the package row, its ordered report list with each report's period settings, and the class/location options and QuickBooks state for the session company.

Module Report Center > Report Packages
Authentication Required — authenticated session cookie
Authorization No hard role check. canEdit (which gates every Edit / Clone / Delete / Add Report control) requires canManage — global admin, firm admin, role 1 or role 5 — and, for a Global package, global admin or role 1/5 specifically. A Custom package only needs canManage.
Content type application/json

Query parameters

Name Type Required Default Description
package_id string Yes — String. Plain or encrypted package id.

Example request

curl -sS 'https://api.example.com/api/reports/viewPackage/detail?package_id=UGtn...' \
  -H 'Cookie: genie.sid=<session-cookie>'

Success response — 200 OK

{
  "error": false,
  "canManage": true,
  "canEdit": true,
  "isGlobalAdmin": true,
  "connection_qbo": 1,
  "update_date": "08/21/25 04:15PM",
  "package": {
    "id": 66, "encryptId": "UGtn...", "package_name": "Monthly Close Pack",
    "package_desc": "Board pack", "type": 2, "reporting_group": 0, "expand_by": 0, "company_id": 12
  },
  "reports": [
    {
      "rpd_id": 2201, "package_id": 66, "report_id": 20, "report_encrypt_id": "Qk9t...",
      "report_name": "Balance Sheet (QuickBooks Mirror)", "order": 1,
      "period_id": 1, "period_sequence": 1, "trailing_periods": 12,
      "show_id": 2, "include_total": 1, "another_period": "0",
      "expand_one": 0, "expand_two": 0, "class_filter": "", "location_filter": "",
      "group_type": 1, "type": 1, "mirror_report": 2, "based_on": 1
    }
  ],
  "qboClass": [],
  "qboLocation": [],
  "reportingGroups": [ { "id": 4, "name": "West Region", "type": 1 } ]
}
Field Type Description
package object id, encryptId, package_name, package_desc, type, reporting_group, expand_by, company_id (falls back to the session company for a Global package).
reports array Ordered report_package_details rows joined to global_report_list, carrying every per-report period/filter setting plus the report's report_name, type, group_type, mirror_report and based_on.
qboClass / qboLocation array Class and Location pick lists for the session company.
update_date string Last QuickBooks sync timestamp for the scope company.
connection_qbo 0 | 1 QuickBooks connection state.
canManage, canEdit, isGlobalAdmin boolean UI capability gates.

Errors

Status Condition Message/shape
400 package_id unresolvable, report_package table missing, or no matching row Package not found
400 package_id absent or not a string Request validation error array
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • Class/Location options and the QuickBooks connection state come from the session company, not report_package.company_id — which is 0 for every Global package and would otherwise leave the dropdowns empty and "Refresh report" permanently disabled across the whole Global tab.
  • Detail rows are not filtered on the report's deleted or company_id, so a package keeps showing a report that was later removed from the Report List.
POST/api/reports/viewPackage/run

Builds the Run Package shell: header facts (as-on date, generated-by/on, scope name), the effective reporting group and expand-by-company flags, and one card descriptor per report in the package. It generates no report data.

Module Report Center > Report Packages
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Request body

Field Type Required Validation / allowed values Description
package_id string Yes String Plain or encrypted package id.
monthVal string No Optional, string. Defaults to the current short month name As-on month.
yearVal string No Optional, string. Defaults to the current year As-on year.
reportingGroup integer No Optional, numeric (numeric strings are coerced), integer Overrides the package's stored reporting_group when > 0.
expanyByCompany integer No Optional, numeric (numeric strings are coerced), integer Overrides the package's stored expand_by when present (including 0).

Example request

curl -sS -X POST 'https://api.example.com/api/reports/viewPackage/run' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: genie.sid=<session-cookie>' \
  -d '{"package_id":"66","monthVal":"Aug","yearVal":"2025","reportingGroup":4,"expanyByCompany":1}'

Success response — 200 OK

{
  "error": false,
  "package": {
    "id": 66,
    "package_name": "Monthly Close Pack",
    "as_on_month": "Aug",
    "as_on_year": "2025",
    "as_on_date": "Aug, 2025",
    "reporting_group": 4,
    "reporting_group_name": "West Region",
    "expand_by": 1,
    "company_name": "Acme Holdings",
    "generated_by": "Dana Reyes",
    "generated_on": "08/21/25 04:15PM",
    "is_same_report_method": true
  },
  "reports": [
    { "report_id": 20, "report_encrypt_id": "Qk9t...", "report_name": "Balance Sheet (QuickBooks Mirror)",
      "order": 1, "group_type": 1, "show_id": 2, "supported": true }
  ]
}
Field Type Description
package.as_on_date string "<monthVal>, <yearVal>".
package.reporting_group / reporting_group_name integer / string The effective group (request beats the package's stored value). Name is '' when no group.
package.expand_by integer Effective Expand-By-Company flag.
package.generated_on string MM/DD/YY hh:mmAM/PM.
package.is_same_report_method boolean Whether every company in the reporting group shares an accounting method. true when no group.
reports[] array One card descriptor per report: report_id, report_encrypt_id, report_name, order, group_type, show_id, supported.
reports[].supported boolean false renders the "Reporting Group doesn't support for this report" card instead of a table.

Errors

Status Condition Message/shape
400 package_id unresolvable, report_package table missing, or no matching row Package not found
400 A request body field fails validation Request validation error array
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • Each card then fetches itself: the client fires one POST /api/reports/viewPackage/run/report per entry in reports. This endpoint is intentionally inexpensive.
  • supported is false (only when a reporting group is in play) when the report's based_on is not GLAccount, when it is mirror 3 / 4 / 5, or when a custom report contains a direct Income-Statement/Balance-Sheet account row.
POST/api/reports/viewPackage/run/report

Generates one report card on the Run Package screen. The generation parameters are rebuilt server-side from the package's detail row — the client only names the package, the report and the as-on scope.

Module Report Center > Report Packages
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Request body

Field Type Required Validation / allowed values Description
package_id string Yes String Plain or encrypted package id.
report_id string Yes String Plain or encrypted report id; must be a member of this package.
monthVal string No Optional, string. Defaults to the current short month name As-on month.
yearVal string No Optional, string. Defaults to the current year As-on year.
reportingGroup integer No Optional, numeric (numeric strings are coerced), integer Overrides the package's stored group when > 0.
expanyByCompany integer No Optional, numeric (numeric strings are coerced), integer Overrides the package's stored expand_by when present.
order integer No Optional, numeric (numeric strings are coerced), integer report_package_details.order — disambiguates the same report added to the package twice.

Example request

curl -sS -X POST 'https://api.example.com/api/reports/viewPackage/run/report' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: genie.sid=<session-cookie>' \
  -d '{"package_id":"66","report_id":"20","order":1,"monthVal":"Aug","yearVal":"2025","reportingGroup":4,"expanyByCompany":1}'

Success response — 200 OK

{
  "error": false,
  "generated": {
    "error": false, "report_id": 20, "report_name": "Balance Sheet (QuickBooks Mirror)",
    "mirror_report": 2, "based_on": 1, "reporting_group": 0,
    "is_fixed": 0, "is_mirror": 1, "mirror_id": 2,
    "headers": ["Aug 25"],
    "headerGroups": { "first": [], "second": [] },
    "columns": [ { "key": "c1", "order": "A", "label": "Aug 25", "format": 1, "rw_precision": 0, "is_override": 0 } ],
    "rows": [ { "account": "TOTAL ASSETS", "account_num": "-", "row_type": 2, "row_class": "rw-format-bold",
                "rw_precision": 0, "format": 1, "class_indent": 0, "empty_formula": false,
                "values": { "c1": 1284300.55 } } ],
    "allowReportWithRG": true, "isSameFiscal": true, "isSameReportMethod": true, "accountingMethods": [],
    "showZero": 2
  }
}
Field Type Description
error boolean false on success; true when this one report failed to generate.
generated object | null The full generator payload (same shape as POST /api/reports/view/generate_income_statement), plus showZero. null on a per-report failure.
generated.showZero 1 | 2 Derived from the detail row's show_id: 1 shows zero rows, 2 hides all-zero rows client-side.
generate_error string Present only on a per-report failure — the error message the card renders in place of its table.

Per-report failure (still 200):

{ "error": true, "generated": null, "generate_error": "Division by zero in formula for row 4" }

Errors

Status Condition Message/shape
400 package_id or report_id unresolvable, or report_package table missing Report package not found.
400 No matching package row Report package not found.
400 The report (with the given order) is not in this package Report not found in this package.
400 The underlying report row cannot be found by the generator Report not found
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • Non-client-supplied parameters. Period mode, period sequence, trailing periods, include-total, expand axes, non-zero flags and class/location filters all come from the package's report_package_details row, not from the request.
  • Derivations worth knowing: main_period = period_id - 1 (the picker is 1-based); main_period = 3 (Months YTD) forces a 12-column trailing run with period = 2; expand axes 3/4 are the non-zero variants of 1/2 and are rewritten accordingly; another_period of '0' yields an empty compare-period list and is_period = 0; for A/R and A/P Aging Summary (mirror_report 3/4) period_sequence is reinterpreted as the aging-bucket size (minimum 30) and trailing_periods as the bucket count (default 4).
  • Failures are contained: one broken report yields one broken card (error: true + generate_error) and is logged server-side, while the rest of the package still renders. Genuine client errors still surface as 400.
POST/api/reports/viewPackage/run/download

Exports the whole package (or one report's sheet) as a styled .xlsx workbook, base64-encoded in the JSON envelope. One worksheet per report.

Module Report Center > Report Packages
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session
Content type application/json envelope carrying a base64 XLSX payload (contentType: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) — not a binary HTTP download

Request body

Field Type Required Validation / allowed values Description
package_id string Yes String Plain or encrypted package id.
report_id string No Optional, string When present, cuts the workbook to that one report's sheet.
order integer No Optional, numeric (numeric strings are coerced), integer Together with report_id, picks a specific occurrence of a repeated report.
monthVal string No Optional, string. Defaults to the current short month name As-on month.
yearVal string No Optional, string. Defaults to the current year As-on year.
reportingGroup integer No Optional, numeric (numeric strings are coerced), integer Overrides the package's stored group when > 0.
expanyByCompany integer No Optional, numeric (numeric strings are coerced), integer Overrides the package's stored expand_by when present.
export_name string No Optional, string; \ / # % are stripped File name. Falls back to <scope name><unix timestamp>.

Example request

curl -sS -X POST 'https://api.example.com/api/reports/viewPackage/run/download' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: genie.sid=<session-cookie>' \
  -d '{"package_id":"66","monthVal":"Aug","yearVal":"2025","reportingGroup":4,"export_name":"Monthly Close Aug 2025"}'

Success response — 200 OK

{
  "error": false,
  "fileName": "Monthly Close Aug 2025.xlsx",
  "contentType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  "contentBase64": "UEsDBBQABgAIAAAAIQ..."
}
Field Type Description
error boolean Always false on success.
fileName string <export_name or scope name + timestamp>.xlsx.
contentType string Always the XLSX MIME type.
contentBase64 string The workbook; the client decodes and saves it.

Errors

Status Condition Message/shape
400 package_id unresolvable, report_package table missing, or no matching row Report package not found.
400 report_id given but not present in the package (with the given order) Report not found in this package.
400 A request body field fails validation Request validation error array
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • Each sheet is generated through the same parameter builder and generator the on-screen cards use, so the workbook cannot disagree with the page. This means a download re-runs every report in the package.
  • Layout per sheet: two merged bold grey size-14 info rows (package name + generated-on; Company: <name> or Reporting Group: <name> + as-on date + generated-by), bold size-12 centred header rows with colspan merges, then the body right-aligned with column A left; negatives red in parentheses, rw-format-* / cw-format-* classes rendered as bold and thin/double/boxed borders, % and X suffixes on percentage and multiple rows.
  • Failure containment: a report that cannot generate, or one the reporting group does not support, becomes a message-only sheet ("Reporting Group doesn't support for this report", or the error text) so the rest of the workbook survives.
  • Sheet names come from the report name with \ / : * ? " < > | [ ] stripped, truncated to 28 characters, and de-duplicated with a numeric suffix. A package with no exportable reports yields a single Report Package sheet reading --No Reports found--.

14.5 Configure

POST/api/reports/configure/update

Multiplexed refresh endpoint: re-reads the report configuration, pulls the QuickBooks book-close date live, or pulls class/location tracking preferences live — selected by type.

Module Report Center > Configure
Authentication Required — authenticated session cookie
Authorization No role requirement, but the report-config and accounting branches reject a target company outside the caller's firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId integer No Optional, numeric (numeric strings are coerced), integer Target company. 0/absent falls back to the firm's internal company.
type string No Optional, string. Compared case-insensitively against report-config and accounting; anything else (including absent) takes the class-tracking branch Which refresh to perform.

Example request

curl -sS -X POST 'https://api.example.com/api/reports/configure/update' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: genie.sid=<session-cookie>' \
  -d '{"companyId":12,"type":"report-config"}'

Success response — 200 OK. Three shapes, selected by type.

type: "report-config" — re-reads report_configure:

{ "error": false, "config_id": 55, "report_method": 2, "fiscal_month": 1, "company_id": 12 }

type: "accounting" — fetches and persists the QuickBooks book-close date:

{ "error": false, "close_book_date": "12/31/2024", "close_book_date_value": "2024-12-31", "company": 12 }

Any other type — fetches and persists class/location tracking preferences:

{ "error": false, "track_class": true, "assign_class": 1, "track_location": false }
Field Type Description
config_id integer report_configure row id, 0 when none exists.
report_method 1 | 2 Accounting method; defaults to 2.
fiscal_month 1–12 Fiscal year start month; defaults to 1.
close_book_date string MM/DD/YYYY, or '-' when QuickBooks reports no close date.
close_book_date_value string Raw YYYY-MM-DD (or '').
track_class / track_location boolean QuickBooks class/location tracking preferences.
assign_class integer The QuickBooks class-assignment preference.

Company-resolution failure (still 200):

{ "error": true, "message": "Company not found." }
{ "error": true, "msg": "Please configure firm internal company.", "track_class": false, "assign_class": 0, "track_location": false }

Errors

Status Condition Message/shape
400 companyId not an integer / type not a string Request validation error array
401 Not authenticated { "message": "These credentials do not match our records." }
401/403 Target company does not belong to the caller's firm Rejected by the firm-ownership check

Notes

  • The accounting branch is the "Refresh Data" button on Monthly Close → QuickBooks Close Dates. It fetches Preferences.AccountingInfoPrefs.BookCloseDate live from QuickBooks and writes it to company.qbo_close_date.
  • That fetch never throws: a disconnected company or a failed call degrades to the currently stored value rather than erroring the row, so a client refreshing many companies in a loop is never broken by one bad company.
  • The class-tracking branch also writes: a successful live fetch is upserted into qbo_categories_configuration before being returned; only if the live fetch fails does it fall back to the stored config.
  • The field name is companyId here, while /reports/configure/save and /switch-company use company_id.
GET/api/reports/configure

Loads the Report Configure screen: the company list and current scope, the company's accounting method and fiscal month, and the Chart-of-Accounts change counter.

Module Report Center > Configure
Authentication Required — authenticated session cookie
Authorization No additional role requirement beyond an authenticated session. The visible company list is role-scoped (a global admin or role 1/3 sees every company; a firm admin sees their firm's; company-group roles 4/7/8 see their group's).
Content type application/json

Query parameters — none.

Example request

curl -sS 'https://api.example.com/api/reports/configure' \
  -H 'Cookie: genie.sid=<session-cookie>'

Success response — 200 OK

{
  "error": false,
  "companies": [
    { "id": 12, "encId": "aVs2...", "name": "Acme Holdings", "qbo_connection": 1, "active": 1, "deleted": 0 }
  ],
  "selectedCompany": { "id": 12, "encId": "aVs2...", "name": "Acme Holdings", "qbo_connection": 1 },
  "config_id": 55,
  "report_method": 2,
  "fiscal_month": 1,
  "account_changes_unseen": 0,
  "changes_redirect": "/company/balanceSheet?changes=1"
}
Field Type Description
companies array { id, encId, name, qbo_connection, active, deleted }, ordered firm-internal company first then by name, including deleted = 1 rows for the "Include Inactive Companies" toggle.
selectedCompany object | null { id, encId, name, qbo_connection }. null when the session is scoped to Global (company_id = 0).
config_id integer Latest report_configure row id for the company; 0 when none.
report_method 1 | 2 Accounting method; defaults to 2.
fiscal_month 1–12 Fiscal year start month; defaults to 1.
account_changes_unseen integer Unseen Chart-of-Accounts changes.
changes_redirect string Target of the changes strip's link.

Errors

Status Condition Message/shape
401 Not authenticated { "message": "These credentials do not match our records." }

Notes

  • Side effect: when the session has no company yet, this endpoint defaults it — writing both the company and carryOver session keys, while preserving the period, so an as-on month/year already chosen elsewhere is not cleared. It never overrides an existing selection.
  • A session company id of 0 is Genie's sticky "Global" scope, not "unset": it returns selectedCompany: null and leaves the config values at their defaults rather than silently substituting the firm's internal company.
POST/api/reports/configure/save

Saves a company's accounting method and fiscal-year start month (report_configure).

Module Report Center > Configure
Authentication Required — authenticated session cookie
Authorization Global admin or firm admin only. Otherwise 401. Additionally, the target company must belong to the caller's firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
config_id integer No Optional, numeric (numeric strings are coerced), integer Accepted but not read by this endpoint — the existing row is located by company id.
report_method integer Yes Numeric (numeric strings are coerced), integer; additionally must be 1 or 2 Accounting method (1 and 2 are the only accepted values).
fiscal_month integer Yes Numeric (numeric strings are coerced), integer; additionally must be 1–12 Fiscal year start month.
company_id string No Optional, string. Plain or encrypted id Target company. Absent falls back to the session company, then the firm's internal company.

Example request

curl -sS -X POST 'https://api.example.com/api/reports/configure/save' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: genie.sid=<session-cookie>' \
  -d '{"report_method":2,"fiscal_month":1,"company_id":"12"}'

Success response — 200 OK

{ "error": false, "config_id": 55, "report_method": 2, "fiscal_month": 1, "company_id": 12 }
Field Type Description
error boolean Always false.
config_id integer Updated or newly inserted report_configure id. 0 when the table does not exist.
report_method 1 | 2 Echo of the saved value.
fiscal_month 1–12 Echo of the saved value.
company_id integer Resolved target company.

Errors

Status Condition Message/shape
400 report_method is not 1 or 2 Invalid report method.
400 fiscal_month outside 1–12 Invalid fiscal month.
400 No company could be resolved Company not found.
400 report_method / fiscal_month missing or not integers Request validation error array
401 Not authenticated { "message": "These credentials do not match our records." }
401 Caller is neither global nor firm admin Not allowed to update reports configuration.
401/403 Target company does not belong to the caller's firm Rejected by the firm-ownership check

Notes

  • Upsert on the company: the most recent non-deleted report_configure row for the company is updated in place; if none exists, one is inserted.
  • fiscal_month feeds every report run — the preview engine reads it (clamped to 1–12, defaulting to 1) to build fiscal-year-to-date columns, and the Report Writer returns its short name as fisical_month.
  • Column names are resolved by schema introspection (id/config_id, company_id, report_method, fiscal_month), so a deployment with variant column naming still works.
POST/api/reports/configure/switch-company

Switches the session's active company, resetting the as-on period to today.

Module Report Center > Configure
Authentication Required — authenticated session cookie
Authorization No role requirement, but the target company must belong to the caller's firm.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
company_id string Yes String. Plain or encrypted id The company to switch to.

Example request

curl -sS -X POST 'https://api.example.com/api/reports/configure/switch-company' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: genie.sid=<session-cookie>' \
  -d '{"company_id":"aVs2..."}'

Success response — 200 OK

{
  "error": false,
  "company": {
    "company_id": 12,
    "qbo_connection": 1,
    "global_company_name": "Acme Holdings",
    "global_company_logo": "acme.png"
  }
}
Field Type Description
error boolean Always false.
company object The newly stored session company: company_id, qbo_connection, global_company_name, global_company_logo.

Errors

Status Condition Message/shape
400 company_id does not resolve to an id Company not found.
400 The company row could not be loaded Company not found.
400 company_id absent or not a string Request validation error array
401 Not authenticated { "message": "These credentials do not match our records." }
401/403 Target company does not belong to the caller's firm Rejected by the firm-ownership check

Notes

  • Side effect, session-wide. This writes both session keys — login.company and login.carryOver — and the as-on month/year resets to today. Picking a company here therefore changes the default scope on every other Report Center screen.
  • Sessions are persisted in the shared MySQL sessions table, so the switch is visible to every backend instance immediately.
POST/api/reports/configure/changes-seen

Marks a company's pending Chart-of-Accounts changes as seen, clearing the "Changes have been made to the Chart of Accounts" strip.

Module Report Center > Configure
Authentication Required — authenticated session cookie
Authorization No role requirement, but the target company must belong to the caller's firm.
Content type application/json

Request body (the same body schema as switch-company)

Field Type Required Validation / allowed values Description
company_id string Yes String. Plain or encrypted id Target company. This endpoint falls back to the session company for a falsy value, but the field must still be present and a string.

Example request

curl -sS -X POST 'https://api.example.com/api/reports/configure/changes-seen' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: genie.sid=<session-cookie>' \
  -d '{"company_id":"12"}'

Success response — 200 OK

{ "error": false }
Field Type Description
error boolean Always false.

Errors

Status Condition Message/shape
400 company_id absent or not a string Request validation error array
401 Not authenticated { "message": "These credentials do not match our records." }
401/403 Target company does not belong to the caller's firm Rejected by the firm-ownership check

Notes

  • Side effect: marks the company's account-change records as seen. After this call, account_changes_unseen drops to 0 on /api/reports/list, /api/reports/viewPackage and /api/reports/configure.
  • Because unseen changes also drive the GL-validation badge's Fail / "View variances" state, clearing them flips that badge back to Pass for a QuickBooks-connected company.
  • When neither company_id nor a session company resolves, this endpoint returns { error: false } without doing anything.

↑ Back to Table of Contents


15. Feedback Center

Feedback Center holds the two in-product feedback surfaces: Suggestions (product suggestions and bug reports, with voting, subscriptions and labels) and Support & Issues (a ticket and chat surface staffed by users holding the Feedback Center Administrator role). Both are backed by the issue_list, issue_reply and issue_files tables, with attachments in Google Cloud Storage and notifications sent over SMTP.

Conventions used throughout this section:

  • Paths in this section are served under /api.
  • Global request validation runs on every request. Any body property not declared in the endpoint's request body schema is silently deleted before the endpoint sees it.
  • Authentication is enforced per endpoint rather than by a route-level gate: every request resolves the caller from the session, which must carry a user id and be in the authenticated state; otherwise the request fails with 401 { "message": "These credentials do not match our records." }. The session cookie is genie.sid (httpOnly, SameSite=Lax).
  • No CSRF token is required on any endpoint in this section; CSRF protection applies only to the authentication endpoints.
  • Several :id path parameters and body ids accept either a plain numeric id or an encrypted (base64) id. List responses hand back the encrypted form as encryptId.

15.1 Suggestions

Product suggestions and bug reports (issue_list.type 1 = Suggestion, 2 = Issue/Bug; type 3 is a Support ticket, documented in the next subsection). Supports voting, subscription, module filtering, labels, soft delete and file attachments.

The only role that unlocks administrative behaviour here is Feedback Center Administrator (id 10), resolved from global, non-revoked assign_user_roles grants (company_id IS NULL, role_from IS NULL, not soft-deleted), matched by id or by the role name "Feedback Center Administrator". Delete/"show deleted" additionally accepts Global Master System Administrator (id 1).

GET/api/feedbackcenter/:type

Returns the Suggestions (or Issues) landing payload: the module filter list, per-status counts and the full item list.

Module Feedback Center > Suggestions
Authentication Required — fully authenticated session (two-factor completed)
Authorization No gate on the route. canDelete on each item is true for a Global Master System Administrator (1) or a Feedback Center Administrator (10); everyone else gets false.
Content type application/json

Path parameters

Name Type Required Description
type string Yes 'suggestion' selects issue_list.type = 1; any other value selects type = 2 (Issues/Bugs).

Example request

curl 'https://api.example.com/api/feedbackcenter/suggestion' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "type": 1,
  "menuList": [{ "id": 81, "menu_name": "Task & Time" }],
  "status": [{ "status": 1, "total": 12 }, { "status": 2, "total": 3 }],
  "getFeedback": [
    {
      "id": 4412,
      "encryptId": "b3RoZXI9",
      "title": "Add a bulk assign action to Task List",
      "status": 1,
      "post_anonymously": 0,
      "user_id": 77,
      "user_name": "Alex Nguyen",
      "created_at": "2025-11-28T14:02:11.000Z",
      "voteCount": 9,
      "replyCount": 4,
      "fileCount": 1,
      "deleted": 0,
      "canDelete": false
    }
  ],
  "issueListSugg": 15
}
Field Type Description
type number 1 for suggestions, 2 otherwise.
menuList[] array { id, menu_name } from top-level active menus rows, excluding ids 1 and 11, ordered by name.
status[] array { status, total } counts across non-deleted rows of this type.
getFeedback[] array Item list, newest first.
getFeedback[].encryptId string Encrypted id for detail/download links.
getFeedback[].canDelete boolean Whether the caller may delete/see deleted items.
issueListSugg number Sum of all status[].total.

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Deleted items are excluded here; only POST /api/issue/filter/module can include them (delete_show = 1).
  • user_name on this list is first_name + last_name (not lf_name), and is not anonymised even when post_anonymously = 1 — the flag is carried alongside for the client to apply.

GET/api/new/:type

Returns the "new suggestion / new issue" form bootstrap — the module dropdown and the resolved type.

Module Feedback Center > Suggestions
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Path parameters

Name Type Required Description
type string Yes 'suggestion' → 1; any other value → 2.

Example request

curl 'https://api.example.com/api/new/suggestion' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{ "type": 1, "menuList": [{ "id": 81, "menu_name": "Task & Time" }] }
Field Type Description
type number 1 for suggestions, 2 otherwise.
menuList[] array { id, menu_name } — the module options for the form.

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }

POST/api/issue/feedback/save

Creates a suggestion, issue or support ticket, links any previously staged attachments to it, and e-mails the Genie team.

Module Feedback Center > Suggestions
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session — any authenticated user may open a ticket.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
mouduleId number Yes Numeric (numeric strings are coerced), integer Menu/module id. The field name is spelled exactly as shown and must be sent that way. Written to issue_list.module.
title string Yes String Ticket title; trimmed, must be non-empty.
message string No Optional, string HTML body. Sanitised before insert.
customSwitch number No Optional, Numeric (numeric strings are coerced), integer 1 sets post_anonymously = 1; anything else sets 0.
type number Yes Numeric (numeric strings are coerced), integer 1 = Suggestion, 2 = Issue/Bug, 3 = Support ticket. Drives the redirect and the e-mail template.
insertedIds number[] No Optional, array issue_files.id values returned by POST /api/ajax_upload/issue_files, to be attached to this ticket.

Example request

curl -X POST 'https://api.example.com/api/issue/feedback/save' \
  -H 'Content-Type: application/json' \
  -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/142.0.0.0' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "mouduleId": 81, "title": "Add bulk assign", "message": "<p>Would save a lot of clicks.</p>", "type": 1, "customSwitch": 0, "insertedIds": [9001] }'

Success response — 200 OK

{ "error": false, "id": 4413, "redirect": "/feedbackcenter/suggestion" }
Field Type Description
error boolean false on success.
id number New issue_list.id (plain numeric).
redirect string /support/feedback/chat/<encrypted id> when type = 3; /feedbackcenter/suggestion otherwise.

Errors

Status Condition Message/shape
200 title blank after trim { "error": true, "message": "Title field is required." }
400 issue_list table not present { "message": "Suggestions table is unavailable." } (BadRequest)
400 Missing/invalid mouduleId, title or type Standard request-validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The inbound User-Agent header is parsed into a "<Browser> <version>" label and stored on issue_list.browser; the ticket sidebar renders it as "Browser:".
  • Staged attachments are re-keyed on save: issue_files.issue_list_id is set to the ticket id and issue_or_cmd to 1.
  • A new-ticket e-mail is dispatched fire-and-forget to every active user holding role 10. A mail failure never fails the save.
  • issue_list.classification is written only if the column exists, and always as the literal 'low'. classification is not part of this endpoint's request body schema, so any value supplied here is stripped before the endpoint runs. Classification is set afterwards via POST /api/issue/save/stauts with btnType: "classification".
  • issue_list.status is initialised to 1.

GET/api/issue/list/detail/:id

Full detail view of one suggestion/issue: the ticket, its attachments, its replies, participants, and the caller's own vote/subscription state.

Module Feedback Center > Suggestions
Authentication Required — fully authenticated session (two-factor completed)
Authorization Readable by any authenticated user. can_manage is true only for a Feedback Center Administrator (10) — not for a Global Master System Administrator. When can_manage is false, user_email is stripped from the ticket and from every reply server-side.
Content type application/json

Path parameters

Name Type Required Description
id string Yes issue_list.id, plain numeric or encrypted.

Example request

curl 'https://api.example.com/api/issue/list/detail/4413' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "getIssue": {
    "id": 4413,
    "title": "Add bulk assign",
    "message": "<p>Would save a lot of clicks.</p>",
    "type": 1,
    "status": 1,
    "module": 81,
    "user_id": 77,
    "post_anonymously": 0,
    "browser": "Chrome 142.0.0.0",
    "user_name": "Nguyen, Alex",
    "profile_image": "",
    "labels": ["ui"],
    "voteCount": 9
  },
  "getIssueFiles": [
    {
      "id": 9001,
      "issue_list_id": 4413,
      "reply_id": 0,
      "file_path": "1764341531_ab12.png",
      "original_name": "mockup.png",
      "issue_or_cmd": 1,
      "download_url": "/api/issue/files/download/<encrypted id>/mockup.png"
    }
  ],
  "getReply": [
    {
      "id": 771,
      "issue_list_id": 4413,
      "rply_message": "<p>Agreed.</p>",
      "user_id": 91,
      "user_name": "Patel, Riya",
      "profile_image": "",
      "role_id": "6,10",
      "files": []
    }
  ],
  "partUser": [{ "id": 77, "name": "Nguyen, Alex" }, { "id": 91, "name": "Patel, Riya" }],
  "is_vote": 1,
  "subscribe": 1,
  "role_id": 10,
  "can_manage": true,
  "type": 1
}
Field Type Description
getIssue object The issue_list row plus user_name (lf_name, falling back to first+last), profile_image, parsed labels array and voteCount. user_email present only when can_manage.
getIssueFiles[] array Ticket-body attachments (issue_or_cmd = 1), each with a ready-made download_url.
getReply[] array Non-deleted issue_reply rows, ascending by created_at, each with user_name, profile_image, the raw comma-joined role_id string from users.role, and its own files[] (issue_or_cmd = 2).
partUser[] array Distinct participants ({ id, name }), ticket author first. Never anonymised.
is_vote number 1 if the caller has voted, else 0.
subscribe number 1 if the caller is subscribed, else 0.
role_id number The caller's first global role id, or 0. Informational only.
can_manage boolean Whether the caller holds Feedback Center Administrator.
type number issue_list.type.

Errors

Status Condition Message/shape
400 Ticket id not found { "message": "Suggestion not found." }
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Reply attachments are keyed on the reply id, not the ticket id — issue_files.issue_list_id is overloaded: 0 = staged, ticket id when issue_or_cmd = 1, reply id when issue_or_cmd = 2.
  • download_url embeds an encrypted issue_files.id, so it can be handed straight to the browser.

POST/api/issue/feedback/replay/send

Posts a reply on a suggestion/issue, links any staged attachments to that reply, and notifies subscribers.

Module Feedback Center > Suggestions
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session — any authenticated user may reply to any suggestion/issue. (The Support chat route applies its own ownership check; this one does not.)
Content type application/json

Request body

Field Type Required Validation / allowed values Description
issueId string Yes String issue_list.id, plain numeric or encrypted.
message string No Optional, string HTML reply body; sanitised before insert.
customSwitch number No Optional, Numeric (numeric strings are coerced), integer 1 sets issue_reply.post_anonymously = 1 (when the column exists) and makes the notification e-mail attribute the reply to "Anonymous".
insertedIds number[] No Optional, array Staged issue_files.id values to attach to this reply.

Example request

curl -X POST 'https://api.example.com/api/issue/feedback/replay/send' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "issueId": "4413", "message": "<p>Agreed.</p>", "customSwitch": 0, "insertedIds": [9002] }'

Success response — 200 OK

{ "error": false, "id": 772 }
Field Type Description
error boolean false on success.
id number New issue_reply.id.

Errors

Status Condition Message/shape
400 issue_reply table not present { "message": "Reply table is unavailable." }
400 issueId missing or not a string Standard request-validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The insert is schema-adaptive: post_anonymously, status, created_at and updated_at are written only when those columns exist. status is set to 1.
  • Attachments are linked with issue_or_cmd = 2 and issue_list_id = <reply id>.
  • An e-mail is sent only when the ticket has at least one subscriber — To: the ticket creator, Bcc: the other subscribers. Fire-and-forget.
  • The route path is spelled replay (not reply) and must be requested exactly as shown.

POST/api/issue/vote/count

Toggles the caller's vote on a suggestion and returns the new vote state and total.

Module Feedback Center > Suggestions
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id string Yes String issue_list.id, plain numeric or encrypted.

Example request

curl -X POST 'https://api.example.com/api/issue/vote/count' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "id": "4413" }'

Success response — 200 OK

{ "error": false, "vote": 1, "voteCount": 10 }
Field Type Description
error boolean Always false.
vote number The caller's own state after the toggle: 1 voted, 0 withdrawn.
voteCount number Total issue_vote rows with is_vote = 1 for the ticket.

Errors

Status Condition Message/shape
400 id missing or not a string Standard request-validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • First vote inserts with is_vote = 1; subsequent calls flip the existing row between 1 and 0.
  • If the issue_vote table is absent the endpoint returns { error: false, vote: 0, voteCount: 0 } without writing.

POST/api/issue/filter/module

Re-queries the suggestion/issue list with module, status, search, sort and show-deleted filters applied.

Module Feedback Center > Suggestions
Authentication Required — fully authenticated session (two-factor completed)
Authorization No gate on the route. canDelete on each returned item is true for a Global Master System Administrator (1) or Feedback Center Administrator (10). delete_show = 1 is honoured for any caller — the role check only affects the canDelete flag, not the filter.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
type number Yes Numeric (numeric strings are coerced), integer issue_list.type to list (1 = Suggestion, 2 = Issue).
id number No Optional, Numeric (numeric strings are coerced), integer Module (menus.id) filter. 0/omitted = all modules.
filter_content number No Optional, Numeric (numeric strings are coerced), integer Status preset: 1 → status 1; 2 → status 2; 3 → only the caller's own tickets; 4 → status 3; 5 → status in (1,3); 6 → status 6. Any other value applies no status filter.
search_content string No Optional, string Case-insensitive LIKE '%…%' on title.
sort_type number No Optional, Numeric (numeric strings are coerced), integer 2 → oldest first; 3 → most votes first; 4 → fewest votes first; anything else (default) → newest first.
delete_show number No Optional, Numeric (numeric strings are coerced), integer 1 includes soft-deleted rows; anything else excludes them.

Example request

curl -X POST 'https://api.example.com/api/issue/filter/module' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "type": 1, "id": 81, "filter_content": 1, "search_content": "assign", "sort_type": 3, "delete_show": 0 }'

Success response — 200 OK

{
  "error": false,
  "data": [
    {
      "id": 4412,
      "encryptId": "b3RoZXI9",
      "title": "Add a bulk assign action to Task List",
      "status": 1,
      "post_anonymously": 0,
      "user_id": 77,
      "user_name": "Alex Nguyen",
      "created_at": "2025-11-28T14:02:11.000Z",
      "voteCount": 9,
      "replyCount": 4,
      "fileCount": 1,
      "deleted": 0,
      "canDelete": true
    }
  ]
}
Field Type Description
error boolean Always false.
data[] array Same item shape as getFeedback[] on GET /api/feedbackcenter/:type.

Errors

Status Condition Message/shape
400 Missing/invalid type Standard request-validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

POST/api/issue/save/stauts

Updates a ticket's status and/or classification, optionally appends a label, and notifies the ticket creator on a status change.

Module Feedback Center > Suggestions
Authentication Required — fully authenticated session (two-factor completed)
Authorization Feedback Center Administrator (10) or Global Master System Administrator required; anyone else gets 401. This is a superset of the UI control, which shows the editor only to role 10.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
issueId string Yes String issue_list.id, plain numeric or encrypted.
status number Yes Numeric (numeric strings are coerced), integer New issue_list.status.
labelName string No Optional, string Appended to the JSON labels array (de-duplicated) when the labels column exists.
classification string No Optional, string Lower-cased and written to issue_list.classification when that column exists.
btnType string No Optional, string 'status' triggers the status-change e-mail. 'classification' suppresses the status write unless status > 0.

Example request

curl -X POST 'https://api.example.com/api/issue/save/stauts' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "issueId": "4413", "status": 3, "btnType": "status", "labelName": "ui" }'

Success response — 200 OK

{ "error": false }
Field Type Description
error boolean Always false.

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }
401 Caller is neither Feedback Center Administrator nor global admin { "message": "Not allowed to update suggestion status." }
400 Missing/invalid issueId or status Standard request-validation error array

Notes

  • The route path is spelled stauts and must be requested exactly as shown.
  • When btnType = 'classification' and status is 0, the status column is left untouched.
  • If neither a status nor a classification update applies, this endpoint returns { error: false } without writing.
  • The status-change e-mail fires only when btnType is exactly 'status' (case-insensitive), and goes to the ticket creator with the previous and new status labels.

POST/api/feedbackcenter/DelList

Soft-deletes a suggestion/issue (issue_list.deleted = 1).

Module Feedback Center > Suggestions
Authentication Required — fully authenticated session (two-factor completed)
Authorization Global Master System Administrator (1) or Feedback Center Administrator (10), resolved from global non-revoked grants; a resolved global-admin identity also passes. Anyone else gets 401.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id string Yes String issue_list.id, plain numeric or encrypted.

Example request

curl -X POST 'https://api.example.com/api/feedbackcenter/DelList' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "id": "4413" }'

Success response — 200 OK

{ "error": false }
Field Type Description
error boolean Always false.

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }
401 Caller holds neither role 1 nor role 10 and is not a global admin { "message": "Not allowed to delete suggestion." }
400 id missing or not a string Standard request-validation error array

Notes

  • Soft delete only — the row and its replies/attachments remain. Deleted rows reappear through POST /api/issue/filter/module with delete_show: 1.
  • The route is case-sensitive: DelList must be sent exactly as shown.

POST/api/subscribe/status

Subscribes or unsubscribes the caller from a ticket's reply notifications.

Module Feedback Center > Suggestions
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Request body

Field Type Required Validation / allowed values Description
issue_list_id string Yes String issue_list.id, plain numeric or encrypted.
is_subscribe number Yes Numeric (numeric strings are coerced), integer 1 subscribes; any other value unsubscribes (stored as 0).

Example request

curl -X POST 'https://api.example.com/api/subscribe/status' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "issue_list_id": "4413", "is_subscribe": 1 }'

Success response — 200 OK

{ "error": false, "subscribed": true }
Field Type Description
error boolean Always false.
subscribed boolean true when the stored value is 1.

Errors

Status Condition Message/shape
400 Missing/invalid issue_list_id or is_subscribe Standard request-validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • If the issue_subscribe table is absent the endpoint returns { error: false } (no subscribed key) without writing.
  • Subscription state is what gates the reply e-mail on POST /api/issue/feedback/replay/send — no subscribers means no mail is sent at all.

POST/api/ajax_upload/issue_files

Stages one or more attachments in Google Cloud Storage and returns their issue_files row ids, to be passed later as insertedIds when saving a ticket or reply.

Module Feedback Center > Suggestions
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session
Content type multipart/form-data

Request body

The request is a multipart upload — any field name is accepted and every uploaded part is processed; there is no declared body schema. Files are buffered in memory and no transport-level upload size limit is applied.

Field Type Required Validation / allowed values Description
(any field name) file (repeatable) No Extension must be one of jpeg, png, jpg, gif, svg, doc, docx, txt, pdf, xlsx, xls, mp4, webm, mkv, avi, mpg; size must be <= 10 MB (10 × 1024 × 1024 bytes) Any part failing either check is silently skipped — no error is raised and no id is returned for it. Sending no files yields insertedIds: [].

Example request

curl -X POST 'https://api.example.com/api/ajax_upload/issue_files' \
  -b 'genie.sid=<session-cookie>' \
  -F 'files[]=@mockup.png;type=image/png' \
  -F 'files[]=@spec.pdf;type=application/pdf'

Success response — 200 OK

{ "error": false, "insertedIds": [9001, 9002] }
Field Type Description
error boolean Always false.
insertedIds[] number[] New issue_files.id values, in upload order, for the files that passed validation.

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Each accepted file is renamed to <epoch-ms>_<random>.<ext> and written to the GCS object key <GCS_ISSUE_FILES_PREFIX>/<filename> (default prefix issue_files/) in bucket GCS_BUCKET. The database stores only the bare filename in issue_files.file_path; the object key is derived from the prefix and that filename on both write and read.
  • Rows are inserted with issue_list_id = 0 and issue_or_cmd = 0 (staged/unlinked). They become owned only when the returned ids are passed as insertedIds to POST /api/issue/feedback/save (→ issue_or_cmd = 1), POST /api/issue/feedback/replay/send or POST /api/support/chat/reply/save (→ issue_or_cmd = 2). Ids never passed on stay orphaned.
  • Uploaded files are retrieved through GET /api/issue/files/download/:id[/:name]; the bucket itself stays private.
  • If the issue_files table is absent the endpoint returns { error: false, insertedIds: [] } and uploads nothing.
  • The global body limit (20mb for JSON and urlencoded payloads) does not apply to multipart bodies; the effective per-file cap is the 10 MB check above.

GET/api/issue/files/download/:id

Streams one attachment back to the browser as a download.

Module Feedback Center > Suggestions
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session — any authenticated user may download any attachment whose id they hold. There is no ownership or ticket-visibility check.
Content type The object's stored GCS contentType, falling back to application/octet-stream

Path parameters

Name Type Required Description
id string Yes issue_files.id, plain numeric or encrypted (download_url supplies the encrypted form). If it resolves to 0, this endpoint falls back to a lookup by original_name — which, with no :name segment present, matches on an empty string and therefore normally 404s.

Example request

curl -OJ 'https://api.example.com/api/issue/files/download/9001' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

Binary file body. Headers set by this endpoint:

  • Content-Type: <object content type>
  • Content-Disposition: attachment; filename="<name>"; filename*=UTF-8''<url-encoded name>

The download name is the :name segment when present, else issue_files.original_name, else the stored filename; double quotes are stripped.

Response body: binary stream — no JSON shape.

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }
404 No matching issue_files row, or the row has an empty file_path { "error": true, "message": "File not found." }
404 The derived GCS object does not exist { "error": true, "message": "File not found." }
404 GCS read stream errors before headers are sent { "error": true, "message": "File not found." } (if headers were already sent, the response is simply ended)

Notes

  • The object key is <GCS_ISSUE_FILES_PREFIX>/<issue_files.file_path>; bytes are streamed rather than buffered, so memory stays bounded.
  • The bucket is never made public and no signed URL is issued on this path — all reads go through this authenticated endpoint.

GET/api/issue/files/download/:id/:name

Same as the previous route with an explicit download filename appended — this is the form emitted in download_url.

Module Feedback Center > Suggestions
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session — any authenticated user may download any attachment whose id they hold. There is no ownership or ticket-visibility check.
Content type The object's stored GCS contentType, falling back to application/octet-stream

Path parameters

Name Type Required Description
id string Yes issue_files.id, plain numeric or encrypted.
name string Yes Filename to present in Content-Disposition. Also acts as the lookup key when id resolves to 0: the newest issue_files row with original_name = <name> is served.

Example request

curl -OJ 'https://api.example.com/api/issue/files/download/9001/mockup.png' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

Binary file body with Content-Disposition: attachment; filename="mockup.png"; filename*=UTF-8''mockup.png.

Response body: binary stream — no JSON shape.

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }
404 No row matched by id or by original_name, or file_path is empty { "error": true, "message": "File not found." }
404 Object missing in GCS, or stream error before headers { "error": true, "message": "File not found." }

Notes

  • The name-based fallback is global: an unresolvable id plus a known original_name will serve the most recent file with that name from any ticket.

15.2 Support & Issues

Support tickets are issue_list rows with type = 3, rendered as a chat thread. The single role that matters is Feedback Center Administrator (10) — the "Genie support agent". An agent sees every ticket in the system; everyone else sees only tickets they opened. Global and firm administrators get no special treatment here unless they also hold role 10.

Support status ladder: 1 New · 2 Waiting for user feedback · 3 In progress · 4 Resolved · 7 "Unable to Recreate" for agents, "Resolved" for everyone else · any other value renders as "In progress".

GET/api/support/list

Returns the support ticket list for the caller, plus the module dropdown and the caller's agent flag.

Module Feedback Center > Support & Issues
Authentication Required — fully authenticated session (two-factor completed)
Authorization Feedback Center Administrator (10) widens the result set to every type = 3 ticket; all other callers get il.user_id = <caller> only.
Content type application/json

Example request

curl 'https://api.example.com/api/support/list' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "menuList": [{ "id": 81, "menu_name": "Task & Time" }],
  "renderFilterSupp": [
    {
      "id": 5120,
      "encryptId": "cXdlcnR5",
      "title": "Timesheet will not submit",
      "status": 3,
      "status_text": "In progress",
      "classification": "high",
      "module": 81,
      "module_name": "Task & Time",
      "opened_by": "alex@example.com",
      "opened_on": "2025-11-30T10:41:00.000Z",
      "last_updated": "2025-12-02T16:08:00.000Z",
      "closed_on": null
    }
  ],
  "role_id": 10,
  "isSupportAgent": true
}
Field Type Description
menuList[] array { id, menu_name } module options.
renderFilterSupp[] array Ticket rows, default order updated_at DESC, created_at DESC.
renderFilterSupp[].status_text string Rendered status label; 7 reads "Unable to Recreate" only for agents.
renderFilterSupp[].opened_by string Author's e-mail, falling back to lf_name.
renderFilterSupp[].closed_on string | null updated_at when status = 4, else null.
role_id number The caller's first global role id, or 0.
isSupportAgent boolean Whether the caller holds Feedback Center Administrator.

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Equivalent to calling POST /api/support/filter/feedback with { status: 0, title: "", created_filter: 3 }.
  • Soft-deleted tickets are always excluded from this surface.

GET/api/support/new/form

Bootstrap for the "raise a support ticket" form.

Module Feedback Center > Support & Issues
Authentication Required — fully authenticated session (two-factor completed)
Authorization No gate on the route. isSupportAgent (Feedback Center Administrator, 10) tells the client whether to render the Classification control.
Content type application/json

Example request

curl 'https://api.example.com/api/support/new/form' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "menuList": [{ "id": 81, "menu_name": "Task & Time" }],
  "type": 3,
  "defaultClassification": "low",
  "isSupportAgent": false
}
Field Type Description
menuList[] array { id, menu_name } module options.
type number Always 3 — the value to post as type when submitting.
defaultClassification string Always "low".
isSupportAgent boolean Whether the Classification control should be shown.

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • There is no dedicated support-create endpoint: the form submits to POST /api/issue/feedback/save with type: 3, which returns a redirect into the chat view.

GET/api/support/feedback/chat/:id

Loads one support ticket as a chat thread: the ticket, its attachments, every reply, and whether replying is still allowed.

Module Feedback Center > Support & Issues
Authentication Required — fully authenticated session (two-factor completed)
Authorization Feedback Center Administrator (10) may open any ticket. Everyone else may open only tickets where issue_list.user_id = <caller>; otherwise 401.
Content type application/json

Path parameters

Name Type Required Description
id string Yes issue_list.id, plain numeric or encrypted. Must resolve to a row with type = 3.

Example request

curl 'https://api.example.com/api/support/feedback/chat/5120' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "getIssue": {
    "id": 5120,
    "encryptId": "cXdlcnR5",
    "title": "Timesheet will not submit",
    "message": "<p>The submit button does nothing.</p>",
    "type": 3,
    "status": 3,
    "module": 81,
    "classification": "high",
    "user_id": 77,
    "user_name": "Nguyen, Alex",
    "profile_image": "",
    "labels": [],
    "voteCount": 0
  },
  "getIssueFiles": [],
  "getReply": [
    {
      "id": 880,
      "issue_list_id": 5120,
      "rply_message": "<p>Can you confirm the browser?</p>",
      "user_id": 12,
      "user_name": "Ortiz, Sam",
      "role_id": "6,10",
      "files": []
    }
  ],
  "menuList": [{ "id": 81, "menu_name": "Task & Time" }],
  "role_id": 10,
  "isSupportAgent": true,
  "canSupportReply": true
}
Field Type Description
getIssue object The ticket row plus encryptId, user_name, profile_image, parsed labels and voteCount.
getIssueFiles[] array Ticket-body attachments (issue_or_cmd = 1) with download_url.
getReply[] array Chat messages ascending by created_at, each with its own files[] and the raw comma-joined role_id (used client-side to draw the "Genie Admin" pill).
menuList[] array Module options.
role_id number The caller's first global role id, or 0.
isSupportAgent boolean Whether the caller holds Feedback Center Administrator.
canSupportReply boolean true when status is 1, 2 or 3 — i.e. the ticket is still open for replies.

Errors

Status Condition Message/shape
400 Ticket not found, or its type is not 3 { "message": "Support ticket not found." }
401 No authenticated session { "message": "These credentials do not match our records." }
401 Non-agent caller who did not open the ticket { "message": "Not allowed to access this ticket." }

Notes

  • Unlike GET /api/issue/list/detail/:id, this route does not strip user_email from the ticket or replies for non-agents — though non-agents can only ever reach their own tickets here.

POST/api/support/chat/reply/save

Posts a message into a support ticket's chat, attaches staged files, touches the ticket's updated_at, and notifies the other party.

Module Feedback Center > Support & Issues
Authentication Required — fully authenticated session (two-factor completed)
Authorization Feedback Center Administrator (10) may reply to any ticket; everyone else only to tickets they opened.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
issueId string Yes String issue_list.id, plain numeric or encrypted. Must be a type = 3 ticket.
msg string No Optional, string Message body. Trimmed; an empty result is rejected with 400, so in practice it is required. Sanitised before insert.
moduleId number No Optional, Numeric (numeric strings are coerced), integer Accepted by the request body schema; it does not affect the result.
insertedIds number[] No Optional, array Staged issue_files.id values to attach to this reply.

Example request

curl -X POST 'https://api.example.com/api/support/chat/reply/save' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "issueId": "5120", "msg": "<p>Chrome 142 on Windows.</p>", "insertedIds": [9003] }'

Success response — 200 OK

{ "error": false, "id": 881 }
Field Type Description
error boolean false on success.
id number New issue_reply.id.

Errors

Status Condition Message/shape
400 Ticket not found or type != 3 { "message": "Support ticket not found." }
400 Ticket status is not 1, 2 or 3 { "message": "Ticket is closed for replies." }
400 msg blank after trim { "message": "Please enter the message" }
400 issue_reply table not present { "message": "Reply table is unavailable." }
400 issueId missing or not a string Standard request-validation error array
401 No authenticated session { "message": "These credentials do not match our records." }
401 Non-agent caller who did not open the ticket { "message": "Not allowed to reply to this ticket." }

Notes

  • The reply is written with post_anonymously = 0 — support chat has no anonymous mode.
  • Attachments link with issue_or_cmd = 2 against the new reply id; use POST /api/ajax_upload/support_files (or the issue_files route — the two behave identically) to stage them first.
  • issue_list.updated_at is bumped so the ticket rises in the default list ordering.
  • The chat-reply e-mail goes to the Genie team (role 10 users) when the replier is not role 10, and to the ticket creator when they are. Fire-and-forget.

POST/api/support/filter/feedback

Re-queries the support ticket list with title, status and ordering filters.

Module Feedback Center > Support & Issues
Authentication Required — fully authenticated session (two-factor completed)
Authorization Feedback Center Administrator (10) sees every ticket; everyone else only their own.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
title string No Optional, string Case-insensitive LIKE '%…%' on issue_list.title.
status number No Optional, Numeric (numeric strings are coerced), integer 0/omitted = no status filter; 5 = status in (1,2,3) ("open"); any other positive value = exact status match.
created_filter number No Optional, Numeric (numeric strings are coerced), integer Ordering: 1 → created_at DESC; 2 → created_at ASC; 4 → updated_at ASC, created_at ASC; anything else (default 3) → updated_at DESC, created_at DESC.

Example request

curl -X POST 'https://api.example.com/api/support/filter/feedback' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "title": "timesheet", "status": 5, "created_filter": 1 }'

Success response — 200 OK

{
  "error": false,
  "renderFilterSupp": [
    {
      "id": 5120,
      "encryptId": "cXdlcnR5",
      "title": "Timesheet will not submit",
      "status": 3,
      "status_text": "In progress",
      "classification": "high",
      "module": 81,
      "module_name": "Task & Time",
      "opened_by": "alex@example.com",
      "opened_on": "2025-11-30T10:41:00.000Z",
      "last_updated": "2025-12-02T16:08:00.000Z",
      "closed_on": null
    }
  ]
}
Field Type Description
error boolean Always false.
renderFilterSupp[] array Same row shape as GET /api/support/list.

Errors

Status Condition Message/shape
400 Non-integer status or created_filter Standard request-validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • created_at is applied as a same-direction tiebreaker on the updated_at orderings, so tickets sharing an identical updated_at stamp keep a stable order.

POST/api/support/chat/reply

Resolves a ticket id into the front-end chat URL. A navigation aid; it performs no write.

Module Feedback Center > Support & Issues
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session — the returned URL is not access-checked here; GET /api/support/feedback/chat/:id applies the real gate.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
id string Yes String issue_list.id, plain numeric or encrypted.

Example request

curl -X POST 'https://api.example.com/api/support/chat/reply' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "id": "5120" }'

Success response — 200 OK

{ "error": false, "redirect": "/support/feedback/chat/cXdlcnR5" }
Field Type Description
error boolean Always false.
redirect string Front-end path /support/feedback/chat/<url-encoded encrypted id>.

Errors

Status Condition Message/shape
400 id missing or not a string Standard request-validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • This returns a relative path in a JSON body — it is not an HTTP redirect.
  • An unresolvable id still returns a URL (built from id 0); nothing is validated against the database.

POST/api/ajax_upload/support_files

Stages support-ticket attachments. Functionally identical to POST /api/ajax_upload/issue_files — both write the same issue_files rows.

Module Feedback Center > Support & Issues
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session
Content type multipart/form-data

Request body

The request is a multipart upload; any field name is accepted, with no declared body schema and no transport-level size limit.

Field Type Required Validation / allowed values Description
(any field name) file (repeatable) No Extension in jpeg, png, jpg, gif, svg, doc, docx, txt, pdf, xlsx, xls, mp4, webm, mkv, avi, mpg; size <= 10 MB Non-conforming parts are silently skipped.

Example request

curl -X POST 'https://api.example.com/api/ajax_upload/support_files' \
  -b 'genie.sid=<session-cookie>' \
  -F 'files[]=@console-log.txt;type=text/plain'

Success response — 200 OK

{ "error": false, "insertedIds": [9003] }
Field Type Description
error boolean Always false.
insertedIds[] number[] New issue_files.id values for the accepted files.

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Storage layout, prefix and retrieval are identical to the Suggestions upload route — see its Notes. Files are downloaded through GET /api/issue/files/download/:id[/:name] regardless of which upload route staged them.
  • Pass the returned ids as insertedIds on POST /api/support/chat/reply/save (links with issue_or_cmd = 2) or on POST /api/issue/feedback/save with type: 3 (links with issue_or_cmd = 1).

↑ Back to Table of Contents


16. Integrations

This section covers the third-party services Genie connects to on a firm's behalf: the Google Drive and Google Sheets connection used by "Save to Google Sheet" exports and by GL Account Validation, and the QuickBooks Online connection, sync dispatch and tax-lookup surface. The conventions in the previous section apply here too, with the exception of the browser-visited OAuth routes, which are served at the root and issue real HTTP redirects.


16.1 Google Integration (Drive / Sheets OAuth)

Per-user Google Drive / Google Sheets connection, used by the "Save to Google Sheet" export flow and by GL Account Validation. The paths are exactly as documented — five under /api/..., plus the browser-visited OAuth callback at /google/drive/callback.

Tokens are stored per user in the drive_config table; the browser only ever receives a short-lived Picker access token, never the refresh token.

POST/api/send/drive

Entry point for "Send to Google Drive". Returns a Picker access token when the user is connected, or a consent URL when they are not.

Module Integrations > Google
Authentication Required — fully authenticated session (two-factor completed). Every route in this section is user-scoped and takes the user id from the session, never from the request body.
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Request body

Field Type Required Validation / allowed values Description
sendType string No Optional, string, one of excel, pdf, "" Echoed back so the caller can resume the right export branch after the Picker closes. Defaults to 'excel' when absent or empty.

Example request

curl -X POST 'https://api.example.com/api/send/drive' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "sendType": "excel" }'

Success response — 200 OK

Connected:

{
  "error": false,
  "sendType": "excel",
  "accessToken": "<short-lived Google access token>",
  "picker": {
    "clientId": "<oauth client id>",
    "appId": "<gcp project number>",
    "developerKey": "<browser api key>"
  }
}

Not yet connected (or token unrecoverable):

{
  "error": true,
  "message": "You are not configured with Google Drive. Please configure your Google account.",
  "url": "https://accounts.google.com/o/oauth2/v2/auth?..."
}

Server misconfigured:

{ "error": true, "message": "Google Drive is misconfigured on this server: GOOGLE_CLIENT_SECRET is empty." }
Field Type Description
error boolean false only on the connected path.
sendType string Echo of the requested export type.
accessToken string Short-lived Google access token for PickerBuilder.setOAuthToken(). Scoped to drive.file + spreadsheets.
picker object Public Picker config: clientId, appId (GCP project number, derived from the client id when GOOGLE_PICKER_APP_ID is unset), developerKey (from GOOGLE_PICKER_DEVELOPER_KEY). None of these are secrets.
message string Present on the error paths.
url string Consent URL, present when the user must (re)authorise.

Errors

Status Condition Message/shape
200 Server credentials missing/malformed { "error": true, "message": "Google Drive is misconfigured on this server: …" }
200 User not connected, or refresh failed { "error": true, "message": "…", "url": "<consent url>" }
400 sendType not one of excel, pdf, '' Standard request-validation error array
401 No authenticated session { "message": "These credentials do not match our records." }
500 Any token-layer failure other than a missing or malformed server credential configuration Default framework error shape

Notes

  • Requesting the consent URL mints a single-use random CSRF state and stores it against the caller's session; it is consumed by GET /google/drive/callback, which must therefore be reached with the same session cookie.

GET/api/google/drive/status

Reports whether the caller has a usable Google connection, so the UI can skip the Configure modal.

Module Integrations > Google
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Example request

curl 'https://api.example.com/api/google/drive/status' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "error": false,
  "configured": true,
  "credentialsConfigured": true,
  "picker": { "clientId": "<oauth client id>", "appId": "<gcp project number>", "developerKey": "<browser api key>" }
}
Field Type Description
error boolean Always false.
configured boolean Whether this user has usable stored Drive tokens.
credentialsConfigured boolean Whether the server has both GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET set.
picker object Public Picker config (clientId, appId, developerKey).

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • This route does not mint a consent state and does not attempt a token refresh.

GET/api/google/drive/auth-url

Returns a fresh Google consent URL for the "Configure Google Account" popup.

Module Integrations > Google
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session
Content type application/json

Example request

curl 'https://api.example.com/api/google/drive/auth-url' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{ "error": false, "url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=…&state=…" }
Field Type Description
error boolean false on success, true when the server's credentials are unusable.
url string Consent URL (present on success).
message string Diagnostic describing the credential problem (present on the error path).

Errors

Status Condition Message/shape
200 GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET missing or malformed { "error": true, "message": "Google Drive is misconfigured on this server: …" }
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Each call overwrites the OAuth state stored against the caller's session with a new value, invalidating any consent window opened by a previous call.
  • The URL carries access_type=offline, prompt=select_account consent and include_granted_scopes=true; prompt=consent is what guarantees a refresh token is issued on every authorisation, not just the first.

POST/api/google/drive/disconnect

Clears the caller's stored Google Drive tokens.

Module Integrations > Google
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session; only the caller's own drive_config row is touched.
Content type application/json (no request body)

Example request

curl -X POST 'https://api.example.com/api/google/drive/disconnect' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{ "error": false, "message": "Google account disconnected." }
Field Type Description
error boolean Always false.
message string Always "Google account disconnected.".

Errors

Status Condition Message/shape
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • This clears Genie's stored tokens only; it does not revoke the grant with Google. The user remains able to reconnect without re-approving unless they revoke access in their Google account.
  • No-ops silently when the drive_config table is absent.

POST/api/google/drive/upload

Uploads a base64 workbook into a Drive folder, converting it to a native Google Sheet by default. Reusable by any screen that can produce an .xlsx in memory.

Module Integrations > Google
Authentication Required — fully authenticated session (two-factor completed) — the Google identity used is the caller's own stored token.
Authorization No additional role requirement beyond an authenticated session
Content type application/json (the file travels as base64 in the JSON body, not as multipart)

Request body

Field Type Required Validation / allowed values Description
fileBase64 string Yes String Base64 workbook. A leading data:<type>;base64, prefix is tolerated and stripped.
fileName string Yes String, maximum length 255 Display name. When converting to a Sheet, a trailing .xlsx / .xls / .csv is stripped; an empty result becomes "Export".
folderId string No Optional, string, maximum length 255 Drive folder id from the Picker. Effectively required — this endpoint rejects a blank value before reaching the backend (see Errors).
folderName string No Optional, string, maximum length 255 Display name of the chosen folder; echoed back for the success message only.
mimeType string No Optional, string, maximum length 255 Source MIME type of the payload. Defaults to the xlsx type.
convertToSheet boolean No Optional, boolean Defaults to true (only an explicit false disables conversion).

Example request

curl -X POST 'https://api.example.com/api/google/drive/upload' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{
        "fileBase64": "UEsDBBQABgAIAAAAIQ...",
        "fileName": "Task List.xlsx",
        "folderId": "1AbCdEfGhIjKlMnOpQrStUvWxYz",
        "folderName": "Client Exports",
        "convertToSheet": true
      }'

Success response — 200 OK

{
  "error": false,
  "file": "1XyZaBcDeFgHiJkLmNoPqRsTuV",
  "name": "Task List",
  "view_link": "https://docs.google.com/spreadsheets/d/1XyZaBcDeFgHiJkLmNoPqRsTuV/edit",
  "folderName": "Client Exports"
}
Field Type Description
error boolean false on success.
file string New Drive file id.
name string Name Drive assigned (falls back to the computed name).
view_link string webViewLink, or a constructed Sheets URL when Drive omits it.
folderName string | null Echo of the supplied folderName, or null.

Errors

Status Condition Message/shape
200 folderId blank/absent { "error": true, "message": "Please select a Google Drive folder before uploading." }
200 fileBase64 empty after stripping a data: prefix { "error": true, "message": "Nothing to upload." }
200 User not connected, refresh failed, or Google returned 401 mid-flight { "error": true, "needsAuth": true, "message": "…", "url": "<consent url, when server credentials exist>" }
200 Drive rejected the call (bad folder, permissions, API failure) { "error": true, "message": "…", "status": <http status from Google> }
200 Any other error { "error": true, "message": "<error message or 'Failed to upload the file to Google Drive. Please try again.'>" }
400 Missing/invalid fileBase64 or fileName, or a maximum-length breach Standard request-validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Every failure on this route is reported as HTTP 200 with error: true; needsAuth: true is the client's signal to re-open the Configure Google Account modal.
  • When folderId is supplied the backend first verifies the folder exists, is a folder, and is writable — turning an opaque Drive 404 into "pick a different folder".
  • Conversion to a native Sheet is triggered by an asymmetry Drive requires: the multipart metadata part declares application/vnd.google-apps.spreadsheet while the media part carries the workbook MIME type.
  • Nothing is written to disk; the workbook is streamed from memory. Note the global JSON body limit of 20mb applies to the base64 payload.
  • Under the drive.file scope the app can only write to folders the user handed it through the Picker — which is why picker.appId must be correct or every upload fails as "folder not found".

GET/google/drive/callback

Google's OAuth redirect target. A browser-visited route (not an API call): it exchanges the authorisation code, stores the tokens against the session's user, and renders a small self-closing popup page.

Module Integrations > Google
Authentication Required — the route is session-bound: the user is taken from the session cookie sent with the top-level browser request (SameSite=Lax). Without a recognised session a failure page is rendered
Authorization No additional role requirement beyond an authenticated session
Content type text/html (a self-closing popup page) — not an HTTP redirect

Query parameters

Name Type Required Default Description
code string Yes (success path) — Google authorisation code.
state string Yes (success path) — Must equal the OAuth state stored against the caller's session, character for character, or the exchange is refused as a CSRF mismatch.
error string No — Set by Google when the user cancels or denies consent.

Example request

GET /google/drive/callback?code=4%2F0AeanS0b...&state=6f1c2e6a-9a1d-4f1e-9d63-1e0c9c2c1d77&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive.file HTTP/1.1
Host: api.example.com
Cookie: genie.sid=<session-cookie>

Success response — 200 OK

A minimal HTML document that postMessages the outcome to window.opener and closes itself after ~400 ms:

<!doctype html>
<html><head><meta charset="utf-8"><title>Google Drive</title>…</head><body>
<p>Google Drive connected.</p>
<p>You can close this window.</p>
<script>/* posts { source: 'genie-google-drive', success: true, message: 'Google Drive connected.' } */</script>
</body></html>
Posted message field Type Description
source string Always "genie-google-drive" — the discriminator the opener listens for.
success boolean Whether the connection was established.
message string Human-readable outcome.

Errors

Every outcome — including failure — is delivered as HTTP 200 with an HTML page carrying success: false; there are no non-2xx responses on this route.

Status Condition Message/shape
200 ?error= present, or no code Page with "Google authorisation was cancelled."
200 state absent from session or does not match Page with "Google authorisation failed. Please try again. (the sign-in session was not recognised — please close this window and try again)"
200 Session expired / user not resolvable Page with "Your session has expired. Please sign in again."
200 Token exchange rejected (invalid_client) Page naming the credential problem, e.g. that GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must belong to the same OAuth client
200 Token exchange rejected (redirect_uri_mismatch) Page naming the configured redirect URI and instructing that it be registered on the OAuth client
200 Any other exchange failure Page with "Google authorisation failed. Please try again. (<detail>)"

Notes

  • The CSRF state is consumed only on the success path, immediately before the code exchange, so a cancelled or failed consent leaves the stored state intact for a further attempt. Once consumed it cannot be replayed.
  • postMessage's targetOrigin is pinned to FRONTEND_ORIGIN (falling back to APP_URL, then the request host) rather than '*', so an unrelated window cannot read the result. The request's own Host header is not used first, because this route is proxied server-to-server and the request host is the internal upstream.
  • Interpolated values are HTML-escaped, and inside the inline script every < is emitted as the < escape, so nothing in the message can break out of the script element.
  • This is the only route in this section registered outside the /api prefix; the redirect URI registered with Google is the front-end path ${APP_URL}/google/drive/callback, which the front-end rewrites through to this endpoint.

16.2 QuickBooks Online Integration

Company-level QuickBooks Online connection, sync dispatch and lookups. Genie itself does not perform the QBO sync outside local development — it builds a payload and fires it at an AWS Lambda, which owns every fetch, every write and the token rotation; Genie then polls sync status.

These endpoints are served under /api. Authentication and firm scoping are applied per endpoint, as documented below.

POST/api/qbo/import/all

Dispatches a QuickBooks sync for one company across one or more modules, marking the requested modules as running immediately.

Module Integrations > QuickBooks Online
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement on this route (unlike POST /api/company/qbo/syncall, which additionally enforces the QBO edit rule). When companyId/company_id is omitted, the target is the caller's firm-internal company, so the caller's firm bounds the request.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
lambda string Yes String Modules to sync. 'all' (or empty) = every module. Otherwise a comma-separated list of module names (account, reports, customer, vendor, invoice, bill, sr, po, cm, pl, bs, gl, ar, ap, class, location, vc, cf, products, terms) or their numeric ids. reports (id 2) is always filtered out — it is a UI grouping placeholder; the five report modules are ids 10–14.
companyId number No Optional, Numeric (numeric strings are coerced), integer Target company.
company_id number No Optional, Numeric (numeric strings are coerced), integer Snake-case alias; companyId wins. When both are absent the caller's firm-internal company is used.
count number No Optional, Numeric (numeric strings are coerced), integer Accepted by the request body schema; it does not affect the result.
start string No Optional, string Explicit window start. When both start and end are present they are used exactly as supplied instead of deriving the window from company.qb_sync_type.
end string No Optional, string Explicit window end.

Example request

curl -X POST 'https://api.example.com/api/qbo/import/all' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "lambda": "account,customer,gl", "companyId": 412, "start": "2025-11-01", "end": "2025-11-30" }'

Success response — 200 OK

{
  "error": false,
  "lambda_req": {
    "companies": [{ "id": 412, "modules": ["account", "customer", "gl"] }],
    "start_date": "2025-11-01",
    "end_date": "2025-11-30",
    "method": "Accrual"
  }
}
Field Type Description
error boolean false when the sync was dispatched.
lambda_req object The exact payload sent to the sync Lambda.
lambda_req.companies[] array { id, modules: [<module name>, …] }.
lambda_req.start_date / end_date string Resolved sync window.
lambda_req.method string "Cash" when report_configure.report_method = 1, else "Accrual".

Errors

Status Condition Message/shape
200 No target company resolvable { "error": true, "msg": "connection error" }
200 lambda resolves to no valid modules { "error": true, "msg": "Invalid QuickBooks sync module." }
200 Company has no qbo_realmid, or the token check rejected it { "error": true, "msg": "connection error", "company_id": <id>, "client_id": 0 }
200 LAMBDA_SYNC is not configured { "error": true, "msg": "QuickBooks sync service is not configured (LAMBDA_SYNC)." }
400 lambda missing or not a string Standard request-validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Before dispatching, this endpoint asks LAMBDA_TOKEN_URL to verify/refresh the company's QuickBooks token. A non-200 from that endpoint is treated as "assume the connection is fine"; only an explicit error: true in a 200 body counts as a failure.
  • The requested modules are optimistically marked "running" so the UI's spinners appear before the Lambda writes its first status row. On a LAMBDA_SYNC misconfiguration they are immediately marked finished with a generic error.
  • When APP_ENV=local, no Lambda is called — the application performs the sync itself, covering the core account and report modules, fire-and-forget.
  • The response returns as soon as the request is dispatched; progress is read back through POST /api/qbo/syncstatus.

POST/api/qbo/syncstatus

Polling endpoint for QuickBooks sync progress. Called roughly every 5 seconds by the QBO page while a sync runs.

Module Integrations > QuickBooks Online
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement, and no firm-scope check — status for any company id is returned to any authenticated caller.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
companyId number | string No Optional; no type constraint Numeric or encrypted company id. No integer constraint is applied, so the encrypted form is accepted rather than rejected with a 400.
company_id number | string No Optional; no type constraint Snake-case alias; companyId wins.

Example request

curl -X POST 'https://api.example.com/api/qbo/syncstatus' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "companyId": 412 }'

Success response — 200 OK

{
  "syncStatus": "{\"m_1\":true,\"m_3\":false}",
  "syncError": "{\"m_3\":\"There is an error with QuickBooks connection\"}",
  "syncTime": "{\"m_1\":\"12/03/2025 09:14 AM\"}",
  "ajaxRun": true
}
Field Type Description
syncStatus string JSON-encoded string (not an object) mapping m_<moduleId> → boolean "currently running".
syncError string JSON-encoded string mapping m_<moduleId> → a sanitised error message. Raw Intuit fault payloads are never surfaced; they go to the server log and the client sees "There is an error with QuickBooks connection".
syncTime string JSON-encoded string mapping m_<moduleId> → last-synced timestamp.
ajaxRun boolean Whether the client should keep polling. Forced false when company.qbo_connection = 0.

Errors

Status Condition Message/shape
200 Company id absent or unresolvable { "syncStatus": "{}", "syncError": "{}", "syncTime": "{}", "ajaxRun": false }
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The three map fields are strings containing JSON — clients must parse them before use.
  • The Lambda's reported state is authoritative once it has written anything; otherwise the locally-tracked optimistic state is returned.

GET/api/qbo/connect

Returns a QuickBooks consent URL for the caller's firm-internal company.

Module Integrations > QuickBooks Online
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement on this route. The target company is always the caller's own firm-internal company, so the caller's firm bounds it. (The admin equivalent, GET /api/company/qbo/connect/:id, does enforce the QBO edit rule.)
Content type application/json

Example request

curl 'https://api.example.com/api/qbo/connect' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "error": false,
  "url": "https://appcenter.intuit.com/connect/oauth2?client_id=…&state=1764341531123-a1b2c3d4",
  "companyId": 412,
  "state": "1764341531123-a1b2c3d4"
}
Field Type Description
error boolean false on success.
url string Intuit consent URL.
companyId number Company the connection will be bound to.
state string Opaque CSRF/correlation state, <epoch-ms>-<random>.

Errors

Status Condition Message/shape
200 No firm-internal company configured for the caller's firm { "error": true, "message": "Firm internal company is not configured." }
200 QBO_CLIENT_ID / QBO_CLIENT_SECRET not set { "error": true, "message": "QuickBooks OAuth credentials are not configured (QBO_CLIENT_ID / QBO_CLIENT_SECRET)." }
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The consent URL requests scopes com.intuit.quickbooks.accounting, openid, profile, email, phone, address.
  • The state is single-use and is held as temporary server-side storage for 15 minutes (QUICKBOOKS_OAUTH_STATE_TTL_MS); after that window it is no longer accepted. The OAuth callback must be served by the same application instance that issued the consent URL, so multi-instance deployments must preserve instance affinity for these routes. See Deployment & Operational Considerations.

GET/api/qbo/connection/status

Reports whether the caller's firm-internal company is currently connected to QuickBooks.

Module Integrations > QuickBooks Online
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session; scoped to the caller's firm-internal company.
Content type application/json

Example request

curl 'https://api.example.com/api/qbo/connection/status' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{ "connected": true, "companyId": 412, "companyName": "Northwind Advisory LLP" }
Field Type Description
connected boolean true when company.qbo_connection = 1.
companyId number The firm-internal company id, or 0 when none is configured.
companyName string Company name, or "".

Errors

Status Condition Message/shape
200 No firm-internal company configured { "connected": false, "companyId": 0, "companyName": "" }
401 No authenticated session { "message": "These credentials do not match our records." }

GET/api/qbo/callback

Intuit's OAuth redirect target. Exchanges the authorisation code, binds the realm to the pending company, captures the QuickBooks company name, and redirects the popup to a front-end hand-off page.

Module Integrations > QuickBooks Online
Authentication Not required (OAuth callback) — the caller is identified solely by the single-use state issued with the consent URL, which is held in temporary server-side storage on the issuing instance
Authorization No role requirement. Authorisation is carried by the unguessable, single-use, 15-minute state. The request must reach the same application instance that issued the consent URL.
Content type 302 redirect (Location header); no response body

Query parameters

Name Type Required Default Description
code string Yes (success path) — Intuit authorisation code.
realmId string Yes (success path) — QuickBooks company (realm) id.
state string Yes (success path) — The state minted by GET /api/qbo/connect or GET /api/company/qbo/connect/:id. Consumed on first use.
error string No — Set by Intuit when consent is denied.

Every query parameter received is forwarded unchanged into the callback URI used for the token exchange.

Example request

GET /api/qbo/callback?code=AB11...&state=1764341531123-a1b2c3d4&realmId=9130350000000000 HTTP/1.1
Host: api.example.com

Success response — 302 Found

Location is always on FRONTEND_ORIGIN, at the hand-off page /task/qbo/callback:

  • With a returnTo recorded at connect time: ${FRONTEND_ORIGIN}/task/qbo/callback?redirect=<url-encoded returnTo> — e.g. …?redirect=%2Fadmin%2Fcompany%2F<encrypted id>%2Fquickbooks%3Fs%3D1.
  • Without one: ${FRONTEND_ORIGIN}/task/qbo/callback?success=1.

Response body: none (redirect).

Errors

Failures are also delivered as 302 redirects, never as error status codes.

Status Condition Message/shape
302 Intuit returned ?error= Location: ${FRONTEND_ORIGIN}/task/qbo/callback?error=<intuit error>
302 Missing code, realmId or state …?error=Missing%20QuickBooks%20OAuth%20callback%20parameters.
302 Unknown state, or state older than 15 minutes …?error=QuickBooks%20OAuth%20state%20expired.%20Please%20try%20again.
302 QBO_CLIENT_ID / QBO_CLIENT_SECRET unset …?error=QuickBooks%20OAuth%20credentials%20are%20not%20configured.
302 Token exchange failed, or the response lacked realm / access / refresh token …?error=<Intuit error description or 'Failed to exchange QuickBooks authorization code.'>
302 The company table has none of the QuickBooks columns …?error=Company%20table%20is%20missing%20QuickBooks%20columns.
302 Realm conflict — this QuickBooks file is already bound to another company …?redirect=<page>?error=true&error_type=1&error_code=<Genie company name>&qbo_error_code=<QuickBooks file name>
302 Company conflict — this company already carries a different realm id …?redirect=<page>?error=true&error_type=2&error_code=<Genie company name>&qbo_error_code=<QuickBooks file name>

Notes

  • The two conflict cases redirect back to the integration page with structured query parameters rather than failing outright, so the page can render its own alert. The company is not repointed to a different QuickBooks file.
  • On success, company is updated with qbo_realmid, qbo_access_token, qbo_refresh_token, qbo_state, qbo_connection = 1, last_sync and updated_at — each only if the column exists.
  • A best-effort CompanyInfo call then stores the QuickBooks company name in company.qbo_file; this is what the integration page renders as "QuickBooks: …". A failure here is logged and ignored.
  • Sync status for modules 1, 3 and 21 is seeded with ajaxRun: false after a successful connection; this too is best-effort.
  • The redirect targets the front-end hand-off page rather than the application directly, because this response lands in the OAuth popup — the hand-off page moves the opener to returnTo (with ?s=1, which auto-triggers "Sync all") and then closes itself.

POST/api/invoicing/tax/qbo

Resolves the sales-tax rate for an invoicing line — a live QuickBooks probe when possible, otherwise the stored customer rate.

Module Integrations > QuickBooks Online
Authentication Required — fully authenticated session (two-factor completed)
Authorization No additional role requirement beyond an authenticated session. The live probe runs against the caller's firm-internal company, so the QuickBooks identity used is always the caller's firm's.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
customerQbo number No Optional, Numeric (numeric strings are coerced), integer qbo_customer.id. One of customerQbo / customerId is required in practice — with neither, the response is a flat taxRate: 0.
customerId number No Optional, Numeric (numeric strings are coerced), integer Fallback for customerQbo.
companyId number No Optional, Numeric (numeric strings are coerced), integer Accepted by the request body schema; it does not affect the result — the company comes from the session's firm.
productQbo number No Optional, Numeric (numeric strings are coerced), integer When > 0 and QuickBooks is connected, triggers the live tax probe (a dummy-invoice recompute) instead of the stored rate.
location number No Optional, Numeric (numeric strings are coerced), integer Location/department id passed to the live probe.

Example request

curl -X POST 'https://api.example.com/api/invoicing/tax/qbo' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "customerQbo": 5501, "productQbo": 812, "location": 3 }'

Success response — 200 OK

{ "error": false, "taxRate": 8.25 }
Field Type Description
error boolean Always false.
taxRate number Effective tax rate. 0 when unresolvable.

Errors

Status Condition Message/shape
200 No customer id supplied, qbo_customer table absent, or no tax-rate column present { "error": false, "taxRate": 0 }
400 Non-integer values supplied Standard request-validation error array
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • The live probe is attempted only when productQbo > 0 and a firm-internal company resolves. Any probe failure, or a probe result of 0, falls back to the stored rate.
  • The stored rate is read from qbo_customer.tax_rate, falling back to qbo_customer.taxable_rate, whichever column exists.
  • This route never reports an error condition — an unresolvable rate is indistinguishable from a genuine zero rate.

GET/api/company/qbo/:id

Full payload for the Admin → Company → QuickBooks Online Integration page: connection state, module catalogues, per-module last-synced times, live sync status and the caller's edit permission.

Module Integrations > QuickBooks Online
Authentication Required — fully authenticated session (two-factor completed)
Authorization Readable without a role check and without a firm-scope assertion. The caller's edit rights are reported as global_can_edit: true for users.firm_admin = 1 or a Global Master System Administrator (role id 1 or name match); otherwise false when the company name matches /inactive/i, or when the caller's Company-menu (id 48) access for this company is View Only (access_type = 2); true otherwise. Any lookup failure defaults to true.
Content type application/json

Path parameters

Name Type Required Description
id string Yes company.id, plain numeric or encrypted.

Example request

curl 'https://api.example.com/api/company/qbo/412' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "company": {
    "id": 412,
    "encryptId": "cXdlcnR5",
    "name": "Northwind Advisory LLP",
    "qbo_connection": 1,
    "qbo_realmid": "9130350000000000",
    "qbo_file": "Northwind Advisory (Sandbox)",
    "qb_sync_type": 2
  },
  "yearType": 2,
  "moduleArr": { "1": "Account", "2": "Reports", "3": "Customer", "4": "Vendor", "5": "Invoice", "6": "Bill", "7": "Sales Receipt", "8": "Purchase Order", "9": "Credit Memo", "19": "Cash Flow Statement", "20": "Products", "21": "Terms" },
  "reportModuleArr": { "10": "Profit & Loss report", "11": "Balance Sheet report", "12": "General Ledger report", "13": "AR Aging", "14": "AP Aging" },
  "syncArr": { "m_1": "12/03/2025 09:14 AM" },
  "syncStatus": "{\"m_1\":false}",
  "syncError": "{}",
  "ajaxRun": false,
  "global_can_edit": true,
  "syncDetails": [{ "module_id": "1", "last_synced": "12/03/2025 09:14 AM" }]
}
Field Type Description
company object { id, encryptId, name, qbo_connection, qbo_realmid, qbo_file, qb_sync_type }. qbo_file is the QuickBooks company name captured at OAuth time.
yearType number company.qb_sync_type, surfaced at the top level too. Defaults to 1.
moduleArr object { "<moduleId>": "<label>" } for the primary modules. Key "2" ("Reports") is a UI placeholder the page replaces in place with reportModuleArr.
reportModuleArr object { "<moduleId>": "<label>" } for the five report modules (ids 10–14).
syncArr object { "m_<moduleId>": "<last synced>" }.
syncStatus / syncError string JSON-encoded strings, same format as POST /api/qbo/syncstatus.
ajaxRun boolean Whether to poll. Forced false when qbo_connection != 1.
global_can_edit boolean Whether the caller may connect/disconnect/sync/change the sync year.
syncDetails[] array Flat form of syncArr, retained for compatibility with earlier clients: { module_id, last_synced }.

Errors

Status Condition Message/shape
400 Company id not found { "message": "Company not found." }
401 No authenticated session { "message": "These credentials do not match our records." }

Notes

  • Every column read is schema-guarded; a deployment missing qbo_connection / qbo_realmid / qbo_file / qb_sync_type gets a default rather than a query failure.
  • global_can_edit is enforced server-side on the four mutating routes below, not only in the user interface.

GET/api/company/qbo/connect/:id

Returns a QuickBooks consent URL for a specific company from the Admin integration page, with a returnTo that auto-triggers "Sync all" after connecting.

Module Integrations > QuickBooks Online
Authentication Required — fully authenticated session (two-factor completed)
Authorization Two checks: the company must belong to the caller's firm (company.firm_id = <caller's firm>, deleted = 0), and the caller must hold QuickBooks edit rights on it (a firm admin or Global Master System Administrator always does; otherwise the caller is blocked for an "inactive"-named company or a View-Only Company-menu grant).
Content type application/json

Path parameters

Name Type Required Description
id string Yes company.id, plain numeric or encrypted.

Example request

curl 'https://api.example.com/api/company/qbo/connect/412' \
  -b 'genie.sid=<session-cookie>'

Success response — 200 OK

{
  "error": false,
  "url": "https://appcenter.intuit.com/connect/oauth2?client_id=…&state=1764341531123-a1b2c3d4",
  "companyId": 412,
  "state": "1764341531123-a1b2c3d4"
}
Field Type Description
error boolean false on success.
url string Intuit consent URL.
companyId number Company the connection binds to.
state string Correlation state, valid for 15 minutes, single-use.

Errors

Status Condition Message/shape
200 QBO_CLIENT_ID / QBO_CLIENT_SECRET unset { "error": true, "message": "QuickBooks OAuth credentials are not configured (QBO_CLIENT_ID / QBO_CLIENT_SECRET)." }
401 No authenticated session, or the company is not in the caller's firm { "message": "These credentials do not match our records." }
401 Caller lacks QuickBooks edit rights on this company { "message": "You do not have access to change the QuickBooks connection." }

Notes

  • The recorded returnTo is /admin/company/<encrypted id>/quickbooks?s=1; GET /api/qbo/callback hands the popup back to it, and the ?s=1 marker is what makes the page auto-click "Sync all".
  • This differs from GET /api/qbo/connect only in the target company, the permission checks and the returnTo.

POST/api/company/qbo/disconnect

Disconnects a company from QuickBooks: clears the local connection flag and tokens, asks the Lambda to revoke the grant with Intuit, and drops in-flight sync state.

Module Integrations > QuickBooks Online
Authentication Required — fully authenticated session (two-factor completed)
Authorization The company must belong to the caller's firm and the caller must hold QuickBooks edit rights on it.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
company_id string Yes String company.id, plain numeric or encrypted. Declared as a string so the encrypted form is accepted.
lambda string No Optional, string Accepted by the request body schema; it does not affect this operation.

Example request

curl -X POST 'https://api.example.com/api/company/qbo/disconnect' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "company_id": "412" }'

Success response — 200 OK

{ "error": false }
Field Type Description
error boolean Always false.

Errors

Status Condition Message/shape
400 company_id missing or not a string Standard request-validation error array
401 No authenticated session, or the company is not in the caller's firm { "message": "These credentials do not match our records." }
401 Caller lacks QuickBooks edit rights on this company { "message": "You do not have access to change the QuickBooks connection." }

Notes

  • Writes qbo_connection = 0 and blanks qbo_access_token / qbo_refresh_token (each only if the column exists). qbo_realmid and qbo_file are not cleared, so a later reconnect to a different QuickBooks file still trips the company-conflict branch of the callback.
  • Calls LAMBDA_TOKEN_URL with operation: 'revoke' so the Lambda revokes the grant with Intuit and clears its own copy of the tokens. Skipping this would leave the connection live on Intuit's side.
  • Server-side tracked sync state for the company is dropped so the page stops polling immediately.

POST/api/company/qbo/syncall

Triggers a full (or module-scoped) sync for one company from the Admin integration page.

Module Integrations > QuickBooks Online
Authentication Required — fully authenticated session (two-factor completed)
Authorization The company must belong to the caller's firm and the caller must hold QuickBooks edit rights on it.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
company_id string Yes String company.id, plain numeric or encrypted.
lambda string No Optional, string Modules to sync; same vocabulary as POST /api/qbo/import/all. Defaults to 'all'.

Example request

curl -X POST 'https://api.example.com/api/company/qbo/syncall' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "company_id": "412", "lambda": "all" }'

Success response — 200 OK

{
  "error": false,
  "lambda_req": {
    "companies": [{ "id": 412, "modules": ["account", "customer", "vendor", "invoice", "bill", "sr", "po", "cm", "pl", "bs", "gl", "ar", "ap", "class", "location", "vc", "cf", "products", "terms"] }],
    "start_date": "2025-01-01",
    "end_date": "2025-12-31",
    "method": "Accrual"
  }
}

Response shape is that of POST /api/qbo/import/all, which this route delegates to.

Errors

Status Condition Message/shape
200 company.qbo_connection = 0 { "error": true, "msg": "connection error", "company_id": <id> }
200 Any failure of the underlying sync dispatch (no realm id, token check rejected, LAMBDA_SYNC unset, invalid modules) See the error table for POST /api/qbo/import/all
400 company_id missing or not a string Standard request-validation error array
401 No authenticated session, or the company is not in the caller's firm { "message": "These credentials do not match our records." }
401 Caller lacks QuickBooks edit rights on this company { "message": "You do not have access to change the QuickBooks connection." }

Notes

  • This is the permission-enforcing entry point for the same sync dispatch: it adds the firm-scope and QuickBooks edit checks that POST /api/qbo/import/all does not apply, plus an up-front qbo_connection check.
  • The client treats { error: true, msg: 'connection error' } as "QuickBooks dropped us" and re-renders the page as disconnected.
  • Progress is polled through POST /api/qbo/syncstatus.

POST/api/company/qbo/settings/save

Two operations behind one route: change a company's QuickBooks sync-year setting, or clear the recorded last-sync times for some/all modules so they re-sync from scratch.

Module Integrations > QuickBooks Online
Authentication Required — fully authenticated session (two-factor completed)
Authorization The company must belong to the caller's firm and the caller must hold QuickBooks edit rights on it.
Content type application/json

Request body

Field Type Required Validation / allowed values Description
company_id string Yes String company.id, plain numeric or encrypted.
sync_type number No Optional, Numeric (numeric strings are coerced), integer 1 selects the sync-year branch: company.qb_sync_type is set to type_id. Any other value (or omission) selects the reset-sync-times branch.
type_id number No Optional, Numeric (numeric strings are coerced), integer New company.qb_sync_type value on the sync-year branch. Defaults to 1.
module_id string No Optional, string Comma-separated module ids whose sync times should be cleared. Ignored when type_all is "1".
type_all number No Optional, Numeric (numeric strings are coerced), integer When it stringifies to "1", every module for the company is reset regardless of module_id.

Example request

Change the sync year:

curl -X POST 'https://api.example.com/api/company/qbo/settings/save' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "company_id": "412", "sync_type": 1, "type_id": 2 }'

Reset the recorded sync times for two modules:

curl -X POST 'https://api.example.com/api/company/qbo/settings/save' \
  -H 'Content-Type: application/json' \
  -b 'genie.sid=<session-cookie>' \
  -d '{ "company_id": "412", "module_id": "1,12" }'

Success response — 200 OK

{ "error": false }
Field Type Description
error boolean Always false.

Errors

Status Condition Message/shape
400 company_id missing or not a string Standard request-validation error array
401 No authenticated session, or the company is not in the caller's firm { "message": "These credentials do not match our records." }
401 Caller lacks QuickBooks edit rights on this company { "message": "You do not have access to change the QuickBooks connection." }

Notes

  • The reset branch nulls qb_sync_details.sync_time (and qbo_time where that column exists), which is what makes the next sync re-pull the full window.
  • Both branches return { error: false } unconditionally — including when the qb_sync_details table or the qb_sync_type column is absent and nothing was written.
  • Only the value 1 widens the reset to all modules; type_all is compared as a string, so any other value leaves module_id in control.

16.3 Integration Notes

16.3.1 Google OAuth (Drive / Sheets)

Scopes — the default scope set requests exactly three:

  • https://www.googleapis.com/auth/spreadsheets
  • https://www.googleapis.com/auth/drive.file
  • https://www.googleapis.com/auth/spreadsheets.readonly

drive.file is the per-file scope: it grants access only to files the app creates and to files/folders the user explicitly hands over through the Google Drive Picker. That is exactly how this flow works — the Picker's browse UI runs under the user's own Google session, not Genie's token — so the narrow scope is intentional least-privilege. The scope list can be overridden with GOOGLE_DRIVE_SCOPES (space- or comma-separated); it falls back to the three above when unset or empty.

Credentials and redirect URI — the module reuses the same OAuth client as "Sign in with Google":

Variable Purpose
GOOGLE_CLIENT_ID OAuth client id. Validated to end in .apps.googleusercontent.com.
GOOGLE_CLIENT_SECRET OAuth client secret. Rejected with a specific diagnostic if it starts with AIza (that is an API key, not a client secret).
GOOGLE_DRIVE_REDIRECT_URI Redirect URI registered with Google. Defaults to ${APP_URL}/google/drive/callback.
APP_URL Base used to derive the default redirect URI (defaults to http://localhost:3000).
FRONTEND_ORIGIN postMessage target origin for the callback popup; falls back to APP_URL, then the request host.
GOOGLE_PICKER_APP_ID Optional. GCP project number for the Picker's setAppId. Derived from the leading numeric segment of GOOGLE_CLIENT_ID when unset.
GOOGLE_PICKER_DEVELOPER_KEY Browser API key handed to the Picker. Public by design.
GOOGLE_DRIVE_SCOPES Optional scope override.

The registered redirect URI is the front-end path (${APP_URL}/google/drive/callback), which the front-end rewrites server-to-server through to the backend endpoint at GET /google/drive/callback.

Flow — the client calls GET /api/google/drive/auth-url (or POST /api/send/drive, which returns a consent URL when unconnected). The server mints a single-use random state, stores it against the caller's session, and builds an authorisation URL with access_type=offline, prompt=select_account consent and include_granted_scopes=true. prompt=consent is required: without it, re-authorising an already-linked account yields no refresh token and the connection fails at the next expiry. Google redirects the popup to the callback, whose session cookie rides along on the top-level GET (SameSite=Lax) and identifies the user — so the callback must be reached with the same session that requested the consent URL. The state must come back character for character; it is consumed only on the success path (so a cancelled consent does not prevent subsequent attempts), and once consumed a replayed code cannot be re-submitted.

Token storage and refresh — tokens live in the drive_config table (DRIVE_CONFIG_TABLE), one row per user, plus an optional shared row with user_id IS NULL used as a "default account" identity for server-side Sheets reads (GL Account Validation). Access tokens are treated as valid for GOOGLE_TOKEN_DEFAULT_TTL_SECONDS (3600) and refreshed GOOGLE_TOKEN_REFRESH_SKEW_SECONDS (60) before expiry, using the stored refresh token; the refreshed pair is written straight back to the same table. Token errors are classified: invalid_grant / invalid_client / unauthorized_client require re-consent, while network / server_error are retryable, so a transient 5xx from Google never pushes a healthy connection back through the consent screen. POST /api/google/drive/disconnect clears the caller's row locally; it does not revoke with Google. The refresh token never leaves the server — only the short-lived Picker access token is returned to the browser, and only because the Google Drive Picker has no server-side variant.

16.3.2 QuickBooks Online OAuth and connection

Scopes — the consent URL is built against Intuit's OAuth 2.0 authorisation endpoint with scopes com.intuit.quickbooks.accounting, openid, profile, email, phone, address.

Variable Purpose
QBO_CLIENT_ID Intuit app client id. Absent → the connect routes return "QuickBooks OAuth credentials are not configured".
QBO_CLIENT_SECRET Intuit app client secret.
QBO_ENV production selects the production Intuit environment and API base; any other value (and the default) selects sandbox. Compared lower-case after trimming.
QBO_REDIRECT_URI Redirect URI registered on the Intuit app. Defaults to ${BACKEND_ORIGIN}/api/qbo/callback.
BACKEND_ORIGIN Base for the default redirect URI. Defaults to http://localhost:${PORT}.
PORT Used only in that fallback (default 4000).
FRONTEND_ORIGIN Base for every post-callback redirect, including the /task/qbo/callback hand-off page. Defaults to http://localhost:3000.
APP_ENV local disables the Lambda entirely: the token check is skipped and the application performs the sync itself.
LAMBDA_SYNC AWS Lambda / API Gateway endpoint that performs the actual QuickBooks sync. Unset → sync cannot be dispatched.
LAMBDA_TOKEN_URL Endpoint that verifies/refreshes and revokes QuickBooks tokens on Genie's behalf. Unset → the token check is skipped and the connection is assumed healthy.
LAMBDA_CONTENT_TYPE Content-Type sent on both Lambda calls. Defaults to application/x-www-form-urlencoded: the body is a JSON string posted under a form content type, because announcing application/json can cause API Gateway to route the body where it is not read.

Flow — GET /api/qbo/connect (firm-internal company) or GET /api/company/qbo/connect/:id (a specific company) mints a <epoch-ms>-<random> state, records { userId, companyId, createdAt, returnTo } against it in temporary server-side storage with a 15-minute validity window (QUICKBOOKS_OAUTH_STATE_TTL_MS), and returns the Intuit consent URL. Intuit redirects the popup to GET /api/qbo/callback, which consumes the state (single-use, and rejected once the 15 minutes have elapsed), exchanges the code, and before writing anything checks two conflicts: the realm already bound to another company (error_type=1) and this company already carrying a different realm (error_type=2). Both redirect back to the integration page with structured query parameters instead of silently repointing the company. On success it writes qbo_realmid, qbo_access_token, qbo_refresh_token, qbo_state, qbo_connection = 1, last_sync and updated_at onto the company row, then best-effort fetches CompanyInfo.CompanyName into company.qbo_file.

Because the OAuth state is held as temporary server-side storage on the instance that issued the consent URL, GET /api/qbo/callback must be served by that same application instance. Multi-instance deployments must therefore preserve instance affinity for the connect and callback routes; without it, a callback reaching a different instance fails with QuickBooks OAuth state expired. Please try again. See Deployment & Operational Considerations.

Token ownership — outside APP_ENV=local, the Lambda owns the QuickBooks tokens. Genie never rotates them itself: before dispatching a sync it posts to LAMBDA_TOKEN_URL with operation: 'update' and waits (that answer decides whether the sync is dispatched at all); on disconnect it posts operation: 'revoke'. A non-200 from that endpoint is treated as "connection is fine"; only an explicit error: true in a 200 body is a failure. Errors returned from Intuit are never surfaced to the client as received — the full fault is written to the server log and the client sees the generic "There is an error with QuickBooks connection".

16.3.3 File attachment storage

Feedback Center attachments (both Suggestions and Support) are stored in Google Cloud Storage through a generic, feature-agnostic storage layer.

Variable Purpose
GCS_BUCKET Target bucket name. Required before any read or write; unset logs a warning at boot and every storage call then fails.
GCP_PROJECT_ID Optional. Normally inferred from Application Default Credentials.
GCS_ISSUE_FILES_PREFIX Object-key prefix for issue_files objects. Defaults to issue_files/; a missing trailing slash is added.
GOOGLE_APPLICATION_CREDENTIALS Local development only — path to a service-account key file. On Cloud Run the attached service account is used via ADC and nothing needs configuring.

Authentication is Application Default Credentials; no credentials appear in code or in the database. The database column issue_files.file_path stores only the bare filename (e.g. 1764341531_ab12.png); the full object key is derived from the configured prefix and that filename on both write and read, so the prefix can change without a schema change. Uploads are non-resumable single-shot writes tagged with the browser-supplied content type. Downloads are streamed through the authenticated GET /api/issue/files/download/:id[/:name] endpoint rather than via a public object or a signed URL, so the bucket stays private and server memory stays bounded. Signed-URL direct download is not used by this flow. Local disk is not used: the runtime filesystem is ephemeral, so attachments written there would not survive a restart or a scale event.

16.3.4 Outbound e-mail

All Feedback Center notifications go through the application's generic SMTP mail transport.

Variable Purpose
MAIL_HOST SMTP host. Unset → every outgoing e-mail is silently skipped (a warning is logged once).
MAIL_PORT SMTP port. Defaults to 587. Port 465 uses implicit TLS (secure: true); everything else uses STARTTLS (secure: false).
MAIL_USERNAME SMTP username. When absent, the transport is created without auth.
MAIL_PASSWORD SMTP password.
MAIL_ENCRYPTION Accepted as a configuration key, but TLS mode is derived from the port alone and this value is not read.
MAIL_SENDER_ADDRESS From address on every message.
MAIL_SENDER_NAME Default From display name (defaults to Genie). Feedback Center overrides it per-message with genie administrator.
FRONTEND_ORIGIN / APP_URL Base for the deep links in every template (FRONTEND_ORIGIN first, then APP_URL).

Mail is fire-and-forget and never throws to the caller — a failure is logged and the user action that triggered it still succeeds.

Four events send mail:

Event Triggering endpoint Recipients
New suggestion / issue / support ticket POST /api/issue/feedback/save All active users holding assign_user_roles.role_id = 10 (the Genie team). No mail if that set is empty.
Suggestion / issue reply POST /api/issue/feedback/replay/send To: the ticket creator; Bcc: the other subscribers. Sent only when the ticket has at least one subscriber. Attributed to "Anonymous" when customSwitch = 1.
Support chat reply POST /api/support/chat/reply/save The Genie team when the replier is not role 10; the ticket creator when they are. No mail if there is no recipient.
Status change POST /api/issue/save/stauts with btnType = "status" The ticket creator, with the previous and new status labels. Not sent on a classification-only or label-only update.

Deep links point at /issue/list/detail/<encrypted id> for suggestions and issues, and /support/feedback/chat/<encrypted id> for type = 3 tickets. All HTML bodies are sanitised: paste artefacts (background, font-family, font-size, -webkit-text-fill-color, <font> tags) are stripped and near-white text colours are rewritten to a readable dark value — otherwise content pasted from a dark editor is invisible in both the ticket bubble and the e-mail.

↑ Back to Table of Contents


17. Deployment & Operational Considerations

This section collects the behaviours that affect how the Genie API is deployed, scaled and operated. Each item is stated in terms of its impact on a deployment rather than as an internal implementation note.

Area Consideration Impact on the deployment
Sessions Sessions are stored in the database, not in application memory, and every instance reads the same store. No session affinity is required for normal API traffic. The sessions table must exist in the schema before the service starts; it is not created automatically.
Two-step export downloads POST .../export builds a file, holds it in temporary server-side storage on the instance that produced it, and returns a single-use token; GET .../export/generated/:token then streams it. Tokens are single use and do not survive a restart of that instance. The download request must reach the same application instance that created the export. Multi-instance deployments must preserve instance affinity for these routes, or the download returns "not found" even immediately after a successful export. Affects the export/download pairs in Admin, Client Success, Monthly Close, Projects, Reports Builder and Invoicing.
Token-only download routes On several of the two-step download routes the single-use token is the only credential presented; no session is read. Treat these URLs as bearer credentials: they should not be logged, forwarded or shared, and they stop working after first use.
QuickBooks Online OAuth The state value issued by GET /api/qbo/connect (or GET /api/company/qbo/connect/:id) is single use and valid for 15 minutes, and is held in temporary server-side storage on the issuing instance. The Intuit callback (GET /api/qbo/callback) must be served by the instance that issued the consent URL. Load balancing must preserve that continuity for the OAuth routes.
Google Drive / Sheets OAuth The Drive connection flow depends on continuity between the authorization request and the callback, which carries the caller's session. Preserve request continuity between GET /api/google/drive/auth-url and the GET /google/drive/callback completion.
Temporary processing files Company logos, generated import templates and generated export workbooks are written to temporary server-side storage during the request workflow. Deployment architecture should allow for temporary local storage per instance, and should not assume such files are visible to other instances or survive a restart.
Sign-in throttling Failed sign-in attempts are counted per instance (6 attempts, 60-second decay, keyed on email and client IP). With several instances behind a load balancer the effective allowance scales with the number of instances. Deployments that require a strict global limit should enforce it at the edge.
Scheduled work Task recurrence and invoice recurrence generation run on a schedule inside the application process rather than as a separate worker, and are not exposed as HTTP endpoints. Deployment planning should account for scheduled work running inside each application instance.
QuickBooks sync dispatch Outside local environments, company sync is dispatched to an external sync function rather than executed in the request. The sync function endpoint and token must be reachable from the deployment (LAMBDA_SYNC, LAMBDA_TOKEN_URL). Sync status is reported through the Admin QBO Sync Status endpoints.
Request size Bodies up to 20 MB are accepted for both JSON and URL-encoded content; larger bodies return 413. Any reverse proxy, gateway or CDN in front of the service must permit request bodies of the same size, otherwise large Report Writer saves fail before reaching the API.
TLS termination and cookies The service trusts proxy headers so that session cookies are marked Secure when the deployment terminates TLS upstream. Run the service behind an HTTPS-terminating proxy and forward the standard X-Forwarded-* headers, or session cookies will not be issued as secure.
Browser origin Exactly one browser origin is permitted for direct cross-origin calls, configured as FRONTEND_ORIGIN, with credentials enabled. Browser clients should call through the web application's own paths (§3.1). A second origin requires a configuration change.
Health checks GET / and GET /health both verify database connectivity and return 503 when it fails, while the process itself stays up. Use either path for liveness and readiness probes. A 503 from these endpoints indicates a database-connectivity problem rather than a failed process.

↑ Back to Table of Contents


18. Appendix A — Complete Endpoint Index

Every endpoint exposed by the Genie API — 476 documented entries covering 475 distinct method-and-path combinations — grouped by section and subsection. Each path links to its full entry. Auth indicates whether an authenticated session cookie is required: Yes, No, or Token where a single-use token in the URL is the only credential.

One path, POST /api/task/firm, is documented in two places because it is shared by two screens; the canonical entry is identified in both.

A.1 Authentication, Session & Core

25 endpoints — full entries in 8. Authentication, Session & Core.

8.3 Health & CSRF

Method Endpoint Purpose Auth
GET / Backend liveness ping used by hosting/uptime checks. Verifies the database connection as well as the process. No
GET /health Explicit health endpoint. Returns the same payload and behaviour as GET /, for platforms that require a named health path. No
GET /csrf Issues (or returns the existing) synchronizer CSRF token for this session and echoes it in a JS-readable cookie. Must be called before any… No

8.4 Credential Login & Password Recovery

Method Endpoint Purpose Auth
POST /login Verifies email and password and establishes the pre-2FA session. No
GET /login/check The post-credential branch point: tells the client whether the user must select a 2FA method, enrol in Google Authenticator, enter an emailed OTP, or… Yes
POST /password/email Requests a password-reset link for an email address. No
POST /password/reset Consumes a reset token, sets the new password, and signs the user straight in. No

8.5 Two-Factor Verification

Method Endpoint Purpose Auth
POST /login/select-2fa For users whose auth_type is still 0, records their chosen second factor and returns everything the verify screen needs to continue. Yes
POST /login/verify/googleotp Verifies a Google Authenticator TOTP code and completes login. Yes
POST /login/verify/otp Verifies the emailed OTP (for auth_type 2 and 3) and completes login. Yes
POST /login/verify/resend Issues a fresh emailed OTP and restarts the expiry clock. Yes

8.6 Sign in with Google

Method Endpoint Purpose Auth
GET /google/redirect Starts the Google OAuth flow by redirecting the browser to Google's consent screen. No
GET /google/callback Google's OAuth redirect target. Exchanges the authorization code, then logs the user in, auto-links the Google account to a matching Genie account,… No
GET /google/signup Validates the linking code from GET /google/callback against the 1-minute google cookie and returns the Google email for the "One Last Time" linking… No
POST /google/signup Completes account linking: verifies the user's existing Genie email + password, attaches the Google account, and signs them in. No

8.7 Logout

Method Endpoint Purpose Auth
POST /api/logout Tears down the authenticated session for browser clients. No

8.8 Session State & Company Selection

Method Endpoint Purpose Auth
GET /api/check/session The bootstrap call every screen makes at mount. Validates the session and returns the signed-in user, their roles, their per-company menu access map,… Yes
GET /api/company/session Reads the shared company selection and as-on period out of the session, so a fresh browser or a second tab starts on the same company. Yes
POST /api/company/session Changes the company every screen is scoped to. This is the single shared switch point — picking a company on Reports also changes it on Client… Yes
POST /api/company/session/period Changes only the as-on month/year, keeping the selected company. Backs Monthly Close's month/year selects. Yes

8.9 Notifications

Method Endpoint Purpose Auth
GET /api/user-based/notification Returns the 10 most recent notifications for the signed-in user plus a total count — the header bell feed. Yes
GET /api/user-based/notification/all Returns the complete notification list for the signed-in user — the "See All" screen. Yes

8.10 Company & Group Tree

Method Endpoint Purpose Auth
POST /api/get/web-treeview Returns the company/group hierarchy behind the shared "Select company/group" picker, plus flat lists of every visible group and company. Yes

8.11 Role Map Diagnostics

Method Endpoint Purpose Auth
POST /api/utilities/configure/debug/role-map-compare Read-only dry run that compares what the role-map engine would derive for a set of companies against what the database currently holds. Used to… Yes
POST /api/utilities/configure/debug/role-map-inputs Read-only diagnostic dump of the raw inputs the role-map engine reads for a single company — useful when role-map-compare reports a mismatch and you… Yes

A.2 Admin

71 endpoints — full entries in 9. Admin.

9.1 Company

Method Endpoint Purpose Auth
POST /api/company/list Returns the company dropdown for the signed-in user, scoped by their role and by their access to the requested module. Yes
GET /api/firm/list Returns the "Account" (firm) dropdown shown above the Company dropdown for a Global Master System Administrator, who has no firm of their own to… Yes
POST /api/firm/company/list Returns the Company dropdown for one specific firm — the second step after a Global Master System Administrator picks an Account from GET… Yes
POST /api/company/view_all The Admin > Company list: a paged, searchable company table plus every page-level flag and dropdown source the screen needs. Yes
POST /api/company/search Search variant of the company list. Identical contract and behaviour to POST /api/company/view_all. Yes
POST /api/company/newcompany Creates a company, seeds its accounting defaults and service roles, attaches it to a company group, and makes it the session's selected company. Yes
GET /api/company/check_company_exists Live duplicate check for the company-name field in the add form. Yes
POST /api/company/import/template/download Builds the bulk-import Excel template for a firm and returns a one-shot download link. Yes
POST /api/company/import/template Uploads a filled-in import workbook, validates every row, and queues the valid ones for background company creation. Yes
GET /api/company/import/queue/status Reports whether a bulk company-import job is currently running for the caller. Yes
GET /api/company/import/template/generated/:token Streams a previously generated import template workbook and then deletes it. Token
POST /api/company/redirect Called on every company-row click: pins the company into the session, touches its "Last used" timestamp, and returns the edit URL plus registration… Yes
POST /api/company/delete Reactivates, deactivates, or soft-deletes a company. Yes
GET /api/company/details/:id Loads the company edit screen: core details, billing information, the edit permission flag and the QuickBooks reconnection warning. Yes
POST /api/company/save Saves the company edit form (name, domain, budget flag). Yes
POST /api/company/logo/upload Uploads a company logo image. Yes
POST /api/company/logo/remove Removes a company's logo file and clears company.logo. Yes
GET /api/company/logo/:id Streams a company's logo image. Yes
POST /api/company/billInfo/save Upserts the company's billing contact block. Yes
GET /api/company/defineGroups/:id Loads the "Define Reporting Groups" screen for a company: the global groups, the company's custom groups and the Name/Number mode. Yes
POST /api/company/defineGroups Creates a new reporting group — either a global one or a company-specific custom one. Yes
POST /api/company/defineGroups/edit Renames an existing reporting group. Yes
DELETE /api/company/defineGroups Soft-deletes a reporting group, unless it is still referenced by Report Writer. Yes
POST /api/company/defineGroups/type/edit Switches a company between mapping its chart of accounts by account Name and by account Number. Yes
POST /api/company/mapping/get Loads the chart-of-accounts to reporting-group mapping grid for one statement, in either by-name or by-number mode. Yes
POST /api/company/mapping/save Saves the chart-of-accounts mapping for one statement and one mapping mode. Yes
POST /api/company/industry/get Loads the Industry Verticals and Business Model Verticals pickers for a company. Yes
POST /api/company/industry/save Replaces a company's selected verticals for one vertical type. Yes

9.2 Company Groups

Method Endpoint Purpose Auth
POST /api/company-group/list Returns the company-group tree, optionally filtered by a search term, plus the tree's action flags. Yes
POST /api/company-group Creates a company group. Yes
GET /api/company-group/:id Loads the company-group detail screen: the group, its parent options, child-group and company checklists, the users with access, and the placement… Yes
POST /api/company-group/details/save Saves the company-group detail form: name, parent, child groups, member companies and the Manager select. Yes
POST /api/company-group/placement Renders a preview of where a group and its selected children/companies would sit in the tree, using unsaved checkbox state. Yes
POST /api/company-group/user-access/load Loads the per-group user-access editor: the assignable users, the module matrix, and the selected user's current grants. Yes
POST /api/company-group/user-access/save Grants or updates one user's access within a company group, in either group-wide or per-company mode. Yes
DELETE /api/company-group/user-access Revokes a user's access to a company group. Yes
DELETE /api/company-group Deletes a company group and its whole descendant subtree. Yes
POST /api/company-group/firm-accounting-managers Loads the "Manage Access to Internal Firm Accounting" screen for the caller's firm. Yes
POST /api/company-group/save-firm-accounting-managers Assigns the Firm Internal Accounting Manager role on the firm's internal company and rebuilds the corresponding internal group and access fan-out. Yes
DELETE /api/company-group/company Detaches one company from one company group, without touching the group. Yes

9.3 Users

Method Endpoint Purpose Auth
GET /api/user/list Returns the Admin > Users list (active users) together with every page-level flag and dropdown the screen needs. Yes
GET /api/user/module/access Returns the Permission Exclusion configuration — the module_permission rows grouped by module. Yes
POST /api/user/global Switches the session out of any selected company and into "Global" scope. Yes
POST /api/user/search Filtered/sorted user list. Returns the same envelope as GET /api/user/list minus allRoles. Yes
POST /api/user/add_new Creates a user, assigns roles, and emails them a create-password link. Yes
POST /api/user/save/active Applies a status change to a user: activate, deactivate, or delete. Yes
POST /api/user/delete Finalizes the User Check workflow — the "In Active" / "Delete" button that appears once every dependency count reaches zero. Yes
POST /api/user/check Loads the "User Check" screen: every open dependency the user still holds, grouped into sections, with the alternatives available for reassignment. Yes
POST /api/user/check/update Applies one section's reassignments from the User Check screen and returns the refreshed counts. Yes
POST /api/subscription/user/switch Impersonates another user ("Login as this user"): rewrites the session to that user and pins their first accessible company. Yes
GET /api/user/details/:id Loads the user detail screen: profile, roles, module/company access, multi-inbox emails, and the flags that lock parts of the form. Yes
GET /api/user/details/:id/:type Same as GET /api/user/details/:id, but flags the request as the "profile" variant. Yes
POST /api/user/details/save Saves the user detail form: names, emails, phone, profile path and global role assignments. Yes
POST /api/user/details/auth/save Sets the user's two-factor authentication method and, for Google Authenticator, returns the enrolment secret and QR code. Yes
POST /api/user/save_new_access Grants a user access to a company or a company group from the "New Company/Group Access" modal. Yes
POST /api/user/new_access Loads the data behind the "New Company/Group Access" modal: the selectable tree, the module list and the plan-derived menu sets. Yes
POST /api/user/details/access/save Saves edits to a user's existing company and group access grids from the detail screen. Yes
POST /api/user/batch/access/save Applies one access matrix to many groups and companies at once ("batch access"). Yes
POST /api/user/details/profile/save Uploads a profile picture and returns it as an inline data URL. Yes

9.4 Reporting Groups

Method Endpoint Purpose Auth
POST /api/reporting-group/list Returns the three reporting-group tabs (firm, cross-firm, multi-entity) for the reporting-groups landing screen. Yes
POST /api/reporting-group/add Creates a reporting group. Yes
POST /api/reporting-group/search Type-scoped search across reporting groups. Yes
POST /api/reporting-group/delete Soft-deletes a reporting group. Yes
POST /api/reporting-group/clone Duplicates an existing reporting group under a new name. Yes
GET /api/reporting-group/:id Loads the reporting-group editor: the group, the company-group dropdown, the candidate companies and the currently selected ones. Yes
POST /api/reporting-group/company/group Returns the companies available under a company group; the reporting-group editor calls it whenever the company-group dropdown changes. Yes
POST /api/reporting-group/company/save Saves the reporting-group editor: renames the group, re-points its company group, and syncs its member companies. Yes

9.5 Logs (Task Audit / Activity Log)

Method Endpoint Purpose Auth
GET /api/logs/filters Returns everything the log page's filter bar needs: the user dropdown and the company/company-group lists. Yes
POST /api/logs/filter Returns the filtered activity-log rows. Yes
POST /api/logs/details Returns the module-access before/after detail for one log row. Yes

9.6 QBO Sync Status

Method Endpoint Purpose Auth
GET /api/qbo/sync/history Returns the sync-history board: every company, every client, and the Lambda sync runs their histories reference. Yes

A.3 Client Success

41 endpoints — full entries in 10. Client Success.

10.1 Client Info — 10.1.1 Company Info Configuration

Method Endpoint Purpose Auth
GET /api/cs/company-info/:type Lists the Company Info field definitions for either the global or the firm scope, together with whether the current user may edit them. Yes
PUT /api/cs/company-info Creates a new Company Info field definition in the global or firm scope. Yes
PATCH /api/cs/company-info/:id Updates an existing Company Info field definition. Yes
DELETE /api/cs/company-info/:id Soft-deletes a Company Info field definition. Yes
POST /api/cs/company-info/change/order Persists a drag-and-drop reordering of the Company Info field definitions. Yes

10.1 Client Info — 10.1.2 Client Contacts Configuration

Method Endpoint Purpose Auth
GET /api/cs/company-contacts/:type Lists the Client Contacts column definitions for the global or firm scope. Yes
PUT /api/cs/company-contacts Creates a new Client Contacts column definition. Yes
PATCH /api/cs/company-contacts/:id Updates a Client Contacts column definition, unless it is a static system column. Yes
DELETE /api/cs/company-contacts/:id Soft-deletes a Client Contacts column definition, unless it is static. Yes
POST /api/cs/company-contacts/change/order Persists a reordering of the Client Contacts column definitions. Yes

10.1 Client Info — 10.1.3 Company Data

Method Endpoint Purpose Auth
POST /api/cs/company-info/details Returns one company's Company Info tab: every field definition in the company's scope, joined to that company's stored value. Yes
POST /api/cs/company-info/details-save Saves one Company Info value for one company — an upsert into company_info_details. Yes
POST /api/cs/company-contacts/details Returns one company's Client Contacts grid: the column definitions plus the row/value matrix, along with the company's email-capture flag. Yes
POST /api/cs/company-contacts/details-save Saves one cell of the Client Contacts grid — an upsert keyed on (field, company, row order). Yes
POST /api/cs/company-contacts/deleteDetail Deletes an entire contact row (all columns at one row-order position) for a company. Yes
POST /api/cs/company-contacts Toggles the company-level email-capture flag (company.email_capture). Yes

10.1 Client Info — 10.1.4 Template Download and Import

Method Endpoint Purpose Auth
POST /api/cs/company-info/download/file Generates an Excel import template for one of four Client Info models and returns a single-use download link. Yes
POST /api/cs/company-info/import/file Uploads a filled-in Excel template and imports its rows. Yes
GET /api/cs/company-info/download/generated/:token Streams a previously generated template or error workbook, then deletes it. Token

10.2 Configure — 10.2.1 Service Roles

Method Endpoint Purpose Auth
POST /api/utilities/configure/get Loads the Client Success Configure screen for either the Global or the Firm tab: the service role list with pricing and flags, the service lines, and… Yes
POST /api/utilities/configure/save Creates a new client service role. Yes
POST /api/utilities/configure/update Multi-purpose role/setting update. The colName field selects one of three distinct behaviours: the firm-level Service Line Approver master switch,… Yes
POST /api/utilities/configure/delete Changes a service role's activation state — restore, inactivate, or delete. Inactivating a role that is still assigned returns the list of companies… Yes
POST /api/utilities/service/role/update Persists a drag-and-drop reordering of the service role list. Yes
POST /api/utilities/configure/firm/update Toggles "Use Firm Settings" (firms.use_service_role). Turning it on clones the global configuration into the firm and queues a role-map rebuild. Yes
POST /api/utilities/configure/getRolesAndCompany For a role being retired: lists the alternate roles available and the companies currently using it. Step one of the reassignment flow. Yes
POST /api/utilities/configure/updateAlterRoles Reassigns every company using a role to a chosen alternate, repoints tasks to a budget role, and inactivates the original. Step two of the… Yes

10.2 Configure — 10.2.2 Client Info Start Date

Method Endpoint Purpose Auth
POST /api/utilities/firm-configure Reads the effective Start Date values for one company: its custom value and the global default. Yes
POST /api/utilities/firm-configure/update Saves the Start Date custom value and/or the global default. Yes

10.2 Configure — 10.2.3 Service Team

Method Endpoint Purpose Auth
POST /api/utilities/configure/get-roles Loads the whole Service Team panel for one company: the role catalogue, the assigned team, selectable users, service lines, override state and the… Yes
POST /api/utilities/configure/save-roles Assigns a user to a service role for a company, or updates the company's role overrides. Which of the two it does is selected by override. Yes
POST /api/utilities/configure/save-roles-only Adds a service role to a company's Service Team without assigning anybody — the row is created (or reset) with user_id = NULL. Yes
POST /api/utilities/configure/remove-roles Removes the user from a Service Team row, leaving the role in place. Yes
POST /api/utilities/configure/save-role-price Sets the hourly rate on one Service Team row. Yes
POST /api/utilities/configure/override-default-price Toggles a company's "override default pricing" flag, and optionally restores every one of its Service Team rows back to the role default rates. Yes
POST /api/utilities/configure/save-sl-invoice-approver Saves one of the two per-company Service Line Invoice Approver checkboxes — the master switch, or one specific service line. Yes

10.2 Configure — 10.2.4 Client Tags

Method Endpoint Purpose Auth
POST /api/utilities/configure/tags/get Lists the firm's active client tags. Yes
POST /api/utilities/configure/tags/save Saves the firm's tag list: creates new names, revives previously deleted ones, and soft-deletes any active tag not present in the payload. Yes
POST /api/utilities/configure/save/tags Compatibility route: retained for compatibility with earlier clients. POST /api/utilities/configure/tags/save is the canonical route; behaviour is… Yes
POST /api/utilities/configure/tags/update Renames a single tag by id, preserving its task associations. Yes
POST /api/utilities/configure/tags/delete Soft-deletes a single tag by id. Yes

A.4 Tasks & Time

184 endpoints — full entries in 11. Tasks & Time.

11.1 Tasks

Method Endpoint Purpose Auth
GET /api/task/filter-options Returns every dropdown option set the Task List filter panel and the Add/Edit Task form need, scoped to the caller's firm and company access. Yes
GET /api/task/multi-task/list Returns a flat { roleOptions, userOptions } pair for the Reports Builder preview's Role/User global-filter dropdowns. Yes
POST /api/task/company/projects Returns the assignable roles/users and the project list for one company — the payload the Add/Edit Task form loads after a company is picked. Yes
POST /api/task/get-assigned-name Resolves the single user who should be auto-assigned for a given service role at a given company. Yes
POST /api/task/company/authUserRoles Given a company and a set of candidate service roles, returns the role/user pair to pre-select when exactly one role matches. Yes
POST /api/task/bulk/fetch/clients Lists the company/project pairs eligible for a bulk task creation run, optionally narrowed by project, service line, invoice detail or invoicing type. Yes
POST /api/task/bulk/create/tasks Creates one task per selected client/project from a task template, in a single call. Yes
PUT /api/task Creates a single task. Yes
PATCH /api/task/:id Updates an existing task using the same field set as create. Yes
POST /api/task/get-task Loads the full Task Details payload for one task: field values, resolved dropdown labels, steps, resources, recurrence, latest time history, the… Yes
POST /api/task/clone Creates a copy of an existing task, including its steps and resources, under a new name. Yes
POST /api/task/makeToTemplate Converts an existing task into a reusable task template, carrying over its settings, steps and recurrence. Yes
GET /api/task/:id/time-history Returns the caller's most recent time-history row for a task — the timer state the Task Details view resumes from. Yes
PUT /api/task/time-history Creates (or updates, when id is supplied) a time-history row for a task — used when the timer is stopped. Yes
POST /api/task/time-history/update Updates only the end time of an existing time-history row. Yes
POST /api/task/task-history Returns the per-entry time log for a task: who logged time, when, how much was captured and how much was reported. Yes
POST /api/task/time/disposition Records the outcome of working a task — Complete / Skip / Defer / Reassign, or Reopen — together with the time captured and its… Yes
POST /api/task/recurrence/all Creates or updates the recurrence rule attached to a task. Yes
PATCH /api/task/update/status/:id Changes a task's service-line status (the workflow status, not the disposition). Yes
PATCH /api/task/time-notes/:id Replaces a task's time notes. Yes
GET /api/task/:id/step/list Lists the checklist steps attached to a task, in order. Yes
POST /api/task/step/all Saves the full set of checklist steps for a task in one call — updating existing rows and inserting new ones. Yes
DELETE /api/task/step/:id Deletes one checklist step. Yes
GET /api/task/:id/resource/list Lists the reference links (resources) attached to a task. Yes
POST /api/task/resource/all Saves the full set of resources for a task. Yes
DELETE /api/task/resource/:id Deletes one task resource. Yes
POST /api/task/list The main Task List query: filtered, sorted and paginated tasks for the caller's company scope. Yes
POST /api/task/list/export-excel Exports the full filtered Task List as an Excel workbook, returned as a base64 data: URI. Yes
POST /api/task/list/export-google-sheet Builds the same Task List workbook and uploads it straight into a chosen Google Drive folder, where Drive converts it to a native Sheet. Yes
POST /api/task/multi-task/update Applies one or more field changes to a set of tasks at once (the Multi-Edit grid action). Yes
POST /api/task/multi-task/delete Soft-deletes a set of tasks. Yes
DELETE /api/task/:id Deletes a single task. Yes
POST /api/task/multi-task/skip Marks a set of tasks as skipped. Yes
POST /api/task/home/list Returns the Home dashboard's saved view definition together with the task list rendered through it. Yes
GET /api/task/getOpenTask/list Returns the caller's currently open tasks with their captured time, for the timer/open-task widget. Yes
POST /api/task/audit/list The Task Audit Log: paginated, filtered audit rows, optionally accompanied by the dropdown option sets the filter bar needs. Yes
GET /api/task/company/resource/:companyId Lists the Client Info company resources (shared reference links) for a company. Yes
POST /api/task/company/resource/all Saves the full set of Client Info company resources for a company. Yes
DELETE /api/task/company/resource/:id Deletes one Client Info company resource. Yes

11.2 Task Board

Method Endpoint Purpose Auth
POST /api/task/board/view Returns the boards visible to the caller, optionally together with the task list rendered through the default board. Yes
POST /api/task/board/addEditBoard Creates a new board or replaces an existing one, including its full column list. Yes
POST /api/task/board/removeBoard Soft-deletes a board and its columns. Yes
POST /api/task/board/default Sets — or clears — the caller's personal default board. Yes
POST /api/task/board/updatePin Pins or unpins a board for the calling user. Yes
POST /api/task/board/reorderBoards Re-orders boards to match the submitted sequence. Yes

11.3 Calendar

Method Endpoint Purpose Auth
POST /api/task/calendar Returns the calendar grid for a date range: tasks grouped by user and day, plus the user picker and the caller's permission to view other users. Yes
POST /api/task/calendar/position Applies a drag: reschedules a task to a new day, optionally reassigns it to another user, and persists the per-day ordering of the affected columns. Yes

11.4 Projects

Method Endpoint Purpose Auth
POST /api/task/project/based/data Returns the service line, billing, invoicing and fixed-fee facts for one project — used to auto-fill the Add/Edit Task form once a project is chosen. Yes
POST /api/task/project/getProjectData The Projects grid query: a paginated, filtered project list plus the dropdown option sets the page needs. Yes
PUT /api/task/project Creates a project. Yes
PATCH /api/task/project/:id Updates a project. Yes
DELETE /api/task/project/:id Soft-deletes a project, appending (deleted) to its name. Yes
POST /api/task/project/active/status Activates or inactivates a project. Yes
POST /api/task/project/comment Returns a project's per-period comments, guaranteeing an entry exists for the current month. Yes
POST /api/task/project/comment/save Saves a project's per-period comments. Yes
POST /api/task/project/download/file Generates the Project Import Excel template and returns a one-time download link. Yes
POST /api/task/project/import/file Uploads a filled-in Project Import template, validates every row, imports the valid ones and reports the rest. Yes
POST /api/task/project/import/save Saves a set of reviewed/corrected import rows without re-uploading a file. Yes
GET /api/task/project/download/generated/:token Streams a previously generated project template or import-error workbook. Token
POST /api/task/project/budget/default Returns the firm's default budget configuration data: service roles with their default percentages, service lines, and the projects the defaults can… Yes
POST /api/task/project/budget/servicerole Returns the Primary Budget grid for one project: its monthly budget/minimum plus the per-service-role price, default percentage, custom percentage… Yes
POST /api/task/project/budget/override Returns the per-role override grid (custom percentage and monthly hours) for one project. Yes
POST /api/task/project/save/budget Saves a project's primary monthly budget and its per-service-role allocation, transactionally. Yes
POST /api/task/project/budget/save/servicerole Saves the firm's default budget allocation, or one of the two per-project override variants — the branch is chosen by type. Yes
POST /api/task/project/budget/validate Checks that every service role referenced by a budget allocation has a usable Price Per Hour before the allocation is applied. Yes
POST /api/task/project/budget/error/export Turns a list of budget-apply error messages into a downloadable CSV. Yes

11.5 Task & Time Templates

Method Endpoint Purpose Auth
POST /api/task/template/groupList Lists template groups for the Global or Firm Templates tab, with optional service-line / task-type Yes
GET /api/task/template/group/included/:id Returns one template group together with the templates included in it — the "included templates" panel. Yes
GET /api/task/template/edit/group/:id Backs the Edit Template Group page: the group plus both sides of the template picker (already selected Yes
GET /api/task/template/group/based/template/:id Returns a template group and the templates it contains — the read-only "apply this group" view used when Yes
PUT /api/task/template/groupList Creates a template group (or updates one when id is supplied in the body), including its template Yes
PATCH /api/task/template/groupList/:id Updates an existing template group. Identical to the PUT above except the id comes from the path. Yes
DELETE /api/task/template/groupList/:id Soft-deletes a template group. Yes
POST /api/task/template/cloneTemplateGroup Clones an existing template group under a new name, optionally re-selecting which templates it contains. Yes
POST /api/task/template/templateList Lists individual templates for the Global or Firm Templates tab, with the same scoping and filtering Yes
PUT /api/task/template/templateList Creates a template (or updates one when id is supplied). Yes
PATCH /api/task/template/templateList/:id Updates an existing template; the id comes from the path. Yes
DELETE /api/task/template/templateList/:id Soft-deletes a template, its settings and its group mappings — with a two-step confirmation when the Yes
POST /api/task/template/cloneTemplate Clones a single template (and its settings) under a new name. Yes
POST /api/task/template/template-setting Loads the Template Settings editor: the saved settings for a template plus every dropdown's option set. Yes
PUT /api/task/template/template-setting Saves the Template Settings editor: the settings row, its steps and its recurrence. Yes
POST /api/task/template/template-setting/service-line/options Returns the task-type and task-status option lists for one service line — the dependent-dropdown refresh Yes
POST /api/task/template/removeStep Soft-deletes one step from a template, addressed by its ordinal position. Yes
POST /api/task/template/clone The "Use Firm Task/Project Templates" first-time switch: copies the whole global template catalogue into Yes
POST /api/task/template/firm/checkBoxData Returns the firm's template usage flags and whether the firm already has task/project data — the state the Yes
POST /api/task/firm Flips the firm's template usage flags. This path is also documented in the Configure subsection, and that entry is the canonical one — see the note… (duplicate path) Yes
POST /api/task/template/from/group Creates real tasks from a template group's templates — the "apply template group" action. Takes a raw Yes

11.6 Configure

Method Endpoint Purpose Auth
POST /api/task/configs/serviceline/group Loads the whole Tasks & Time Configure page in one call: priorities, billing types, service lines with Yes
POST /api/task/configs/clone/status Reports whether the firm is set to use its own configuration and whether the global-to-firm clone has Yes
POST /api/task/configs/serviceline/clone Copies the global priorities, service lines, task types and statuses into the firm — the "Use Firm Yes
POST /api/task/configs/set/priority/high Marks one priority as the firm's (or the global) "high priority", clearing the flag on all others in the Yes
POST /api/task/configs/priority/inactive Inactivates a priority after repointing everything that used it to a replacement priority. Yes
POST /api/task/config/service-line/inactive Inactivates a service line, repointing tasks to a replacement service line, task type and task status. Yes
POST /api/task/type/inactive Inactivates a task type after repointing everything that used it. Yes
POST /api/task/change/priority/order Persists the drag-and-drop ordering of priorities. Yes
POST /api/task/config/change/order Persists the drag-and-drop ordering of service lines, task types or statuses, selected by from. Yes
POST /api/task/home/order Replaces the firm's Home View column list and ordering, creating the Home View board if it does not exist. Yes
POST /api/task/config/save Adds a configuration value. Only colName = "priority" is implemented. Yes
PUT /api/task/configs/serviceline Creates or updates a service line. Yes
PUT /api/task/type Creates or updates a task type. Yes
PATCH /api/task/type/:id Updates a task type; the id comes from the path. Yes
DELETE /api/task/type/:id/:status Deletes or (in)activates a task type, depending on the status segment. Yes
PUT /api/task/status Creates or updates a task status under a service line. Yes
PATCH /api/task/status/:id Updates a task status; the id comes from the path. Yes
DELETE /api/task/status/:id Soft-deletes a task status. Yes
POST /api/task/manage/status Renames a task status. Yes
POST /api/task/status/inactive Inactivates a task status after repointing tasks to a replacement status. Yes
DELETE /api/task/configs/serviceline/:id/:deleted Deletes or (in)activates a service line, depending on the deleted segment. Yes
POST /api/task/config/update Renames a configuration value. Only colName = "priority" is implemented. Yes
POST /api/task/config/delete Deletes or (in)activates a configuration value. Only colName = "priority" is implemented. Yes
POST /api/task/update-alternate-type Repoints one task's lookup columns after a configuration value it referenced was inactivated — the Yes
POST /api/task/firm Flips a firm-level usage flag (use_task / use_project / use_service_line), optionally recording that the configuration clone has been done. This is… Yes
POST /api/task/utilization/config Loads the Utilization configuration tab: the firm's service-role targets, its users' overrides, and the Yes
POST /api/task/utilization/config/save Saves one utilization row — either a role-level target or a user-level override. Yes

11.7 Reports (Reports Builder)

Method Endpoint Purpose Auth
POST /api/tat/reports/list Returns every report the caller can see for one module, bucketed by placement, plus the user list used by Yes
GET /api/tat/reports/profitability/access Tells the client whether the current user may see profitability reports. Yes
POST /api/tat/reports/update Favourites, un-favourites or deletes a report from the list screen. Yes
POST /api/tat/reports/clone Copies a report — its list row and all of its column details — under a new name and placement. Yes
POST /api/tat/reports/share Shares a report with a set of users in the caller's firm. Yes
POST /api/tat/reports/update/order Persists drag-and-drop ordering within one bucket of the report list. Yes
POST /api/tat/reports/details Loads the report editor: the report's saved columns and row groups, plus the full catalogue of columns Yes
POST /api/tat/reports/getcolumn Returns the default value sets used to populate filter dropdowns in the report editor. Yes
POST /api/tat/reports/getdate Returns the report's reference date column and the date window the preview should open with. Yes
POST /api/tat/reports/save Creates or updates a report definition — the list row, its row groups and its full column-detail set. Yes
POST /api/tat/reports/preview Runs a saved report and returns rows, row-group summaries and totals — the main reporting call. Yes
POST /api/tat/reports/make/export Generates an XLSX workbook for a report and returns a one-time download link. Yes
GET /api/tat/reports/export/generated/:token Streams a previously generated export workbook. One-time, session-bound. Yes
POST /api/tat/reports/standard/report/dropdown Returns the option lists for the Custom tab's pre-built ("standard") report filters. Yes
POST /api/tat/reports/standard/report/preview Runs one of the four pre-built standard reports. Yes
POST /api/tat/reports/standard/report/export Generates an XLSX workbook for one of the standard reports and returns a one-time download link. Yes

11.8 Invoicing — 11.8.1 Create Invoice

Method Endpoint Purpose Auth
GET /api/invoicing/fetch/dropdown Returns every dropdown/reference list the Create Invoice and Ad Hoc Invoice screens need in one payload — companies, company groups, projects,… Yes
POST /api/invoicing/batch/list The single list endpoint behind the Create Invoice screen. Depending on status it returns the un-invoiced queue, creates preview invoices from the… Yes
POST /api/invoicing/todo/pending/approval-list Lists the batch invoices awaiting approval that are attached to a given to-do, for the Home/To-do invoice modal. Yes
POST /api/invoicing/preview/un-invoice-to-preview Converts explicitly selected un-invoiced queue rows into Preview batch invoices. Yes
POST /api/invoicing/queue/delete Removes lines from the un-invoiced queue without invoicing them: hourly lines have their billable time moved to non-billable, fixed-price lines are… Yes
POST /api/invoicing/queue/not-approved/timesheet/list Returns the users/weeks whose timesheets are not yet approved for the companies in scope — the blocking warning shown before previewing invoices. Yes
POST /api/invoicing/product/config Resolves the description, QBO class and rate to apply to an invoice line when a product/item is selected. Yes
POST /api/invoicing/preview/delete Deletes preview (or any) invoices with a full cascade: restores hours and fixed-price occurrences to the queue, removes approver to-dos, and voids… Yes
POST /api/invoicing/preview/update Saves an edited invoice: header fields, line items, dates and amounts. Editing an already-approved or posted invoice unposts it and returns it to the… Yes
POST /api/invoicing/preview/route/approval Routes one or more preview invoices for approval: seeds approvers, generates approver to-dos and sets the batch status to Invoice. Yes
POST /api/invoicing/preview/disposition Records an approver's decision on an invoice (approve / reject / unapprove / re-submit) and recomputes the batch status. Yes
POST /api/invoicing/preview/post/comment Adds a quick comment to an invoice and records a Commented audit entry. Yes
POST /api/invoicing/fetch/custorcomp Two-way lookup between a Genie company and its mapped QuickBooks customer — used by the Ad Hoc Invoice screen to fill the counterpart field when… Yes
POST /api/invoicing/adhocinvoice/save Creates an ad-hoc invoice or credit memo directly, bypassing the un-invoiced queue. The new document is created in Invoice status with approvers and… Yes

11.8 Invoicing — 11.8.2 Configure

Method Endpoint Purpose Auth
POST /api/task/invoicing/config Loads an Invoicing Configure screen's data set. The from discriminator selects which screen: the default (managers, users, messages, dates), the… Yes
POST /api/task/invoicing/config/save-invoice-manager Sets the firm's Invoice Managers (role 14) by assigning and de-assigning the role. Yes
POST /api/task/invoicing/config/save-invoice-approvers Sets the firm's default invoice approvers per service role, then re-resolves approvers on every un-approved invoice in the firm. Yes
POST /api/task/invoicing/config/save-sl-approvers Sets the firm's default approvers per service line + service role, then re-resolves approvers on existing un-approved invoices. Yes
POST /api/task/invoicing/config/save-date One endpoint serving three mutually exclusive date settings: the invoice start date, the approver to-do due-date offset, and the… Yes
POST /api/task/invoicing/config/save-message Saves the firm's default "Message on Invoice" and "Message on Statement" text. Yes
POST /api/task/invoicing/config/gl-product Loads the mapping data for one of three Configure screens, selected by from: GL Product/Service mapping, Labor Items, or Service-Line Class Mapping. Yes
POST /api/task/invoicing/config/gl-customer Loads the GL Customer mapping grid — each of the firm's companies with its mapped QBO customer, payment term, class and location, plus the dropdown… Yes
POST /api/task/invoicing/config/save-gl-product Saves the firm's GL Product/Service mapping — which QBO item, description, class and location each service role bills to. Yes
POST /api/task/invoicing/config/save-labor-items Flags which QBO items count as labor for the firm's internal company. Yes
POST /api/task/invoicing/config/save-gl-customer Saves the GL Customer mapping — which QBO customer, payment term, class and location each company invoices to. Yes
POST /api/task/invoicing/config/save-sl-class Saves the Service-Line → QBO Class mapping for the firm. Yes
POST /api/task/invoicing/config/export/file Generates the GL Customer Mapping Excel template (a blank mapping sheet plus an "Available Data" reference sheet) and returns a one-shot download… Yes
POST /api/task/invoicing/config/import/file Imports a completed GL Customer Mapping workbook. Valid rows are saved even when other rows fail. Yes
GET /api/task/invoicing/config/export/file/generated/:token Streams a previously generated Excel template (GL Customer Mapping or Fixed Price Invoicing) and deletes it. Token
POST /api/task/invoicing/config/fixed-price Loads the fixed-price item list for a project, together with the QBO product list needed to edit it. Yes
POST /api/task/invoicing/config/fixed-price/save Inserts or updates the fixed-price item lines for a project. Yes
DELETE /api/task/invoicing/config/fixed-price/:id Soft-deletes a single fixed-price item line. Yes
POST /api/task/project/fixed/download/file Generates the Fixed Price Invoicing Excel template for the firm — a blank entry sheet plus "Existing Data" and "Available Data" reference sheets —… Yes
POST /api/task/project/fixed/import/file Imports a completed Fixed Price Invoicing workbook, inserting new price-list rows and updating existing ones by their internal id. Yes

11.8 Invoicing — 11.8.3 Post to GL

Method Endpoint Purpose Auth
GET /api/invoicing/post-to-gl/get Returns the Post to GL grid — one row per posting batch, with its derived status, invoice counts and any QuickBooks error messages. Yes
POST /api/invoicing/post-to-gl/post Posts an entire batch to QuickBooks Online, or unposts it. This is the primary QuickBooks write path. Yes
POST /api/invoicing/bulk/action Applies Post, UnPost or Delete to an explicit set of invoice ids from the invoice list. Yes

11.8 Invoicing — 11.8.4 Timesheet Details

Method Endpoint Purpose Auth
POST /api/invoicing/timesheet/details Returns the timesheet entries behind a set of invoice line items, for the Timesheet Details modal on an invoice. Yes
POST /api/invoicing/timesheet/details/edit Saves edits made in the Timesheet Details modal: writes the grid back to time_history, optionally returns entries to the un-invoiced queue, rebuilds… Yes
POST /api/invoicing/timesheet/details/export Turns the rows currently shown in the Timesheet Details grid into a CSV payload for client-side download. Yes

11.8 Invoicing — 11.8.5 Invoicing Audit Log

Method Endpoint Purpose Auth
POST /api/invoicing/audit/list Returns the Invoicing Audit Log — every recorded invoice action with the user, role, invoice number and before/after values — with filtering, sorting… Yes
POST /api/invoicing/audit/dropdownData Returns just the filter dropdown data for the Invoicing Audit Log — the firm's users and the roles actually assigned within it. Yes

11.9 Timesheet — 11.9.2 Timesheet Entry

Method Endpoint Purpose Auth
POST /api/task/timesheet/weekly Loads the dropdown catalogues for the Daily/Weekly timesheet grid — selectable users, companies, projects (grouped by company), billing types,… Yes
POST /api/task/timesheet/filter Returns one user's timesheet rows for a week (or a single day), together with the edit-permission flags, the week's disposition status, and the… Yes
POST /api/task/timesheet/save Persists a batch of timesheet rows for one user — creating manual entries, updating existing rows, and soft-deleting removed ones — and writes an… Yes
POST /api/task/timesheet/weekly/status Records a disposition transition (submit / unsubmit / approve / unapprove / reject / void / unvoid) for one user's week, applies the approval flags… Yes

11.9 Timesheet — 11.9.3 Timesheet Reports

Method Endpoint Purpose Auth
POST /api/task/timesheet/user/list Returns the user dropdown for the Timesheet Reports screen for a given week. Yes
POST /api/task/timesheet/weekly/list Returns the Timesheet Reports roster: one row per candidate user for the week, with their disposition status and whether the caller may open their… Yes
POST /api/task/timesheet/weekly/export Builds a styled .xlsx workbook of timesheet data and returns it as a base64 data: URI in the JSON body. Four different payload shapes select four… Yes

11.9 Timesheet — 11.9.4 Timesheet Configure

Method Endpoint Purpose Auth
POST /api/task/timesheet/config Loads the Timesheet Configuration grid for one firm: every visible firm user with their approver, permission flags, cost rate and full/part-time… Yes
GET /api/task/timesheet/firm/dropdown Returns every non-deleted firm as { id, name }, for the Global Timesheet Configuration screen's firm selector. Yes
POST /api/task/timesheet/config/save Saves the Timesheet Configuration grid. Two mutually exclusive modes: toggling the firm's use_timesheet master switch, or rewriting the per-user… Yes

11.9 Timesheet — 11.9.5 Timesheet Audit Log

Method Endpoint Purpose Auth
POST /api/task/timesheet/audit/list Returns a paged, filtered, sorted list of timesheet audit records — the Created / Updated / Deleted trail written by POST /api/task/timesheet/save.… Yes
POST /api/task/timesheet/audit/dropdownData Returns the user list that populates the Audit Log's "User" filter dropdown. Yes

A.5 Monthly Close

83 endpoints — full entries in 12. Monthly Close.

12.1 Close Status

Method Endpoint Purpose Auth
POST /api/mc/peer/review/status/filter Returns one company-group node's Close Status rows — that group's own direct companies (paged) plus its direct child-group ids, which the client… Yes
POST /api/mc/status/manager/closeDeadline The Calendar tab: per-manager completed/total counts bucketed by the firm's configured calendar periods (day-of-month ranges), plus the drill-down… Yes
POST /api/mc/status/manager/unClosedDeadline The Relative tab: per-manager counts of unclosed companies bucketed by how many days past/due the deadline is. Yes
POST /api/mc/status/manager/completionResults The Completion tab: per-manager closed-on-time / closed-late / unclosed / no-deadline counts with percentages. Yes
POST /api/mc/status/manager/makeExport Generates the drill-down modal export workbook from rows the client posts back, and returns a one-shot download link. Yes
POST /api/mc/status/manager/makeIndivudualExport Generates the grid export workbook by re-running the requested tab's own query server-side, and returns a one-shot download link. (The route path is… Yes
GET /api/mc/status/manager/download/generated/:token Streams a previously generated Close Status export workbook and deletes it. Token

12.2 Close Details

Method Endpoint Purpose Auth
POST /api/mc/monthly/close/filter The Close Details landing grid: the nine sections with their status, comment counts and the whole-close QA action flags for the selected company and… Yes
POST /api/mc/status/update Skip / Submit / Approve a single section, or run the corresponding "… All" action across the whole close; also supports reverting each of those. Yes
POST /api/other/section/details The "Other Monthly Close Items" panel header: task counts, combined comment counts and the two saved checkbox flags for the company and date. Yes
POST /api/mc/bankcard/review/filter Loads the Bank & Card Reconciliations review screen: bank accounts, credit cards, undeposited-funds data, balances and the section's QA status. Yes
POST /api/mc/bankcard/flag/allcount Phase-2 call for the Bank & Card screen: given every account's qbo_id, returns the flagged-transaction count per account. Yes
GET /api/mc/bank/view/reconciliations/:id The "View flagged List" drill-down for one bank or credit-card account. Yes
POST /api/mc/save/bank/reconciliations Per-row autosave for the Bank & Card screen: the reconciliation checkbox, the "all transactions posted" checkbox, statement dates, and the… Yes
POST /api/mc/ar/review/filter Loads the Accounts Receivable review screen. Yes
POST /api/mc/ap/review/filter Loads the Accounts Payable review screen. Yes
POST /api/mc/checkbox/save Generic save for the per-section reconciliation attestation checkboxes (e.g. "does AR Aging tie to the Balance Sheet AR balance?"). Yes
GET /api/mc/ar/review/list/:id Paginated transaction list behind one Accounts Receivable review bucket. Yes
GET /api/mc/ap/review/list/:id Paginated transaction list behind one Accounts Payable review bucket. Yes
POST /api/mc/common/accept Accept or revoke a single flagged transaction on any section's list screen. Yes
POST /api/mc/common/accept/all Batch accept/revoke — the header "select all" action on a list screen. Yes
POST /api/mc/balancesheet/review/filter Loads the Balance Sheet General review screen. Yes
GET /api/mc/balancesheet/review/list/:id One of the six Balance Sheet General "View List" drill-downs. Yes
POST /api/mc/incomestatement/review/filter Loads the Income Statement General review screen. Yes
GET /api/mc/incomestatement/review/list/:id One of the two Income Statement General "View List" drill-downs. Yes
POST /api/mc/pr/review/filter Loads the Payroll review screen. Yes
GET /api/mc/pr/review/list/:id The Payroll review's single "View List" drill-down (wages plus payroll-tax GL accounts). Yes
POST /api/mc/revenue/review/filter Loads the Uncharacterized Revenue and Expense review screen. Yes
GET /api/mc/revenue/review/list/:id One of the four Uncharacterized "View List" drill-downs, paginated. Yes
POST /api/mc/cash/flow/statement/filter Loads the Cash Flow Statement review screen — five checkbox options plus the section QA status. Yes
POST /api/mc/filter/other/reivew Loads the "Other" section's review checklist for the company and month. (The path segment is spelled reivew, exactly as shown, and must be requested… Yes

12.3 Open Comments

Method Endpoint Purpose Auth
POST /api/mc/comment/view Reads the Monthly Close comment threads for one company, month and comment type (optionally narrowed to a single transaction). Yes
POST /api/comment/module Builds the Open Comments filter tree — the Module › Submodule › Section hierarchy from comment_module, plus the assignable-user list for the selected… Yes
POST /api/mc/thread/sort The Open Comments grid: every comment thread visible to the caller across Monthly Close, Accounting, Task and Invoice, filtered and sorted. Yes
POST /api/mc/thread/sort/fetch-users Returns the users who can be assigned on a new or existing thread for one company and module. Yes
POST /api/mc/comment/create Posts a Monthly Close comment (starting a thread, or replying into an existing one). Yes
POST /api/mc/comment/resolve Resolves or un-resolves one or more Monthly Close comments. Yes
POST /api/comment/edit Edits the text of one Monthly Close comment. Yes
POST /api/comment/editAssignUser Replaces the assignee set on one Monthly Close comment. Yes
POST /api/thread/create Starts a new comment thread from the Open Comments screen (or from the Invoicing/Task comment panels). Yes
POST /api/thread/comment/create Replies into an existing thread on any of the four sources. Yes
POST /api/comment/resolve Resolves or un-resolves an Open Comments thread (or a single comment within one). Yes
POST /api/open-comment/edit Edits one comment (and optionally its assignees) from the Open Comments grid, on any source. Yes

12.4 GL Validation

Method Endpoint Purpose Auth
POST /api/mc/glvalidation/work/filter Lists the GL accounts for one tab with their source configuration, scraped source balance, GL balance, variance and comment counts. Yes
POST /api/mc/glvalidation/work/modal Returns what the "Source Selection" modal should pre-fill for one account. Yes
POST /api/mc/glvalidation/work/save Saves one account's source configuration, immediately re-reads the sheet, and returns the freshly recomputed row. Yes
POST /api/mc/glvalidation/work/refresh Re-scrapes every Google-sourced account listed in the payload. Yes
POST /api/mc/glvalidation/comments/view The GL Validation comment sidebar's read: every accounting_comments thread for one GL account in one close month. Yes
POST /api/mc/glvalidation/work/bulkupdate "Bulk Update Google Sheet": points every Google-sourced account on the visible tab at one workbook, then re-scrapes. Yes

12.5 Firm Configuration

Method Endpoint Purpose Auth
GET /api/mc/firm/config/filter Loads the firm-level Monthly Close configuration: the calendar and relative period definitions effective for the current month, plus the firm's… Yes
POST /api/mc/firm/config/save Replaces the firm's calendar and/or relative period definitions. Yes
POST /api/mc/firm/config/close-deadline/save Sets the firm's weekend close-deadline adjustment rule. Yes
POST /api/mc/configure/update Kicks off a background refresh of the stored QuickBooks book-close date for the selected companies. Yes
POST /api/mc/ar/config/filter/bank Loads the Bank & Card Configure screen: the per-transaction-type day thresholds plus the undeposited-funds GL account picker. Yes
POST /api/mc/ar/config/save/bank Autosave for the Bank & Card Configure screen — thresholds and/or the undeposited-funds GL account. Yes
POST /api/mc/default/save-global Writes a firm-wide default value (mc_default.global_default). Yes
POST /api/mc/ar/config/filter Loads the AR or AP Configure screen — one shared endpoint, discriminated by configTypeId. Yes
POST /api/mc/ar/config/save Saves one AR/AP threshold value. Yes
POST /api/mc/balancesheet/config/filter Loads the Balance Sheet General Configure screen. Yes
POST /api/mc/balancesheet/config/save Autosave for Balance Sheet General Configure: variance trio, thresholds, section toggles, and the GL-account checkboxes. Yes
POST /api/mc/incomestatement/config/filter Loads the Income Statement General Configure screen. Yes
POST /api/mc/incomestatement/config/save Autosave for Income Statement General Configure. Yes
POST /api/mc/payroll/filter Loads the Payroll Configure screen. Yes
POST /api/mc/payroll/config/save Autosave for Payroll Configure: the four scalars and the account checkboxes. Yes
POST /api/mc/revenue/config/filter Loads the Uncharacterized Revenue and Expense Configure screen. Yes
POST /api/mc/revenue/config/save Autosave for Uncharacterized Configure: the two class flags, or one account checkbox. Yes
POST /api/mc/cash/flow/statement/save Saves the Cash Flow Statement review's five checkboxes for one company and month. Yes
POST /api/mc/other/config/filter Loads the "Other" checklist items configured for a company or group. Yes
POST /api/mc/other/config/save Creates, renames, retypes, sets the options of, or soft-deletes one "Other" checklist item. Yes
POST /api/other/section/check Persists the two "Other Monthly Close Items" panel checkboxes for a company and period. Yes
POST /api/mc/filter/other/save Saves one answer on the "Other" review checklist. Yes

12.6 Review Assignments

Method Endpoint Purpose Auth
POST /api/get/primary_accountant Returns the distinct primary accountants recorded across the companies in scope, for the grid's Primary Accountant filter. Yes
POST /api/get/user_reviewers Returns the reviewer option list — either the eligibility roster or the recorded reviewers, depending on isReviewAssignment. Yes
POST /api/mc/peer/review/assignment/filter Loads the Review Assignments grid — one group block per call, plus the month headers, reviewer values, completion flags and manager list. Yes
POST /api/mc/peer/review/save Saves one grid cell (a month's reviewer, the primary accountant, or the close deadline), or copies a month's reviewers forward. Yes
POST /api/mc/peer/review/user-list Returns the users assignable as a reviewer for one company. Yes
POST /api/mc/peer/review/assignment/download/file Generates the blank Review Assignments import template and returns a one-shot download link. Yes
POST /api/mc/peer/review/assignment/import/file Imports a filled-in Review Assignments template. Yes
GET /api/mc/peer/review/assignment/download/generated/:token Streams a generated Review Assignments template (blank or error-annotated) and deletes it. Token

12.7 QuickBooks Close Dates

Method Endpoint Purpose Auth
POST /api/mc/report/closedate Returns the close-date report rows for a company group (or a single company) plus the manager filter options. Yes
POST /api/mc/report/closedate/export Generates the close-date report workbook and returns a one-shot download link. Yes
GET /api/mc/report/closedate/export/generated/:token Streams a generated close-date report workbook to the user who requested it, and deletes it. Yes

12.8 Company Group Tree

Method Endpoint Purpose Auth
POST /api/get/treeview Returns the company-group tree and flat group/company option lists the Monthly Close scope picker renders. Yes

A.6 Comments Center

8 endpoints — full entries in 13. Comments Center.

13.1 Task Comments

Method Endpoint Purpose Auth
POST /api/task/view/comment Loads every comment thread on a task, with each comment's author, assignees and per-thread resolve state, plus the list of users assignable on the… Yes
POST /api/task/view/comment/save Adds a comment to a task, creating a new comment_thread when no threadId is supplied, and optionally assigning users to that comment. Yes
POST /api/task/view/comment/edit Edits one task comment: either replaces its text, or replaces its full assignee set. Yes
POST /api/task/comment/resolve Toggles a comment thread between resolved and re-opened, cascading the new state to every comment in the thread. Yes

13.2 Invoicing Comments

Method Endpoint Purpose Auth
POST /api/invoicing/view/comment Loads the comment threads on an invoice batch plus the assignable-user list for the comment composer. Yes
POST /api/invoicing/view/comment/save Posts a comment on an invoice batch — a reply when threadId is supplied, otherwise a new thread. Yes
POST /api/invoicing/view/comment/edit Edits one invoice comment: replaces its text, or replaces its full assignee set. Yes
POST /api/invoicing/comment/resolve Toggles an invoice comment thread between resolved and re-opened. Yes

A.7 Report Center

27 endpoints — full entries in 14. Report Center.

14.1 Report List

Method Endpoint Purpose Auth
GET /api/reports/list Loads the Report List screen: the company scope picker, the Global and Custom report columns for one report group, the GL-validation badge and the… Yes
GET /api/reports/searchReport Search alias for the Report List. Identical behaviour to GET /api/reports/list. Yes
POST /api/reports/addReport Creates a new, empty report definition in global_report_list and returns the URLs for previewing and editing it. Yes
GET /api/reports/deleteReport Removes a report definition (soft delete when the deleted column exists). Yes
POST /api/reports/clone Clones a report definition under a new name, including a deep copy of its writer (header, columns and detail rows). Yes

14.2 Report Preview

Method Endpoint Purpose Auth
GET /api/reports/preview/:id Bootstraps the Report Preview screen for one report: the report row, the preview form's controls and state, any saved preview parameters, and — as a… Yes
POST /api/reports/view/generate_income_statement Runs a report definition against the session company's ledger and returns the full grid (headers, column metadata and valued rows). This is the core… Yes
POST /api/reports/rgCompany/classLoc Returns the merged class/location list for a reporting group's companies, plus the fiscal-month and accounting-method consistency flags the preview… Yes
POST /api/reports/view/save_preview Persists the preview form's parameters for the current (report, company, user) triple so the screen reopens with the same selections. Yes
POST /api/reports/view/download Regenerates the report from the supplied preview parameters and returns a styled .xlsx workbook, base64-encoded in the JSON envelope. Yes

14.3 Report Writer

Method Endpoint Purpose Auth
GET /api/reports/writer Loads everything the Report Writer screen needs on open: the report row, its writer header, the company scope, class/location and reporting-group… Yes
POST /api/reports/writer/based-on Returns the rows and columns for the selected "Based on" axis. GL-Account reports get the editable row/column grid; the multi-axis modes get a… Yes
POST /api/reports/writer/search-rows Backs the Add-row sidebar and the GL Account Editor pick lists: account groups (global and custom) on one tab, GL accounts on the other. Yes
POST /api/reports/writer/reporting-group/companies Returns the companies inside a reporting group plus the merged class/location list the Apply Filter sidebar shows. Yes
POST /api/reports/writer/save Persists the whole writer: the report row, the writer header, every row, every fixed-period column and (for multi-axis reports) the GL Account Editor… Yes

14.4 Report Packages

Method Endpoint Purpose Auth
GET /api/reports/viewPackage Lists report packages split into Global and Custom tabs, with the company scope, GL-validation badge and Chart-of-Accounts change counter the screen… Yes
POST /api/reports/viewPackage/save Creates, updates or clones a report package, and (conditionally) rewrites the list of reports inside it. Yes
GET /api/reports/viewPackage/delete Soft-deletes a package and all of its detail rows. Yes
GET /api/reports/viewPackage/detail Loads one package's edit screen: the package row, its ordered report list with each report's period settings, and the class/location options and… Yes
POST /api/reports/viewPackage/run Builds the Run Package shell: header facts (as-on date, generated-by/on, scope name), the effective reporting group and expand-by-company flags, and… Yes
POST /api/reports/viewPackage/run/report Generates one report card on the Run Package screen. The generation parameters are rebuilt server-side from the package's detail row — the client… Yes
POST /api/reports/viewPackage/run/download Exports the whole package (or one report's sheet) as a styled .xlsx workbook, base64-encoded in the JSON envelope. One worksheet per report. Yes

14.5 Configure

Method Endpoint Purpose Auth
POST /api/reports/configure/update Multiplexed refresh endpoint: re-reads the report configuration, pulls the QuickBooks book-close date live, or pulls class/location tracking… Yes
GET /api/reports/configure Loads the Report Configure screen: the company list and current scope, the company's accounting method and fiscal month, and the Chart-of-Accounts… Yes
POST /api/reports/configure/save Saves a company's accounting method and fiscal-year start month (report_configure). Yes
POST /api/reports/configure/switch-company Switches the session's active company, resetting the as-on period to today. Yes
POST /api/reports/configure/changes-seen Marks a company's pending Chart-of-Accounts changes as seen, clearing the "Changes have been made to the Chart of Accounts" strip. Yes

A.8 Feedback Center

20 endpoints — full entries in 15. Feedback Center.

15.1 Suggestions

Method Endpoint Purpose Auth
GET /api/feedbackcenter/:type Returns the Suggestions (or Issues) landing payload: the module filter list, per-status counts and the full item list. Yes
GET /api/new/:type Returns the "new suggestion / new issue" form bootstrap — the module dropdown and the resolved type. Yes
POST /api/issue/feedback/save Creates a suggestion, issue or support ticket, links any previously staged attachments to it, and e-mails the Genie team. Yes
GET /api/issue/list/detail/:id Full detail view of one suggestion/issue: the ticket, its attachments, its replies, participants, and the caller's own vote/subscription state. Yes
POST /api/issue/feedback/replay/send Posts a reply on a suggestion/issue, links any staged attachments to that reply, and notifies subscribers. Yes
POST /api/issue/vote/count Toggles the caller's vote on a suggestion and returns the new vote state and total. Yes
POST /api/issue/filter/module Re-queries the suggestion/issue list with module, status, search, sort and show-deleted filters applied. Yes
POST /api/issue/save/stauts Updates a ticket's status and/or classification, optionally appends a label, and notifies the ticket creator on a status change. Yes
POST /api/feedbackcenter/DelList Soft-deletes a suggestion/issue (issue_list.deleted = 1). Yes
POST /api/subscribe/status Subscribes or unsubscribes the caller from a ticket's reply notifications. Yes
POST /api/ajax_upload/issue_files Stages one or more attachments in Google Cloud Storage and returns their issue_files row ids, to be passed later as insertedIds when saving a ticket… Yes
GET /api/issue/files/download/:id Streams one attachment back to the browser as a download. Yes
GET /api/issue/files/download/:id/:name Same as the previous route with an explicit download filename appended — this is the form emitted in download_url. Yes

15.2 Support & Issues

Method Endpoint Purpose Auth
GET /api/support/list Returns the support ticket list for the caller, plus the module dropdown and the caller's agent flag. Yes
GET /api/support/new/form Bootstrap for the "raise a support ticket" form. Yes
GET /api/support/feedback/chat/:id Loads one support ticket as a chat thread: the ticket, its attachments, every reply, and whether replying is still allowed. Yes
POST /api/support/chat/reply/save Posts a message into a support ticket's chat, attaches staged files, touches the ticket's updated_at, and notifies the other party. Yes
POST /api/support/filter/feedback Re-queries the support ticket list with title, status and ordering filters. Yes
POST /api/support/chat/reply Resolves a ticket id into the front-end chat URL. A navigation aid; it performs no write. Yes
POST /api/ajax_upload/support_files Stages support-ticket attachments. Functionally identical to POST /api/ajax_upload/issue_files — both write the same issue_files rows. Yes

A.9 Integrations

17 endpoints — full entries in 16. Integrations.

16.1 Google Integration (Drive / Sheets OAuth)

Method Endpoint Purpose Auth
POST /api/send/drive Entry point for "Send to Google Drive". Returns a Picker access token when the user is connected, or a consent URL when they are not. Yes
GET /api/google/drive/status Reports whether the caller has a usable Google connection, so the UI can skip the Configure modal. Yes
GET /api/google/drive/auth-url Returns a fresh Google consent URL for the "Configure Google Account" popup. Yes
POST /api/google/drive/disconnect Clears the caller's stored Google Drive tokens. Yes
POST /api/google/drive/upload Uploads a base64 workbook into a Drive folder, converting it to a native Google Sheet by default. Reusable by any screen that can produce an .xlsx in… Yes
GET /google/drive/callback Google's OAuth redirect target. A browser-visited route (not an API call): it exchanges the authorisation code, stores the tokens against the… Yes

16.2 QuickBooks Online Integration

Method Endpoint Purpose Auth
POST /api/qbo/import/all Dispatches a QuickBooks sync for one company across one or more modules, marking the requested modules as running immediately. Yes
POST /api/qbo/syncstatus Polling endpoint for QuickBooks sync progress. Called roughly every 5 seconds by the QBO page while a sync runs. Yes
GET /api/qbo/connect Returns a QuickBooks consent URL for the caller's firm-internal company. Yes
GET /api/qbo/connection/status Reports whether the caller's firm-internal company is currently connected to QuickBooks. Yes
GET /api/qbo/callback Intuit's OAuth redirect target. Exchanges the authorisation code, binds the realm to the pending company, captures the QuickBooks company name, and… No
POST /api/invoicing/tax/qbo Resolves the sales-tax rate for an invoicing line — a live QuickBooks probe when possible, otherwise the stored customer rate. Yes
GET /api/company/qbo/:id Full payload for the Admin → Company → QuickBooks Online Integration page: connection state, module catalogues, per-module last-synced times, live… Yes
GET /api/company/qbo/connect/:id Returns a QuickBooks consent URL for a specific company from the Admin integration page, with a returnTo that auto-triggers "Sync all" after… Yes
POST /api/company/qbo/disconnect Disconnects a company from QuickBooks: clears the local connection flag and tokens, asks the Lambda to revoke the grant with Intuit, and drops… Yes
POST /api/company/qbo/syncall Triggers a full (or module-scoped) sync for one company from the Admin integration page. Yes
POST /api/company/qbo/settings/save Two operations behind one route: change a company's QuickBooks sync-year setting, or clear the recorded last-sync times for some/all modules so they… Yes

↑ Back to Table of Contents


19. Appendix B — Environment Variables

Configuration is supplied through the deployment environment. No values are reproduced in this document — only the variable names the service reads and what each one controls.

B.1 Application

Variable Used for Default
PORT HTTP listen port 4000
NODE_ENV Marks the genie.sid, XSRF-TOKEN, enc_log and google cookies Secure when set to production unset
APP_ENV When set to local or development, enables a development-only two-factor bypass for a restricted set of accounts, and runs QuickBooks sync inside the application rather than dispatching it to the external sync function unset — no bypass
APP_URL Application base URL. Used to derive OAuth redirect URIs and as a fallback base for links in outgoing e-mail http://localhost:3000
SESSION_SECRET Session signing secret A development fallback is used when unset; a value must be configured in any real deployment
FRONTEND_ORIGIN The single browser origin allowed for direct cross-origin calls; credentials are enabled http://localhost:3000
BACKEND_ORIGIN Backend base URL used when building callback URLs —
APP_NAME Product name used in outgoing e-mail subjects and templates Genie
FRONTEND_URL Alternative web application base URL used when building password-reset links, after FRONTEND_ORIGIN —
SHOW_DEV_RESET_LINK When true and NODE_ENV is not production, the password-reset response also returns the reset link for local testing. It is never returned in production unset — the link is e-mailed only

B.2 Database

Variable Used for
DB_HOST, DB_PORT Database host and port (ignored when INSTANCE_CONNECTION_NAME is set)
DB_USERNAME, DB_PASSWORD, DB_DATABASE Credentials and schema name
DB_SYNCHRONIZE 'true' enables automatic schema synchronisation — should remain off in production
DB_SSL 'true' connects over SSL
INSTANCE_CONNECTION_NAME Cloud SQL instance. When set, both the application and the session store connect over the instance's Unix socket instead of host and port

The sessions table is expected to exist in the schema already; the service does not create it.

B.3 Mail (SMTP)

Variable Used for
MAIL_DRIVER Mail transport selector
MAIL_HOST, MAIL_PORT SMTP endpoint. Port 465 uses implicit TLS; any other port uses STARTTLS
MAIL_USERNAME, MAIL_PASSWORD SMTP credentials
MAIL_SENDER_ADDRESS, MAIL_SENDER_NAME From address and display name
MAIL_ENCRYPTION Present in configuration but not read by the service — the encryption mode is determined by MAIL_PORT

Mail is sent for password-reset links, emailed sign-in codes, and Feedback Center notifications (new tickets, replies and status changes).

B.4 QuickBooks Online

Variable Used for
QBO_CLIENT_ID, QBO_CLIENT_SECRET Intuit OAuth 2.0 application credentials
QBO_REDIRECT_URI OAuth callback registered with Intuit
QBO_ENV sandbox or production — selects the Intuit API base URL
LAMBDA_SYNC, LAMBDA_TOKEN_URL, LAMBDA_CONTENT_TYPE External QuickBooks sync function endpoint, token endpoint and content type
QUICKBOOKS_OAUTH_STATE_TTL_MS Validity window of the OAuth state value (15 minutes by default)

B.5 Google (Drive / Sheets)

Variable Used for
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET Google OAuth 2.0 client credentials
GOOGLE_DRIVE_REDIRECT_URI OAuth callback for the Drive and Sheets connection. Defaults to ${APP_URL}/google/drive/callback
GOOGLE_PICKER_APP_ID Google Picker application id used by the web application
GOOGLE_PICKER_DEVELOPER_KEY Browser API key handed to the Google Picker (public by design)
GOOGLE_DRIVE_SCOPES Overrides the OAuth scopes requested for the Drive and Sheets connection
GOOGLE_APPLICATION_CREDENTIALS Path to a service-account key file, for environments where application default credentials are not available
GOOGLE_TOKEN_DEFAULT_TTL_SECONDS, GOOGLE_TOKEN_REFRESH_SKEW_SECONDS Assumed access-token lifetime and the margin before expiry at which a token is refreshed

B.6 Object storage

Variable Used for
GCS_BUCKET Google Cloud Storage bucket holding Feedback Center attachments
GCS_ISSUE_FILES_PREFIX Key prefix within that bucket for issue attachments

B.7 Web application

Variable Used for
BACKEND_URL The backend origin that the web application proxies API requests to

Credential handling: every variable above that holds a secret — database password, session secret, SMTP password, OAuth client secrets — must be supplied through the deployment's secret management. No secret values appear in this document.

↑ Back to Table of Contents


20. Appendix C — Health Checks & Operations

C.1 Health endpoints

Endpoint Purpose
GET / Hosting and liveness ping
GET /health Explicit health check

Both verify database connectivity and return:

{
  "status": "ok",
  "service": "smartbook-genie-backend",
  "message": "Backend and database are healthy.",
  "checks": { "database": { "status": "up", "latencyMs": 3 } },
  "timestamp": "2026-09-01T09:15:22.104Z"
}

If the database check fails, the response is HTTP 503 with status: "error", checks.database.status: "down" and an error message. The process itself stays up, which distinguishes "the service is down" from "the service is up but cannot reach its database".

C.2 Logging and request tracing

Three diagnostic layers are active in the running service:

  1. Arrival and completion tracing for report requests — the method and URL on arrival, and the status, method, URL and elapsed time on completion.
  2. An error log for every 5xx response, whether or not the request was traced.
  3. A warning when a connection is closed before a response was sent — the signature of an upstream proxy timing out, which is otherwise indistinguishable from a request that never arrived.

Failures that occur while a response is being written are returned as JSON rather than as plain text, so a client always receives a parseable body.

C.3 Scheduled work

The service runs scheduled background work, including task recurrence generation and invoice recurrence generation. These are not HTTP endpoints and are not callable by API clients. See Deployment & Operational Considerations for the deployment impact.

C.4 Support information to capture

When raising an API issue, the following makes diagnosis fastest:

Item Why it helps
Full request path and HTTP method Several paths differ only by method
Request body (with secrets removed) Most endpoints are POST with the payload in the body
Complete response body, not only the status code Business-rule failures are reported inside a 200/201 body (§5.2)
Timestamp with timezone Matches server-side request traces
Whether the call was made through the web application or directly to the backend Determines whether proxy rules are involved (§3.1)

End of document — Genie API Documentation, 476 documented endpoints, 1 September 2026.

↑ Back to Table of Contents