Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Time Tracking & Finance

Alloy includes built-in time tracking, approval workflows, labor-rate management, and capitalization reporting. Engineers log time against tickets, managers approve entries, and finance teams generate ASC 350-40 / IAS 38 compliant reports — all through the same API. This guide covers the full lifecycle from logging your first hour to exporting a capitalization CSV.

1. Prerequisites

You need a running Alloy server and a registered user. Set these shell variables for the examples below:

BASE_URL="http://localhost:3000"

Register a user and capture the token:

curl -s -X POST "$BASE_URL/api/v1/auth/register" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "guide-tf@alloy.dev",
    "password": "guide-tf-pass1",
    "display_name": "TF Guide User"
  }' | jq .
{
  "user_id": "...",
  "email": "guide-tf@alloy.dev",
  "display_name": "TF Guide User",
  "access_token": "..."
}

Save the token and user ID:

TOKEN="<access_token from above>"
USER_ID="<user_id from above>"

Create an organization:

curl -s -X POST "$BASE_URL/api/v1/orgs" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "name": "Finance Org",
    "slug": "guide-tf-org"
  }' | jq .
{
  "id": "...",
  "name": "Finance Org",
  "slug": "guide-tf-org"
}
ORG_ID="<id from above>"

Create a project with finance fields:

curl -s -X POST "$BASE_URL/api/v1/projects" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d "{
    \"org_id\": \"$ORG_ID\",
    \"key\": \"FIN\",
    \"name\": \"Finance Feature\",
    \"description\": \"Project for time tracking guide\",
    \"capitalization_type\": \"Capex\",
    \"development_phase\": \"AppDevelopment\",
    \"cost_center_id\": \"ENG-001\",
    \"budget_cents\": 5000000,
    \"budget_period\": \"Quarterly\",
    \"amortization_months\": 36
  }" | jq .
{
  "id": "...",
  "org_id": "...",
  "key": "FIN",
  "name": "Finance Feature",
  "description": "Project for time tracking guide",
  "ticket_counter": 0,
  "capitalization_type": "Capex",
  "development_phase": "AppDevelopment",
  "cost_center_id": "ENG-001",
  "budget_cents": 5000000,
  "budget_period": "Quarterly",
  "amortization_months": 36
}
PROJECT_ID="<id from above>"

Create a ticket to log time against:

curl -s -X POST "$BASE_URL/api/v1/projects/$PROJECT_ID/tickets" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d "{
    \"title\": \"Implement SSO login\",
    \"description\": \"Add SAML-based single sign-on\",
    \"reporter_id\": \"$USER_ID\"
  }" | jq .
{
  "id": "...",
  "project_id": "...",
  "ticket_number": 1,
  "title": "Implement SSO login",
  "description": "Add SAML-based single sign-on",
  "status": "...",
  "assignee_id": null,
  "reporter_id": "...",
  "created_at": "...",
  "updated_at": "..."
}
TICKET_ID="<id from above>"

2. Understanding Activity Types

Every time entry requires an activity_type that classifies the work performed. Alloy defines 13 activity types used for reporting and capitalization accounting:

Activity TypeDescriptionCapEx Eligible
CodingWriting or modifying application codeYes
TestingWriting or running testsYes
CodeReviewReviewing pull requests and codeYes
DesignUI/UX design workYes
ArchitectureSystem design and architecture decisionsYes
PMProject management tasksDepends on phase
RequirementsGathering and documenting requirementsPlanning phase only
TrainingLearning, onboarding, or training activitiesNo
MaintenanceInfrastructure and dependency maintenanceNo (OpEx)
BugFixingInvestigating and fixing bugsYes (Development phase)
DocumentationWriting or updating documentationYes
DeploymentDeploying, releasing, or CI/CD workYes
MeetingsMeetings, standups, and ceremoniesDepends on context

Activity types map to capitalization categories in reports. Work classified as Coding, Testing, or Design during a Development phase project is typically capitalizable under ASC 350-40.

3. Creating Time Entries

Log time against a ticket with a specific activity type and duration:

curl -s -X POST "$BASE_URL/api/v1/time-entries" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"user_id\": \"$USER_ID\",
    \"ticket_id\": \"$TICKET_ID\",
    \"project_id\": \"$PROJECT_ID\",
    \"date\": \"2026-03-28\",
    \"duration_minutes\": 120,
    \"description\": \"Implemented SAML flow\",
    \"activity_type\": \"Coding\"
  }" | jq .
{
  "id": "...",
  "user_id": "...",
  "ticket_id": "...",
  "project_id": "...",
  "date": "2026-03-28",
  "duration_minutes": 120,
  "description": "Implemented SAML flow",
  "activity_type": "Coding",
  "status": "Draft",
  "approved_by": null,
  "approved_at": null,
  "created_at": "...",
  "updated_at": "..."
}
TIME_ENTRY_ID="<id from above>"

New entries always start in Draft status. You can log additional entries with different activity types (e.g. CodeReview, Testing) against the same ticket.

4. Viewing Time Entries

Retrieve a single time entry by ID:

curl -s "$BASE_URL/api/v1/time-entries/$TIME_ENTRY_ID" \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "id": "...",
  "user_id": "...",
  "ticket_id": "...",
  "project_id": "...",
  "date": "2026-03-28",
  "duration_minutes": 120,
  "description": "Implemented SAML flow",
  "activity_type": "Coding",
  "status": "Draft",
  "approved_by": null,
  "approved_at": null,
  "created_at": "...",
  "updated_at": "..."
}

List all time entries for a ticket:

curl -s "$BASE_URL/api/v1/tickets/$TICKET_ID/time-entries" \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "items": [
    {
      "id": "...",
      "user_id": "...",
      "ticket_id": "...",
      "project_id": "...",
      "date": "2026-03-28",
      "duration_minutes": 120,
      "description": "Implemented SAML flow",
      "activity_type": "Coding",
      "status": "Draft",
      "approved_by": null,
      "approved_at": null,
      "created_at": "...",
      "updated_at": "..."
    }
  ],
  "next_cursor": null,
  "has_more": false
}

List all time entries for a user:

curl -s "$BASE_URL/api/v1/users/$USER_ID/time-entries" \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "items": [
    {
      "id": "...",
      "user_id": "...",
      "ticket_id": "...",
      "project_id": "...",
      "date": "...",
      "duration_minutes": 120,
      "description": "...",
      "activity_type": "...",
      "status": "...",
      "approved_by": null,
      "approved_at": null,
      "created_at": "...",
      "updated_at": "..."
    }
  ],
  "next_cursor": null,
  "has_more": false
}

Both list endpoints support cursor and limit query parameters for pagination, plus optional project_id and date filters.

5. Editing Time Entries

Update a draft time entry before submitting it. You can change the date, duration, description, or activity type:

curl -s -X PATCH "$BASE_URL/api/v1/time-entries/$TIME_ENTRY_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "duration_minutes": 150,
    "description": "Implemented SAML flow and wrote integration tests"
  }' | jq .
{
  "id": "...",
  "user_id": "...",
  "ticket_id": "...",
  "project_id": "...",
  "date": "2026-03-28",
  "duration_minutes": 150,
  "description": "Implemented SAML flow and wrote integration tests",
  "activity_type": "Coding",
  "status": "Draft",
  "approved_by": null,
  "approved_at": null,
  "created_at": "...",
  "updated_at": "..."
}

To delete a time entry entirely:

curl -s -X DELETE "$BASE_URL/api/v1/time-entries/$TIME_ENTRY_ID" \
  -H "Authorization: Bearer $TOKEN"

Returns 204 No Content on success.

6. Submitting for Approval

When your time entries are ready, submit them for manager review. This transitions the entry from Draft to Submitted:

curl -s -X POST "$BASE_URL/api/v1/time-entries/$TIME_ENTRY_ID/submit" \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "id": "...",
  "user_id": "...",
  "ticket_id": "...",
  "project_id": "...",
  "date": "2026-03-28",
  "duration_minutes": 150,
  "description": "Implemented SAML flow and wrote integration tests",
  "activity_type": "Coding",
  "status": "Submitted",
  "approved_by": null,
  "approved_at": null,
  "created_at": "...",
  "updated_at": "..."
}

Once submitted, the entry cannot be edited until it is either approved or rejected. The status lifecycle is:

Draft → Submitted → Approved
                  → Rejected

7. Approving Time Entries

Managers and admins can list all submitted entries awaiting approval:

curl -s "$BASE_URL/api/v1/time-entries/submitted" \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "items": [
    {
      "id": "...",
      "user_id": "...",
      "ticket_id": "...",
      "project_id": "...",
      "date": "2026-03-28",
      "duration_minutes": 150,
      "description": "Implemented SAML flow and wrote integration tests",
      "activity_type": "Coding",
      "status": "Submitted",
      "approved_by": null,
      "approved_at": null,
      "created_at": "...",
      "updated_at": "..."
    }
  ],
  "next_cursor": null,
  "has_more": false
}

Approve a submitted entry (requires Admin role):

curl -s -X POST "$BASE_URL/api/v1/time-entries/$TIME_ENTRY_ID/approve" \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "id": "...",
  "user_id": "...",
  "ticket_id": "...",
  "project_id": "...",
  "date": "2026-03-28",
  "duration_minutes": 150,
  "description": "Implemented SAML flow and wrote integration tests",
  "activity_type": "Coding",
  "status": "Approved",
  "approved_by": "...",
  "approved_at": "...",
  "created_at": "...",
  "updated_at": "..."
}

Only Approved time entries are included in capitalization reports.

8. Setting Labor Rates

Labor rates define the loaded cost per hour for each user, used in capitalization calculations. Rates require Admin or Owner role.

Create a labor rate:

curl -s -X POST "$BASE_URL/api/v1/labor-rates" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"user_id\": \"$USER_ID\",
    \"org_id\": \"$ORG_ID\",
    \"loaded_rate_cents\": 15000,
    \"effective_date\": \"2026-01-01\"
  }" | jq .
{
  "id": "...",
  "user_id": "...",
  "org_id": "...",
  "loaded_rate_cents": 15000,
  "effective_date": "2026-01-01",
  "created_at": "...",
  "updated_at": "..."
}
LABOR_RATE_ID="<id from above>"

A loaded_rate_cents of 15000 means $150.00 per hour. Rates take effect from the effective_date forward. You can set multiple rates over time to track salary changes:

curl -s -X POST "$BASE_URL/api/v1/labor-rates" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"user_id\": \"$USER_ID\",
    \"org_id\": \"$ORG_ID\",
    \"loaded_rate_cents\": 16000,
    \"effective_date\": \"2026-07-01\"
  }" | jq .
{
  "id": "...",
  "user_id": "...",
  "org_id": "...",
  "loaded_rate_cents": 16000,
  "effective_date": "2026-07-01",
  "created_at": "...",
  "updated_at": "..."
}

Get the current effective rate for a user:

curl -s "$BASE_URL/api/v1/users/$USER_ID/orgs/$ORG_ID/labor-rates/current" \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "id": "...",
  "user_id": "...",
  "org_id": "...",
  "loaded_rate_cents": 15000,
  "effective_date": "2026-01-01",
  "created_at": "...",
  "updated_at": "..."
}

List all historical rates:

curl -s "$BASE_URL/api/v1/users/$USER_ID/orgs/$ORG_ID/labor-rates" \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "items": [
    {
      "id": "...",
      "user_id": "...",
      "org_id": "...",
      "loaded_rate_cents": 16000,
      "effective_date": "2026-07-01",
      "created_at": "...",
      "updated_at": "..."
    },
    {
      "id": "...",
      "user_id": "...",
      "org_id": "...",
      "loaded_rate_cents": 15000,
      "effective_date": "2026-01-01",
      "created_at": "...",
      "updated_at": "..."
    }
  ],
  "next_cursor": null,
  "has_more": false
}

9. Generating Capitalization Reports

Capitalization reports aggregate approved time entries with labor rates, grouped by project and activity type. They support ASC 350-40 / IAS 38 software cost accounting.

Generate a report for a specific month:

curl -s "$BASE_URL/api/v1/reports/capitalization?period=2026-03" \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "period": "2026-03",
  "projects": [
    {
      "project_id": "...",
      "project_key": "FIN",
      "project_name": "Finance Feature",
      "capitalization_type": "Capex",
      "development_phase": "AppDevelopment",
      "cost_center_id": "ENG-001",
      "total_hours": 2.5,
      "total_amount_cents": 37500,
      "breakdown": [
        {
          "activity_type": "Coding",
          "hours": 2.5,
          "amount_cents": 37500
        }
      ]
    }
  ]
}

Include budget tracking with include_budget=true:

curl -s "$BASE_URL/api/v1/reports/capitalization?period=2026-03&include_budget=true" \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "period": "2026-03",
  "projects": [
    {
      "project_id": "...",
      "project_key": "FIN",
      "project_name": "Finance Feature",
      "capitalization_type": "Capex",
      "development_phase": "AppDevelopment",
      "cost_center_id": "ENG-001",
      "total_hours": 2.5,
      "total_amount_cents": 37500,
      "budget_cents": 5000000,
      "budget_period": "Quarterly",
      "spent_cents": 37500,
      "budget_remaining_cents": 4962500,
      "budget_utilization_pct": 0.75,
      "breakdown": [
        {
          "activity_type": "Coding",
          "hours": 2.5,
          "amount_cents": 37500
        }
      ]
    }
  ]
}

Group results by team or user:

curl -s "$BASE_URL/api/v1/reports/capitalization?period=2026-03&group_by=user" \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "period": "2026-03",
  "users": [
    {
      "user_id": "...",
      "display_name": "TF Guide User",
      "total_hours": 2.5,
      "total_amount_cents": 37500,
      "projects": [
        {
          "project_id": "...",
          "project_key": "FIN",
          "project_name": "Finance Feature",
          "total_hours": 2.5,
          "total_amount_cents": 37500,
          "breakdown": [
            {
              "activity_type": "Coding",
              "hours": 2.5,
              "amount_cents": 37500
            }
          ]
        }
      ]
    }
  ]
}

Filter by cost center, activity type, or tag:

curl -s "$BASE_URL/api/v1/reports/capitalization?period=2026-03&cost_center_id=ENG-001&activity_type=Coding" \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "period": "2026-03",
  "projects": [
    {
      "project_id": "...",
      "project_key": "FIN",
      "project_name": "Finance Feature",
      "capitalization_type": "Capex",
      "development_phase": "AppDevelopment",
      "cost_center_id": "ENG-001",
      "total_hours": 2.5,
      "total_amount_cents": 37500,
      "breakdown": [
        {
          "activity_type": "Coding",
          "hours": 2.5,
          "amount_cents": 37500
        }
      ]
    }
  ]
}

10. Exporting and Analyzing Data

Export the capitalization report as CSV for spreadsheet analysis or ERP import:

curl -s "$BASE_URL/api/v1/reports/capitalization/export?period=2026-03" \
  -H "Authorization: Bearer $TOKEN" -o report.csv

The CSV includes columns: Period,Project,ProjectKey,Employee,Department,CostCenter,Hours,ActivityType,Phase,CapExOpEx,LoadedRate,Amount,Team,Tags,BudgetCents,SpentCents,Utilization

Filter the export by tag for department-level reporting:

curl -s "$BASE_URL/api/v1/reports/capitalization/export?period=2026-03&tag=department:engineering" \
  -H "Authorization: Bearer $TOKEN" -o engineering-report.csv

Tag time entries for additional reporting dimensions. First, tag a time entry:

curl -s -X PUT "$BASE_URL/api/v1/orgs/$ORG_ID/time_entry/$TIME_ENTRY_ID/tags" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tags": [
      {"key": "billable", "value": "true"},
      {"key": "client", "value": "acme-corp"}
    ]
  }' | jq .
[
  {
    "id": "...",
    "org_id": "...",
    "entity_type": "time_entry",
    "entity_id": "...",
    "key": "billable",
    "value": "true",
    "created_at": "...",
    "updated_at": "..."
  },
  {
    "id": "...",
    "org_id": "...",
    "entity_type": "time_entry",
    "entity_id": "...",
    "key": "client",
    "value": "acme-corp",
    "created_at": "...",
    "updated_at": "..."
  }
]

Search for all entities with a specific tag:

curl -s "$BASE_URL/api/v1/orgs/$ORG_ID/tags/search?key=billable&value=true" \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "items": [
    {
      "id": "...",
      "org_id": "...",
      "entity_type": "time_entry",
      "entity_id": "...",
      "key": "billable",
      "value": "true",
      "created_at": "...",
      "updated_at": "..."
    }
  ],
  "next_cursor": null,
  "has_more": false
}

CLI shortcut: alloy time log --ticket FIN-1 --duration 2h --type Coding

MCP tools: Use log_time, get_time_report, submit_time_entry, approve_time_entry, and get_capitalization_report for programmatic access through AI assistants.


Learn More