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.
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 & payroll
Import punches, payroll hours, and day adjustments
Webhooks
HTTPS callbacks for leave, punches, mileage, and adjustments
Base URL
https://timeoffpay.us/api/v1
Authentication
All API requests must include your API key in the request header:
X-API-Key: tk_your_api_key_here
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. If you check specific boxes when creating the key, each route requires one of:
| Permission | Used for |
|---|---|
| employees:read | GET employees, schedules, time entries, payroll hours, forgot-punch list, mileage, locations, company, holidays, leave summary |
| employees:write | Create/update employees, schedules, one-day hours, import punches, forgot-punch approve/reject, day adjustments, mileage write, work locations, webhooks |
| leave:read | GET leave requests |
| leave:write | Create, update, approve, or reject leave requests |
| 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://timeoffpay.us/api/v1/employees" \
-H "X-API-Key: tk_your_api_key_here" \
-H "Content-Type: application/json"
JavaScript Example:
fetch('https://timeoffpay.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": "[email protected]",
"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": "[email protected]",
"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.
| 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.
/billing/clients and /billing/invoices (list/create/show/send/payments/pdf). Estimates, expenses, recurring, and Stripe Connect are not part of this API.
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: [email protected]
- • Response time: < 24 hours
- • Available: Monday-Friday, 9 AM - 5 PM PST
- • Enterprise support available