End-to-end integration workflows, common failure modes, and reusable Bash, Node.js, and Python clients.
On this page
15. End-to-end workflows
A — Create user and assign work context
# 1) Create
USER_JSON=$(curl -s -X POST "$API_BASE/user" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "apikey: $API_KEY" \
-d '{
"FirstName":"Jane","LastName":"Doe","EmailAddress":"jane@example.com",
"PayType":"HOURLY","AccessLevel":"EMP","TimezoneID":15,"SupervisorID":50,
"EnableHourlyTracking":true,"EnableProjectTracking":true
}')
# Parse UserID from USER_JSON when errors is empty
USER_ID=101
# 2) Assign customers, projects, account codes
curl -s -X POST "$API_BASE/user/$USER_ID/customers" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" -H "apikey: $API_KEY" \
-d '{"CustomerID":"10,11","DefaultCustomerID":10}'
curl -s -X POST "$API_BASE/user/$USER_ID/projects" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" -H "apikey: $API_KEY" \
-d '{"ProjectID":"200,201","DefaultProjectID":200}'
curl -s -X POST "$API_BASE/user/$USER_ID/accountcodes" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" -H "apikey: $API_KEY" \
-d '{
"AccountCodeID":"30,31,32",
"DefaultHourlyAccountCodeID":30,
"DefaultProjectAccountCodeID":31,
"DefaultExpenseAccountCodeID":32
}'
B — Offboard (resolve blockers → archive)
USER_ID=101
curl -s -X PATCH "$API_BASE/user/$USER_ID/archive" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" -H "apikey: $API_KEY" \
-d '{"DateOfTermination":"2026-08-05"}'
# If errorCodes include resolvable blockers:
curl -s -X PATCH "$API_BASE/user/$USER_ID/archive/resolve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" -H "apikey: $API_KEY" \
-d '{
"OnClockAction":"FORCE_CLOCK_OUT",
"OpenTimerAction":"STOP_ALL",
"UnpaidHourlyAction":"ARCHIVE",
"UnpaidProjectAction":"ARCHIVE",
"OpenExpenseAction":"RECONCILE"
}'
curl -s -X PATCH "$API_BASE/user/$USER_ID/archive" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" -H "apikey: $API_KEY" \
-d '{"DateOfTermination":"2026-08-05"}'
C — Look up items then assign
# Find customers
curl -s "$API_BASE/items/customer?FullTextSearch=acme&MaxRows=10" \
-H "Authorization: Bearer $TOKEN" -H "apikey: $API_KEY"
# Assign one found ID as default
curl -s -X PUT "$API_BASE/user/101/customer/10" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" -H "apikey: $API_KEY" \
-d '{"IsDefault":true}'
16. Common errors
| Symptom | Likely cause |
|---|---|
| HTTP 401 | Missing/invalid apikey or Bearer token; auth mismatch; archived API user |
| HTTP 420 | Rate limit exceeded |
"errors": [...] with HTTP 200 |
Validation or business rule failure — treat as failure |
| Read-only item message | Cannot assign/unassign that customer/project/account code |
| Default ID rejected | Not in assign list, or wrong tracking type for account codes |
| Archive hard-stop codes | Primary contact / self / permission — resolve manually |
ArchiveUserResolutionInvalidPolicy |
Typo in policy enum |
| Collection DELETE validation error | Forgot query-string ID list (body is ignored) |
17. Client snippets
Bash helper
api() {
local method="$1" path="$2" body="${3:-}"
if [[ -n "$body" ]]; then
curl -s -X "$method" "$API_BASE$path" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "apikey: $API_KEY" \
-d "$body"
else
curl -s -X "$method" "$API_BASE$path" \
-H "Authorization: Bearer $TOKEN" \
-H "apikey: $API_KEY"
fi
}
api GET "/health-check" # health needs no headers — call curl directly
api GET "/users?Status=USER_STATUS_ACTIVE&MaxRows=5"
api POST "/user/101/customers" '{"CustomerID":"10,11","DefaultCustomerID":10}'
api DELETE "/user/101/projects?ProjectID=201"
api PATCH "/user/101/archive" '{"DateOfTermination":"2026-08-05"}'
Node.js (fetch)
const API_BASE = process.env.API_BASE;
const headers = {
Authorization: `Bearer ${process.env.TOKEN}`,
apikey: process.env.API_KEY,
"Content-Type": "application/json",
};
async function api(method, path, body) {
const res = await fetch(`${API_BASE}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
const json = await res.json();
if (json.errors?.length) {
const err = new Error(json.errors.join(" | "));
err.errorCodes = json.errorCodes || [];
err.response = json;
throw err;
}
return json.data ?? json;
}
await api("GET", "/items/customer?MaxRows=10");
await api("POST", "/user/101/projects", { ProjectID: "200,201", DefaultProjectID: 200 });
await api("PATCH", "/user/101/archive/resolve", {
OnClockAction: "FORCE_CLOCK_OUT",
OpenTimerAction: "STOP_ALL",
UnpaidHourlyAction: "ARCHIVE",
UnpaidProjectAction: "ARCHIVE",
OpenExpenseAction: "RECONCILE",
});
await api("PATCH", "/user/101/archive", { DateOfTermination: "2026-08-05" });
Python (requests)
import os
import requests
API_BASE = os.environ["API_BASE"]
HEADERS = {
"Authorization": f"Bearer {os.environ['TOKEN']}",
"apikey": os.environ["API_KEY"],
"Content-Type": "application/json",
}
def api(method, path, json=None, params=None):
r = requests.request(method, f"{API_BASE}{path}", headers=HEADERS, json=json, params=params)
r.raise_for_status()
payload = r.json()
if payload.get("errors"):
raise RuntimeError(f"{payload['errors']} codes={payload.get('errorCodes', [])}")
return payload.get("data", payload)
api("GET", "/server/timezones")
api("PUT", "/user/101/accountcode/30", json={"IsDefaultHourly": True})
api("DELETE", "/user/101/customers", params={"CustomerID": "11,12"})
api("GET", "/report/user/info", params={"UserList": "101,102"})