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 Node.js Python
Copy curl https://app.getoatmilk.com/api/v1/contractor/organizations.list \
-H "Authorization: Bearer $OATMILK_OAUTH_TOKEN" const response = await fetch ( "https://app.getoatmilk.com/api/v1/contractor/organizations.list" , {
headers : {
Authorization : `Bearer ${process.env.OATMILK_OAUTH_TOKEN}` ,
},
});
const { data, error } = await response. json ();
if (! response. ok) throw new Error ( `${error.code}: ${error.message}` );
console. log ( data); import os
import requests
response = requests. get (
"https://app.getoatmilk.com/api/v1/contractor/organizations.list" ,
headers={
"Authorization" : f"Bearer {os.environ['OATMILK_OAUTH_TOKEN']}" ,
},
timeout= 30 ,
)
payload = response. json ()
if not response. ok:
raise RuntimeError ( f"{payload['error']['code']}: {payload['error']['message']}" )
print ( payload[ "data" ]) Send the chosen company's ID as X-Accounting-Organization on every request after this one.
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 Node.js Python
Copy curl https://app.getoatmilk.com/api/v1/contractor/hours.form \
-H "Authorization: Bearer $OATMILK_OAUTH_TOKEN" \
-H "X-Accounting-Organization: $OATMILK_ORGANIZATION_ID" const response = await fetch ( "https://app.getoatmilk.com/api/v1/contractor/hours.form" , {
headers : {
Authorization : `Bearer ${process.env.OATMILK_OAUTH_TOKEN}` ,
"X-Accounting-Organization" : process. env. OATMILK_ORGANIZATION_ID,
},
});
const { data, error } = await response. json ();
if (! response. ok) throw new Error ( `${error.code}: ${error.message}` );
console. log ( data); import os
import requests
response = requests. get (
"https://app.getoatmilk.com/api/v1/contractor/hours.form" ,
headers={
"Authorization" : f"Bearer {os.environ['OATMILK_OAUTH_TOKEN']}" ,
"X-Accounting-Organization" : os. environ[ "OATMILK_ORGANIZATION_ID" ],
},
timeout= 30 ,
)
payload = response. json ()
if not response. ok:
raise RuntimeError ( f"{payload['error']['code']}: {payload['error']['message']}" )
print ( payload[ "data" ]) 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 Node.js Python
Copy 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"
}' const response = await fetch ( "https://app.getoatmilk.com/api/v1/contractor/hours.create" , {
method : "POST" ,
headers : {
Authorization : `Bearer ${process.env.OATMILK_OAUTH_TOKEN}` ,
"X-Accounting-Organization" : process. env. OATMILK_ORGANIZATION_ID,
"Content-Type" : "application/json" ,
"Idempotency-Key" : crypto. randomUUID (),
},
body : JSON. stringify ({
"date" : "2026-09-15" ,
"minutes" : 90 ,
"description" : "Design review with the finance team"
}),
});
const { data, error } = await response. json ();
if (! response. ok) throw new Error ( `${error.code}: ${error.message}` );
console. log ( data); import os
import uuid
import requests
response = requests. post (
"https://app.getoatmilk.com/api/v1/contractor/hours.create" ,
headers={
"Authorization" : f"Bearer {os.environ['OATMILK_OAUTH_TOKEN']}" ,
"X-Accounting-Organization" : os. environ[ "OATMILK_ORGANIZATION_ID" ],
"Idempotency-Key" : str ( uuid. uuid4 ()),
},
json={
"date" : "2026-09-15" ,
"minutes" : 90 ,
"description" : "Design review with the finance team" ,
},
timeout= 30 ,
)
payload = response. json ()
if not response. ok:
raise RuntimeError ( f"{payload['error']['code']}: {payload['error']['message']}" )
print ( payload[ "data" ]) sync-hours.js Copy 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 Node.js Python
Copy 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"
}' const response = await fetch ( "https://app.getoatmilk.com/api/v1/contractor/timesheet.submit" , {
method : "POST" ,
headers : {
Authorization : `Bearer ${process.env.OATMILK_OAUTH_TOKEN}` ,
"X-Accounting-Organization" : process. env. OATMILK_ORGANIZATION_ID,
"Content-Type" : "application/json" ,
"Idempotency-Key" : crypto. randomUUID (),
},
body : JSON. stringify ({
"date" : "2026-09-15"
}),
});
const { data, error } = await response. json ();
if (! response. ok) throw new Error ( `${error.code}: ${error.message}` );
console. log ( data); import os
import uuid
import requests
response = requests. post (
"https://app.getoatmilk.com/api/v1/contractor/timesheet.submit" ,
headers={
"Authorization" : f"Bearer {os.environ['OATMILK_OAUTH_TOKEN']}" ,
"X-Accounting-Organization" : os. environ[ "OATMILK_ORGANIZATION_ID" ],
"Idempotency-Key" : str ( uuid. uuid4 ()),
},
json={
"date" : "2026-09-15" ,
},
timeout= 30 ,
)
payload = response. json ()
if not response. ok:
raise RuntimeError ( f"{payload['error']['code']}: {payload['error']['message']}" )
print ( payload[ "data" ]) 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.