Import time entries

How to create, update, and read time entries with the API, and the rules that limit which dates, tickets, and people an import can write.

Written for
integration-developer
Roles
member, admin, owner, agent
Requires
API token with the write scope, owned by a workspace user
Feature
time

A time entry in SAQ is a date, a duration in minutes, and a ticket; there is no timer. This recipe imports entries from another timesheet tool or a script with POST /api/w/acme/time, and explains the rules that decide what an import can and cannot write.

Before you start

  • A token with the write scope whose owner is a workspace user. Client users never log time.
  • The ticket ids to log against. Each entry needs a ticket that belongs to a project (Inbox tickets refuse time), visible to the token's owner, on a project they are a member of.
  • The dates must fall in an open period, and not before the workspace's first period start once a period has been closed.

Who the entry belongs to

Every entry is logged as the token's owner. There is no on-behalf-of field: you cannot import Kari's hours with Ola's token, and an agent account key logs time as the agent, not as a person. To import several people's hours, each person mints their own token (Account → API tokens) and the import uses the matching token per person.

Create an entry

curl -X POST "https://saq.no/api/w/acme/time" \
  -H "Authorization: Bearer saq_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "ticketId": "01924c3e-6b2a-7d11-9d0e-3a1f4b5c6d7e",
    "spentOn": "2026-09-18",
    "startedAt": "09:30",
    "minutes": 90,
    "note": "Reproduced the checkout error and wrote the fix.",
    "billable": true
  }'
Field Rule
ticketId Required. The ticket UUID (not the human id)
spentOn Required. YYYY-MM-DD, the civil date in the owner's time zone
minutes Required. Integer, at least 1
startedAt Optional. HH:MM or HH:MM:SS wall-clock start; informational only
note Optional, ≤10 000 characters. Staff-only; never shown to clients
billable Optional, default true

The response is 201 { "id": "…" }. SAQ snapshots the ticket's project and billing client, the owner's consultant category (project override, else their workspace default), and the owner's time zone onto the entry.

Refusals to expect, all 409 conflict unless noted:

  • ticket_unrouted (400): the ticket is in the Inbox or has no project.
  • closed_period_log: the date is in a closed period. Log forgotten work in an open period; only an Owner or Admin can post a correction in the app.
  • before_first_period_note: the date is before the first period start. Log it today and put the real date in the note.
  • 403 forbidden: the owner is not a member of the ticket's project, or the token lacks the write scope.

Update an entry

curl -X PATCH "https://saq.no/api/w/acme/time/01924c3e-0000-7000-8000-000000000030" \
  -H "Authorization: Bearer saq_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "minutes": 120, "note": "Included the deploy." }'

Any of spentOn, startedAt, minutes (non-zero), note, billable can be sent. A person edits their own entries; an Owner or Admin token may also edit other people's, and only Owners and Admins may change billingClientId or categoryId. An entry that a billing run has allocated is frozen (entry_billed), and in a closed period only billable, billing client, and category can change (Owner or Admin).

Deleting and correcting

DELETE /api/w/acme/time/{id} is not open to API tokens; delete entries in the app under Time. Corrections of closed time (signed adjustments dated today) are made by an Owner or Admin in the app and are not available through the API either. Plan an import so that entries in open periods are right before the period is closed.

Read entries for a range

curl "https://saq.no/api/w/acme/time?from=2026-09-01&to=2026-09-30" \
  -H "Authorization: Bearer saq_YOUR_TOKEN"

Query parameters: from and to (civil dates, at most 366 days apart), optional ticketId, and optional userId (Owner or Admin only; others always get their own entries). The response has entries (each with id, ticketRef, title, projectName, clientName, spentOn, startedAt, timezone, minutes, note, billable, billed, closedAt, correctsEntryId), the periods in the range with their closedAt, the owner's expectedWeekMinutes, and timezone. Use it to verify an import and to detect entries that already exist before re-running it: there is no idempotency key, so a retried POST creates a duplicate.

Rate budget

An import runs against 120 requests per minute per token and the workspace's shared plan budget. Sleep until the next minute on 429 rate_limited.