# Log contractor hours from a time tracker

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

Source: https://app.getoatmilk.com/docs/tutorials/contractor-timesheets

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](https://app.getoatmilk.com/docs/contractors.md) and the contractor's own sign-in.

## 1. Choose the company

```bash
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.

```bash
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.

```bash
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"
}'
```

```js title="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:

```bash
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.
