Timesheets

Returns employees' monthly timesheets — planned, actual, and approved minutes, with pagination and filters by period, department, and employee.

Scope

Requires the timesheets.read scope. Without it, the key gets 403 missing_scope. See Scopes and permissions → Scope table for which permission the key's creator needs to grant this scope.

When a timesheet exists

A timesheet is a document a manager builds manually in the Smengo app (the Timesheets page, the "Timesheets" permission); timesheets are not generated automatically or on a schedule. Until the timesheet for a month has been built, the endpoint returns an empty list for that period. An empty response is not an error — before drawing conclusions, check whether the period has been built at all.

  • An employee with no planned days in the period gets no row — timesheet rows are built from schedule days. The API alone can't distinguish "the employee didn't work" from "the timesheet hasn't been built" — the only signal is whether other employees have rows for the same period.
  • Terminated employees are excluded entirely — historical timesheets included. Last month's timesheet of an employee deleted from the app yesterday is no longer visible through the API. For accounting archives, export timesheets before deleting employees. Employees with revoked access (access_revoked) do remain in timesheets.
  • Timesheet rows carry no names — only employee_id. A report with names needs a second scope, employees.read (a request to Employees): with timesheets.read alone you get nothing but UUIDs. On the upside, an employee_id from a timesheet always resolves in /employees — both resources exclude terminated employees symmetrically.

Query parameters

Parameter Required Description
limit no Integer 1..200, defaults to 100.
cursor no The meta.next_cursor value from the previous response — for the next page.
from, to no YYYY-MM-DD dates in the organization's timezone, boundaries inclusive. Matched against period — the first day of the month a timesheet belongs to. Passed only as a pair; the maximum range is 366 days. When omitted, you get the current month's timesheets.
department_id no A department UUID. Matched through the employee's department: you get the timesheets of that department's employees. An unknown or another organization's department_id is not an error, it just returns an empty list.
employee_id no An employee UUID — exact match. An unknown or foreign employee_id also returns an empty list, not an error.

The resource has no other parameters: any extra parameter returns 400 validation_error — it's explicitly rejected, not silently ignored (see Conventions → Filters). Passing only from without to (or vice versa) is also rejected — 400 with the hint pass both from and to, or neither.

Mind the period matching: the range from=2026-06-15&to=2026-07-15 catches neither the June nor the July timesheet — the first days of both months (2026-06-01, 2026-07-01) fall outside the range. To get June and July timesheets, request from=2026-06-01&to=2026-07-31.

Request

Without parameters — the current month's timesheets:

curl -s https://smengo.com/api/v1/timesheets \
  -H "Authorization: Bearer smg_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

For two months:

curl -s "https://smengo.com/api/v1/timesheets?from=2026-06-01&to=2026-07-31" \
  -H "Authorization: Bearer smg_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

Response

{
  "data": [
    {
      "id": "e2a4c6b8-0d1f-4e3a-9b7c-5f6a8d0e2c31",
      "employee_id": "5e6d9f2a-3b7c-4e2f-9a1d-7c8e2f6b4a10",
      "period": "2026-06-01",
      "planned_minutes": 10080,
      "actual_minutes": 10230,
      "approved_minutes": 10080,
      "days_from_plan": 0,
      "shift_count": 21,
      "status": "locked"
    },
    {
      "id": "1f3d5b7a-9c0e-4d2f-8b6a-4c8e0a2d6f13",
      "employee_id": "0b2f5e7a-9c31-4d6e-8a2b-4f1e6c9d3a52",
      "period": "2026-07-01",
      "planned_minutes": 9600,
      "actual_minutes": 9012,
      "approved_minutes": 0,
      "days_from_plan": 3,
      "shift_count": 20,
      "status": "draft"
    }
  ],
  "meta": { "next_cursor": null }
}

Response fields

Field Type Description
id string (uuid) The timesheet's unique identifier.
employee_id string (uuid) The employee the timesheet belongs to (see Employees).
period string (YYYY-MM-01) The timesheet's month — always the first day of the month, in the organization's timezone; the list is sorted by this field (plus id as a tiebreaker).
planned_minutes number (int ≥ 0) Planned minutes for the month — from the schedule.
actual_minutes number (int ≥ 0) Actual minutes — from check-ins.
approved_minutes number (int ≥ 0) Approved minutes.
days_from_plan number (int) The number of planned days with no valid check-in/check-out pair, where the actual was taken equal to the plan. Computed when the timesheet is built — an indicator of how much of the "actual" is really substituted from the plan.
shift_count number (int) The number of the employee's planned days in the period.
status "draft" | "approved" | "locked" The timesheet's status: draft, approved, or locked.

All durations are in whole minutes — there are no fractional hours or decimal values in the response.

Monetary fields are not exposed in v1 — rates, payroll amounts, and the cost of worked time are not available through the API. AI explanations (explanations) and manual adjustments (adjustments) are not selected either, and terminated employees are excluded from the response entirely.

Pagination

The list is sorted by period, id. If there are more timesheets than limit, meta.next_cursor holds a cursor for the next page — pass it verbatim as cursor on the next request:

curl -s "https://smengo.com/api/v1/timesheets?limit=1&cursor=eyJrIjoiMjAyNi0wNy0wMSIsImlkIjoiZTJhNGM2YjgtMGQxZi00ZTNhLTliN2MtNWY2YThkMGUyYzMxIn0" \
  -H "Authorization: Bearer smg_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

meta.next_cursor is null once there are no more timesheets. Full pagination rules are in Conventions → Pagination.

What's next

Was this article helpful?