API
API Reference
Build custom integrations against your Tidyflow tenant — sync clients, create jobs, post time logs, and pull reporting data.
Base URL & versioning
https://app.tidyflow.com/api/v1
There is currently one version (v1). Breaking changes will be introduced under a new prefix.
Authentication
The Tidyflow API uses personal access tokens sent as a Bearer header.
Generate a token
- Log in to Tidyflow.
- Open Account → API Tokens.
- Click + New token, give it a name (e.g.
Zapier,Internal sync), and copy the token immediately — it will not be shown again. - Revoke a token at any time by deleting it from the same page.
Each token inherits the permissions of the user that created it and is scoped to that user’s tenant. Tokens do not expire; revoke any token that may have been exposed.
Sending the token
Add the token to every request:
Authorization: Bearer <your-token>
Accept: application/json
A missing or invalid token returns 401 Unauthenticated.
Conventions
Identifiers
Every resource exposes a stable uuid — always use the UUID in URLs and references, not the numeric id.
Pagination
List endpoints return a standard Laravel paginator:
{
"data": [ ... ],
"links": { "first": "...", "last": "...", "prev": null, "next": "..." },
"meta": { "current_page": 1, "last_page": 10, "per_page": 50, "total": 480 }
}
Control page size with ?limit= (clients, contacts) or ?per_page= (time logs). Default page size is large — set an explicit limit for predictable results.
Filtering
Filters are passed as query parameters. Array filters use repeated keys: ?tags[]=12&tags[]=15.
Errors
| Status | Meaning |
|---|---|
401 | Missing or invalid token |
403 | Token does not have access to this record (wrong tenant or insufficient permission) |
404 | Resource not found |
422 | Validation error — errors object lists each field |
429 | Rate limit exceeded — slow down and retry |
Validation error body:
{
"message": "The given data was invalid.",
"errors": {
"name": ["The name field is required."]
}
}
Rate limits
Login is rate-limited; standard read/write endpoints are not throttled per-token today, but expect limits to be introduced as the API matures. Keep traffic reasonable and back off on 429.
Users
Read-only. Useful for resolving user IDs when assigning clients or jobs.
List users
GET /api/v1/users
Query parameters: search.
Get user by ID
GET /api/v1/users/{id}
Find user by email
GET /api/v1/users/email/{email}
Returns the user object, or { "data": null } if no user matches.
User shape
{
"id": 42,
"fullname": "Jane Doe",
"firstname": "Jane",
"lastname": "Doe",
"email": "[email protected]",
"initials": "JD",
"avatar_url": "https://...",
"role": "Admin",
"timezone": "Australia/Sydney",
"is_contractor": false,
"is_owner": false,
"archived_date": null
}
Clients
List clients
GET /api/v1/clients
Query parameters:
| Name | Type | Description |
|---|---|---|
search | string | Match on name, registration number, etc. |
tags[] | int[] | Tag IDs |
country_id | int | Country filter |
limit | int | Page size |
Search clients (autocomplete)
GET /api/v1/clients/search?q=acme
Returns up to 10 lightweight matches:
{ "clients": [ { "id": 7, "uuid": "...", "name": "Acme Pty Ltd" } ] }
Get a client
GET /api/v1/clients/{uuid}
Create a client
POST /api/v1/clients
Body:
| Field | Required | Notes |
|---|---|---|
name | ✓ | Max 100 chars |
client_id | Your internal ref (must be unique) | |
reg_number | Business registration number (must be unique) | |
website | ||
country_id | ID from the countries reference | |
user_id | Account manager (Tidyflow user ID) | |
state, city, street_address, postal_code | ||
client_groups | Array of client group IDs | |
contact | Optional nested object — creates and links a primary contact in one call |
Nested contact object:
| Field | Required | Notes |
|---|---|---|
firstname | ✓ | |
lastname | ✓ | |
email | ✓ | Must be unique across all contacts |
title | e.g. Director | |
phone_number | ||
include_for_client_requests | boolean |
Returns the created client in the same shape as GET /clients/{uuid}.
Update a client
PUT /api/v1/clients/{uuid}
All fields are optional — send only what you want to change. Accepts the same fields as create (excluding the nested contact), plus tags (array of tag IDs).
Client shape
{
"uuid": "...",
"name": "Acme Pty Ltd",
"client_id": "AC-001",
"reg_number": "12 345 678 901",
"website": "https://acme.com",
"country": "Australia",
"state": "NSW",
"city": "Sydney",
"street_address": "1 George St",
"postal_code": "2000",
"year_end": "06-30",
"year_end_text": "30 June",
"is_archived": false,
"contacts": [ ... ]
}
Custom fields defined in your tenant are merged into the response keyed by field name.
Contacts
List contacts
GET /api/v1/contacts
Query parameters: search, limit.
Get a contact
GET /api/v1/contacts/{uuid}
Create a contact
POST /api/v1/contacts
| Field | Required | Notes |
|---|---|---|
firstname | ✓ | |
lastname | ✓ | |
email | ✓ | Must be unique across all contacts |
preferred_name | Defaults to firstname | |
phone_number | ||
portal_enabled | boolean — defaults to true (contact gets a client portal login) |
Update a contact
PUT /api/v1/contacts/{uuid}
All fields optional.
Contact shape
{
"uuid": "...",
"firstname": "Jane",
"lastname": "Doe",
"preferred_name": "Jane",
"email": "[email protected]",
"phone_number": "+61 400 000 000",
"portal_enabled": true
}
Jobs
The Tidyflow UI calls these “Jobs”. The API path uses /jobs.
List jobs
GET /api/v1/jobs
Query parameters:
| Name | Type | Description |
|---|---|---|
search | string | Match on job name |
client_uuid | string | Filter to one client |
users[] | int[] | Assigned user IDs |
tags[] | int[] | Tag IDs |
statuses[] | string[] | Status slugs |
start_date_range | string | Date range filter |
due_date_range | string | Date range filter |
include_completed | bool | Default false |
exclude_subtasks | bool | |
exclude_client_requests | bool | |
sort_by, sort_direction | string |
Get a job
GET /api/v1/jobs/{uuid}
Includes client, assignees, followers, tags, parent, and subtasks.
Create a job
POST /api/v1/jobs
| Field | Required | Notes |
|---|---|---|
name | ✓ | Max 150 chars |
description | Up to 30,000 chars | |
client_uuid | Link the job to a client | |
user_id | Assignee | |
status | A status slug from your tenant | |
tags | Array of tag IDs | |
start_date | ISO date, must be ≤ due_date | |
due_date | ISO date | |
internal_due_date, extended_due_date | ISO dates | |
estimated_hours | Numeric, 0–1000 |
Job shape (key fields)
{
"uuid": "...",
"name": "FY24 financials",
"description": "...",
"status": "in_progress",
"status_name": "In progress",
"client_uuid": "...",
"client": { ... },
"users": [ ... ],
"tags": [ ... ],
"start_date": "2026-05-01",
"due_date": "2026-06-30",
"estimated_hours": 12,
"is_overdue": false,
"is_completed": false,
"completed_date": null,
"is_repeating": false,
"is_subtask": false,
"parent_uuid": null,
"sub_tasks": [ ... ]
}
Repeating-job fields are also returned — see the full payload in a real response if you need to replicate recurrence.
Time logs
Read-only listing of recorded time.
List time logs
GET /api/v1/time-logs
Query parameters:
| Name | Type | Description |
|---|---|---|
range | string | Date range |
client_uuid | string | Filter by client |
tasks[] | string[] | Job UUIDs |
users[] | int[] | User IDs |
search | string | Match on note |
per_page | int | Page size |
Time log shape
{
"uuid": "...",
"client": { "uuid": "...", "name": "Acme Pty Ltd" },
"task": { "uuid": "...", "name": "FY24 financials" },
"user": { "id": 42, "fullname": "Jane Doe" },
"note": "Reviewed trial balance",
"hours": 1.5,
"hours_time": "01:30",
"date": "2026-05-01",
"formatted_date": "1 May 2026",
"created_at": "2026-05-01T09:14:22Z",
"updated_at": "2026-05-01T09:14:22Z"
} Was this page helpful?
What went wrong?
Thanks for your feedback.
Last updated May 2, 2026