Tutorials

Log contractor hours from a time tracker

Send time entries to a contractor's timesheet and submit it for approval.

15 minutes · Intermediate

Contractors often track time somewhere else: a timer app, a calendar, a spreadsheet. This tutorial copies a week of entries into their Oatmilk timesheet and submits it, using the contractor API and the contractor's own sign-in.

1. Choose the company

curl https://app.getoatmilk.com/api/v1/contractor/organizations.list \
  -H "Authorization: Bearer $OATMILK_OAUTH_TOKEN"

Send the chosen company's ID as X-Accounting-Organization on every request after this one.

2. Check the hours form

Some companies ask for more than a date, minutes and a description, such as a project. hours.form lists the extra fields to fill.

curl https://app.getoatmilk.com/api/v1/contractor/hours.form \
  -H "Authorization: Bearer $OATMILK_OAUTH_TOKEN" \
  -H "X-Accounting-Organization: $OATMILK_ORGANIZATION_ID"

3. Add each entry

Give each entry an idempotency key built from your tracker's own ID, so running the sync twice never adds an entry twice.

curl https://app.getoatmilk.com/api/v1/contractor/hours.create \
  -H "Authorization: Bearer $OATMILK_OAUTH_TOKEN" \
  -H "X-Accounting-Organization: $OATMILK_ORGANIZATION_ID" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "date": "2026-09-15",
  "minutes": 90,
  "description": "Design review with the finance team"
}'
sync-hours.js
async function contractorCall(action, input, idempotencyKey) {
  const response = await fetch(`https://app.getoatmilk.com/api/v1/contractor/${action}`, {
    method: "POST",
    headers: { Authorization: `Bearer ${token}`, "X-Accounting-Organization": organizationId, "Content-Type": "application/json", "Idempotency-Key": idempotencyKey },
    body: JSON.stringify(input),
  });
  const { data, error } = await response.json();
  if (!response.ok) throw new Error(error.code);
  return data;
}

for (const entry of trackerEntries) {
  await contractorCall("hours.create", { date: entry.date, minutes: entry.minutes, description: entry.note }, `tracker-${entry.id}`);
}

4. Submit the timesheet

Submit every draft entry in the pay period that contains a date:

curl https://app.getoatmilk.com/api/v1/contractor/timesheet.submit \
  -H "Authorization: Bearer $OATMILK_OAUTH_TOKEN" \
  -H "X-Accounting-Organization: $OATMILK_ORGANIZATION_ID" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "date": "2026-09-15"
}'

Finance reviews the hours in Oatmilk. On the company's side, webhooks receive contractor.hours.submitted now and contractor.hours.approved once they're approved.