Time Entry API
The time entry endpoints let you create, update, and manage time logs programmatically. This page covers the field semantics that are essential for correct integration — particularly the hours vs minutes distinction.
hours and minutes — Field Semantics
Time entries store duration in two related fields. Understanding their relationship prevents rounding surprises.
hours — canonical stored field
hours is the canonical stored representation. It is a text string representing a decimal hour value in quarter-hour increments (multiples of 0.25) up to 24:
"0.25" → 15 minutes
"1.5" → 1 hour 30 minutes
"2.0" → 2 hours
"7.75" → 7 hours 45 minutes
Parse it as a float when you need a number: parseFloat(entry.hours).
minutes — integer convenience field
minutes is an integer convenience representation of the same duration. When provided, the server derives hours by rounding to the nearest quarter-hour:
hours = round(minutes / 60, 0.25)
minutes sent | Exact fraction | hours stored (nearest 0.25) |
|---|---|---|
150 | 2.500 | "2.5" |
90 | 1.500 | "1.5" |
45 | 0.750 | "0.75" |
100 | 1.667 | "1.75" ← rounds to nearest 0.25 |
10 | 0.167 | "0.25" ← rounds up to nearest 0.25 |
Providing either or both fields on POST
On POST /projects/:id/time-entries, you may provide either hours or minutes, or both. At least one is required.
| Provided | Behavior |
|---|---|
minutes only | hours = round(minutes / 60, 0.25) |
hours only | minutes = round(hours * 60) |
| Both | minutes takes precedence; hours is re-derived from minutes using the 0.25 rounding rule |
| Neither | 400 validation_error |
Example — both fields provided, minutes wins:
// Request (hours="2.0" is ignored; minutes=100 takes precedence)
{
"userId": "user-uuid-...",
"workDate": "2026-05-26",
"minutes": 100,
"hours": "2.0"
}
// Stored result: hours = round(100/60, 0.25) = "1.75"
// Response
{
"hours": "1.75",
"minutes": 100,
...
}
Both fields are always present in every response:
{
"id": "te-uuid-...",
"hours": "2.5",
"minutes": 150,
...
}
Send minutes as your primary input and let Align derive hours. This guarantees alignment to the quarter-hour grid without manual rounding.
Time Entry Status Values
| Status | Meaning |
|---|---|
draft | Created, not yet submitted |
submitted | Awaiting manager approval |
locked | Approved and locked — immutable until unlocked |
rejected | Rejected by manager; can be edited and resubmitted |
invoiced | Included in a Stripe invoice — permanently immutable |
When a manager approves a time entry, the status transitions to locked (not approved). locked entries contribute to the project's actual-cost rollup. Use status = "locked" in your local state machine to represent an approved entry.
Creating a Time Entry
curl -X POST https://app.alignsoft.us/api/v1/projects/<projectId>/time-entries \
-H "Authorization: Bearer $ALIGN_KEY" \
-H "Content-Type: application/json" \
-d '{
"userId": "user-uuid-...",
"workDate": "2026-05-26",
"minutes": 150,
"description": "Implemented OAuth login flow",
"entryId": "entry-uuid-...",
"billable": true,
"externalRef": {
"provider": "illumera",
"externalType": "time_log",
"externalId": "illumera-log-9876"
}
}'
Request fields:
| Field | Required | Description |
|---|---|---|
userId | Yes | ID of the user logging time (must belong to the org) |
workDate | Yes | ISO date or datetime for the day worked (e.g. "2026-05-26") |
minutes | One of minutes/hours | Duration in minutes (integer 1–1440). If both provided, minutes takes precedence. |
hours | One of minutes/hours | Duration as a decimal text string, multiples of 0.25 up to 24 (e.g. "2.5"). |
description | No | Free-text note (max 2000 chars) |
entryId | No | Link to a specific project entry (bug/feature/task) |
releaseId | No | Link to a specific release |
billable | No | Whether the time is billable (defaults to true) |
externalRef | No | Idempotency key from your system |
Response (201 Created):
{
"id": "te-uuid-...",
"organizationId": "org-uuid-...",
"projectId": "proj-uuid-...",
"userId": "user-uuid-...",
"entryId": "entry-uuid-...",
"workDate": "2026-05-26T00:00:00.000Z",
"hours": "2.5",
"minutes": 150,
"notes": "Implemented OAuth login flow",
"billable": true,
"status": "draft",
"source": "illumera",
"createdAt": "2026-05-26T09:00:00.000Z",
"updatedAt": "2026-05-26T09:00:00.000Z"
}
Updating a Time Entry
Only draft and rejected entries can be updated. Updating a rejected entry automatically moves it back to draft so it can be resubmitted.
curl -X PATCH https://app.alignsoft.us/api/v1/time-entries/<id> \
-H "Authorization: Bearer $ALIGN_KEY" \
-H "Content-Type: application/json" \
-d '{
"minutes": 180
}'
The response reflects the updated minutes and the newly derived hours ("3").
Time Entry Status Lifecycle
draft → submitted → locked (approved)
↘ rejected → (edit reverts to draft) → submitted
locked → unlocked (manager/admin) → draft → submitted → locked
locked → invoiced (billing run — immutable)
| Status | Can edit? | Next transition |
|---|---|---|
draft | Yes | submitted (submit action) |
submitted | No | locked (approve) or rejected (reject) |
locked | No | draft (unlock by manager/admin) or invoiced (billing run) |
rejected | Yes (reverts to draft on save) | submitted (resubmit) |
invoiced | No — permanent | — |
Submitting, Approving, and Rejecting
| Action | Endpoint | Required role |
|---|---|---|
| Submit for approval | POST /time-entries/:id/submit | Owner or manager/admin |
Approve (sets status → locked) | POST /time-entries/:id/approve | manager or admin |
| Reject | POST /time-entries/:id/reject | manager or admin |
| Unlock locked entry | POST /time-entries/:id/unlock | manager or admin |
See Authorization for the full scope and permission matrix.
List and Filter
# Org-wide list (org-scoped key only)
GET /api/v1/time-entries?status=submitted&page=1&pageSize=25
# Project-scoped list
GET /api/v1/projects/:projectId/time-entries?userId=<id>&startDate=2026-05-01&endDate=2026-05-31
Both endpoints return paginated results:
{
"data": [ /* time entry rows */ ],
"page": 1,
"pageSize": 25,
"total": 142
}
Available query filters: userId, status, startDate (ISO date), endDate (ISO date).