API Reference - digitalunconciousness/shiftledger GitHub Wiki
All API endpoints return JSON. Authenticated endpoints require either:
- A valid
sl_sessioncookie (set automatically by the browser on login), or - An
Authorization: Bearer <token>header (for mobile clients and API access)
Unauthorized requests return 401. Admin-only endpoints return 403 for non-admin users.
Public health check. Returns {"status":"ok","db":"ok"}. No authentication required.
Check the current auth state.
Response:
-
{"status": "setup"}— no users exist, first-run setup needed -
{"status": "login"}— users exist but no valid session -
{"status": "authenticated", "user": {...}}— logged in; includes user object
Create the first admin account. Only works when no users exist.
Body:
{
"username": "admin",
"display_name": "Admin User",
"password": "secret",
"color": "#f5a623"
}-
username— 2–50 chars, alphanumeric + underscores (required) -
display_name— 1–100 chars (required) -
password— 4–200 chars (required) -
color— hex color, optional (auto-assigned if omitted)
Response: 200 with user object and token. Sets sl_session cookie.
Errors: 400 if setup already completed.
Body:
{
"username": "admin",
"password": "secret"
}Response: 200 with user object and token. Sets sl_session cookie (30-day expiry).
Errors: 401 invalid credentials.
Clears the session cookie and deletes the server-side session record.
Response: {"success": true}
Self-registration endpoint. Allows users to create their own accounts without admin intervention. If no users exist, the first signup is granted admin.
Body: Same as /api/auth/setup.
Response: 200 with user object and token. Sets sl_session cookie.
Errors: 409 if username already taken; 429 if rate limit exceeded.
Create a new user as an admin. Same body format as /api/auth/setup.
Response: 200 with new user object.
Errors: 409 if username already taken. 403 if not admin.
Rotate the current session token. Issues a new token and invalidates the old one. Used by mobile clients for token rotation.
Response: {"token": "<new_token>", "user": {...}}
Errors: 401 if the current session is invalid or expired.
Change the authenticated user's own password.
Body:
{
"current_password": "old-secret",
"new_password": "new-secret"
}Errors: 401 if current_password is wrong; 400 if the new password was used recently (password history check).
Return the authenticated user's profile, including household memberships.
Response: User object with an additional households array:
{
"id": 1,
"username": "alice",
"display_name": "Alice",
"is_admin": true,
"color": "#f5a623",
"created_at": "...",
"households": [
{ "id": 1, "name": "Family", "member_count": 3 }
]
}List all users. Returns id, username, display_name, is_admin, color, created_at.
Get a specific user. Admins can fetch any user; regular users can only fetch themselves.
Response: User object with households array.
Update a user. Admins can update any user; regular users can only update themselves.
Body (all fields optional):
{
"display_name": "New Name",
"color": "#3ecf8e",
"password": "newpass",
"is_admin": true
}is_admin can only be changed by an admin.
Permanently delete a user and all their sessions.
Errors: 400 if trying to delete yourself.
Reset another user's password without knowing the current one.
Body:
{ "new_password": "resetpassword" }Response: {"success": true}
Retrieve the audit log for sensitive operations.
Response: Array of audit log entries, newest first:
[
{
"id": 42,
"user_id": 1,
"username": "admin",
"action": "change_password",
"target_id": 2,
"meta": "{}",
"created_at": "2026-03-24T04:00:00.000Z"
}
]Common action values: login, logout, register, setup, change_password, reset_password, delete_user, create_household, leave_household.
Households allow multiple users to pool their data — combined shifts appear on the shared dashboard, and jobs/templates are visible across members.
List all households the authenticated user belongs to.
Response: Array of household objects with member_count.
List members of a household. The caller must be a member.
Response: Array of { user_id, username, display_name, color, role, joined_at }.
Create a new household. The creator is automatically added as an admin member and an invite_code is generated.
Body:
{ "name": "My Family" }Response: Household object including invite_code.
Join a household using its invite code.
Body:
{ "invite_code": "ABC12345" }Errors: 404 if code is invalid; 409 if already a member.
Update a household's name. The caller must be a household admin member.
Body: { "name": "New Name" }
Delete a household. The caller must be a household admin member. Removes all memberships and invitations.
Leave a household. If the caller is the last admin, the household is deleted.
Remove another member from a household. The caller must be a household admin.
Errors: 403 if not a household admin.
Send an invitation to another user by username.
Body:
{ "username": "bob" }Errors: 404 if username not found; 409 if already a member or invitation is pending.
Response: Invitation object.
List pending invitations sent for a specific household. The caller must be a household admin.
List all pending invitations received by the authenticated user.
Response: Array of { id, household_id, household_name, inviter_username, status, created_at }.
Accept a pending invitation. Adds the user to the household.
Errors: 404 if invitation not found or not addressed to the caller.
Decline a pending invitation.
Create a new shift.
Body:
{
"date": "2026-03-10",
"hourly_rate": 15.00,
"hours_worked": 8.0,
"tip_mode": "total",
"tip_input": 120.00,
"notes": "Busy Saturday night",
"job_id": 1
}-
date—YYYY-MM-DDformat (required) -
hourly_rate— non-negative number (required) -
hours_worked— non-negative number (required) -
tip_mode—"total"or"per_hour"(required) -
tip_input— non-negative number (required) -
notes— string, optional (default"") -
job_id— integer or null, optional (defaultnull)
Server computes: total_tips, wage_total, grand_total.
Response: {"id": 1, "total_tips": 120, "wage_total": 120, "grand_total": 240}
List shifts. Soft-deleted shifts are excluded.
Query params:
-
from— start date (YYYY-MM-DD) -
to— end date (YYYY-MM-DD) -
user_id— filter by user (admin can pass any user_id; non-admins can only filter within their household)
Returns shifts joined with user display name/color and job name/color. Ordered by date DESC.
Update a shift. Same body as POST. You must own the shift or be an admin.
Soft-delete a shift (sets deleted_at timestamp). You must own it or be an admin.
Restore a soft-deleted shift (clears deleted_at).
List active employers visible to the current user, ordered by name.
Each employer includes: id, user_id, name, no_tax, archived, created_at.
Create an employer.
Body:
{
"name": "Restaurant Group LLC",
"no_tax": false
}-
name— 1–100 chars (required) -
no_tax— boolean, optional (defaultfalse)-
truemeans earnings associated with this employer are excluded from taxable totals
-
Response: 201 with the created employer object.
Update an employer. Same body as POST.
Errors: 404 if not found/archived, 403 if not owner (unless admin).
Archive an employer (archived = 1) and clear employer links on related jobs/fixed incomes.
Response: {"success": true}
List all jobs visible to the current user. Ordered by employer name (when linked), then job name.
Includes:
-
tip_payment,archived -
employer_id(nullable) -
employer_nameandemployer_no_tax(joined from employers) -
tip_calc_round(boolean-like integer 0/1 in SQLite-backed rows)
Body:
{
"name": "Restaurant ABC",
"default_rate": 15.00,
"color": "#3ecf8e",
"overtime_threshold": 40,
"overtime_multiplier": 1.5,
"tip_payment": "cash",
"employer_id": 2,
"tip_calc_round": false
}-
name— 1–100 chars (required) -
default_rate— non-negative number (default0) -
color— hex color (default#f5a623) -
overtime_threshold— weekly hours before OT (default40) -
overtime_multiplier— OT pay multiplier (default1.5) -
tip_payment—"cash"or"paycheck"(default"cash")-
"cash"— tips received nightly in cash; excluded from paycheck gross but still taxed -
"paycheck"— tips included in the paycheck
-
-
employer_id— integer ornull, optional (defaultnull) -
tip_calc_round— boolean, optional (defaultfalse)
Update a job. Same body as POST.
Archive a job (sets archived = 1). Does not delete associated shifts.
List active fixed recurring income streams visible to the current user (newest first).
Returns fixed_incomes rows plus employer_name when linked.
Create a fixed recurring income stream.
Body:
{
"employer_id": 2,
"amount": 250.00,
"recurrence": "semimonthly",
"anchor_date": "",
"semimonthly_day1": 1,
"semimonthly_day2": 15,
"custom_interval_days": null,
"custom_dates": "",
"notes": "Housing stipend"
}-
employer_id— integer ornull, optional -
amount— positive number (required) -
recurrence— one of:weekly,biweekly,semimonthly,monthly,custom -
anchor_date—YYYY-MM-DDrequired for weekly/biweekly/monthly (and custom when using interval days) -
semimonthly_day1/semimonthly_day2— required and must differ for semimonthly recurrence -
custom_interval_days/custom_dates— for custom recurrence, provide either interval days or valid CSV dates -
notes— optional string (max 500 chars)
Response: 201 with { "id": <new_id> }
Update a fixed recurring income stream. Same body as POST.
Archive a fixed recurring income stream (archived = 1).
Response: {"success": true}
List all templates visible to the current user, with joined job name.
Body:
{
"name": "Weeknight Shift",
"job_id": 1,
"hourly_rate": 15.00,
"hours_worked": 6.0,
"tip_mode": "total",
"tip_input": 80.00,
"notes": ""
}Permanently delete a template.
List all goals, newest first.
Return historical goal performance — past periods with target and actual amounts.
Response: Array of { period, period_type, target_amount, actual_amount, achieved }.
Body:
{
"period": "weekly",
"target_amount": 1000.00,
"active": true
}-
period—"weekly"or"monthly" -
target_amount— non-negative number -
active— boolean, optional (defaulttrue)
Setting active: true deactivates any other goal with the same period.
Update a goal. Same body as POST.
Permanently delete a goal.
Returns all settings from the meta table as key-value pairs, including:
-
pay_week_start_day— 0 (Sun) through 6 (Sat), default 1 (Mon) -
pay_period_type—weekly,biweekly,semimonthly, ormonthly -
pay_period_anchor— anchor date for biweekly periods
Update settings. Body is a flat object of key-value pairs:
{
"pay_week_start_day": "1",
"pay_period_type": "biweekly",
"pay_period_anchor": "2026-01-06"
}List all tax/deduction items for the current user, ordered by sort_order.
Update a single tax config item by ID.
Body: label, rate (0–1 decimal), flat_amount, enabled.
Add a new tax/deduction item.
Body: key (unique slug), label, rate (0–1), flat_amount.
Remove a tax/deduction item.
Preset rate bundles based on 2026 IRS brackets and national-average state rates. Designed for quick setup — not a substitute for tax advice.
List all presets and current tax metadata.
Response:
{
"profiles": [
{
"id": "single_50k",
"filing_status": "Single",
"income_range": "~$42k–$60k/yr",
"label": "Single, ~$50k/yr – no dependents",
"approx_annual_income": 50000,
"description": "...",
"rates": {
"federal": 0.08,
"state": 0.05,
"social_security": 0.062,
"medicare": 0.0145
}
}
],
"meta": {
"tax_year": 2026,
"last_updated": "2026-03-16T06:23:13.567Z",
"next_auto_refresh": "2026-07-01T05:00:00.000Z",
"refresh_policy": "Automatically refreshes baseline rates every 6 months (Jan 1 / Jul 1).",
"note": "State rates shown are approximate national averages. Adjust to your state."
}
}Filing statuses available: Single, Head of Household, Married Filing Jointly.
Apply a preset to tax_config. Behavior differs by role:
-
Admin: overwrites the global (system-default)
tax_configentries for Federal, State, Social Security, and Medicare rates. -
Regular user: upserts per-user
tax_configentries, leaving other users' configs untouched.
Response: { "success": true, "profile": "single_50k", "applied": { "federal": "updated", ... } }
Force an immediate baseline rate refresh, bypassing the normal six-month schedule.
Response:
{
"success": true,
"refreshed": true,
"year": 2026,
"next_at": "2026-07-01T05:00:00.000Z",
"meta": { ... }
}The auto-refresh can be disabled by setting the
tax_auto_refresh_enabledmeta key to0in the database.
List recorded paychecks for the current user, newest first.
Response: Array of paycheck objects with period_start, period_end, gross_amount, net_amount, notes.
Record an actual paycheck.
Body:
{
"period_start": "2026-03-09",
"period_end": "2026-03-15",
"gross_amount": 700.00,
"net_amount": 560.00,
"notes": "Spring break week"
}Delete a paycheck record.
Recalculate stored net amounts for a paycheck using the current tax config rates.
Returns the estimated paycheck for the current pay period.
Response:
{
"period": {
"type": "Weekly",
"start": "2026-03-09",
"end": "2026-03-15",
"total_days": 7,
"elapsed_days": 5,
"remaining_days": 2
},
"current": {
"wages": 600.00,
"tips": 400.00,
"cash_tips": 300.00,
"paycheck_tips": 100.00,
"gross": 1000.00,
"paycheck_gross": 700.00,
"hours": 40.0,
"shifts": 5
},
"previous": {
"gross": 950.00,
"hours": 38.0,
"shifts": 5
},
"taxes": [
{"key": "federal", "label": "Federal Income Tax", "rate": 0.22, "flat_amount": 0, "amount": 220.00}
],
"total_tax": 350.00,
"net_pay": 350.00,
"projected_gross": 980.00,
"projected_net": 490.00,
"projected_cash_tips": 420.00
}Key fields:
-
current.gross— total earnings (wages + all tips) -
current.cash_tips— tips from jobs configured as "cash" (already received nightly) -
current.paycheck_tips— tips from jobs configured as "paycheck" -
current.paycheck_gross— wages + paycheck tips only (what appears on your check) -
total_tax— taxes on ALL earnings including cash tips -
net_pay— paycheck gross minus total tax (your check amount) -
projected_cash_tips— projected cash tips through end of period
Returns earnings aggregations for multiple periods.
Query params: user_id — optional filter.
Response keys: this_week, last_week, biweekly, this_month, last_month, ytd, all_time
Each contains: shifts, total_hours, total_wages, total_tips, grand_total, avg_shift, avg_tips_per_hour.
Aggregated time-series data for charts.
Query params: period — "week" (12 weeks), "month" (12 months, default), or "year" (3 years).
Returns array of: period, shifts, total_hours, total_wages, total_tips, grand_total, tips_per_hour, effective_rate.
Current-week overtime status.
Response: total_hours, threshold, multiplier, regular_hours, overtime_hours, is_overtime.
Estimated tax liability.
Query params: period — "month" or "ytd" (default).
Response: wages, tips, total, est_wage_tax, est_tip_tax, est_total_tax, wage_rate, tip_rate.
Average effective hourly rate by day of week (0=Sun through 6=Sat).
Top and bottom shifts by grand total.
Query params: count — number of results per list (default 5).
Response: {"best": [...], "worst": [...]}
Monthly tip percentage trend.
Returns array of: period, total_tips, grand_total, tip_pct.
Download a styled PDF report.
Query params:
-
from,to— date range (optional; omit for all-time) -
label— custom title for the report header
Returns application/pdf.
Download shifts as CSV.
Query params: from, to — date range (optional).
Returns text/csv.
Import shifts from CSV. Send the raw CSV text as the request body with any content type.
Limits: 5 MB max.
Response: {"imported": 15, "errors": 2}
All endpoints validated by Zod return 400 with:
{
"error": "Validation failed",
"details": {
"field_name": ["Error message"]
}
}