Common workflows
End-to-end Hoursmith API examples — create a client and project, log time, and read back invoices.
These walkthroughs stitch the endpoints together into real tasks. They assume a $TOKEN
environment variable holding your API token and use the base URL
https://my.hoursmith.app/api/v1.
Set up a client and project
# 1. Create a client (POST requires an Idempotency-Key)
curl https://my.hoursmith.app/api/v1/clients \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "name": "Globex", "email": "billing@globex.com", "currency": "USD" }'
# Response: { "data": { "id": "cl_...", "name": "Globex", ... } }
# 2. Create an hourly project for that client
curl https://my.hoursmith.app/api/v1/projects \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "clientId": "cl_...", "name": "Website redesign" }'See the exact accepted fields on Create a client and Create a project.
Create or reconcile a fixed-fee project
Fixed fee projects require a contract value, do not use hourlyRate, and may have an optional
budgetHours effort cap:
curl https://my.hoursmith.app/api/v1/projects \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"clientId": "cl_...",
"name": "Brand launch",
"rateType": "FIXED",
"fixedFeeAmount": "12000.00",
"budgetHours": "160.00"
}'New fixed-fee projects initialize with no prior billing. To make an older or imported fixed-fee project invoiceable, explicitly reconcile what was invoiced before the project-linked ledger began:
curl -X PATCH https://my.hoursmith.app/api/v1/projects/pr_... \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "fixedFeePriorInvoicedAmount": "2500.00" }'Use "0.00" when nothing was invoiced previously. Member responses omit this field and other
project money/reconciliation fields.
Log time
The canonical way to record time is durationSeconds. You can also send a human duration string
like "1h30m" or "1:30".
curl https://my.hoursmith.app/api/v1/time-entries \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"projectId": "pr_...",
"entryDate": "2026-06-11",
"durationSeconds": 5400,
"note": "Homepage layout",
"billable": true
}'await fetch('https://my.hoursmith.app/api/v1/time-entries', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.HOURSMITH_API_TOKEN}`,
'Idempotency-Key': crypto.randomUUID(),
'Content-Type': 'application/json',
},
body: JSON.stringify({
projectId: 'pr_...',
entryDate: '2026-06-11',
duration: '1h30m',
note: 'Homepage layout',
}),
});billable: true is effective only on an Hourly project. Fixed fee and Non-billable project policy
stores new task/time work as non-billable, and their list/get responses and filters report it that
way even for immutable historical rows.
Log time by start and end instead
If you know when the work happened rather than how long it took, send a startTime and endTime
pair as 24-hour "HH:mm" and skip the duration entirely — Hoursmith works it out:
curl https://my.hoursmith.app/api/v1/time-entries \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"projectId": "pr_...",
"entryDate": "2026-07-29",
"startTime": "09:00",
"endTime": "10:30",
"note": "Homepage layout"
}'A few rules worth knowing:
- Send both or neither. A start with no end is what a running timer looks like, so it's rejected.
- Don't also send a duration. With both times present the duration is derived from them; a duration that contradicts them is an error rather than a silent overwrite.
- The times are wall-clock in the entry owner's timezone.
- An end at or before the start means the work ran past midnight.
- On
PATCH, omit both to leave existing times alone, or sendnullfor both to clear them.
Check the running timer
curl "https://my.hoursmith.app/api/v1/time-entries?running=true&limit=1" \
-H "Authorization: Bearer $TOKEN"Every time entry carries an isRunning flag — that, not stoppedAt === null, is how you tell a
live timer from a finished entry. A manual entry can now carry a start and end time of its own, so a
present startedAt no longer means "still running".
Pull a month of time for a project
curl "https://my.hoursmith.app/api/v1/time-entries?projectId=pr_...&entryDateFrom=2026-06-01&entryDateTo=2026-06-30&sort=-entryDate&limit=200" \
-H "Authorization: Bearer $TOKEN"Then page with the nextCursor until it's null.
Find work done in a specific window
entryDate is the day an entry is filed under; startedAt is when the work actually began. To
ask "what did I work on yesterday morning?", filter on startedAt:
curl "https://my.hoursmith.app/api/v1/time-entries?startedAtFrom=2026-07-28T09:00:00Z&startedAtTo=2026-07-28T12:00:00Z&sort=startedAt" \
-H "Authorization: Bearer $TOKEN"Entries logged as a plain duration have no start time and are left out of any startedAt range.
Find unpaid invoices
Invoices are read-only over the API. List the sent ones and check each invoice's paymentStatus
field (unpaid, partially_paid, paid, refunded):
curl "https://my.hoursmith.app/api/v1/invoices?status=sent&sort=-issueDate" \
-H "Authorization: Bearer $TOKEN"const res = await fetch('https://my.hoursmith.app/api/v1/invoices?status=sent', {
headers: { Authorization: `Bearer ${token}` },
});
const { data } = await res.json();
const unpaid = data.filter((inv) => inv.paymentStatus !== 'paid');Creating and sending invoices stays in the app (or happens automatically as you bill tracked time). The API gives you read access to the results so you can sync them into your own reporting.
Tips for robust integrations
- Generate one
Idempotency-Keyper logical write and reuse it on retries. - Handle
429withRetry-After, and409for locked records. - Store ids you create (client, project) so later calls can reference them.