Getting Started
This page documents the company integration API (X-API-Key).
Use it from payroll, accounting, and HR tools to read and write staff, leave, schedules, time punches, payroll hours, mileage, and work locations.
A short quickstart with working examples is on the MCP and API page.
Download the machine contract as OpenAPI 3.1
or connect Cursor, Claude, ChatGPT, Codex, or Grok at POST https://timeoffmanager.us/api/mcp
(host setup on the MCP page).
Native invoicing (estimates, expenses, Stripe) is the web product — it is not duplicated here.
Employee Management
Create, update, and manage employee records
Leave Requests
Submit, approve, and track time-off requests
Time & timesheets
Import punches, export hours for payroll software, and day notes
Webhooks
HTTPS callbacks for leave, punches, mileage, and adjustments
Base URL
https://timeoffmanager.us/api/v1
Authentication
All API requests must include your API key in the request header:
X-API-Key: tk_REPLACE_ME
Authorization: Bearer tk_REPLACE_ME
Use in ChatGPT, Claude, Cursor, Codex, or Grok: POST https://timeoffmanager.us/api/mcp with the same Bearer header.
Download OpenAPI: /api/openapi.yaml.
Note: Create keys in the admin dashboard.
Integrations can still read a JSON catalog from GET /api/v1 when the request asks for JSON. This page is the human reference.
Permissions
If a key has no permissions list, it can call every company endpoint. New resource pairs (time:write, schedule:read, …) are optional. Existing employees:read / employees:write keys still work on time entries, mileage, locations, pay periods, and company GET. Each route accepts any one of:
| Permission | Used for |
|---|---|
| employees:read | GET employees; also satisfies today’s time/mileage/location/payroll/company reads (legacy alias) |
| employees:write | Create/update employees; also satisfies today’s time/mileage/location/schedule writes (legacy alias) |
| time:read / time:write | Time entries and forgot-punch (or use employees:*) |
| schedule:read / schedule:write | Weekly pattern and one-day hours (or use employees:*) |
| mileage:* / payroll:* / location:* / settings:* | Fine-grained alternatives; settings:write also covers webhooks |
| leave:read | GET leave requests |
| leave:write | Create, update, approve, or reject leave requests; leave types, holidays, and blackout dates |
| billing:read / billing:write | Thin invoice/client slice only, and only if invoicing is enabled for the company |
{
"error": "Forbidden",
"message": "This API key does not have the required permission: employees:read"
}
Other APIs (not this page)
- Mobile apps use Sanctum bearer tokens (
/api/v1/auth/login) — not documented here. - Blog / social automation uses a site-wide key, not company
X-API-Keyvalues.
Quick Start Example
cURL Example:
curl -X GET "https://timeoffmanager.us/api/v1/employees" \
-H "X-API-Key: tk_your_api_key_here" \
-H "Content-Type: application/json"
JavaScript Example:
fetch('https://timeoffmanager.us/api/v1/employees', {
headers: {
'X-API-Key': 'tk_your_api_key_here',
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => console.log(data));
API Endpoints
Employee Management
Retrieve a paginated list of people in your company (employees, managers, and company admins). Platform operator accounts are excluded. Pass role to filter.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| department | string | Filter by department |
| role | string | Filter by role (e.g. employee, company_admin) |
| is_active | boolean | Filter by active status |
| search | string | Search by name, email, or employee number |
| per_page | integer | Results per page (default: 20) |
Example Response:
{
"data": [
{
"id": 123,
"employee_number": "EMP-001",
"first_name": "John",
"last_name": "Doe",
"name": "John Doe",
"email": "john.doe@company.com",
"role": "employee",
"company_id": 1,
"title": "Senior Developer",
"department": "Engineering",
"manager_id": 45,
"hire_date": "2022-01-15",
"employment_type": "full-time",
"work_phone": "+1-555-0123",
"mobile_phone": "+1-555-0124",
"work_locations": [
{
"address": "123 Main St, New York, NY 10001",
"is_primary": true
}
],
"emergency_contact": {
"name": "Jane Doe",
"phone": "+1-555-0125",
"relationship": "Spouse"
},
"payroll_id": "PAY-001",
"country_code": "US",
"timezone": "America/New_York",
"is_active": true,
"created_at": "2022-01-15T10:30:00Z",
"updated_at": "2024-12-15T14:20:00Z"
}
],
"meta": {
"current_page": 1,
"last_page": 5,
"per_page": 20,
"total": 95
}
}
Create a new employee record in your company.
Request Body:
{
"employee_number": "EMP-002",
"first_name": "Jane",
"last_name": "Smith",
"email": "jane.smith@company.com",
"password": "SecurePass123!",
"title": "HR Manager",
"department": "Human Resources",
"manager_id": 45,
"hire_date": "2023-03-01",
"employment_type": "full-time",
"work_phone": "+1-555-0123",
"mobile_phone": "+1-555-0124",
"work_locations": [
{
"address": "456 Oak Ave, Los Angeles, CA 90210",
"is_primary": true
}
],
"emergency_contact": {
"name": "Bob Smith",
"phone": "+1-555-0125",
"relationship": "Brother"
},
"payroll_id": "PAY-002",
"country_code": "US",
"timezone": "America/Los_Angeles",
"ssn_sin": "123-45-6789"
}
Field Descriptions:
| Field | Type | Required | Description |
|---|---|---|---|
| first_name | string | ✓ | Employee's first name |
| last_name | string | ✓ | Employee's last name |
| string | ✓ | Unique email address | |
| password | string | ✓ | Required. Minimum 8 characters. This is the employee’s login password. |
| employee_number | string | — | Unique employee identifier |
| title | string | — | Job title |
| department | string | — | Department name |
| manager_id | integer | — | ID of reporting manager |
| hire_date | date | — | Date of hire (YYYY-MM-DD) |
| employment_type | enum | — | full-time, part-time, contract, temporary |
| work_phone | string | — | Work phone number |
| mobile_phone | string | — | Mobile phone number |
| work_locations | array | — | Array of work location objects |
| emergency_contact | object | — | Emergency contact information |
| payroll_id | string | — | External payroll system ID |
| ssn_sin | string | — | SSN/SIN (encrypted in storage) |
| country_code | string(2) | ✓ | ISO country code (default: CA) |
| timezone | string | ✓ | Timezone identifier |
Retrieve detailed information about a specific employee, including PTO balance.
Response includes:
- Complete employee profile information
- Current-year PTO via the same balance rules as the dashboard (vacation / sick / personal buckets when configured)
- Pending leave days
Update an existing employee's information. All fields are optional except for validation constraints.
Note: Use "sometimes" validation rules - only provided fields will be updated.
Soft delete by setting employee status to inactive. This preserves historical leave data while preventing new requests.
Leave Requests
Retrieve leave requests with optional filtering.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| employee_id | integer | Filter by employee ID |
| status | string | Filter by status (pending, approved, rejected) |
| type | string | Filter by type (vacation, sick, personal, unpaid) |
| year | integer | Filter by year |
employee_id must belong to your company (foreign IDs return 422, not 404).
{
"employee_id": 123,
"type": "vacation",
"start_date": "2026-10-01",
"end_date": "2026-10-03",
"notes": "Family trip"
}
type: vacation, sick, personal, unpaid, other (annual is still accepted).
Approve a pending leave request. Fires webhook leave_request.approved.
Optional body: {"rejection_reason": "…"}. Fires leave_request.rejected.
Time entries
Read requires employees:read. Import and adjust require employees:write. Posting the same employee + clock_in again returns the existing row (idempotent).
| Method | Path | Notes |
|---|---|---|
| GET | /time-entries | Query: employee_id, from, to, status (active|completed|adjusted), approved, per_page |
| POST | /time-entries | employee_id or employee_email, clock_in, optional clock_out, break_minutes, notes, source. Already-completed imports fire time_entry.completed. |
| GET | /time-entries/{id} | Single punch |
| PUT | /time-entries/{id} | Fix times, add break, or close an open shift |
Schedules
Read requires employees:read. Writes require employees:write. One-day hours create an approved schedule exception for that date only — they do not rewrite Monday–Sunday.
| Method | Path | Notes |
|---|---|---|
| GET | /employees/{id}/schedule | Weekly pattern plus upcoming one-day overrides |
| PUT | /employees/{id}/schedule | effective_date, schedules[].day_of_week (0=Sun–6=Sat), is_working, start_time, end_time, break_duration, work_location_id |
| POST | /employees/{id}/one-day-overrides | override_date, start_time, end_time, work_location_id, optional break_duration |
| POST | /employees/{id}/one-day-overrides/{id}/cancel | Today or future dates only |
Forgot-punch requests
Approve creates a completed time entry. Fires forgot_punch.approved.
| Method | Path | Notes |
|---|---|---|
| GET | /forgot-punch-requests | Query: employee_id, status, from, to, per_page |
| POST | /forgot-punch-requests/{id}/approve | Optional reviewer_notes |
| POST | /forgot-punch-requests/{id}/reject | Optional reviewer_notes |
Payroll
GET /reports/payroll-hours uses the same paid-hours engine as the Excel report (scheduled pay, holidays, approved day adjustments). Day adjustments do not change payroll until approved unless you pass apply_immediately. GET /reports/payroll-hours/export can download adp_run (ADP RUN Time Sheet Import) or paychex_spi hours CSVs. Hours only — this is not a payroll engine.
| Method | Path | Notes |
|---|---|---|
| GET | /pay-periods | Company pay periods |
| GET | /pay-periods/{id}/summary | Punch totals for that period |
| GET | /reports/payroll-hours | Required query: from, to (YYYY-MM-DD). Optional include_zero_hours |
| GET | /payroll-day-adjustments | Query: employee_id, status, from, to |
| POST | /payroll-day-adjustments | employee_id, work_date, note (required), hours_delta (−12 to 12), apply_immediately |
| POST | /payroll-day-adjustments/{id}/approve | Optional hours_delta, reviewer_notes. Fires payroll_day_adjustment.approved |
| POST | /payroll-day-adjustments/{id}/reject | Optional reviewer_notes |
Mileage & work locations
| Method | Path | Notes |
|---|---|---|
| GET | /mileage-trips | employee_id, from, to, status, per_page |
| POST | /mileage-trips | Requires mileage enabled. employee_id, trip_date, start_address, end_address, distance_km, purpose |
| POST | /mileage-trips/{id}/approve | Fires mileage_trip.approved |
| POST | /mileage-trips/{id}/reject | Optional rejection_reason |
| GET | /work-locations | Company locations |
| POST | /work-locations | name required; address/city optional (empty strings stored when omitted) |
| PUT | /work-locations/{id} | Partial update |
Webhooks
HTTPS URLs only. The signing secret is returned once on create — HMAC-SHA256 of the raw body in X-TimeOff-Signature.
| Method | Path |
|---|---|
| GET | /webhooks |
| POST | /webhooks |
| DELETE | /webhooks/{id} |
Subscribe to any of:
leave_request.created, leave_request.approved, leave_request.rejected, time_entry.completed, time_entry.approved, payroll_run.approved, payroll_day_adjustment.approved, mileage_trip.approved, forgot_punch.approved, invoice.paid
Other Resources
Retrieve public holidays for a specific country. Query: year (defaults to current year).
Example Response:
{
"data": [
{
"name": "New Year's Day",
"date": "2024-01-01",
"type": "public_holiday",
"country_code": "US"
}
],
"meta": {
"country": "US",
"year": 2024,
"total": 10,
"weekdays": 8
}
}
Query year. Returns dashboard-accurate buckets: vacation_available, sick_available, remaining_days, pending_days.
id, name, country_code, timezone, total_employees (all company staff, not only role=employee).
Built-in types plus active custom types for the company.
Error Handling
The API uses standard HTTP status codes to indicate success or failure of requests.
| Status Code | Description |
|---|---|
200 |
Success - Request completed successfully |
201 |
Created - Resource created successfully |
401 |
Unauthorized - Invalid or missing API key |
403 |
Forbidden — missing permission, invoicing disabled, or mileage not enabled |
404 |
Not Found - Resource not found |
422 |
Unprocessable Entity - Validation error |
429 |
Too Many Requests - Rate limit exceeded |
Error Response Format
{
"error": "Validation Error",
"message": "The given data was invalid.",
"errors": {
"email": ["The email field is required."],
"employee_number": ["The employee number has already been taken."]
}
}
Rate Limiting
Each API key has its own hourly cap (default 1000 requests per hour, configurable when you create the key). Exceeding it returns HTTP 429.
Rate-limit headers are not currently sent; count against the key’s rate_limit setting.
SDKs & Libraries
While we don't yet provide official SDKs, our RESTful API works seamlessly with any HTTP client library in your preferred programming language.
Node.js / JavaScript
Use fetch, axios, or any HTTP client:
axios, fetch, request
Python
Compatible with popular libraries:
requests, httpx, urllib
PHP
Works with standard HTTP clients:
Guzzle, cURL, file_get_contents
Need Help?
Getting Started
- • Create a free account to get started
- • Generate your API keys from the dashboard
- • Test endpoints using our interactive examples
- • Review our integration guides
Support Channels
- • Email: support@timeoffmanager.com
- • Response time: < 24 hours
- • Available: Monday-Friday, 9 AM - 5 PM PST
- • Enterprise support available