{"openapi":"3.1.0","info":{"title":"FairTest API","version":"1.0.1","description":"Public REST API for FairTest — drug & alcohol testing management. Use API keys to authenticate and access your organisation's data programmatically.\n\n## Authentication\n\nAll requests require an API key passed in the `Authorization` header:\n\n```\nAuthorization: Bearer ft_your_api_key_here\n```\n\nAPI keys can be created in **Setup > API Keys** within the FairTest dashboard. Each key has configurable scopes:\n\n- **read** — Read employees, tests, selections, and reference data\n- **write** — Create and update employees and tests\n- **selections** — Run random selections\n\n## Pagination\n\nList endpoints support pagination via query parameters:\n\n- `page` — Page number (default: 1)\n- `per_page` — Items per page (default: 50, max: 100)\n\nPaginated responses include a `pagination` object with `page`, `per_page`, `total`, and `total_pages`.\n\n## Errors\n\nErrors return JSON with an `error` field:\n\n```json\n{ \"error\": \"Description of what went wrong\" }\n```\n\nHTTP status codes follow standard conventions: 400 (bad request), 401 (unauthorized), 403 (forbidden), 404 (not found), 500 (server error).","contact":{"name":"FairTest Support","url":"https://app.fairtest.com.au"}},"servers":[{"url":"https://app.fairtest.com.au","description":"Production"}],"security":[{"apiKey":[]}],"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"API key starting with `ft_`. Create one in Setup > API Keys."}},"schemas":{"Employee":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"external_id":{"type":["string","null"],"description":"External system identifier (e.g. payroll ID)"},"first_name":{"type":"string"},"last_name":{"type":"string"},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"location_id":{"type":["string","null"],"format":"uuid"},"department_id":{"type":["string","null"],"format":"uuid"},"job_classification_id":{"type":["string","null"],"format":"uuid"},"pay_type_id":{"type":["string","null"],"format":"uuid"},"employment_type":{"type":["string","null"],"enum":["full_time","part_time","casual","contractor",null]},"active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"EmployeeDetail":{"allOf":[{"$ref":"#/components/schemas/Employee"},{"type":"object","properties":{"has_declared_medications":{"type":"boolean"}}}]},"Test":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"employee_id":{"type":"string","format":"uuid"},"tester_id":{"type":"string","format":"uuid"},"test_type_id":{"type":"string","format":"uuid"},"selection_event_id":{"type":["string","null"],"format":"uuid"},"tested_at":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["pending","completed","refused","not_found","deferred"]},"result":{"type":["string","null"],"enum":["negative","positive","inconclusive","void",null]},"confirmation_required":{"type":["boolean","null"]},"confirmation_result":{"type":["string","null"],"enum":["confirmed_positive","confirmed_negative",null]},"confirmation_date":{"type":["string","null"],"format":"date-time"},"notes":{"type":["string","null"]},"source":{"type":"string","enum":["manual","drager","api","csv"]},"external_reference":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"TestDetail":{"allOf":[{"$ref":"#/components/schemas/Test"},{"type":"object","properties":{"selection_pool_id":{"type":["string","null"],"format":"uuid"},"latitude":{"type":["number","null"]},"longitude":{"type":["number","null"]},"location_address":{"type":["string","null"]}}}]},"EmployeeCreate":{"type":"object","required":["first_name","last_name"],"properties":{"external_id":{"type":"string","description":"External system identifier (e.g. payroll ID)"},"first_name":{"type":"string"},"last_name":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"location_id":{"type":"string","format":"uuid"},"department_id":{"type":"string","format":"uuid"},"job_classification_id":{"type":"string","format":"uuid"},"pay_type_id":{"type":"string","format":"uuid"},"employment_type":{"type":"string","enum":["full_time","part_time","casual","contractor"]},"active":{"type":"boolean","default":true}}},"EmployeeUpdate":{"type":"object","properties":{"external_id":{"type":["string","null"],"description":"External system identifier (e.g. payroll ID)"},"first_name":{"type":"string"},"last_name":{"type":"string"},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"location_id":{"type":["string","null"],"format":"uuid"},"department_id":{"type":["string","null"],"format":"uuid"},"job_classification_id":{"type":["string","null"],"format":"uuid"},"pay_type_id":{"type":["string","null"],"format":"uuid"},"employment_type":{"type":["string","null"],"enum":["full_time","part_time","casual","contractor",null]},"active":{"type":"boolean"}}},"TestUpdate":{"type":"object","properties":{"status":{"type":"string","enum":["pending","completed","refused","not_found","deferred"]},"result":{"type":["string","null"],"enum":["negative","positive","inconclusive","void",null]},"test_type_id":{"type":"string","format":"uuid","description":"ID of the test type"},"tested_at":{"type":"string","format":"date-time","description":"When the test was administered"},"notes":{"type":["string","null"],"description":"Optional notes"},"external_reference":{"type":["string","null"],"description":"External system reference ID"},"confirmation_required":{"type":["boolean","null"]},"confirmation_result":{"type":["string","null"],"enum":["confirmed_positive","confirmed_negative",null]},"confirmation_date":{"type":["string","null"],"format":"date-time"}}},"TestCreate":{"type":"object","required":["employee_id","tester_id","test_type_id","tested_at","status"],"properties":{"employee_id":{"type":"string","format":"uuid","description":"ID of the employee being tested"},"tester_id":{"type":"string","format":"uuid","description":"ID of the user administering the test"},"test_type_id":{"type":"string","format":"uuid","description":"ID of the test type"},"tested_at":{"type":"string","format":"date-time","description":"When the test was administered"},"status":{"type":"string","enum":["pending","completed","refused","not_found","deferred"]},"result":{"type":"string","enum":["negative","positive","inconclusive","void"],"description":"Test result (optional)"},"notes":{"type":"string","description":"Optional notes"},"external_reference":{"type":"string","description":"External system reference ID"}}},"SelectionEvent":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"initiated_by":{"type":"string","format":"uuid"},"scope":{"type":"string","enum":["organisation","location","department","job_classification","location_and_job_classification"]},"scope_location_id":{"type":["string","null"],"format":"uuid"},"scope_department_id":{"type":["string","null"],"format":"uuid"},"scope_job_classification_id":{"type":["string","null"],"format":"uuid"},"selection_method":{"type":"string","enum":["percentage","count"]},"selection_percentage":{"type":["number","null"]},"selection_count":{"type":["integer","null"]},"pool_count":{"type":"integer","description":"Total employees in the selection pool"},"selected_count":{"type":"integer","description":"Number of employees selected"},"status":{"type":"string","enum":["active","completed","cancelled"]},"notes":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"}}},"SelectionPoolEntry":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"employee_id":{"type":"string","format":"uuid"},"was_selected":{"type":"boolean"},"sort_position":{"type":"integer"}}},"RunSelectionRequest":{"type":"object","required":["scope","selection_method"],"properties":{"scope":{"type":"string","enum":["organisation","location","department","job_classification","location_and_job_classification"],"description":"Scope of employees to include"},"scope_location_id":{"type":"string","format":"uuid","description":"Required when scope is 'location' or 'location_and_job_classification'"},"scope_department_id":{"type":"string","format":"uuid","description":"Required when scope is 'department'"},"scope_job_classification_id":{"type":"string","format":"uuid","description":"Required when scope is 'job_classification' or 'location_and_job_classification'"},"selection_method":{"type":"string","enum":["percentage","count"]},"selection_percentage":{"type":"number","minimum":1,"maximum":100,"description":"Required when method is 'percentage'"},"selection_count":{"type":"integer","minimum":1,"description":"Required when method is 'count'"},"notes":{"type":"string","description":"Optional notes for this selection"}}},"Location":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":["string","null"]},"timezone":{"type":["string","null"]},"active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"Department":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"TestType":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"JobClassification":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"PayType":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer"},"per_page":{"type":"integer"},"total":{"type":"integer"},"total_pages":{"type":"integer"}}},"Error":{"type":"object","properties":{"error":{"type":"string"}}}}},"paths":{"/api/v1/employees":{"get":{"summary":"List employees","description":"Returns a paginated list of employees in your organisation. Supports filtering by active status, location, department, job classification, and text search.","operationId":"listEmployees","tags":["Employees"],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"per_page","in":"query","schema":{"type":"integer","default":50,"maximum":100}},{"name":"active","in":"query","schema":{"type":"boolean"},"description":"Filter by active status"},{"name":"location_id","in":"query","schema":{"type":"string","format":"uuid"},"description":"Filter by location"},{"name":"department_id","in":"query","schema":{"type":"string","format":"uuid"},"description":"Filter by department"},{"name":"job_classification_id","in":"query","schema":{"type":"string","format":"uuid"},"description":"Filter by job classification"},{"name":"search","in":"query","schema":{"type":"string"},"description":"Search by name, email, or external ID"}],"responses":{"200":{"description":"List of employees","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Employee"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Create employee","description":"Create a new employee in your organisation. Foreign key references (location, department, etc.) must belong to your organisation.","operationId":"createEmployee","tags":["Employees"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmployeeCreate"},"example":{"first_name":"Jane","last_name":"Smith","email":"jane.smith@example.com","employment_type":"full_time"}}}},"responses":{"201":{"description":"Employee created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Employee"}}}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Referenced resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/employees/{id}":{"get":{"summary":"Get employee","description":"Returns a single employee by ID, including medication declaration status.","operationId":"getEmployee","tags":["Employees"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Employee detail","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EmployeeDetail"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"summary":"Update employee","description":"Update an existing employee. Only provided fields are updated. Foreign key references must belong to your organisation.","operationId":"updateEmployee","tags":["Employees"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmployeeUpdate"},"example":{"email":"new.email@example.com","active":false}}}},"responses":{"200":{"description":"Employee updated","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Employee"}}}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Employee not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/tests":{"get":{"summary":"List tests","description":"Returns a paginated list of tests. Supports filtering by employee, status, result, test type, date range, and selection event.","operationId":"listTests","tags":["Tests"],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"per_page","in":"query","schema":{"type":"integer","default":50,"maximum":100}},{"name":"employee_id","in":"query","schema":{"type":"string","format":"uuid"},"description":"Filter by employee"},{"name":"status","in":"query","schema":{"type":"string","enum":["pending","completed","refused","not_found","deferred"]}},{"name":"result","in":"query","schema":{"type":"string","enum":["negative","positive","inconclusive","void"]}},{"name":"test_type_id","in":"query","schema":{"type":"string","format":"uuid"},"description":"Filter by test type"},{"name":"selection_event_id","in":"query","schema":{"type":"string","format":"uuid"},"description":"Filter by selection event"},{"name":"date_from","in":"query","schema":{"type":"string","format":"date-time"},"description":"Tests on or after this date"},{"name":"date_to","in":"query","schema":{"type":"string","format":"date-time"},"description":"Tests on or before this date"}],"responses":{"200":{"description":"List of tests","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Test"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}}}},"post":{"summary":"Create test","description":"Log a new test result. The employee and test type must belong to your organisation. The `source` field is automatically set to `api`.","operationId":"createTest","tags":["Tests"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestCreate"},"example":{"employee_id":"550e8400-e29b-41d4-a716-446655440000","tester_id":"550e8400-e29b-41d4-a716-446655440001","test_type_id":"550e8400-e29b-41d4-a716-446655440002","tested_at":"2026-03-07T09:30:00Z","status":"completed","result":"negative"}}}},"responses":{"201":{"description":"Test created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Test"}}}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Employee or test type not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/tests/{id}":{"get":{"summary":"Get test","description":"Returns a single test by ID, including location coordinates if captured.","operationId":"getTest","tags":["Tests"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Test detail","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TestDetail"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"summary":"Update test","description":"Update an existing test record. Only provided fields are updated (true PATCH semantics). Requires `write` scope.","operationId":"updateTest","tags":["Tests"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestUpdate"},"example":{"status":"completed","result":"negative","notes":"Updated after confirmation"}}}},"responses":{"200":{"description":"Test updated","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TestDetail"}}}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Test not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/selections":{"get":{"summary":"List selections","description":"Returns a paginated list of random selection events. Supports filtering by status and date range.","operationId":"listSelections","tags":["Selections"],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"per_page","in":"query","schema":{"type":"integer","default":50,"maximum":100}},{"name":"status","in":"query","schema":{"type":"string","enum":["active","completed","cancelled"]}},{"name":"date_from","in":"query","schema":{"type":"string","format":"date-time"},"description":"Selections on or after this date"},{"name":"date_to","in":"query","schema":{"type":"string","format":"date-time"},"description":"Selections on or before this date"}],"responses":{"200":{"description":"List of selection events","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SelectionEvent"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}}}}},"/api/v1/selections/{id}":{"get":{"summary":"Get selection","description":"Returns a selection event by ID, including the full pool of employees with their selection status.","operationId":"getSelection","tags":["Selections"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Selection detail with pool","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"allOf":[{"$ref":"#/components/schemas/SelectionEvent"},{"type":"object","properties":{"pool":{"type":"array","items":{"$ref":"#/components/schemas/SelectionPoolEntry"}}}}]}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/selections/run":{"post":{"summary":"Run selection","description":"Run a new random selection. Randomly selects employees from the specified scope using a cryptographically seeded algorithm. Triggers email notifications to configured recipients.\n\n**Note:** The `ukg_punched_in` scope is not available via the public API.","operationId":"runSelection","tags":["Selections"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunSelectionRequest"},"examples":{"percentage":{"summary":"Select 10% of all employees","value":{"scope":"organisation","selection_method":"percentage","selection_percentage":10}},"count_by_location":{"summary":"Select 5 employees from a location","value":{"scope":"location","scope_location_id":"550e8400-e29b-41d4-a716-446655440000","selection_method":"count","selection_count":5,"notes":"Weekly selection — Sydney office"}}}}}},"responses":{"201":{"description":"Selection created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"pool_count":{"type":"integer"},"selected_count":{"type":"integer"},"selected_employee_ids":{"type":"array","items":{"type":"string","format":"uuid"}}}}}}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/organisation":{"get":{"summary":"Get organisation","description":"Returns details about the organisation associated with the API key. Useful for verifying authentication and displaying the connected account.","operationId":"getOrganisation","tags":["Organisation"],"responses":{"200":{"description":"Organisation details","content":{"application/json":{"schema":{"type":"object","properties":{"organisation_id":{"type":"string","format":"uuid"},"organisation_name":{"type":"string"},"slug":{"type":"string"},"org_type":{"type":"string","enum":["employer","provider"]},"active":{"type":"boolean"},"employee_label":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"created_at":{"type":"string","format":"date-time"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/team":{"get":{"summary":"List team members","description":"Returns active team members (users) in your organisation. Useful for populating tester dropdowns.","operationId":"listTeamMembers","tags":["Organisation"],"responses":{"200":{"description":"List of team members","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]},"role":{"type":"string","enum":["owner","admin","manager","tester"]}}}}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/locations":{"get":{"summary":"List locations","description":"Returns all locations in your organisation.","operationId":"listLocations","tags":["Reference Data"],"responses":{"200":{"description":"List of locations","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Location"}}}}}}}}}},"/api/v1/departments":{"get":{"summary":"List departments","description":"Returns all departments in your organisation.","operationId":"listDepartments","tags":["Reference Data"],"responses":{"200":{"description":"List of departments","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Department"}}}}}}}}}},"/api/v1/test-types":{"get":{"summary":"List test types","description":"Returns all test types configured for your organisation.","operationId":"listTestTypes","tags":["Reference Data"],"responses":{"200":{"description":"List of test types","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TestType"}}}}}}}}}},"/api/v1/job-classifications":{"get":{"summary":"List job classifications","description":"Returns all job classifications in your organisation.","operationId":"listJobClassifications","tags":["Reference Data"],"responses":{"200":{"description":"List of job classifications","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/JobClassification"}}}}}}}}}},"/api/v1/pay-types":{"get":{"summary":"List pay types","description":"Returns all pay types in your organisation.","operationId":"listPayTypes","tags":["Reference Data"],"responses":{"200":{"description":"List of pay types","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PayType"}}}}}}}}}}},"tags":[{"name":"Organisation","description":"Organisation details and team members"},{"name":"Employees","description":"View and manage employee details"},{"name":"Tests","description":"Log and query drug & alcohol test records"},{"name":"Selections","description":"Random selection events for testing"},{"name":"Reference Data","description":"Locations, departments, test types, job classifications, and pay types"}]}