MENU navbar-image

Introduction

REST API for managing invoices, clients, projects, time tracking, and more.

Authentication

Tidybill uses Bearer token authentication. Create a personal access token from Settings > API in your Tidybill dashboard (requires Pro plan).

Include the token in every request:

Authorization: Bearer YOUR_TOKEN

Multi-tenancy

All resource endpoints require an X-Company-Id header to specify which company you're operating on. You can list your companies via GET /api/companies.

X-Company-Id: 1

Money

All currency amounts are stored in cents (e.g. $10.50 = 1050). Most fields are integers, except unit_price which supports up to 6 decimal places for sub-cent pricing (e.g. 3.5 = $0.035). Line item totals and all other monetary fields are rounded to whole cents. Tax rates are in basis points (e.g. 15% = 1500).

Authenticating requests

To authenticate requests, include an Authorization header with the value "Bearer {YOUR_AUTH_KEY}".

All authenticated endpoints are marked with a requires authentication badge in the documentation below.

Create a personal access token from Settings > API in your Tidybill dashboard. Requires a Pro plan.

Authentication

Register

Example request:
curl --request POST \
    "https://tidybill.app/api/auth/register" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"b\",
    \"email\": \"[email protected]\",
    \"password\": \"-0pBNvYgxw\"
}"
const url = new URL(
    "https://tidybill.app/api/auth/register"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "b",
    "email": "[email protected]",
    "password": "-0pBNvYgxw"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 1,
        "name": "John",
        "email": "[email protected]"
    }
}
 

Request      

POST api/auth/register

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

name   string     

Must not be greater than 255 characters. Example: b

email   string     

Must be a valid email address. Must not be greater than 255 characters. Example: [email protected]

password   string     

Must be at least 8 characters. Example: -0pBNvYgxw

Login

Example request:
curl --request POST \
    "https://tidybill.app/api/auth/login" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"email\": \"[email protected]\",
    \"password\": \"|]|{+-\"
}"
const url = new URL(
    "https://tidybill.app/api/auth/login"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "email": "[email protected]",
    "password": "|]|{+-"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 1,
        "name": "John",
        "email": "[email protected]"
    }
}
 

Example response (401):


{
    "message": "Invalid credentials."
}
 

Request      

POST api/auth/login

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

email   string     

Must be a valid email address. Example: [email protected]

password   string     

Example: |]|{+-

Send password reset link

Example request:
curl --request POST \
    "https://tidybill.app/api/auth/forgot-password" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"email\": \"[email protected]\"
}"
const url = new URL(
    "https://tidybill.app/api/auth/forgot-password"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "email": "[email protected]"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "message": "If an account exists, a reset link has been sent."
}
 

Request      

POST api/auth/forgot-password

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

email   string     

Must be a valid email address. Example: [email protected]

Reset password

Example request:
curl --request POST \
    "https://tidybill.app/api/auth/reset-password" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"token\": \"architecto\",
    \"email\": \"[email protected]\",
    \"password\": \"-0pBNvYgxw\"
}"
const url = new URL(
    "https://tidybill.app/api/auth/reset-password"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "token": "architecto",
    "email": "[email protected]",
    "password": "-0pBNvYgxw"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "message": "Password has been reset."
}
 

Example response (422):


{
    "message": "This password reset token is invalid."
}
 

Request      

POST api/auth/reset-password

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

token   string     

Example: architecto

email   string     

Must be a valid email address. Example: [email protected]

password   string     

Must be at least 8 characters. Example: -0pBNvYgxw

Redirect to Google OAuth

Example request:
curl --request GET \
    --get "https://tidybill.app/api/auth/google" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/auth/google"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (302, Redirect to Google):



 

Example response (302):

Show headers
cache-control: no-cache, private
location: https://accounts.google.com/o/oauth2/auth?client_id=122074070973-q2t44i2big2vg257o5bkbn5kjhuu1d34.apps.googleusercontent.com&redirect_uri=https%3A%2F%2Ftidybill.app%2Fapi%2Fauth%2Fgoogle%2Fcallback&scope=openid+profile+email&response_type=code
content-type: text/html; charset=utf-8
access-control-allow-origin: https://tidybill.app
access-control-allow-credentials: true
set-cookie: XSRF-TOKEN=eyJpdiI6IjBCalBaQVRIeFE2Zi90d0U3MHlOR1E9PSIsInZhbHVlIjoiVnhqWkE3amhmRkw1bmptRjVTY05LTmRsejJkbHlVdGYrOTJxNlBDYndrWjBIN2FWSTdzTjg3Njc2ankzdEFIR2s1RlpBM011VVEySVRIdnVYemlsUVJ6VDR1bWprL2kwbWo3QUNuSHFWdGRwMS9XTDkrQXdYcmI0MzRzTmlWOHoiLCJtYWMiOiI0ZjQzOGU1MWZkNjhhMzdjNzZlZWE1NDY2ZjJhMDQzMWRiYWJhYTA4ZmYyZmI2OWIxZTE3MDM2MjE0YWVmMGRmIiwidGFnIjoiIn0%3D; expires=Sun, 12 Apr 2026 19:38:12 GMT; Max-Age=604799; path=/; domain=tidybill.app; secure; samesite=lax; tidybill_session=eyJpdiI6ImUzNExwdytvUHY2UExCNFFlWW9jbnc9PSIsInZhbHVlIjoiS09BdTh3eEJIODlOSExkR3U3NndQL2FudERDKzF1NDltNFBZY3g1dHdBQ3RySGIxdU9RaWRBV1BNa0xuYTRQSjhWRGYrWVVhSjd4b2pNV0lDaXdmakkxZlNJMVcxOWVveHRvWGxtWmJEdEdwbWRtNVFhSDF3bFYwNzUvUERDaGkiLCJtYWMiOiI0MTI4OGMzZDY0ZTJhMDUzZjc3NDU4ODc1ZDA1Yzc4MzEzODAxMjBmY2NhMzAyZjA0NzMyN2ZjYWNjYzg0YmM3IiwidGFnIjoiIn0%3D; expires=Sun, 12 Apr 2026 19:38:12 GMT; Max-Age=604799; path=/; domain=tidybill.app; secure; httponly; samesite=lax
 

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url='https://accounts.google.com/o/oauth2/auth?client_id=122074070973-q2t44i2big2vg257o5bkbn5kjhuu1d34.apps.googleusercontent.com&amp;redirect_uri=https%3A%2F%2Ftidybill.app%2Fapi%2Fauth%2Fgoogle%2Fcallback&amp;scope=openid+profile+email&amp;response_type=code'" />

        <title>Redirecting to https://accounts.google.com/o/oauth2/auth?client_id=122074070973-q2t44i2big2vg257o5bkbn5kjhuu1d34.apps.googleusercontent.com&amp;redirect_uri=https%3A%2F%2Ftidybill.app%2Fapi%2Fauth%2Fgoogle%2Fcallback&amp;scope=openid+profile+email&amp;response_type=code</title>
    </head>
    <body>
        Redirecting to <a href="https://accounts.google.com/o/oauth2/auth?client_id=122074070973-q2t44i2big2vg257o5bkbn5kjhuu1d34.apps.googleusercontent.com&amp;redirect_uri=https%3A%2F%2Ftidybill.app%2Fapi%2Fauth%2Fgoogle%2Fcallback&amp;scope=openid+profile+email&amp;response_type=code">https://accounts.google.com/o/oauth2/auth?client_id=122074070973-q2t44i2big2vg257o5bkbn5kjhuu1d34.apps.googleusercontent.com&amp;redirect_uri=https%3A%2F%2Ftidybill.app%2Fapi%2Fauth%2Fgoogle%2Fcallback&amp;scope=openid+profile+email&amp;response_type=code</a>.
    </body>
</html>
 

Request      

GET api/auth/google

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Google OAuth callback

Example request:
curl --request GET \
    --get "https://tidybill.app/api/auth/google/callback" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/auth/google/callback"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (302, Redirect to frontend after authentication):



 

Example response (302):

Show headers
cache-control: no-cache, private
location: https://tidybill.app/login?error=google_auth_failed
content-type: text/html; charset=utf-8
access-control-allow-origin: https://tidybill.app
access-control-allow-credentials: true
set-cookie: XSRF-TOKEN=eyJpdiI6IjVWMERBdEhsWnJ3QUFncU5PNVVxVFE9PSIsInZhbHVlIjoiS0tESUNoeHNmN3pCRjJ1ZE9ESmNvQVhZU2dqWU5ldUcxMnpsTjZwdTkvbTFhVCs4OHFHR1JHUjR4TzNQTEhMNTkvWS9kQTAxYW5nL1owd3dLZGVJQVJZQjlKMHhxODVnMEhvMnh1SFB6cEdUUlY0MHMvaE9oUTIxbUJicmFDdjEiLCJtYWMiOiI2ZmY4ZmJjODgzOGFkYzdjYjQ1ZjRiNzQ1OTA3MThjNmYzYjlkNDMzMDdhYmFlZjczOGMyZjQxNWFiNzM5YzM0IiwidGFnIjoiIn0%3D; expires=Sun, 12 Apr 2026 19:38:13 GMT; Max-Age=604800; path=/; domain=tidybill.app; secure; samesite=lax; tidybill_session=eyJpdiI6IjNuVFFhMVNMVUpvTldKOEFIMmVObXc9PSIsInZhbHVlIjoieWNPTDhXazQxeUZCR3FyV0lTYlpRd1ZzTDZvVHFaNEl3L1JyeG1JbGpLeElYbkt0eU5NS2ZsZVBSV1FmU25LRG15Wk5xL0VBTFJ3aVdnS1lCOWdPRWhBS1c4S1pxVEZvc2pDSnNWeHFwb0xFUmFqSFhCb3ExMGdhbytaSkw0ZjgiLCJtYWMiOiI2OTRiN2NiMzJlMzc5Yjg4YTZhMDExZmNkZjk0M2FmZTE4NjVlMzVjNTgxODg5ZGY3MDIxOWYzYjU1ZjU0MTc3IiwidGFnIjoiIn0%3D; expires=Sun, 12 Apr 2026 19:38:13 GMT; Max-Age=604800; path=/; domain=tidybill.app; secure; httponly; samesite=lax
 

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url='https://tidybill.app/login?error=google_auth_failed'" />

        <title>Redirecting to https://tidybill.app/login?error=google_auth_failed</title>
    </head>
    <body>
        Redirecting to <a href="https://tidybill.app/login?error=google_auth_failed">https://tidybill.app/login?error=google_auth_failed</a>.
    </body>
</html>
 

Request      

GET api/auth/google/callback

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Logout

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/auth/logout" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/auth/logout"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "message": "Logged out."
}
 

Request      

POST api/auth/logout

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Get authenticated user

requires authentication

Example request:
curl --request GET \
    --get "https://tidybill.app/api/auth/user" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/auth/user"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 8,
        "name": "Morgan Hirthe",
        "email": "[email protected]",
        "avatar_url": null,
        "current_company_id": null,
        "created_at": "2026-04-05T19:38:14.000000Z",
        "updated_at": "2026-04-05T19:38:14.000000Z"
    }
}
 

Request      

GET api/auth/user

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Update authenticated user

requires authentication

Example request:
curl --request PUT \
    "https://tidybill.app/api/auth/user" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"b\"
}"
const url = new URL(
    "https://tidybill.app/api/auth/user"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "b"
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 9,
        "name": "Ms. Elisabeth Okuneva",
        "email": "[email protected]",
        "avatar_url": null,
        "current_company_id": null,
        "created_at": "2026-04-05T19:38:14.000000Z",
        "updated_at": "2026-04-05T19:38:14.000000Z"
    }
}
 

Request      

PUT api/auth/user

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

name   string  optional    

Must not be greater than 255 characters. Example: b

email   string  optional    

Companies

List companies

requires authentication

Returns all companies the authenticated user is a member of, regardless of role.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/companies" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/companies"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 15,
            "uuid": "0db07a5e-1aa6-4165-ba1e-f4c0990f7705",
            "name": "Price Ltd",
            "legal_name": null,
            "email": "[email protected]",
            "phone": null,
            "address_line_1": null,
            "address_line_2": null,
            "city": null,
            "state": null,
            "postal_code": null,
            "country": null,
            "currency": "USD",
            "tax_number": null,
            "logo_path": null,
            "invoice_prefix": "INV-",
            "quote_prefix": "QUO-",
            "invoice_layout": "classic",
            "default_payment_terms": 30,
            "default_hourly_rate": null,
            "default_tax_name_1": null,
            "default_tax_rate_1": null,
            "default_tax_name_2": null,
            "default_tax_rate_2": null,
            "late_fee_type": null,
            "late_fee_value": null,
            "late_fee_days": null,
            "created_at": "2026-04-05T19:38:14.000000Z",
            "updated_at": "2026-04-05T19:38:14.000000Z"
        },
        {
            "id": 16,
            "uuid": "81e00a44-fe6f-47fe-9604-3dc4ac4cae5a",
            "name": "Leuschke, Bauch and Fritsch",
            "legal_name": null,
            "email": "[email protected]",
            "phone": null,
            "address_line_1": null,
            "address_line_2": null,
            "city": null,
            "state": null,
            "postal_code": null,
            "country": null,
            "currency": "USD",
            "tax_number": null,
            "logo_path": null,
            "invoice_prefix": "INV-",
            "quote_prefix": "QUO-",
            "invoice_layout": "classic",
            "default_payment_terms": 30,
            "default_hourly_rate": null,
            "default_tax_name_1": null,
            "default_tax_rate_1": null,
            "default_tax_name_2": null,
            "default_tax_rate_2": null,
            "late_fee_type": null,
            "late_fee_value": null,
            "late_fee_days": null,
            "created_at": "2026-04-05T19:38:14.000000Z",
            "updated_at": "2026-04-05T19:38:14.000000Z"
        }
    ]
}
 

Request      

GET api/companies

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Create a company

requires authentication

Creates a company, attaches the authenticated user as owner, and creates a default invoice template. Returns 403 if the user's plan company limit is reached.

Example request:
curl --request POST \
    "https://tidybill.app/api/companies" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"b\",
    \"legal_name\": \"n\",
    \"email\": \"[email protected]\",
    \"phone\": \"v\",
    \"address_line_1\": \"d\",
    \"address_line_2\": \"l\",
    \"city\": \"j\",
    \"state\": \"n\",
    \"postal_code\": \"ikhwaykcmyuwpwlv\",
    \"country\": \"qw\",
    \"currency\": \"rsi\",
    \"tax_number\": \"t\",
    \"invoice_prefix\": \"cpscql\",
    \"quote_prefix\": \"dzsnrw\",
    \"default_payment_terms\": 19,
    \"default_hourly_rate\": 33,
    \"default_tax_name_1\": \"j\",
    \"default_tax_rate_1\": 17,
    \"default_tax_name_2\": \"v\",
    \"default_tax_rate_2\": 24
}"
const url = new URL(
    "https://tidybill.app/api/companies"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "b",
    "legal_name": "n",
    "email": "[email protected]",
    "phone": "v",
    "address_line_1": "d",
    "address_line_2": "l",
    "city": "j",
    "state": "n",
    "postal_code": "ikhwaykcmyuwpwlv",
    "country": "qw",
    "currency": "rsi",
    "tax_number": "t",
    "invoice_prefix": "cpscql",
    "quote_prefix": "dzsnrw",
    "default_payment_terms": 19,
    "default_hourly_rate": 33,
    "default_tax_name_1": "j",
    "default_tax_rate_1": 17,
    "default_tax_name_2": "v",
    "default_tax_rate_2": 24
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 17,
        "uuid": "3e26903d-5044-4d8b-ab0e-e1e541451f00",
        "name": "Considine LLC",
        "legal_name": null,
        "email": "[email protected]",
        "phone": null,
        "address_line_1": null,
        "address_line_2": null,
        "city": null,
        "state": null,
        "postal_code": null,
        "country": null,
        "currency": "USD",
        "tax_number": null,
        "logo_path": null,
        "invoice_prefix": "INV-",
        "quote_prefix": "QUO-",
        "invoice_layout": "classic",
        "default_payment_terms": 30,
        "default_hourly_rate": null,
        "default_tax_name_1": null,
        "default_tax_rate_1": null,
        "default_tax_name_2": null,
        "default_tax_rate_2": null,
        "late_fee_type": null,
        "late_fee_value": null,
        "late_fee_days": null,
        "created_at": "2026-04-05T19:38:14.000000Z",
        "updated_at": "2026-04-05T19:38:14.000000Z"
    }
}
 

Example response (403):


{
    "message": "You've reached your free plan limit of 1 companies. Please upgrade.",
    "upgrade_required": true
}
 

Request      

POST api/companies

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

name   string     

Must not be greater than 255 characters. Example: b

legal_name   string  optional    

Must not be greater than 255 characters. Example: n

email   string  optional    

Must be a valid email address. Must not be greater than 255 characters. Example: [email protected]

phone   string  optional    

Must not be greater than 50 characters. Example: v

address_line_1   string  optional    

Must not be greater than 255 characters. Example: d

address_line_2   string  optional    

Must not be greater than 255 characters. Example: l

city   string  optional    

Must not be greater than 255 characters. Example: j

state   string  optional    

Must not be greater than 255 characters. Example: n

postal_code   string  optional    

Must not be greater than 20 characters. Example: ikhwaykcmyuwpwlv

country   string  optional    

Must be 2 characters. Example: qw

currency   string  optional    

Must be 3 characters. Example: rsi

tax_number   string  optional    

Must not be greater than 255 characters. Example: t

invoice_prefix   string  optional    

Must not be greater than 10 characters. Example: cpscql

quote_prefix   string  optional    

Must not be greater than 10 characters. Example: dzsnrw

default_payment_terms   integer  optional    

Must be at least 0. Must not be greater than 365. Example: 19

default_hourly_rate   integer  optional    

Must be at least 0. Example: 33

default_tax_name_1   string  optional    

Must not be greater than 255 characters. Example: j

default_tax_rate_1   integer  optional    

Must be at least 0. Must not be greater than 10000. Example: 17

default_tax_name_2   string  optional    

Must not be greater than 255 characters. Example: v

default_tax_rate_2   integer  optional    

Must be at least 0. Must not be greater than 10000. Example: 24

Get a company

requires authentication

Example request:
curl --request GET \
    --get "https://tidybill.app/api/companies/2" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/companies/2"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 18,
        "uuid": "1f26aac2-cf55-4962-b801-73a762a46bee",
        "name": "Price Ltd",
        "legal_name": null,
        "email": "[email protected]",
        "phone": null,
        "address_line_1": null,
        "address_line_2": null,
        "city": null,
        "state": null,
        "postal_code": null,
        "country": null,
        "currency": "USD",
        "tax_number": null,
        "logo_path": null,
        "invoice_prefix": "INV-",
        "quote_prefix": "QUO-",
        "invoice_layout": "classic",
        "default_payment_terms": 30,
        "default_hourly_rate": null,
        "default_tax_name_1": null,
        "default_tax_rate_1": null,
        "default_tax_name_2": null,
        "default_tax_rate_2": null,
        "late_fee_type": null,
        "late_fee_value": null,
        "late_fee_days": null,
        "created_at": "2026-04-05T19:38:14.000000Z",
        "updated_at": "2026-04-05T19:38:14.000000Z"
    }
}
 

Request      

GET api/companies/{company_id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 2

Update a company

requires authentication

Example request:
curl --request PUT \
    "https://tidybill.app/api/companies/2" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"b\",
    \"legal_name\": \"n\",
    \"email\": \"[email protected]\",
    \"phone\": \"v\",
    \"address_line_1\": \"d\",
    \"address_line_2\": \"l\",
    \"city\": \"j\",
    \"state\": \"n\",
    \"postal_code\": \"ikhwaykcmyuwpwlv\",
    \"country\": \"qw\",
    \"currency\": \"rsi\",
    \"tax_number\": \"t\",
    \"invoice_prefix\": \"cpscql\",
    \"quote_prefix\": \"dzsnrw\",
    \"default_payment_terms\": 19,
    \"default_hourly_rate\": 33,
    \"default_tax_name_1\": \"j\",
    \"default_tax_rate_1\": 17,
    \"default_tax_name_2\": \"v\",
    \"default_tax_rate_2\": 24
}"
const url = new URL(
    "https://tidybill.app/api/companies/2"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "b",
    "legal_name": "n",
    "email": "[email protected]",
    "phone": "v",
    "address_line_1": "d",
    "address_line_2": "l",
    "city": "j",
    "state": "n",
    "postal_code": "ikhwaykcmyuwpwlv",
    "country": "qw",
    "currency": "rsi",
    "tax_number": "t",
    "invoice_prefix": "cpscql",
    "quote_prefix": "dzsnrw",
    "default_payment_terms": 19,
    "default_hourly_rate": 33,
    "default_tax_name_1": "j",
    "default_tax_rate_1": 17,
    "default_tax_name_2": "v",
    "default_tax_rate_2": 24
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 19,
        "uuid": "426db81d-0fdf-43ee-9022-d67d5e37bcd8",
        "name": "Considine LLC",
        "legal_name": null,
        "email": "[email protected]",
        "phone": null,
        "address_line_1": null,
        "address_line_2": null,
        "city": null,
        "state": null,
        "postal_code": null,
        "country": null,
        "currency": "USD",
        "tax_number": null,
        "logo_path": null,
        "invoice_prefix": "INV-",
        "quote_prefix": "QUO-",
        "invoice_layout": "classic",
        "default_payment_terms": 30,
        "default_hourly_rate": null,
        "default_tax_name_1": null,
        "default_tax_rate_1": null,
        "default_tax_name_2": null,
        "default_tax_rate_2": null,
        "late_fee_type": null,
        "late_fee_value": null,
        "late_fee_days": null,
        "created_at": "2026-04-05T19:38:14.000000Z",
        "updated_at": "2026-04-05T19:38:14.000000Z"
    }
}
 

Request      

PUT api/companies/{company_id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 2

Body Parameters

name   string     

Must not be greater than 255 characters. Example: b

legal_name   string  optional    

Must not be greater than 255 characters. Example: n

email   string  optional    

Must be a valid email address. Must not be greater than 255 characters. Example: [email protected]

phone   string  optional    

Must not be greater than 50 characters. Example: v

address_line_1   string  optional    

Must not be greater than 255 characters. Example: d

address_line_2   string  optional    

Must not be greater than 255 characters. Example: l

city   string  optional    

Must not be greater than 255 characters. Example: j

state   string  optional    

Must not be greater than 255 characters. Example: n

postal_code   string  optional    

Must not be greater than 20 characters. Example: ikhwaykcmyuwpwlv

country   string  optional    

Must be 2 characters. Example: qw

currency   string  optional    

Must be 3 characters. Example: rsi

tax_number   string  optional    

Must not be greater than 255 characters. Example: t

invoice_prefix   string  optional    

Must not be greater than 10 characters. Example: cpscql

quote_prefix   string  optional    

Must not be greater than 10 characters. Example: dzsnrw

default_payment_terms   integer  optional    

Must be at least 0. Must not be greater than 365. Example: 19

default_hourly_rate   integer  optional    

Must be at least 0. Example: 33

default_tax_name_1   string  optional    

Must not be greater than 255 characters. Example: j

default_tax_rate_1   integer  optional    

Must be at least 0. Must not be greater than 10000. Example: 17

default_tax_name_2   string  optional    

Must not be greater than 255 characters. Example: v

default_tax_rate_2   integer  optional    

Must be at least 0. Must not be greater than 10000. Example: 24

Delete a company

requires authentication

Only the company owner can delete a company. Returns 403 for any other role.

Example request:
curl --request DELETE \
    "https://tidybill.app/api/companies/2" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/companies/2"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Example response (403):


{
    "message": "Only the owner can delete a company."
}
 

Request      

DELETE api/companies/{company_id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 2

Switch active company

requires authentication

Updates the user's current_company_id so subsequent requests default to this company.

Example request:
curl --request POST \
    "https://tidybill.app/api/companies/2/switch" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/companies/2/switch"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 20,
        "uuid": "f7fb15be-3a49-4a8b-bca9-17805160b129",
        "name": "Price Ltd",
        "legal_name": null,
        "email": "[email protected]",
        "phone": null,
        "address_line_1": null,
        "address_line_2": null,
        "city": null,
        "state": null,
        "postal_code": null,
        "country": null,
        "currency": "USD",
        "tax_number": null,
        "logo_path": null,
        "invoice_prefix": "INV-",
        "quote_prefix": "QUO-",
        "invoice_layout": "classic",
        "default_payment_terms": 30,
        "default_hourly_rate": null,
        "default_tax_name_1": null,
        "default_tax_rate_1": null,
        "default_tax_name_2": null,
        "default_tax_rate_2": null,
        "late_fee_type": null,
        "late_fee_value": null,
        "late_fee_days": null,
        "created_at": "2026-04-05T19:38:14.000000Z",
        "updated_at": "2026-04-05T19:38:14.000000Z"
    }
}
 

Request      

POST api/companies/{company_id}/switch

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 2

List company members

requires authentication

Returns all users belonging to the company with their assigned role from the pivot table.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/companies/2/members" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/companies/2/members"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 1,
            "name": "John",
            "email": "[email protected]",
            "role": "owner"
        }
    ]
}
 

Request      

GET api/companies/{company_id}/members

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 2

Invite a member

requires authentication

Creates an invitation record with a 7-day expiry and emails the invite link. Returns 422 if the user is already a member or a pending invitation exists.

Example request:
curl --request POST \
    "https://tidybill.app/api/companies/2/members/invite" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"email\": \"[email protected]\",
    \"role\": \"architecto\"
}"
const url = new URL(
    "https://tidybill.app/api/companies/2/members/invite"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "email": "[email protected]",
    "role": "architecto"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "message": "Invitation sent.",
    "data": {
        "id": 1,
        "email": "[email protected]",
        "role": "member",
        "expires_at": "2026-04-12T00:00:00.000000Z"
    }
}
 

Example response (422):


{
    "message": "User is already a member."
}
 

Request      

POST api/companies/{company_id}/members/invite

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 2

Body Parameters

email   string     

Must be a valid email address. Example: [email protected]

role   string     

Example: architecto

List pending invitations

requires authentication

Returns all non-expired, non-accepted invitations for the company, ordered by most recent.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/companies/2/members/invitations" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/companies/2/members/invitations"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 1,
            "email": "[email protected]",
            "role": "member",
            "invited_by": "John",
            "expires_at": "2026-04-12T00:00:00.000000Z"
        }
    ]
}
 

Request      

GET api/companies/{company_id}/members/invitations

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 2

Cancel an invitation

requires authentication

Example request:
curl --request DELETE \
    "https://tidybill.app/api/companies/2/members/invitations/4" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/companies/2/members/invitations/4"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

DELETE api/companies/{company_id}/members/invitations/{invitation_id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 2

invitation_id   integer     

The ID of the invitation. Example: 4

Resend an invitation

requires authentication

Regenerates the invitation token, extends the expiry by 7 days, and re-sends the email. Returns 422 if the invitation has already been accepted.

Example request:
curl --request POST \
    "https://tidybill.app/api/companies/2/members/invitations/4/resend" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/companies/2/members/invitations/4/resend"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "message": "Invitation resent."
}
 

Example response (422):


{
    "message": "Invitation already accepted."
}
 

Request      

POST api/companies/{company_id}/members/invitations/{invitation_id}/resend

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 2

invitation_id   integer     

The ID of the invitation. Example: 4

Update a member's role

requires authentication

Changes the role on the company-user pivot. The owner role cannot be changed; returns 403 if attempted.

Example request:
curl --request PUT \
    "https://tidybill.app/api/companies/2/members/2" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"role\": \"architecto\"
}"
const url = new URL(
    "https://tidybill.app/api/companies/2/members/2"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "role": "architecto"
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "message": "Member updated."
}
 

Example response (403):


{
    "message": "Cannot change the owner role."
}
 

Request      

PUT api/companies/{company_id}/members/{user_id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 2

user_id   integer     

The ID of the user. Example: 2

Body Parameters

role   string     

Example: architecto

Remove a member

requires authentication

Example request:
curl --request DELETE \
    "https://tidybill.app/api/companies/2/members/2" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/companies/2/members/2"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Example response (403):


{
    "message": "Cannot remove the company owner."
}
 

Example response (422):


{
    "message": "You cannot remove yourself."
}
 

Request      

DELETE api/companies/{company_id}/members/{user_id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

company_id   integer     

The ID of the company. Example: 2

user_id   integer     

The ID of the user. Example: 2

Clients

Bulk action on clients

requires authentication

Supported actions: archive, unarchive, delete. Returns the count of affected records.

Example request:
curl --request POST \
    "https://tidybill.app/api/clients/bulk-action" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"ids\": [
        16
    ],
    \"action\": \"archive\"
}"
const url = new URL(
    "https://tidybill.app/api/clients/bulk-action"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "ids": [
        16
    ],
    "action": "archive"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "affected": 3
    }
}
 

Request      

POST api/clients/bulk-action

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

ids   integer[]  optional    
action   string     

Example: archive

Must be one of:
  • archive
  • unarchive
  • delete

List clients

requires authentication

Supports name/email search and archived state filtering. Results are paginated and include active/archived counts in the meta.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/clients" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/clients"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 128,
            "uuid": "d7179e59-a6fc-4543-ad1f-1a32b4d1bde9",
            "name": "Price Ltd",
            "email": "[email protected]",
            "phone": "+14324666067",
            "address_line_1": "26432 Leuschke Throughway Apt. 227",
            "address_line_2": null,
            "city": "Lake Audreyborough",
            "state": "Montana",
            "postal_code": "36080-0782",
            "country": "VG",
            "currency": "USD",
            "payment_terms": 30,
            "notes": null,
            "is_archived": false,
            "created_at": "2026-04-05T19:38:14.000000Z",
            "updated_at": "2026-04-05T19:38:14.000000Z"
        },
        {
            "id": 129,
            "uuid": "5631d5c1-7b94-445f-b78a-96df0d7280b0",
            "name": "Fahey, Cartwright and Balistreri",
            "email": "[email protected]",
            "phone": "1-346-252-9368",
            "address_line_1": "20568 Murl Villages",
            "address_line_2": null,
            "city": "New Modesta",
            "state": "Iowa",
            "postal_code": "57582-4237",
            "country": "TM",
            "currency": "USD",
            "payment_terms": 30,
            "notes": null,
            "is_archived": false,
            "created_at": "2026-04-05T19:38:14.000000Z",
            "updated_at": "2026-04-05T19:38:14.000000Z"
        }
    ]
}
 

Request      

GET api/clients

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Create a client

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/clients" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"b\",
    \"email\": \"[email protected]\",
    \"phone\": \"i\",
    \"address_line_1\": \"y\",
    \"address_line_2\": \"v\",
    \"city\": \"d\",
    \"state\": \"l\",
    \"postal_code\": \"jnikhwaykcmyuwpw\",
    \"country\": \"lv\",
    \"currency\": \"qwr\",
    \"payment_terms\": 10,
    \"notes\": \"i\",
    \"is_archived\": false
}"
const url = new URL(
    "https://tidybill.app/api/clients"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "b",
    "email": "[email protected]",
    "phone": "i",
    "address_line_1": "y",
    "address_line_2": "v",
    "city": "d",
    "state": "l",
    "postal_code": "jnikhwaykcmyuwpw",
    "country": "lv",
    "currency": "qwr",
    "payment_terms": 10,
    "notes": "i",
    "is_archived": false
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 130,
        "uuid": "d0ae1404-48ba-4c80-81e2-18f7d3562e16",
        "name": "Fritsch-O'Keefe",
        "email": "[email protected]",
        "phone": "283.476.7809",
        "address_line_1": "67339 Gaylord Meadow Suite 788",
        "address_line_2": null,
        "city": "Verliebury",
        "state": "Colorado",
        "postal_code": "61747-3805",
        "country": "KP",
        "currency": "USD",
        "payment_terms": 30,
        "notes": null,
        "is_archived": false,
        "created_at": "2026-04-05T19:38:14.000000Z",
        "updated_at": "2026-04-05T19:38:14.000000Z"
    }
}
 

Request      

POST api/clients

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

name   string     

Must not be greater than 255 characters. Example: b

email   string  optional    

Must be a valid email address. Must not be greater than 255 characters. Example: [email protected]

phone   string  optional    

Must not be greater than 50 characters. Example: i

address_line_1   string  optional    

Must not be greater than 255 characters. Example: y

address_line_2   string  optional    

Must not be greater than 255 characters. Example: v

city   string  optional    

Must not be greater than 255 characters. Example: d

state   string  optional    

Must not be greater than 255 characters. Example: l

postal_code   string  optional    

Must not be greater than 20 characters. Example: jnikhwaykcmyuwpw

country   string  optional    

Must be 2 characters. Example: lv

currency   string  optional    

Must be 3 characters. Example: qwr

payment_terms   integer  optional    

Must be at least 0. Must not be greater than 365. Example: 10

notes   string  optional    

Must not be greater than 5000 characters. Example: i

is_archived   boolean  optional    

Example: false

Get a client

requires authentication

Includes the client's contacts and associated projects.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/clients/4" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/clients/4"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 131,
        "uuid": "fe59f5ac-a325-4d12-9ea3-af4385b8a9b1",
        "name": "Price Ltd",
        "email": "[email protected]",
        "phone": "+14324666067",
        "address_line_1": "26432 Leuschke Throughway Apt. 227",
        "address_line_2": null,
        "city": "Lake Audreyborough",
        "state": "Montana",
        "postal_code": "36080-0782",
        "country": "VG",
        "currency": "USD",
        "payment_terms": 30,
        "notes": null,
        "is_archived": false,
        "created_at": "2026-04-05T19:38:15.000000Z",
        "updated_at": "2026-04-05T19:38:15.000000Z"
    }
}
 

Request      

GET api/clients/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the client. Example: 4

Update a client

requires authentication

Example request:
curl --request PUT \
    "https://tidybill.app/api/clients/4" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"b\",
    \"email\": \"[email protected]\",
    \"phone\": \"i\",
    \"address_line_1\": \"y\",
    \"address_line_2\": \"v\",
    \"city\": \"d\",
    \"state\": \"l\",
    \"postal_code\": \"jnikhwaykcmyuwpw\",
    \"country\": \"lv\",
    \"currency\": \"qwr\",
    \"payment_terms\": 10,
    \"notes\": \"i\",
    \"is_archived\": true
}"
const url = new URL(
    "https://tidybill.app/api/clients/4"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "b",
    "email": "[email protected]",
    "phone": "i",
    "address_line_1": "y",
    "address_line_2": "v",
    "city": "d",
    "state": "l",
    "postal_code": "jnikhwaykcmyuwpw",
    "country": "lv",
    "currency": "qwr",
    "payment_terms": 10,
    "notes": "i",
    "is_archived": true
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 132,
        "uuid": "b185a943-0f68-43f2-93a2-d720c608be3a",
        "name": "Fritsch-O'Keefe",
        "email": "[email protected]",
        "phone": "283.476.7809",
        "address_line_1": "67339 Gaylord Meadow Suite 788",
        "address_line_2": null,
        "city": "Verliebury",
        "state": "Colorado",
        "postal_code": "61747-3805",
        "country": "KP",
        "currency": "USD",
        "payment_terms": 30,
        "notes": null,
        "is_archived": false,
        "created_at": "2026-04-05T19:38:15.000000Z",
        "updated_at": "2026-04-05T19:38:15.000000Z"
    }
}
 

Request      

PUT api/clients/{id}

PATCH api/clients/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the client. Example: 4

Body Parameters

name   string  optional    

Must not be greater than 255 characters. Example: b

email   string  optional    

Must be a valid email address. Must not be greater than 255 characters. Example: [email protected]

phone   string  optional    

Must not be greater than 50 characters. Example: i

address_line_1   string  optional    

Must not be greater than 255 characters. Example: y

address_line_2   string  optional    

Must not be greater than 255 characters. Example: v

city   string  optional    

Must not be greater than 255 characters. Example: d

state   string  optional    

Must not be greater than 255 characters. Example: l

postal_code   string  optional    

Must not be greater than 20 characters. Example: jnikhwaykcmyuwpw

country   string  optional    

Must be 2 characters. Example: lv

currency   string  optional    

Must be 3 characters. Example: qwr

payment_terms   integer  optional    

Must be at least 0. Must not be greater than 365. Example: 10

notes   string  optional    

Must not be greater than 5000 characters. Example: i

is_archived   boolean  optional    

Example: true

Delete a client

requires authentication

Example request:
curl --request DELETE \
    "https://tidybill.app/api/clients/4" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/clients/4"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

DELETE api/clients/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the client. Example: 4

Move client to another company

requires authentication

Moves the client and all their associated invoices, quotes, recurring invoices, projects, time entries, and expenses to the target company in a single transaction.

Example request:
curl --request POST \
    "https://tidybill.app/api/clients/4/move" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"target_company_id\": 16
}"
const url = new URL(
    "https://tidybill.app/api/clients/4/move"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "target_company_id": 16
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 133,
        "uuid": "345c2f0a-3b35-4f33-a9d5-80a146687983",
        "name": "Bailey Ltd",
        "email": "[email protected]",
        "phone": "1-973-868-2042",
        "address_line_1": "77432 Amber Crossing",
        "address_line_2": null,
        "city": "Leuschkeland",
        "state": "Kentucky",
        "postal_code": "25744",
        "country": "MD",
        "currency": "USD",
        "payment_terms": 30,
        "notes": null,
        "is_archived": false,
        "created_at": "2026-04-05T19:38:15.000000Z",
        "updated_at": "2026-04-05T19:38:15.000000Z"
    }
}
 

Example response (422):


{
    "data": {
        "message": "Client is already in this company."
    }
}
 

Request      

POST api/clients/{client_id}/move

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

client_id   integer     

The ID of the client. Example: 4

Body Parameters

target_company_id   integer     

Example: 16

Get client stats

requires authentication

Returns total invoiced, total paid, outstanding balance (active invoices only), and total tracked hours for the client.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/clients/4/stats" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/clients/4/stats"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "total_invoiced": 10000,
        "total_paid": 5000,
        "outstanding": 5000,
        "total_hours": 12.5
    }
}
 

Request      

GET api/clients/{client_id}/stats

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

client_id   integer     

The ID of the client. Example: 4

Get client statement data

requires authentication

Returns a list of non-draft, non-voided invoices for the date range with running balance, plus total invoiced, paid, and outstanding amounts.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/clients/4/statement?from=2026-01-01&to=2026-03-31" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"from\": \"2026-04-05T19:38:15\",
    \"to\": \"2026-04-05T19:38:15\"
}"
const url = new URL(
    "https://tidybill.app/api/clients/4/statement"
);

const params = {
    "from": "2026-01-01",
    "to": "2026-03-31",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "from": "2026-04-05T19:38:15",
    "to": "2026-04-05T19:38:15"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "client": {},
        "from": "2026-01-01",
        "to": "2026-03-31",
        "total_invoiced": 10000,
        "total_paid": 5000,
        "total_outstanding": 5000,
        "items": []
    }
}
 

Request      

GET api/clients/{client_id}/statement

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

client_id   integer     

The ID of the client. Example: 4

Query Parameters

from   string  optional    

Start date (YYYY-MM-DD). Example: 2026-01-01

to   string  optional    

End date (YYYY-MM-DD). Example: 2026-03-31

Body Parameters

from   string  optional    

Must be a valid date. Example: 2026-04-05T19:38:15

to   string  optional    

Must be a valid date. Example: 2026-04-05T19:38:15

Download client statement as PDF

requires authentication

Generates a PDF statement using the company's default invoice template and streams it as a file download.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/clients/4/statement/pdf?from=2026-01-01&to=2026-03-31" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"from\": \"2026-04-05T19:38:15\",
    \"to\": \"2026-04-05T19:38:15\"
}"
const url = new URL(
    "https://tidybill.app/api/clients/4/statement/pdf"
);

const params = {
    "from": "2026-01-01",
    "to": "2026-03-31",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "from": "2026-04-05T19:38:15",
    "to": "2026-04-05T19:38:15"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200, PDF binary):


{
    "content-type": "application/pdf"
}
 

Request      

GET api/clients/{client_id}/statement/pdf

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

client_id   integer     

The ID of the client. Example: 4

Query Parameters

from   string  optional    

Start date (YYYY-MM-DD). Example: 2026-01-01

to   string  optional    

End date (YYYY-MM-DD). Example: 2026-03-31

Body Parameters

from   string  optional    

Must be a valid date. Example: 2026-04-05T19:38:15

to   string  optional    

Must be a valid date. Example: 2026-04-05T19:38:15

Send client statement by email

requires authentication

Generates the statement PDF and emails it to the client's primary contact or email address. Accepts an optional note for a custom body and a cc list of additional recipients.

Example request:
curl --request POST \
    "https://tidybill.app/api/clients/4/statement/send" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"from\": \"2026-04-05T19:38:15\",
    \"to\": \"2026-04-05T19:38:15\",
    \"note\": \"b\",
    \"cc\": [
        \"[email protected]\"
    ]
}"
const url = new URL(
    "https://tidybill.app/api/clients/4/statement/send"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "from": "2026-04-05T19:38:15",
    "to": "2026-04-05T19:38:15",
    "note": "b",
    "cc": [
        "[email protected]"
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "message": "Statement sent successfully."
}
 

Example response (422):


{
    "message": "Client has no email address."
}
 

Request      

POST api/clients/{client_id}/statement/send

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

client_id   integer     

The ID of the client. Example: 4

Body Parameters

from   string     

Must be a valid date. Example: 2026-04-05T19:38:15

to   string     

Must be a valid date. Example: 2026-04-05T19:38:15

note   string  optional    

Must not be greater than 2000 characters. Example: b

cc   string[]  optional    

Must be a valid email address.

List client contacts

requires authentication

Example request:
curl --request GET \
    --get "https://tidybill.app/api/clients/4/contacts" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/clients/4/contacts"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 124,
            "name": "Morgan Hirthe",
            "email": "[email protected]",
            "phone": "+14324666067",
            "is_primary": false,
            "created_at": "2026-04-05T19:38:15.000000Z"
        },
        {
            "id": 125,
            "name": "Ms. Anais Conroy",
            "email": "[email protected]",
            "phone": "1-678-926-5062",
            "is_primary": false,
            "created_at": "2026-04-05T19:38:15.000000Z"
        }
    ]
}
 

Request      

GET api/clients/{client_id}/contacts

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

client_id   integer     

The ID of the client. Example: 4

Create a client contact

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/clients/4/contacts" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"b\",
    \"email\": \"[email protected]\",
    \"phone\": \"i\",
    \"is_primary\": true
}"
const url = new URL(
    "https://tidybill.app/api/clients/4/contacts"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "b",
    "email": "[email protected]",
    "phone": "i",
    "is_primary": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 126,
        "name": "Mr. Gerhard Dach Jr.",
        "email": "[email protected]",
        "phone": "+1-626-249-0432",
        "is_primary": false,
        "created_at": "2026-04-05T19:38:15.000000Z"
    }
}
 

Request      

POST api/clients/{client_id}/contacts

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

client_id   integer     

The ID of the client. Example: 4

Body Parameters

name   string     

Must not be greater than 255 characters. Example: b

email   string  optional    

Must be a valid email address. Must not be greater than 255 characters. Example: [email protected]

phone   string  optional    

Must not be greater than 50 characters. Example: i

is_primary   boolean  optional    

Example: true

Get a client contact

requires authentication

Example request:
curl --request GET \
    --get "https://tidybill.app/api/clients/4/contacts/1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/clients/4/contacts/1"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 127,
        "name": "Morgan Hirthe",
        "email": "[email protected]",
        "phone": "+14324666067",
        "is_primary": false,
        "created_at": "2026-04-05T19:38:15.000000Z"
    }
}
 

Request      

GET api/clients/{client_id}/contacts/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

client_id   integer     

The ID of the client. Example: 4

id   integer     

The ID of the contact. Example: 1

Update a client contact

requires authentication

Example request:
curl --request PUT \
    "https://tidybill.app/api/clients/4/contacts/1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"b\",
    \"email\": \"[email protected]\",
    \"phone\": \"i\",
    \"is_primary\": true
}"
const url = new URL(
    "https://tidybill.app/api/clients/4/contacts/1"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "b",
    "email": "[email protected]",
    "phone": "i",
    "is_primary": true
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 128,
        "name": "Mr. Gerhard Dach Jr.",
        "email": "[email protected]",
        "phone": "+1-626-249-0432",
        "is_primary": false,
        "created_at": "2026-04-05T19:38:15.000000Z"
    }
}
 

Request      

PUT api/clients/{client_id}/contacts/{id}

PATCH api/clients/{client_id}/contacts/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

client_id   integer     

The ID of the client. Example: 4

id   integer     

The ID of the contact. Example: 1

Body Parameters

name   string     

Must not be greater than 255 characters. Example: b

email   string  optional    

Must be a valid email address. Must not be greater than 255 characters. Example: [email protected]

phone   string  optional    

Must not be greater than 50 characters. Example: i

is_primary   boolean  optional    

Example: true

Delete a client contact

requires authentication

Example request:
curl --request DELETE \
    "https://tidybill.app/api/clients/4/contacts/1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/clients/4/contacts/1"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

DELETE api/clients/{client_id}/contacts/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

client_id   integer     

The ID of the client. Example: 4

id   integer     

The ID of the contact. Example: 1

Invoices

Bulk action on invoices.

requires authentication

Supported actions: archive, unarchive, mark-sent (draft invoices only), delete (draft invoices only, also unbills linked time entries). Returns the count of affected records.

Example request:
curl --request POST \
    "https://tidybill.app/api/invoices/bulk-action" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"ids\": [
        16
    ],
    \"action\": \"unarchive\"
}"
const url = new URL(
    "https://tidybill.app/api/invoices/bulk-action"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "ids": [
        16
    ],
    "action": "unarchive"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "affected": 3
    }
}
 

Request      

POST api/invoices/bulk-action

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

ids   integer[]  optional    
action   string     

Example: unarchive

Must be one of:
  • archive
  • unarchive
  • mark-sent
  • delete

Bulk record payments for multiple invoices.

requires authentication

Records a payment against each invoice in a single transaction. If send_notification is true, a payment received email is sent to each affected client.

Example request:
curl --request POST \
    "https://tidybill.app/api/invoices/bulk-record-payment" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"payments\": [
        {
            \"invoice_id\": 16,
            \"amount\": 22,
            \"payment_date\": \"2026-04-05T19:38:19\",
            \"payment_method\": \"g\",
            \"reference\": \"z\",
            \"notes\": \"m\"
        }
    ],
    \"send_notification\": false
}"
const url = new URL(
    "https://tidybill.app/api/invoices/bulk-record-payment"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "payments": [
        {
            "invoice_id": 16,
            "amount": 22,
            "payment_date": "2026-04-05T19:38:19",
            "payment_method": "g",
            "reference": "z",
            "notes": "m"
        }
    ],
    "send_notification": false
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "recorded": 2,
        "errors": []
    }
}
 

Request      

POST api/invoices/bulk-record-payment

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

payments   object[]     

Must have at least 1 items. Must not have more than 100 items.

invoice_id   integer     

Example: 16

amount   integer     

Must be at least 1. Example: 22

payment_date   string     

Must be a valid date. Example: 2026-04-05T19:38:19

payment_method   string  optional    

Must not be greater than 100 characters. Example: g

reference   string  optional    

Must not be greater than 255 characters. Example: z

notes   string  optional    

Must not be greater than 1000 characters. Example: m

send_notification   boolean  optional    

Example: false

List invoices.

requires authentication

Supports filtering by status, client_id, invoice_number, and a comma-separated ids list. Results are paginated and include aggregate totals and status counts in the meta.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/invoices?filter%5Bstatus%5D=sent&filter%5Bclient_id%5D=1&filter%5Binvoice_number%5D=INV-001&filter%5Bids%5D=1%2C2%2C3&sort=-issue_date" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/invoices"
);

const params = {
    "filter[status]": "sent",
    "filter[client_id]": "1",
    "filter[invoice_number]": "INV-001",
    "filter[ids]": "1,2,3",
    "sort": "-issue_date",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 1599,
            "uuid": "a2dbfa6b-cc9d-4b33-96f9-0c2b607910bb",
            "client_id": 155,
            "recurring_invoice_id": null,
            "invoice_number": "INV-79198",
            "status": "draft",
            "is_archived": false,
            "status_label": "Draft",
            "status_color": "gray",
            "issue_date": "2026-04-05",
            "due_date": "2026-05-05",
            "currency": "USD",
            "subtotal": 0,
            "tax_total": 0,
            "discount_type": null,
            "discount_value": null,
            "discount_total": 0,
            "total": 0,
            "amount_paid": 0,
            "amount_due": 0,
            "notes": null,
            "terms": null,
            "footer": null,
            "sent_at": null,
            "viewed_at": null,
            "paid_at": null,
            "late_fee_amount": 0,
            "late_fee_applied_at": null,
            "reminders_sent": 0,
            "created_at": "2026-04-05T19:38:19.000000Z",
            "updated_at": "2026-04-05T19:38:19.000000Z"
        },
        {
            "id": 1600,
            "uuid": "458979ce-7d33-4578-a55f-d98c55ed88f2",
            "client_id": 156,
            "recurring_invoice_id": null,
            "invoice_number": "INV-21385",
            "status": "draft",
            "is_archived": false,
            "status_label": "Draft",
            "status_color": "gray",
            "issue_date": "2026-04-05",
            "due_date": "2026-05-05",
            "currency": "USD",
            "subtotal": 0,
            "tax_total": 0,
            "discount_type": null,
            "discount_value": null,
            "discount_total": 0,
            "total": 0,
            "amount_paid": 0,
            "amount_due": 0,
            "notes": null,
            "terms": null,
            "footer": null,
            "sent_at": null,
            "viewed_at": null,
            "paid_at": null,
            "late_fee_amount": 0,
            "late_fee_applied_at": null,
            "reminders_sent": 0,
            "created_at": "2026-04-05T19:38:19.000000Z",
            "updated_at": "2026-04-05T19:38:19.000000Z"
        }
    ]
}
 

Request      

GET api/invoices

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

filter[status]   string  optional    

Filter by status. Example: sent

filter[client_id]   integer  optional    

Filter by client ID. Example: 1

filter[invoice_number]   string  optional    

Partial match on invoice number. Example: INV-001

filter[ids]   string  optional    

Comma-separated list of invoice IDs. Example: 1,2,3

sort   string  optional    

Sort field (prefix with - for descending). Example: -issue_date

Create an invoice.

requires authentication

Creates a draft invoice. Pass a line_items array to create and attach line items in the same request.

Example request:
curl --request POST \
    "https://tidybill.app/api/invoices" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"client_id\": \"architecto\",
    \"issue_date\": \"2026-04-05T19:38:19\",
    \"due_date\": \"2052-04-28\",
    \"currency\": \"ngz\",
    \"discount_type\": \"percentage\",
    \"discount_value\": 16,
    \"notes\": \"n\",
    \"terms\": \"g\",
    \"footer\": \"z\",
    \"is_archived\": false,
    \"line_items\": [
        {
            \"description\": \"Velit et fugiat sunt nihil accusantium.\",
            \"quantity\": 52,
            \"unit_price\": 8,
            \"tax_name\": \"k\",
            \"tax_rate\": 14,
            \"sort_order\": 16
        }
    ]
}"
const url = new URL(
    "https://tidybill.app/api/invoices"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "client_id": "architecto",
    "issue_date": "2026-04-05T19:38:19",
    "due_date": "2052-04-28",
    "currency": "ngz",
    "discount_type": "percentage",
    "discount_value": 16,
    "notes": "n",
    "terms": "g",
    "footer": "z",
    "is_archived": false,
    "line_items": [
        {
            "description": "Velit et fugiat sunt nihil accusantium.",
            "quantity": 52,
            "unit_price": 8,
            "tax_name": "k",
            "tax_rate": 14,
            "sort_order": 16
        }
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 1601,
        "uuid": "5ce597fd-4196-459a-ac53-a97436b8c7b6",
        "client_id": 157,
        "recurring_invoice_id": null,
        "invoice_number": "INV-70546",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "due_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "amount_paid": 0,
        "amount_due": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "paid_at": null,
        "late_fee_amount": 0,
        "late_fee_applied_at": null,
        "reminders_sent": 0,
        "created_at": "2026-04-05T19:38:19.000000Z",
        "updated_at": "2026-04-05T19:38:19.000000Z"
    },
    "status": "201"
}
 

Request      

POST api/invoices

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

client_id   string     

The id of an existing record in the clients table. Example: architecto

issue_date   string  optional    

Must be a valid date. Example: 2026-04-05T19:38:19

due_date   string  optional    

Must be a valid date. Must be a date after or equal to issue_date. Example: 2052-04-28

currency   string  optional    

Must be 3 characters. Example: ngz

discount_type   string  optional    

Example: percentage

Must be one of:
  • percentage
  • fixed
discount_value   integer  optional    

Example: 16

notes   string  optional    

Must not be greater than 2000 characters. Example: n

terms   string  optional    

Must not be greater than 2000 characters. Example: g

footer   string  optional    

Must not be greater than 500 characters. Example: z

is_archived   boolean  optional    

Example: false

line_items   object[]  optional    
description   string     

Must not be greater than 1000 characters. Example: Velit et fugiat sunt nihil accusantium.

quantity   number     

Must be at least 0.0001. Example: 52

unit_price   number     

Must be at least 0. Example: 8

service_id   string  optional    

The id of an existing record in the services table.

tax_name   string  optional    

Must not be greater than 50 characters. Example: k

tax_rate   number  optional    

Must be at least 0. Must not be greater than 100. Example: 14

sort_order   integer  optional    

Example: 16

Get an invoice.

requires authentication

Includes the client, all line items (including late fee items), and payment records.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/invoices/1088" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/invoices/1088"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 1602,
        "uuid": "245c8ecd-a2e5-4497-85e0-fdad85549e30",
        "client_id": 158,
        "recurring_invoice_id": null,
        "invoice_number": "INV-54634",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "due_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "amount_paid": 0,
        "amount_due": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "paid_at": null,
        "late_fee_amount": 0,
        "late_fee_applied_at": null,
        "reminders_sent": 0,
        "created_at": "2026-04-05T19:38:19.000000Z",
        "updated_at": "2026-04-05T19:38:19.000000Z"
    }
}
 

Request      

GET api/invoices/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the invoice. Example: 1088

invoice   integer     

The invoice ID. Example: 1

Update an invoice.

requires authentication

If line_items is provided, the full set of line items is synced (added, updated, and removed). Omit line_items to update invoice fields only without touching line items.

Example request:
curl --request PUT \
    "https://tidybill.app/api/invoices/1088" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"issue_date\": \"2026-04-05T19:38:19\",
    \"due_date\": \"2052-04-28\",
    \"currency\": \"ngz\",
    \"discount_type\": \"percentage\",
    \"discount_value\": 16,
    \"notes\": \"n\",
    \"terms\": \"g\",
    \"footer\": \"z\",
    \"is_archived\": true,
    \"line_items\": [
        {
            \"description\": \"Velit et fugiat sunt nihil accusantium.\",
            \"quantity\": 52,
            \"unit_price\": 8,
            \"tax_name\": \"k\",
            \"tax_rate\": 14,
            \"sort_order\": 16
        }
    ]
}"
const url = new URL(
    "https://tidybill.app/api/invoices/1088"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "issue_date": "2026-04-05T19:38:19",
    "due_date": "2052-04-28",
    "currency": "ngz",
    "discount_type": "percentage",
    "discount_value": 16,
    "notes": "n",
    "terms": "g",
    "footer": "z",
    "is_archived": true,
    "line_items": [
        {
            "description": "Velit et fugiat sunt nihil accusantium.",
            "quantity": 52,
            "unit_price": 8,
            "tax_name": "k",
            "tax_rate": 14,
            "sort_order": 16
        }
    ]
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 1603,
        "uuid": "64c63ff1-396c-4f1f-a073-f1ff3c3a2184",
        "client_id": 159,
        "recurring_invoice_id": null,
        "invoice_number": "INV-63526",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "due_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "amount_paid": 0,
        "amount_due": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "paid_at": null,
        "late_fee_amount": 0,
        "late_fee_applied_at": null,
        "reminders_sent": 0,
        "created_at": "2026-04-05T19:38:19.000000Z",
        "updated_at": "2026-04-05T19:38:19.000000Z"
    }
}
 

Request      

PUT api/invoices/{id}

PATCH api/invoices/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the invoice. Example: 1088

invoice   integer     

The invoice ID. Example: 1

Body Parameters

client_id   string  optional    

The id of an existing record in the clients table.

issue_date   string  optional    

Must be a valid date. Example: 2026-04-05T19:38:19

due_date   string  optional    

Must be a valid date. Must be a date after or equal to issue_date. Example: 2052-04-28

currency   string  optional    

Must be 3 characters. Example: ngz

discount_type   string  optional    

Example: percentage

Must be one of:
  • percentage
  • fixed
discount_value   integer  optional    

Example: 16

notes   string  optional    

Must not be greater than 2000 characters. Example: n

terms   string  optional    

Must not be greater than 2000 characters. Example: g

footer   string  optional    

Must not be greater than 500 characters. Example: z

is_archived   boolean  optional    

Example: true

line_items   object[]  optional    
description   string     

Must not be greater than 1000 characters. Example: Velit et fugiat sunt nihil accusantium.

quantity   number     

Must be at least 0.0001. Example: 52

unit_price   number     

Must be at least 0. Example: 8

service_id   string  optional    

The id of an existing record in the services table.

tax_name   string  optional    

Must not be greater than 50 characters. Example: k

tax_rate   number  optional    

Must be at least 0. Must not be greater than 100. Example: 14

sort_order   integer  optional    

Example: 16

Delete an invoice.

requires authentication

Soft deletes the invoice and marks any linked time entries as unbilled so they can be re-invoiced.

Example request:
curl --request DELETE \
    "https://tidybill.app/api/invoices/1088" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/invoices/1088"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

DELETE api/invoices/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the invoice. Example: 1088

invoice   integer     

The invoice ID. Example: 1

List line items for an invoice.

requires authentication

Example request:
curl --request GET \
    --get "https://tidybill.app/api/invoices/1088/line-items" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/invoices/1088/line-items"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 10149,
            "service_id": null,
            "time_entry_id": null,
            "description": "Nostrum qui commodi incidunt iure.",
            "quantity": 1.02,
            "unit_price": "13053.000000",
            "amount": 13314,
            "tax_name_1": null,
            "tax_rate_1": null,
            "tax_amount_1": 0,
            "tax_name_2": null,
            "tax_rate_2": null,
            "tax_amount_2": 0,
            "sort_order": 0,
            "is_late_fee": false,
            "created_at": "2026-04-05T19:38:19.000000Z",
            "updated_at": "2026-04-05T19:38:19.000000Z"
        },
        {
            "id": 10150,
            "service_id": null,
            "time_entry_id": null,
            "description": "Nemo voluptate accusamus ut et.",
            "quantity": 8.73,
            "unit_price": "36405.000000",
            "amount": 317816,
            "tax_name_1": null,
            "tax_rate_1": null,
            "tax_amount_1": 0,
            "tax_name_2": null,
            "tax_rate_2": null,
            "tax_amount_2": 0,
            "sort_order": 0,
            "is_late_fee": false,
            "created_at": "2026-04-05T19:38:19.000000Z",
            "updated_at": "2026-04-05T19:38:19.000000Z"
        }
    ]
}
 

Request      

GET api/invoices/{invoice_id}/line-items

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

invoice_id   integer     

The ID of the invoice. Example: 1088

invoice   integer     

The invoice ID. Example: 1

Add a line item to an invoice.

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/invoices/1088/line-items" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"description\": \"Eius et animi quos velit et.\",
    \"quantity\": 60,
    \"unit_price\": 42,
    \"tax_name_1\": \"l\",
    \"tax_rate_1\": 19,
    \"tax_name_2\": \"n\",
    \"tax_rate_2\": 5,
    \"sort_order\": 16
}"
const url = new URL(
    "https://tidybill.app/api/invoices/1088/line-items"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "description": "Eius et animi quos velit et.",
    "quantity": 60,
    "unit_price": 42,
    "tax_name_1": "l",
    "tax_rate_1": 19,
    "tax_name_2": "n",
    "tax_rate_2": 5,
    "sort_order": 16
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 10151,
        "service_id": null,
        "time_entry_id": null,
        "description": "Quos velit et fugiat sunt nihil accusantium harum.",
        "quantity": 9.96,
        "unit_price": "11278.000000",
        "amount": 112329,
        "tax_name_1": null,
        "tax_rate_1": null,
        "tax_amount_1": 0,
        "tax_name_2": null,
        "tax_rate_2": null,
        "tax_amount_2": 0,
        "sort_order": 0,
        "is_late_fee": false,
        "created_at": "2026-04-05T19:38:19.000000Z",
        "updated_at": "2026-04-05T19:38:19.000000Z"
    },
    "status": "201"
}
 

Request      

POST api/invoices/{invoice_id}/line-items

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

invoice_id   integer     

The ID of the invoice. Example: 1088

invoice   integer     

The invoice ID. Example: 1

Body Parameters

service_id   string  optional    

The id of an existing record in the services table.

description   string     

Must not be greater than 1000 characters. Example: Eius et animi quos velit et.

quantity   number     

Must be at least 0.0001. Example: 60

unit_price   number     

Must be at least 0. Example: 42

tax_name_1   string  optional    

Must not be greater than 50 characters. Example: l

tax_rate_1   integer  optional    

Must be at least 0. Must not be greater than 10000. Example: 19

tax_name_2   string  optional    

Must not be greater than 50 characters. Example: n

tax_rate_2   integer  optional    

Must be at least 0. Must not be greater than 10000. Example: 5

sort_order   integer  optional    

Example: 16

Update a line item on an invoice.

requires authentication

Example request:
curl --request PUT \
    "https://tidybill.app/api/invoices/1088/line-items/20" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"description\": \"Eius et animi quos velit et.\",
    \"quantity\": 60,
    \"unit_price\": 42,
    \"tax_name_1\": \"l\",
    \"tax_rate_1\": 19,
    \"tax_name_2\": \"n\",
    \"tax_rate_2\": 5,
    \"sort_order\": 16
}"
const url = new URL(
    "https://tidybill.app/api/invoices/1088/line-items/20"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "description": "Eius et animi quos velit et.",
    "quantity": 60,
    "unit_price": 42,
    "tax_name_1": "l",
    "tax_rate_1": 19,
    "tax_name_2": "n",
    "tax_rate_2": 5,
    "sort_order": 16
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 10152,
        "service_id": null,
        "time_entry_id": null,
        "description": "Quos velit et fugiat sunt nihil accusantium harum.",
        "quantity": 9.96,
        "unit_price": "11278.000000",
        "amount": 112329,
        "tax_name_1": null,
        "tax_rate_1": null,
        "tax_amount_1": 0,
        "tax_name_2": null,
        "tax_rate_2": null,
        "tax_amount_2": 0,
        "sort_order": 0,
        "is_late_fee": false,
        "created_at": "2026-04-05T19:38:19.000000Z",
        "updated_at": "2026-04-05T19:38:19.000000Z"
    }
}
 

Request      

PUT api/invoices/{invoice_id}/line-items/{lineItem_id}

PATCH api/invoices/{invoice_id}/line-items/{lineItem_id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

invoice_id   integer     

The ID of the invoice. Example: 1088

lineItem_id   integer     

The ID of the lineItem. Example: 20

invoice   integer     

The invoice ID. Example: 1

lineItem   integer     

The line item ID. Example: 1

Body Parameters

service_id   string  optional    

The id of an existing record in the services table.

description   string     

Must not be greater than 1000 characters. Example: Eius et animi quos velit et.

quantity   number     

Must be at least 0.0001. Example: 60

unit_price   number     

Must be at least 0. Example: 42

tax_name_1   string  optional    

Must not be greater than 50 characters. Example: l

tax_rate_1   integer  optional    

Must be at least 0. Must not be greater than 10000. Example: 19

tax_name_2   string  optional    

Must not be greater than 50 characters. Example: n

tax_rate_2   integer  optional    

Must be at least 0. Must not be greater than 10000. Example: 5

sort_order   integer  optional    

Example: 16

Delete a line item from an invoice.

requires authentication

Example request:
curl --request DELETE \
    "https://tidybill.app/api/invoices/1088/line-items/20" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/invoices/1088/line-items/20"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

DELETE api/invoices/{invoice_id}/line-items/{lineItem_id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

invoice_id   integer     

The ID of the invoice. Example: 1088

lineItem_id   integer     

The ID of the lineItem. Example: 20

invoice   integer     

The invoice ID. Example: 1

lineItem   integer     

The line item ID. Example: 1

Archive an invoice.

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/invoices/1088/archive" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/invoices/1088/archive"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

POST api/invoices/{invoice_id}/archive

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

invoice_id   integer     

The ID of the invoice. Example: 1088

invoice   integer     

The invoice ID. Example: 1

Unarchive an invoice.

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/invoices/1088/unarchive" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/invoices/1088/unarchive"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

POST api/invoices/{invoice_id}/unarchive

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

invoice_id   integer     

The ID of the invoice. Example: 1088

invoice   integer     

The invoice ID. Example: 1

Move an invoice to another company.

requires authentication

The authenticated user must have access to the target company. Reassigns the invoice, its payments, credits, time entries, and expenses to the target company.

Example request:
curl --request POST \
    "https://tidybill.app/api/invoices/1088/move" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/invoices/1088/move"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 1608,
        "uuid": "3ea400bd-683a-4261-a610-0afa3c801d65",
        "client_id": 164,
        "recurring_invoice_id": null,
        "invoice_number": "INV-80304",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "due_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "amount_paid": 0,
        "amount_due": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "paid_at": null,
        "late_fee_amount": 0,
        "late_fee_applied_at": null,
        "reminders_sent": 0,
        "created_at": "2026-04-05T19:38:19.000000Z",
        "updated_at": "2026-04-05T19:38:19.000000Z"
    }
}
 

Request      

POST api/invoices/{invoice_id}/move

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

invoice_id   integer     

The ID of the invoice. Example: 1088

invoice   integer     

The invoice ID. Example: 1

Import time entries as line items onto an invoice.

requires authentication

Converts the specified unbilled time entries into invoice line items and marks them as billed. Accepts an array of time_entry_ids.

Example request:
curl --request POST \
    "https://tidybill.app/api/invoices/1088/import-time" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"time_entry_ids\": [
        16
    ]
}"
const url = new URL(
    "https://tidybill.app/api/invoices/1088/import-time"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "time_entry_ids": [
        16
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 1609,
        "uuid": "fa2ce905-7b4f-4195-afcc-00ffb631e736",
        "client_id": 165,
        "recurring_invoice_id": null,
        "invoice_number": "INV-24492",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "due_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "amount_paid": 0,
        "amount_due": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "paid_at": null,
        "late_fee_amount": 0,
        "late_fee_applied_at": null,
        "reminders_sent": 0,
        "created_at": "2026-04-05T19:38:19.000000Z",
        "updated_at": "2026-04-05T19:38:19.000000Z"
    }
}
 

Request      

POST api/invoices/{invoice_id}/import-time

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

invoice_id   integer     

The ID of the invoice. Example: 1088

invoice   integer     

The invoice ID. Example: 1

Body Parameters

time_entry_ids   integer[]  optional    

The id of an existing record in the time_entries table.

Send an invoice by email.

requires authentication

Sends the invoice to the client's email address and transitions a draft invoice to sent status. Returns 422 if the client has no email configured.

Example request:
curl --request POST \
    "https://tidybill.app/api/invoices/1088/send" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/invoices/1088/send"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 1610,
        "uuid": "c15f8d2e-94cf-4858-b434-429303945b48",
        "client_id": 166,
        "recurring_invoice_id": null,
        "invoice_number": "INV-14650",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "due_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "amount_paid": 0,
        "amount_due": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "paid_at": null,
        "late_fee_amount": 0,
        "late_fee_applied_at": null,
        "reminders_sent": 0,
        "created_at": "2026-04-05T19:38:19.000000Z",
        "updated_at": "2026-04-05T19:38:19.000000Z"
    }
}
 

Request      

POST api/invoices/{invoice_id}/send

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

invoice_id   integer     

The ID of the invoice. Example: 1088

invoice   integer     

The invoice ID. Example: 1

Mark an invoice as sent without emailing.

requires authentication

Transitions the invoice status to sent and logs the activity, but does not send any email to the client.

Example request:
curl --request POST \
    "https://tidybill.app/api/invoices/1088/mark-sent" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/invoices/1088/mark-sent"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 1611,
        "uuid": "9b5f355b-edb9-48c6-863a-589f17b401ca",
        "client_id": 167,
        "recurring_invoice_id": null,
        "invoice_number": "INV-62492",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "due_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "amount_paid": 0,
        "amount_due": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "paid_at": null,
        "late_fee_amount": 0,
        "late_fee_applied_at": null,
        "reminders_sent": 0,
        "created_at": "2026-04-05T19:38:19.000000Z",
        "updated_at": "2026-04-05T19:38:19.000000Z"
    }
}
 

Request      

POST api/invoices/{invoice_id}/mark-sent

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

invoice_id   integer     

The ID of the invoice. Example: 1088

invoice   integer     

The invoice ID. Example: 1

Mark an invoice as fully paid.

requires authentication

Sets amount_paid to the invoice total and amount_due to zero in a single transaction, then transitions the status to paid.

Example request:
curl --request POST \
    "https://tidybill.app/api/invoices/1088/mark-paid" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/invoices/1088/mark-paid"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 1612,
        "uuid": "501f9d28-97c4-4bfb-b2af-1d9e6114fda7",
        "client_id": 168,
        "recurring_invoice_id": null,
        "invoice_number": "INV-45193",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "due_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "amount_paid": 0,
        "amount_due": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "paid_at": null,
        "late_fee_amount": 0,
        "late_fee_applied_at": null,
        "reminders_sent": 0,
        "created_at": "2026-04-05T19:38:19.000000Z",
        "updated_at": "2026-04-05T19:38:19.000000Z"
    }
}
 

Request      

POST api/invoices/{invoice_id}/mark-paid

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

invoice_id   integer     

The ID of the invoice. Example: 1088

invoice   integer     

The invoice ID. Example: 1

Record a payment against an invoice.

requires authentication

Records a partial or full payment and updates amount_paid and amount_due. Automatically transitions the invoice to partial or paid status based on the remaining balance.

Example request:
curl --request POST \
    "https://tidybill.app/api/invoices/1088/record-payment" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"amount\": 1,
    \"payment_date\": \"2026-04-05T19:38:19\",
    \"payment_method\": \"card\",
    \"reference\": \"n\",
    \"notes\": \"g\"
}"
const url = new URL(
    "https://tidybill.app/api/invoices/1088/record-payment"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "amount": 1,
    "payment_date": "2026-04-05T19:38:19",
    "payment_method": "card",
    "reference": "n",
    "notes": "g"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 1613,
        "uuid": "8f637055-efeb-4f8b-9aaa-8e46e57b8fac",
        "client_id": 169,
        "recurring_invoice_id": null,
        "invoice_number": "INV-45117",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "due_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "amount_paid": 0,
        "amount_due": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "paid_at": null,
        "late_fee_amount": 0,
        "late_fee_applied_at": null,
        "reminders_sent": 0,
        "created_at": "2026-04-05T19:38:19.000000Z",
        "updated_at": "2026-04-05T19:38:19.000000Z"
    }
}
 

Request      

POST api/invoices/{invoice_id}/record-payment

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

invoice_id   integer     

The ID of the invoice. Example: 1088

invoice   integer     

The invoice ID. Example: 1

Body Parameters

amount   integer     

Must be at least 1. Must not be greater than 999999999. Example: 1

payment_date   string     

Must be a valid date. Example: 2026-04-05T19:38:19

payment_method   string  optional    

Example: card

Must be one of:
  • bank_transfer
  • card
  • cash
  • cheque
  • paypal
  • ach
  • 2checkout
  • other
reference   string  optional    

Must not be greater than 255 characters. Example: n

notes   string  optional    

Must not be greater than 2000 characters. Example: g

Void an invoice.

requires authentication

Transitions the invoice to cancelled status and marks any linked time entries as unbilled so they can be re-invoiced.

Example request:
curl --request POST \
    "https://tidybill.app/api/invoices/1088/void" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/invoices/1088/void"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 1614,
        "uuid": "a712eab5-a5ad-4a1b-8496-4966297be422",
        "client_id": 170,
        "recurring_invoice_id": null,
        "invoice_number": "INV-30805",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "due_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "amount_paid": 0,
        "amount_due": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "paid_at": null,
        "late_fee_amount": 0,
        "late_fee_applied_at": null,
        "reminders_sent": 0,
        "created_at": "2026-04-05T19:38:19.000000Z",
        "updated_at": "2026-04-05T19:38:19.000000Z"
    }
}
 

Request      

POST api/invoices/{invoice_id}/void

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

invoice_id   integer     

The ID of the invoice. Example: 1088

invoice   integer     

The invoice ID. Example: 1

Duplicate an invoice.

requires authentication

Creates a new draft invoice with the same line items and client as the original, assigned the next available invoice number.

Example request:
curl --request POST \
    "https://tidybill.app/api/invoices/1088/duplicate" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/invoices/1088/duplicate"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 1615,
        "uuid": "526b85dc-f533-4d45-85fc-ea5483106ae6",
        "client_id": 171,
        "recurring_invoice_id": null,
        "invoice_number": "INV-93082",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "due_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "amount_paid": 0,
        "amount_due": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "paid_at": null,
        "late_fee_amount": 0,
        "late_fee_applied_at": null,
        "reminders_sent": 0,
        "created_at": "2026-04-05T19:38:19.000000Z",
        "updated_at": "2026-04-05T19:38:19.000000Z"
    },
    "status": "201"
}
 

Request      

POST api/invoices/{invoice_id}/duplicate

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

invoice_id   integer     

The ID of the invoice. Example: 1088

invoice   integer     

The invoice ID. Example: 1

Download invoice as PDF.

requires authentication

Generates the invoice PDF on demand and streams it inline using the company's default template.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/invoices/1088/pdf" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/invoices/1088/pdf"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, PDF file download):

Binary data - 
 

Request      

GET api/invoices/{invoice_id}/pdf

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

invoice_id   integer     

The ID of the invoice. Example: 1088

invoice   integer     

The invoice ID. Example: 1

Quotes

Bulk action on quotes.

requires authentication

Supported actions: archive, unarchive, mark-sent (draft quotes only), delete (draft quotes only). Returns the count of affected records.

Example request:
curl --request POST \
    "https://tidybill.app/api/quotes/bulk-action" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"ids\": [
        16
    ],
    \"action\": \"mark-sent\"
}"
const url = new URL(
    "https://tidybill.app/api/quotes/bulk-action"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "ids": [
        16
    ],
    "action": "mark-sent"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "affected": 3
    }
}
 

Request      

POST api/quotes/bulk-action

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

ids   integer[]  optional    
action   string     

Example: mark-sent

Must be one of:
  • archive
  • unarchive
  • mark-sent
  • delete

List quotes.

requires authentication

Supports filtering by status, client_id, and quote_number. Results are paginated and include active/archived counts in the meta.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/quotes?filter%5Bstatus%5D=sent&filter%5Bclient_id%5D=1&filter%5Bquote_number%5D=QUO-001&sort=-issue_date" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/quotes"
);

const params = {
    "filter[status]": "sent",
    "filter[client_id]": "1",
    "filter[quote_number]": "QUO-001",
    "sort": "-issue_date",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 154,
            "uuid": "3a2924fe-1de9-4cf1-a89c-03baf0e76d42",
            "client_id": 172,
            "quote_number": "QUO-92439",
            "status": "draft",
            "is_archived": false,
            "status_label": "Draft",
            "status_color": "gray",
            "issue_date": "2026-04-05",
            "expiry_date": "2026-05-05",
            "currency": "USD",
            "subtotal": 0,
            "tax_total": 0,
            "discount_type": null,
            "discount_value": null,
            "discount_total": 0,
            "total": 0,
            "notes": null,
            "terms": null,
            "footer": null,
            "sent_at": null,
            "viewed_at": null,
            "accepted_at": null,
            "declined_at": null,
            "converted_to_invoice_id": null,
            "converted_to_project_id": null,
            "pdf_path": null,
            "created_at": "2026-04-05T19:38:19.000000Z",
            "updated_at": "2026-04-05T19:38:19.000000Z"
        },
        {
            "id": 155,
            "uuid": "307265bb-89f7-4c75-b5e8-cc58585bbe7f",
            "client_id": 173,
            "quote_number": "QUO-09313",
            "status": "draft",
            "is_archived": false,
            "status_label": "Draft",
            "status_color": "gray",
            "issue_date": "2026-04-05",
            "expiry_date": "2026-05-05",
            "currency": "USD",
            "subtotal": 0,
            "tax_total": 0,
            "discount_type": null,
            "discount_value": null,
            "discount_total": 0,
            "total": 0,
            "notes": null,
            "terms": null,
            "footer": null,
            "sent_at": null,
            "viewed_at": null,
            "accepted_at": null,
            "declined_at": null,
            "converted_to_invoice_id": null,
            "converted_to_project_id": null,
            "pdf_path": null,
            "created_at": "2026-04-05T19:38:19.000000Z",
            "updated_at": "2026-04-05T19:38:19.000000Z"
        }
    ]
}
 

Request      

GET api/quotes

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

filter[status]   string  optional    

Filter by status. Example: sent

filter[client_id]   integer  optional    

Filter by client ID. Example: 1

filter[quote_number]   string  optional    

Partial match on quote number. Example: QUO-001

sort   string  optional    

Sort field (prefix with - for descending). Example: -issue_date

Create a quote.

requires authentication

Creates a draft quote. Pass a line_items array to create and attach line items in the same request.

Example request:
curl --request POST \
    "https://tidybill.app/api/quotes" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"client_id\": \"architecto\",
    \"issue_date\": \"2026-04-05T19:38:19\",
    \"expiry_date\": \"2052-04-28\",
    \"currency\": \"ngz\",
    \"discount_type\": \"percentage\",
    \"discount_value\": 16,
    \"notes\": \"n\",
    \"terms\": \"g\",
    \"footer\": \"z\",
    \"is_archived\": true,
    \"line_items\": [
        {
            \"description\": \"Velit et fugiat sunt nihil accusantium.\",
            \"quantity\": 52,
            \"unit_price\": 8,
            \"tax_name\": \"k\",
            \"tax_rate\": 14,
            \"sort_order\": 16
        }
    ]
}"
const url = new URL(
    "https://tidybill.app/api/quotes"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "client_id": "architecto",
    "issue_date": "2026-04-05T19:38:19",
    "expiry_date": "2052-04-28",
    "currency": "ngz",
    "discount_type": "percentage",
    "discount_value": 16,
    "notes": "n",
    "terms": "g",
    "footer": "z",
    "is_archived": true,
    "line_items": [
        {
            "description": "Velit et fugiat sunt nihil accusantium.",
            "quantity": 52,
            "unit_price": 8,
            "tax_name": "k",
            "tax_rate": 14,
            "sort_order": 16
        }
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 156,
        "uuid": "36c78c49-6db6-49a2-89b1-14e1472123d9",
        "client_id": 174,
        "quote_number": "QUO-26855",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "expiry_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "accepted_at": null,
        "declined_at": null,
        "converted_to_invoice_id": null,
        "converted_to_project_id": null,
        "pdf_path": null,
        "created_at": "2026-04-05T19:38:19.000000Z",
        "updated_at": "2026-04-05T19:38:19.000000Z"
    },
    "status": "201"
}
 

Request      

POST api/quotes

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

client_id   string     

The id of an existing record in the clients table. Example: architecto

issue_date   string  optional    

Must be a valid date. Example: 2026-04-05T19:38:19

expiry_date   string  optional    

Must be a valid date. Must be a date after or equal to issue_date. Example: 2052-04-28

currency   string  optional    

Must be 3 characters. Example: ngz

discount_type   string  optional    

Example: percentage

Must be one of:
  • percentage
  • fixed
discount_value   integer  optional    

Example: 16

notes   string  optional    

Must not be greater than 2000 characters. Example: n

terms   string  optional    

Must not be greater than 2000 characters. Example: g

footer   string  optional    

Must not be greater than 500 characters. Example: z

is_archived   boolean  optional    

Example: true

line_items   object[]  optional    
description   string     

Must not be greater than 1000 characters. Example: Velit et fugiat sunt nihil accusantium.

quantity   number     

Must be at least 0.0001. Example: 52

unit_price   number     

Must be at least 0. Example: 8

service_id   string  optional    

The id of an existing record in the services table.

tax_name   string  optional    

Must not be greater than 50 characters. Example: k

tax_rate   number  optional    

Must be at least 0. Must not be greater than 100. Example: 14

sort_order   integer  optional    

Example: 16

Get a quote.

requires authentication

Includes the client and all line items.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/quotes/1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/quotes/1"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 157,
        "uuid": "5c547376-ab85-43b3-a8d4-78a9825f8192",
        "client_id": 175,
        "quote_number": "QUO-47352",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "expiry_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "accepted_at": null,
        "declined_at": null,
        "converted_to_invoice_id": null,
        "converted_to_project_id": null,
        "pdf_path": null,
        "created_at": "2026-04-05T19:38:20.000000Z",
        "updated_at": "2026-04-05T19:38:20.000000Z"
    }
}
 

Request      

GET api/quotes/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the quote. Example: 1

quote   integer     

The quote ID. Example: 1

Update a quote.

requires authentication

If line_items is provided, the full set of line items is synced. Omit line_items to update quote fields only without touching line items.

Example request:
curl --request PUT \
    "https://tidybill.app/api/quotes/1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"issue_date\": \"2026-04-05T19:38:20\",
    \"expiry_date\": \"2052-04-28\",
    \"currency\": \"ngz\",
    \"discount_type\": \"percentage\",
    \"discount_value\": 16,
    \"notes\": \"n\",
    \"terms\": \"g\",
    \"footer\": \"z\",
    \"is_archived\": false,
    \"line_items\": [
        {
            \"description\": \"Velit et fugiat sunt nihil accusantium.\",
            \"quantity\": 52,
            \"unit_price\": 8,
            \"tax_name\": \"k\",
            \"tax_rate\": 14,
            \"sort_order\": 16
        }
    ]
}"
const url = new URL(
    "https://tidybill.app/api/quotes/1"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "issue_date": "2026-04-05T19:38:20",
    "expiry_date": "2052-04-28",
    "currency": "ngz",
    "discount_type": "percentage",
    "discount_value": 16,
    "notes": "n",
    "terms": "g",
    "footer": "z",
    "is_archived": false,
    "line_items": [
        {
            "description": "Velit et fugiat sunt nihil accusantium.",
            "quantity": 52,
            "unit_price": 8,
            "tax_name": "k",
            "tax_rate": 14,
            "sort_order": 16
        }
    ]
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 158,
        "uuid": "be67fa0b-ce1b-4e12-b309-0ff89a74305e",
        "client_id": 176,
        "quote_number": "QUO-23164",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "expiry_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "accepted_at": null,
        "declined_at": null,
        "converted_to_invoice_id": null,
        "converted_to_project_id": null,
        "pdf_path": null,
        "created_at": "2026-04-05T19:38:20.000000Z",
        "updated_at": "2026-04-05T19:38:20.000000Z"
    }
}
 

Request      

PUT api/quotes/{id}

PATCH api/quotes/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the quote. Example: 1

quote   integer     

The quote ID. Example: 1

Body Parameters

client_id   string  optional    

The id of an existing record in the clients table.

issue_date   string  optional    

Must be a valid date. Example: 2026-04-05T19:38:20

expiry_date   string  optional    

Must be a valid date. Must be a date after or equal to issue_date. Example: 2052-04-28

currency   string  optional    

Must be 3 characters. Example: ngz

discount_type   string  optional    

Example: percentage

Must be one of:
  • percentage
  • fixed
discount_value   integer  optional    

Example: 16

notes   string  optional    

Must not be greater than 2000 characters. Example: n

terms   string  optional    

Must not be greater than 2000 characters. Example: g

footer   string  optional    

Must not be greater than 500 characters. Example: z

is_archived   boolean  optional    

Example: false

line_items   object[]  optional    
description   string     

Must not be greater than 1000 characters. Example: Velit et fugiat sunt nihil accusantium.

quantity   number     

Must be at least 0.0001. Example: 52

unit_price   number     

Must be at least 0. Example: 8

service_id   string  optional    

The id of an existing record in the services table.

tax_name   string  optional    

Must not be greater than 50 characters. Example: k

tax_rate   number  optional    

Must be at least 0. Must not be greater than 100. Example: 14

sort_order   integer  optional    

Example: 16

Delete a quote.

requires authentication

Only draft quotes can be deleted. Returns 422 for any other status.

Example request:
curl --request DELETE \
    "https://tidybill.app/api/quotes/1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/quotes/1"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

DELETE api/quotes/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the quote. Example: 1

quote   integer     

The quote ID. Example: 1

List line items for a quote.

requires authentication

Example request:
curl --request GET \
    --get "https://tidybill.app/api/quotes/1/line-items" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/quotes/1/line-items"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 1512,
            "service_id": null,
            "time_entry_id": null,
            "description": "Nostrum qui commodi incidunt iure.",
            "quantity": 1.02,
            "unit_price": "13053.000000",
            "amount": 13314,
            "tax_name_1": null,
            "tax_rate_1": null,
            "tax_amount_1": 0,
            "tax_name_2": null,
            "tax_rate_2": null,
            "tax_amount_2": 0,
            "sort_order": 0,
            "is_late_fee": false,
            "created_at": "2026-04-05T19:38:20.000000Z",
            "updated_at": "2026-04-05T19:38:20.000000Z"
        },
        {
            "id": 1513,
            "service_id": null,
            "time_entry_id": null,
            "description": "Nemo voluptate accusamus ut et.",
            "quantity": 8.73,
            "unit_price": "36405.000000",
            "amount": 317816,
            "tax_name_1": null,
            "tax_rate_1": null,
            "tax_amount_1": 0,
            "tax_name_2": null,
            "tax_rate_2": null,
            "tax_amount_2": 0,
            "sort_order": 0,
            "is_late_fee": false,
            "created_at": "2026-04-05T19:38:20.000000Z",
            "updated_at": "2026-04-05T19:38:20.000000Z"
        }
    ]
}
 

Request      

GET api/quotes/{quote_id}/line-items

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

quote_id   integer     

The ID of the quote. Example: 1

quote   integer     

The quote ID. Example: 1

Add a line item to a quote.

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/quotes/1/line-items" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"description\": \"Eius et animi quos velit et.\",
    \"quantity\": 60,
    \"unit_price\": 42,
    \"tax_name_1\": \"l\",
    \"tax_rate_1\": 19,
    \"tax_name_2\": \"n\",
    \"tax_rate_2\": 5,
    \"sort_order\": 16
}"
const url = new URL(
    "https://tidybill.app/api/quotes/1/line-items"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "description": "Eius et animi quos velit et.",
    "quantity": 60,
    "unit_price": 42,
    "tax_name_1": "l",
    "tax_rate_1": 19,
    "tax_name_2": "n",
    "tax_rate_2": 5,
    "sort_order": 16
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 1514,
        "service_id": null,
        "time_entry_id": null,
        "description": "Quos velit et fugiat sunt nihil accusantium harum.",
        "quantity": 9.96,
        "unit_price": "11278.000000",
        "amount": 112329,
        "tax_name_1": null,
        "tax_rate_1": null,
        "tax_amount_1": 0,
        "tax_name_2": null,
        "tax_rate_2": null,
        "tax_amount_2": 0,
        "sort_order": 0,
        "is_late_fee": false,
        "created_at": "2026-04-05T19:38:20.000000Z",
        "updated_at": "2026-04-05T19:38:20.000000Z"
    },
    "status": "201"
}
 

Request      

POST api/quotes/{quote_id}/line-items

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

quote_id   integer     

The ID of the quote. Example: 1

quote   integer     

The quote ID. Example: 1

Body Parameters

service_id   string  optional    

The id of an existing record in the services table.

description   string     

Must not be greater than 1000 characters. Example: Eius et animi quos velit et.

quantity   number     

Must be at least 0.0001. Example: 60

unit_price   number     

Must be at least 0. Example: 42

tax_name_1   string  optional    

Must not be greater than 50 characters. Example: l

tax_rate_1   integer  optional    

Must be at least 0. Must not be greater than 10000. Example: 19

tax_name_2   string  optional    

Must not be greater than 50 characters. Example: n

tax_rate_2   integer  optional    

Must be at least 0. Must not be greater than 10000. Example: 5

sort_order   integer  optional    

Example: 16

Update a line item on a quote.

requires authentication

Example request:
curl --request PUT \
    "https://tidybill.app/api/quotes/1/line-items/1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"description\": \"Eius et animi quos velit et.\",
    \"quantity\": 60,
    \"unit_price\": 42,
    \"tax_name_1\": \"l\",
    \"tax_rate_1\": 19,
    \"tax_name_2\": \"n\",
    \"tax_rate_2\": 5,
    \"sort_order\": 16
}"
const url = new URL(
    "https://tidybill.app/api/quotes/1/line-items/1"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "description": "Eius et animi quos velit et.",
    "quantity": 60,
    "unit_price": 42,
    "tax_name_1": "l",
    "tax_rate_1": 19,
    "tax_name_2": "n",
    "tax_rate_2": 5,
    "sort_order": 16
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 1515,
        "service_id": null,
        "time_entry_id": null,
        "description": "Quos velit et fugiat sunt nihil accusantium harum.",
        "quantity": 9.96,
        "unit_price": "11278.000000",
        "amount": 112329,
        "tax_name_1": null,
        "tax_rate_1": null,
        "tax_amount_1": 0,
        "tax_name_2": null,
        "tax_rate_2": null,
        "tax_amount_2": 0,
        "sort_order": 0,
        "is_late_fee": false,
        "created_at": "2026-04-05T19:38:20.000000Z",
        "updated_at": "2026-04-05T19:38:20.000000Z"
    }
}
 

Request      

PUT api/quotes/{quote_id}/line-items/{lineItem_id}

PATCH api/quotes/{quote_id}/line-items/{lineItem_id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

quote_id   integer     

The ID of the quote. Example: 1

lineItem_id   integer     

The ID of the lineItem. Example: 1

quote   integer     

The quote ID. Example: 1

lineItem   integer     

The line item ID. Example: 1

Body Parameters

service_id   string  optional    

The id of an existing record in the services table.

description   string     

Must not be greater than 1000 characters. Example: Eius et animi quos velit et.

quantity   number     

Must be at least 0.0001. Example: 60

unit_price   number     

Must be at least 0. Example: 42

tax_name_1   string  optional    

Must not be greater than 50 characters. Example: l

tax_rate_1   integer  optional    

Must be at least 0. Must not be greater than 10000. Example: 19

tax_name_2   string  optional    

Must not be greater than 50 characters. Example: n

tax_rate_2   integer  optional    

Must be at least 0. Must not be greater than 10000. Example: 5

sort_order   integer  optional    

Example: 16

Delete a line item from a quote.

requires authentication

Example request:
curl --request DELETE \
    "https://tidybill.app/api/quotes/1/line-items/1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/quotes/1/line-items/1"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

DELETE api/quotes/{quote_id}/line-items/{lineItem_id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

quote_id   integer     

The ID of the quote. Example: 1

lineItem_id   integer     

The ID of the lineItem. Example: 1

quote   integer     

The quote ID. Example: 1

lineItem   integer     

The line item ID. Example: 1

Archive a quote.

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/quotes/1/archive" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/quotes/1/archive"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

POST api/quotes/{quote_id}/archive

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

quote_id   integer     

The ID of the quote. Example: 1

quote   integer     

The quote ID. Example: 1

Unarchive a quote.

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/quotes/1/unarchive" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/quotes/1/unarchive"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

POST api/quotes/{quote_id}/unarchive

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

quote_id   integer     

The ID of the quote. Example: 1

quote   integer     

The quote ID. Example: 1

Send a quote by email.

requires authentication

Sends the quote to the client's email address and transitions the status to sent. Returns 422 if the client has no email configured.

Example request:
curl --request POST \
    "https://tidybill.app/api/quotes/1/send" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/quotes/1/send"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 163,
        "uuid": "f102d993-55e1-4722-9374-5c984455862a",
        "client_id": 181,
        "quote_number": "QUO-35644",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "expiry_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "accepted_at": null,
        "declined_at": null,
        "converted_to_invoice_id": null,
        "converted_to_project_id": null,
        "pdf_path": null,
        "created_at": "2026-04-05T19:38:20.000000Z",
        "updated_at": "2026-04-05T19:38:20.000000Z"
    }
}
 

Request      

POST api/quotes/{quote_id}/send

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

quote_id   integer     

The ID of the quote. Example: 1

quote   integer     

The quote ID. Example: 1

Mark a quote as sent without emailing.

requires authentication

Transitions the quote status to sent and logs the activity, but does not send any email to the client.

Example request:
curl --request POST \
    "https://tidybill.app/api/quotes/1/mark-sent" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/quotes/1/mark-sent"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 164,
        "uuid": "746befd7-b5a8-45fe-b63b-5a0e68a5f1d9",
        "client_id": 182,
        "quote_number": "QUO-08317",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "expiry_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "accepted_at": null,
        "declined_at": null,
        "converted_to_invoice_id": null,
        "converted_to_project_id": null,
        "pdf_path": null,
        "created_at": "2026-04-05T19:38:20.000000Z",
        "updated_at": "2026-04-05T19:38:20.000000Z"
    }
}
 

Request      

POST api/quotes/{quote_id}/mark-sent

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

quote_id   integer     

The ID of the quote. Example: 1

quote   integer     

The quote ID. Example: 1

Convert a quote to an invoice.

requires authentication

Creates a new draft invoice from the quote's line items and client. The quote must be in sent, viewed, or accepted status.

Example request:
curl --request POST \
    "https://tidybill.app/api/quotes/1/convert-to-invoice" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/quotes/1/convert-to-invoice"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 1616,
        "uuid": "91cb4b60-aff6-4487-b436-46de2e36ed56",
        "client_id": 183,
        "recurring_invoice_id": null,
        "invoice_number": "INV-98251",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "due_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "amount_paid": 0,
        "amount_due": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "paid_at": null,
        "late_fee_amount": 0,
        "late_fee_applied_at": null,
        "reminders_sent": 0,
        "created_at": "2026-04-05T19:38:20.000000Z",
        "updated_at": "2026-04-05T19:38:20.000000Z"
    },
    "status": "201"
}
 

Request      

POST api/quotes/{quote_id}/convert-to-invoice

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

quote_id   integer     

The ID of the quote. Example: 1

quote   integer     

The quote ID. Example: 1

Convert a quote to a project.

requires authentication

Creates a new project from the quote details. The quote must be in sent, viewed, or accepted status.

Example request:
curl --request POST \
    "https://tidybill.app/api/quotes/1/convert-to-project" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/quotes/1/convert-to-project"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 43,
        "client_id": 184,
        "name": "implement intuitive e-tailers",
        "status": "active",
        "is_archived": false,
        "billing_method": "hourly",
        "hourly_rate": 14351,
        "budget_hours": null,
        "budget_amount": null,
        "start_date": null,
        "end_date": null,
        "created_at": "2026-04-05T19:38:20.000000Z",
        "updated_at": "2026-04-05T19:38:20.000000Z"
    },
    "status": "201"
}
 

Request      

POST api/quotes/{quote_id}/convert-to-project

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

quote_id   integer     

The ID of the quote. Example: 1

quote   integer     

The quote ID. Example: 1

Duplicate a quote.

requires authentication

Creates a new draft quote with the same line items and client as the original, assigned the next available quote number.

Example request:
curl --request POST \
    "https://tidybill.app/api/quotes/1/duplicate" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/quotes/1/duplicate"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 165,
        "uuid": "4b5d9dda-6611-407e-8472-ad4f39c91403",
        "client_id": 185,
        "quote_number": "QUO-82093",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "expiry_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "accepted_at": null,
        "declined_at": null,
        "converted_to_invoice_id": null,
        "converted_to_project_id": null,
        "pdf_path": null,
        "created_at": "2026-04-05T19:38:20.000000Z",
        "updated_at": "2026-04-05T19:38:20.000000Z"
    },
    "status": "201"
}
 

Request      

POST api/quotes/{quote_id}/duplicate

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

quote_id   integer     

The ID of the quote. Example: 1

quote   integer     

The quote ID. Example: 1

Download quote as PDF.

requires authentication

Generates the quote PDF on demand and streams it inline using the company's default template.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/quotes/1/pdf" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/quotes/1/pdf"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, PDF file download):

Binary data - 
 

Request      

GET api/quotes/{quote_id}/pdf

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

quote_id   integer     

The ID of the quote. Example: 1

quote   integer     

The quote ID. Example: 1

Projects

Bulk action on projects.

requires authentication

Supported actions: archive, unarchive, complete, delete. Returns the count of affected records.

Example request:
curl --request POST \
    "https://tidybill.app/api/projects/bulk-action" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"ids\": [
        16
    ],
    \"action\": \"archive\"
}"
const url = new URL(
    "https://tidybill.app/api/projects/bulk-action"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "ids": [
        16
    ],
    "action": "archive"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "affected": 3
    }
}
 

Request      

POST api/projects/bulk-action

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

ids   integer[]  optional    
action   string     

Example: archive

Must be one of:
  • archive
  • unarchive
  • complete
  • delete

List projects.

requires authentication

Supports filtering by status, client_id, and name search. Results are paginated and include per-status and archived counts in the meta.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/projects" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/projects"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 37,
            "client_id": 139,
            "name": "implement intuitive e-tailers",
            "status": "active",
            "is_archived": false,
            "billing_method": "hourly",
            "hourly_rate": 14351,
            "budget_hours": null,
            "budget_amount": null,
            "start_date": null,
            "end_date": null,
            "created_at": "2026-04-05T19:38:15.000000Z",
            "updated_at": "2026-04-05T19:38:15.000000Z"
        },
        {
            "id": 38,
            "client_id": 140,
            "name": "benchmark one-to-one infrastructures",
            "status": "active",
            "is_archived": false,
            "billing_method": "hourly",
            "hourly_rate": 21548,
            "budget_hours": null,
            "budget_amount": null,
            "start_date": null,
            "end_date": null,
            "created_at": "2026-04-05T19:38:15.000000Z",
            "updated_at": "2026-04-05T19:38:15.000000Z"
        }
    ]
}
 

Request      

GET api/projects

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Create a project.

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/projects" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"client_id\": \"architecto\",
    \"name\": \"n\",
    \"status\": \"active\",
    \"billing_method\": \"flat_rate\",
    \"hourly_rate\": 84,
    \"budget_hours\": 12,
    \"budget_amount\": 77,
    \"start_date\": \"2026-04-05T19:38:15\",
    \"end_date\": \"2052-04-28\",
    \"is_archived\": true
}"
const url = new URL(
    "https://tidybill.app/api/projects"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "client_id": "architecto",
    "name": "n",
    "status": "active",
    "billing_method": "flat_rate",
    "hourly_rate": 84,
    "budget_hours": 12,
    "budget_amount": 77,
    "start_date": "2026-04-05T19:38:15",
    "end_date": "2052-04-28",
    "is_archived": true
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 39,
        "client_id": 141,
        "name": "exploit scalable supply-chains",
        "status": "active",
        "is_archived": false,
        "billing_method": "hourly",
        "hourly_rate": 17320,
        "budget_hours": null,
        "budget_amount": null,
        "start_date": null,
        "end_date": null,
        "created_at": "2026-04-05T19:38:15.000000Z",
        "updated_at": "2026-04-05T19:38:15.000000Z"
    }
}
 

Request      

POST api/projects

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

client_id   string     

The id of an existing record in the clients table. Example: architecto

name   string     

Must not be greater than 255 characters. Example: n

status   string  optional    

Example: active

Must be one of:
  • active
  • completed
  • archived
billing_method   string  optional    

Example: flat_rate

Must be one of:
  • hourly
  • flat_rate
  • non_billable
hourly_rate   integer  optional    

Must be at least 0. Example: 84

budget_hours   number  optional    

Must be at least 0. Example: 12

budget_amount   integer  optional    

Must be at least 0. Example: 77

start_date   string  optional    

Must be a valid date. Example: 2026-04-05T19:38:15

end_date   string  optional    

Must be a valid date. Must be a date after or equal to start_date. Example: 2052-04-28

is_archived   boolean  optional    

Example: true

Get a project.

requires authentication

Includes the client and project members with their hourly rates.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/projects/7" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/projects/7"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 40,
        "client_id": 142,
        "name": "implement intuitive e-tailers",
        "status": "active",
        "is_archived": false,
        "billing_method": "hourly",
        "hourly_rate": 14351,
        "budget_hours": null,
        "budget_amount": null,
        "start_date": null,
        "end_date": null,
        "created_at": "2026-04-05T19:38:15.000000Z",
        "updated_at": "2026-04-05T19:38:15.000000Z"
    }
}
 

Request      

GET api/projects/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the project. Example: 7

Update a project.

requires authentication

Example request:
curl --request PUT \
    "https://tidybill.app/api/projects/7" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"b\",
    \"status\": \"archived\",
    \"billing_method\": \"flat_rate\",
    \"hourly_rate\": 39,
    \"budget_hours\": 84,
    \"budget_amount\": 12,
    \"start_date\": \"2026-04-05T19:38:15\",
    \"end_date\": \"2052-04-28\",
    \"is_archived\": false
}"
const url = new URL(
    "https://tidybill.app/api/projects/7"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "b",
    "status": "archived",
    "billing_method": "flat_rate",
    "hourly_rate": 39,
    "budget_hours": 84,
    "budget_amount": 12,
    "start_date": "2026-04-05T19:38:15",
    "end_date": "2052-04-28",
    "is_archived": false
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 41,
        "client_id": 143,
        "name": "exploit scalable supply-chains",
        "status": "active",
        "is_archived": false,
        "billing_method": "hourly",
        "hourly_rate": 17320,
        "budget_hours": null,
        "budget_amount": null,
        "start_date": null,
        "end_date": null,
        "created_at": "2026-04-05T19:38:15.000000Z",
        "updated_at": "2026-04-05T19:38:15.000000Z"
    }
}
 

Request      

PUT api/projects/{id}

PATCH api/projects/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the project. Example: 7

Body Parameters

client_id   string  optional    

The id of an existing record in the clients table.

name   string  optional    

Must not be greater than 255 characters. Example: b

status   string  optional    

Example: archived

Must be one of:
  • active
  • completed
  • archived
billing_method   string  optional    

Example: flat_rate

Must be one of:
  • hourly
  • flat_rate
  • non_billable
hourly_rate   integer  optional    

Must be at least 0. Example: 39

budget_hours   number  optional    

Must be at least 0. Example: 84

budget_amount   integer  optional    

Must be at least 0. Example: 12

start_date   string  optional    

Must be a valid date. Example: 2026-04-05T19:38:15

end_date   string  optional    

Must be a valid date. Must be a date after or equal to start_date. Example: 2052-04-28

is_archived   boolean  optional    

Example: false

Delete a project.

requires authentication

Example request:
curl --request DELETE \
    "https://tidybill.app/api/projects/7" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/projects/7"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

DELETE api/projects/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the project. Example: 7

Move project to another company.

requires authentication

Moves the project (and its time entries) to a company the authenticated user has access to. Detaches all project members and clears the client association.

Example request:
curl --request POST \
    "https://tidybill.app/api/projects/7/move" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"target_company_id\": 16
}"
const url = new URL(
    "https://tidybill.app/api/projects/7/move"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "target_company_id": 16
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 42,
        "client_id": 144,
        "name": "exploit scalable supply-chains",
        "status": "active",
        "is_archived": false,
        "billing_method": "hourly",
        "hourly_rate": 17320,
        "budget_hours": null,
        "budget_amount": null,
        "start_date": null,
        "end_date": null,
        "created_at": "2026-04-05T19:38:15.000000Z",
        "updated_at": "2026-04-05T19:38:15.000000Z"
    }
}
 

Request      

POST api/projects/{project_id}/move

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 7

Body Parameters

target_company_id   integer     

Example: 16

Archive a project.

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/projects/7/archive" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/projects/7/archive"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

POST api/projects/{project_id}/archive

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 7

Unarchive a project.

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/projects/7/unarchive" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/projects/7/unarchive"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

POST api/projects/{project_id}/unarchive

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 7

Get project stats.

requires authentication

Returns hours breakdown (total, billed, unbilled) and budget info for the project.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/projects/7/stats" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/projects/7/stats"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "total_hours": 10.5,
        "billed_hours": 4,
        "unbilled_hours": 6.5,
        "budget_hours": 20,
        "budget_amount": 300000
    }
}
 

Request      

GET api/projects/{project_id}/stats

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 7

List project members.

requires authentication

Returns each member's user details and their project-specific hourly rate from the pivot table.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/projects/7/members" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/projects/7/members"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 1,
            "name": "Jane Smith",
            "email": "[email protected]",
            "hourly_rate": 15000
        }
    ]
}
 

Request      

GET api/projects/{project_id}/members

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 7

Add a project member.

requires authentication

The user must be a member of the current company. Accepts an optional hourly_rate (in cents) to override the company default for this project.

Example request:
curl --request POST \
    "https://tidybill.app/api/projects/7/members" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"user_id\": 16,
    \"hourly_rate\": 39
}"
const url = new URL(
    "https://tidybill.app/api/projects/7/members"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "user_id": 16,
    "hourly_rate": 39
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "message": "Member added."
}
 

Request      

POST api/projects/{project_id}/members

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 7

Body Parameters

user_id   integer     

Example: 16

hourly_rate   integer  optional    

Must be at least 0. Example: 39

Remove a project member.

requires authentication

Detaches the user from the project. The user must be a member of the current company.

Example request:
curl --request DELETE \
    "https://tidybill.app/api/projects/7/members/2" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/projects/7/members/2"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

DELETE api/projects/{project_id}/members/{user_id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 7

user_id   integer     

The ID of the user. Example: 2

Time Tracking

Manage time entries for tracking billable and non-billable work. Time entries store a date and duration (in seconds). The started_at, ended_at, and is_running fields in the response are read-only and only populated when using the Timer endpoints (start/stop/current/discard). To create a manual time entry, provide date and duration only. The hourly_rate is auto-resolved from project, service, or company defaults if not provided. All money values (hourly_rate, calculated_amount) are integers in cents.

Resolve hourly rate.

requires authentication

Returns the hourly rate that would be auto-applied for a given project/service combination. Resolution order: project rate, service rate, company default rate.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/time-entries/resolve-rate?project_id=1&service_id=1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/time-entries/resolve-rate"
);

const params = {
    "project_id": "1",
    "service_id": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "rate": 15000,
    "source": "project"
}
 

Request      

GET api/time-entries/resolve-rate

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

project_id   integer  optional    

optional The project ID to check. Example: 1

service_id   integer  optional    

optional The service ID to check. Example: 1

List unbilled time entries.

requires authentication

Returns billable entries that have not yet been added to an invoice. Excludes running timers.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/time-entries/unbilled" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/time-entries/unbilled"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 4818,
            "user_id": 10,
            "client_id": 145,
            "project_id": null,
            "service_id": null,
            "date": "2026-03-11",
            "duration": 5783,
            "started_at": null,
            "ended_at": null,
            "is_running": false,
            "is_billable": true,
            "is_billed": false,
            "invoice_id": null,
            "hourly_rate": 15266,
            "notes": "Qui commodi incidunt iure odit.",
            "calculated_amount": 24523,
            "duration_hours": 1.61,
            "created_at": "2026-04-05T19:38:16.000000Z",
            "updated_at": "2026-04-05T19:38:16.000000Z"
        },
        {
            "id": 4819,
            "user_id": 11,
            "client_id": 146,
            "project_id": null,
            "service_id": null,
            "date": "2026-03-30",
            "duration": 17654,
            "started_at": null,
            "ended_at": null,
            "is_running": false,
            "is_billable": true,
            "is_billed": false,
            "invoice_id": null,
            "hourly_rate": 5977,
            "notes": "Ex repellendus assumenda et tenetur ab reiciendis.",
            "calculated_amount": 29311,
            "duration_hours": 4.9,
            "created_at": "2026-04-05T19:38:16.000000Z",
            "updated_at": "2026-04-05T19:38:16.000000Z"
        }
    ]
}
 

Request      

GET api/time-entries/unbilled

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Bulk create time entries.

requires authentication

Create up to 50 time entries in a single request. Each entry uses the same schema as the single create endpoint.

Example request:
curl --request POST \
    "https://tidybill.app/api/time-entries/bulk" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"entries\": [
        {
            \"client_id\": 1,
            \"project_id\": 1,
            \"service_id\": 1,
            \"date\": \"2026-03-29\",
            \"duration\": 3600,
            \"is_billable\": true,
            \"hourly_rate\": 15000,
            \"notes\": \"API integration work\"
        }
    ]
}"
const url = new URL(
    "https://tidybill.app/api/time-entries/bulk"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "entries": [
        {
            "client_id": 1,
            "project_id": 1,
            "service_id": 1,
            "date": "2026-03-29",
            "duration": 3600,
            "is_billable": true,
            "hourly_rate": 15000,
            "notes": "API integration work"
        }
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": []
}
 

Request      

POST api/time-entries/bulk

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

entries   object[]     

Array of time entry objects (max 50).

client_id   integer  optional    

optional The client ID. Example: 1

project_id   integer     

The project ID. Example: 1

service_id   integer     

The service ID. Example: 1

date   string     

The date of the work. Example: 2026-03-29

duration   integer     

Duration in seconds. Example: 3600

is_billable   boolean  optional    

optional Whether billable. Example: true

hourly_rate   integer  optional    

optional Hourly rate in cents. Example: 15000

notes   string  optional    

optional Description of work. Example: API integration work

Bulk delete time entries.

requires authentication

Delete up to 100 time entries. Only deletes entries owned by the authenticated user.

Example request:
curl --request DELETE \
    "https://tidybill.app/api/time-entries/bulk" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"ids\": [
        1,
        2,
        3
    ]
}"
const url = new URL(
    "https://tidybill.app/api/time-entries/bulk"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "ids": [
        1,
        2,
        3
    ]
};

fetch(url, {
    method: "DELETE",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

DELETE api/time-entries/bulk

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

ids   integer[]     

Array of time entry IDs to delete (max 100).

List time entries.

requires authentication

Returns a paginated list of time entries, sorted by date descending by default.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/time-entries" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/time-entries"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 4820,
            "user_id": 12,
            "client_id": 147,
            "project_id": null,
            "service_id": null,
            "date": "2026-03-11",
            "duration": 5783,
            "started_at": null,
            "ended_at": null,
            "is_running": false,
            "is_billable": true,
            "is_billed": false,
            "invoice_id": null,
            "hourly_rate": 15266,
            "notes": "Qui commodi incidunt iure odit.",
            "calculated_amount": 24523,
            "duration_hours": 1.61,
            "created_at": "2026-04-05T19:38:16.000000Z",
            "updated_at": "2026-04-05T19:38:16.000000Z"
        },
        {
            "id": 4821,
            "user_id": 13,
            "client_id": 148,
            "project_id": null,
            "service_id": null,
            "date": "2026-03-12",
            "duration": 21598,
            "started_at": null,
            "ended_at": null,
            "is_running": false,
            "is_billable": true,
            "is_billed": false,
            "invoice_id": null,
            "hourly_rate": 12880,
            "notes": "Repellendus assumenda et tenetur ab reiciendis.",
            "calculated_amount": 77273,
            "duration_hours": 6,
            "created_at": "2026-04-05T19:38:17.000000Z",
            "updated_at": "2026-04-05T19:38:17.000000Z"
        }
    ]
}
 

Request      

GET api/time-entries

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Create a time entry.

requires authentication

Create a manual time entry. Provide date (YYYY-MM-DD) and duration (in seconds, e.g. 3600 = 1 hour). Do NOT send started_at, ended_at, or is_running - those are managed by the Timer endpoints. If hourly_rate is omitted, it is auto-resolved from the project, service, or company default rate.

Example request:
curl --request POST \
    "https://tidybill.app/api/time-entries" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"client_id\": 1,
    \"project_id\": 1,
    \"service_id\": 1,
    \"date\": \"2026-03-29\",
    \"duration\": 3600,
    \"is_billable\": true,
    \"is_billed\": false,
    \"hourly_rate\": 15000,
    \"notes\": \"API integration work\"
}"
const url = new URL(
    "https://tidybill.app/api/time-entries"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "client_id": 1,
    "project_id": 1,
    "service_id": 1,
    "date": "2026-03-29",
    "duration": 3600,
    "is_billable": true,
    "is_billed": false,
    "hourly_rate": 15000,
    "notes": "API integration work"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 4822,
        "user_id": 14,
        "client_id": 149,
        "project_id": null,
        "service_id": null,
        "date": "2026-03-16",
        "duration": 1838,
        "started_at": null,
        "ended_at": null,
        "is_running": false,
        "is_billable": true,
        "is_billed": false,
        "invoice_id": null,
        "hourly_rate": 14575,
        "notes": "Sunt nihil accusantium harum mollitia.",
        "calculated_amount": 7441,
        "duration_hours": 0.51,
        "created_at": "2026-04-05T19:38:17.000000Z",
        "updated_at": "2026-04-05T19:38:17.000000Z"
    }
}
 

Request      

POST api/time-entries

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

client_id   integer  optional    

optional The client ID. Example: 1

project_id   integer     

The project ID. Example: 1

service_id   integer     

The service ID. Example: 1

date   string     

The date of the work. Example: 2026-03-29

duration   integer     

Duration in seconds (e.g. 3600 = 1 hour). Example: 3600

is_billable   boolean  optional    

optional Whether the entry is billable. Defaults to true. Example: true

is_billed   boolean  optional    

optional Whether the entry has been billed. Defaults to false. Example: false

hourly_rate   integer  optional    

optional Hourly rate in cents. Auto-resolved if omitted. Example: 15000

notes   string  optional    

optional Description of work performed. Example: API integration work

Get a time entry.

requires authentication

Example request:
curl --request GET \
    --get "https://tidybill.app/api/time-entries/5" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/time-entries/5"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 4823,
        "user_id": 15,
        "client_id": 150,
        "project_id": null,
        "service_id": null,
        "date": "2026-03-11",
        "duration": 5783,
        "started_at": null,
        "ended_at": null,
        "is_running": false,
        "is_billable": true,
        "is_billed": false,
        "invoice_id": null,
        "hourly_rate": 15266,
        "notes": "Qui commodi incidunt iure odit.",
        "calculated_amount": 24523,
        "duration_hours": 1.61,
        "created_at": "2026-04-05T19:38:17.000000Z",
        "updated_at": "2026-04-05T19:38:17.000000Z"
    }
}
 

Request      

GET api/time-entries/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the time entry. Example: 5

Update a time entry.

requires authentication

Same body schema as create. Only the entry owner or a company admin/owner can update.

Example request:
curl --request PUT \
    "https://tidybill.app/api/time-entries/5" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"project_id\": \"architecto\",
    \"service_id\": \"architecto\",
    \"date\": \"2026-04-05T19:38:17\",
    \"duration\": 39,
    \"is_billable\": false,
    \"is_billed\": false,
    \"hourly_rate\": 84,
    \"notes\": \"z\"
}"
const url = new URL(
    "https://tidybill.app/api/time-entries/5"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "project_id": "architecto",
    "service_id": "architecto",
    "date": "2026-04-05T19:38:17",
    "duration": 39,
    "is_billable": false,
    "is_billed": false,
    "hourly_rate": 84,
    "notes": "z"
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 4824,
        "user_id": 16,
        "client_id": 151,
        "project_id": null,
        "service_id": null,
        "date": "2026-03-16",
        "duration": 1838,
        "started_at": null,
        "ended_at": null,
        "is_running": false,
        "is_billable": true,
        "is_billed": false,
        "invoice_id": null,
        "hourly_rate": 14575,
        "notes": "Sunt nihil accusantium harum mollitia.",
        "calculated_amount": 7441,
        "duration_hours": 0.51,
        "created_at": "2026-04-05T19:38:17.000000Z",
        "updated_at": "2026-04-05T19:38:17.000000Z"
    }
}
 

Request      

PUT api/time-entries/{id}

PATCH api/time-entries/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the time entry. Example: 5

Body Parameters

client_id   string  optional    

The id of an existing record in the clients table.

project_id   string     

The id of an existing record in the projects table. Example: architecto

service_id   string     

The id of an existing record in the services table. Example: architecto

date   string     

Must be a valid date. Example: 2026-04-05T19:38:17

duration   integer     

Must be at least 0. Example: 39

is_billable   boolean  optional    

Example: false

is_billed   boolean  optional    

Example: false

hourly_rate   integer  optional    

Must be at least 0. Example: 84

notes   string  optional    

Must not be greater than 2000 characters. Example: z

Delete a time entry.

requires authentication

Only the entry owner or a company admin/owner can delete.

Example request:
curl --request DELETE \
    "https://tidybill.app/api/time-entries/5" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/time-entries/5"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

DELETE api/time-entries/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the time entry. Example: 5

Start a timer.

requires authentication

Starts a new running timer. If a timer is already running, it is stopped first. The started_at is set to the current time. Duration is calculated when the timer is stopped.

Example request:
curl --request POST \
    "https://tidybill.app/api/timer/start" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"client_id\": 1,
    \"project_id\": 1,
    \"service_id\": 1,
    \"notes\": \"Working on API integration\",
    \"is_billable\": true,
    \"hourly_rate\": 15000
}"
const url = new URL(
    "https://tidybill.app/api/timer/start"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "client_id": 1,
    "project_id": 1,
    "service_id": 1,
    "notes": "Working on API integration",
    "is_billable": true,
    "hourly_rate": 15000
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 4825,
        "user_id": 17,
        "client_id": 152,
        "project_id": null,
        "service_id": null,
        "date": "2026-04-01",
        "duration": 21771,
        "started_at": null,
        "ended_at": null,
        "is_running": false,
        "is_billable": true,
        "is_billed": false,
        "invoice_id": null,
        "hourly_rate": 15990,
        "notes": "Et fugiat sunt nihil accusantium.",
        "calculated_amount": 96700,
        "duration_hours": 6.05,
        "created_at": "2026-04-05T19:38:18.000000Z",
        "updated_at": "2026-04-05T19:38:18.000000Z"
    }
}
 

Request      

POST api/timer/start

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

client_id   integer  optional    

optional The client ID. Example: 1

project_id   integer  optional    

optional The project ID. Example: 1

service_id   integer  optional    

optional The service ID. Example: 1

notes   string  optional    

optional Description of work. Example: Working on API integration

is_billable   boolean  optional    

optional Whether billable. Defaults to true. Example: true

hourly_rate   integer  optional    

optional Hourly rate in cents. Auto-resolved if omitted. Example: 15000

Stop the running timer.

requires authentication

Stops the current user's running timer, calculates the duration from started_at to now, and sets ended_at. Returns 404 if no timer is running.

Example request:
curl --request PUT \
    "https://tidybill.app/api/timer/stop" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/timer/stop"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 4826,
        "user_id": 18,
        "client_id": 153,
        "project_id": null,
        "service_id": null,
        "date": "2026-03-11",
        "duration": 5783,
        "started_at": null,
        "ended_at": null,
        "is_running": false,
        "is_billable": true,
        "is_billed": false,
        "invoice_id": null,
        "hourly_rate": 15266,
        "notes": "Qui commodi incidunt iure odit.",
        "calculated_amount": 24523,
        "duration_hours": 1.61,
        "created_at": "2026-04-05T19:38:18.000000Z",
        "updated_at": "2026-04-05T19:38:18.000000Z"
    }
}
 

Request      

PUT api/timer/stop

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Get current running timer.

requires authentication

Returns the current user's running timer, or {"data": null} if no timer is running.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/timer/current" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/timer/current"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 4827,
        "user_id": 19,
        "client_id": 154,
        "project_id": null,
        "service_id": null,
        "date": "2026-03-11",
        "duration": 5783,
        "started_at": null,
        "ended_at": null,
        "is_running": false,
        "is_billable": true,
        "is_billed": false,
        "invoice_id": null,
        "hourly_rate": 15266,
        "notes": "Qui commodi incidunt iure odit.",
        "calculated_amount": 24523,
        "duration_hours": 1.61,
        "created_at": "2026-04-05T19:38:19.000000Z",
        "updated_at": "2026-04-05T19:38:19.000000Z"
    }
}
 

Request      

GET api/timer/current

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Discard the running timer.

requires authentication

Deletes the running timer without saving. Returns 204 whether or not a timer was running.

Example request:
curl --request DELETE \
    "https://tidybill.app/api/timer/discard" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/timer/discard"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

DELETE api/timer/discard

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Services

Bulk action on services.

requires authentication

Supported actions: activate, deactivate, delete. Returns the count of affected records.

Example request:
curl --request POST \
    "https://tidybill.app/api/services/bulk-action" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"ids\": [
        16
    ],
    \"action\": \"deactivate\"
}"
const url = new URL(
    "https://tidybill.app/api/services/bulk-action"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "ids": [
        16
    ],
    "action": "deactivate"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "affected": 3
    }
}
 

Request      

POST api/services/bulk-action

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

ids   integer[]  optional    
action   string     

Example: deactivate

Must be one of:
  • activate
  • deactivate
  • delete

List services.

requires authentication

Supports filtering by is_active, type, and name search. Results are paginated with active/inactive counts in the meta.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/services" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/services"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 71,
            "type": "service",
            "name": "Development",
            "description": "Quidem nostrum qui commodi incidunt iure odit.",
            "default_rate": 14003,
            "unit": "hour",
            "tax_name_1": null,
            "tax_rate_1": null,
            "tax_name_2": null,
            "tax_rate_2": null,
            "is_active": true,
            "created_at": "2026-04-05T19:38:15.000000Z",
            "updated_at": "2026-04-05T19:38:15.000000Z"
        },
        {
            "id": 72,
            "type": "service",
            "name": "Consulting",
            "description": "Facere tempora ex voluptatem laboriosam.",
            "default_rate": 14572,
            "unit": "item",
            "tax_name_1": null,
            "tax_rate_1": null,
            "tax_name_2": null,
            "tax_rate_2": null,
            "is_active": true,
            "created_at": "2026-04-05T19:38:15.000000Z",
            "updated_at": "2026-04-05T19:38:15.000000Z"
        }
    ]
}
 

Request      

GET api/services

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Create a service.

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/services" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"b\",
    \"description\": \"Et animi quos velit et fugiat.\",
    \"default_rate\": 42,
    \"type\": \"product\",
    \"unit\": \"ljnikhwaykcmyuwp\",
    \"tax_name_1\": \"w\",
    \"tax_rate_1\": 89,
    \"tax_name_2\": \"v\",
    \"tax_rate_2\": 3,
    \"is_active\": false
}"
const url = new URL(
    "https://tidybill.app/api/services"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "b",
    "description": "Et animi quos velit et fugiat.",
    "default_rate": 42,
    "type": "product",
    "unit": "ljnikhwaykcmyuwp",
    "tax_name_1": "w",
    "tax_rate_1": 89,
    "tax_name_2": "v",
    "tax_rate_2": 3,
    "is_active": false
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 73,
        "type": "service",
        "name": "Consulting",
        "description": "Et et modi ipsum nostrum.",
        "default_rate": 13789,
        "unit": "day",
        "tax_name_1": null,
        "tax_rate_1": null,
        "tax_name_2": null,
        "tax_rate_2": null,
        "is_active": true,
        "created_at": "2026-04-05T19:38:15.000000Z",
        "updated_at": "2026-04-05T19:38:15.000000Z"
    }
}
 

Request      

POST api/services

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

name   string     

Must not be greater than 255 characters. Example: b

description   string  optional    

Must not be greater than 2000 characters. Example: Et animi quos velit et fugiat.

default_rate   integer  optional    

Must be at least 0. Example: 42

type   string  optional    

Example: product

Must be one of:
  • service
  • product
unit   string  optional    

Must not be greater than 20 characters. Example: ljnikhwaykcmyuwp

tax_name_1   string  optional    

Must not be greater than 50 characters. Example: w

tax_rate_1   integer  optional    

Must be at least 0. Example: 89

tax_name_2   string  optional    

Must not be greater than 50 characters. Example: v

tax_rate_2   integer  optional    

Must be at least 0. Example: 3

is_active   boolean  optional    

Example: false

Get a service.

requires authentication

Example request:
curl --request GET \
    --get "https://tidybill.app/api/services/1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/services/1"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 74,
        "type": "service",
        "name": "Development",
        "description": "Quidem nostrum qui commodi incidunt iure odit.",
        "default_rate": 14003,
        "unit": "hour",
        "tax_name_1": null,
        "tax_rate_1": null,
        "tax_name_2": null,
        "tax_rate_2": null,
        "is_active": true,
        "created_at": "2026-04-05T19:38:15.000000Z",
        "updated_at": "2026-04-05T19:38:15.000000Z"
    }
}
 

Request      

GET api/services/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the service. Example: 1

Update a service.

requires authentication

Example request:
curl --request PUT \
    "https://tidybill.app/api/services/1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"b\",
    \"description\": \"Et animi quos velit et fugiat.\",
    \"default_rate\": 42,
    \"type\": \"product\",
    \"unit\": \"ljnikhwaykcmyuwp\",
    \"tax_name_1\": \"w\",
    \"tax_rate_1\": 89,
    \"tax_name_2\": \"v\",
    \"tax_rate_2\": 3,
    \"is_active\": false
}"
const url = new URL(
    "https://tidybill.app/api/services/1"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "b",
    "description": "Et animi quos velit et fugiat.",
    "default_rate": 42,
    "type": "product",
    "unit": "ljnikhwaykcmyuwp",
    "tax_name_1": "w",
    "tax_rate_1": 89,
    "tax_name_2": "v",
    "tax_rate_2": 3,
    "is_active": false
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 75,
        "type": "service",
        "name": "Consulting",
        "description": "Et et modi ipsum nostrum.",
        "default_rate": 13789,
        "unit": "day",
        "tax_name_1": null,
        "tax_rate_1": null,
        "tax_name_2": null,
        "tax_rate_2": null,
        "is_active": true,
        "created_at": "2026-04-05T19:38:15.000000Z",
        "updated_at": "2026-04-05T19:38:15.000000Z"
    }
}
 

Request      

PUT api/services/{id}

PATCH api/services/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the service. Example: 1

Body Parameters

name   string     

Must not be greater than 255 characters. Example: b

description   string  optional    

Must not be greater than 2000 characters. Example: Et animi quos velit et fugiat.

default_rate   integer  optional    

Must be at least 0. Example: 42

type   string  optional    

Example: product

Must be one of:
  • service
  • product
unit   string  optional    

Must not be greater than 20 characters. Example: ljnikhwaykcmyuwp

tax_name_1   string  optional    

Must not be greater than 50 characters. Example: w

tax_rate_1   integer  optional    

Must be at least 0. Example: 89

tax_name_2   string  optional    

Must not be greater than 50 characters. Example: v

tax_rate_2   integer  optional    

Must be at least 0. Example: 3

is_active   boolean  optional    

Example: false

Delete a service.

requires authentication

Example request:
curl --request DELETE \
    "https://tidybill.app/api/services/1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/services/1"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

DELETE api/services/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the service. Example: 1

Expenses

Bulk action on expenses.

requires authentication

Supported actions: archive, unarchive, delete. Returns the count of affected records.

Example request:
curl --request POST \
    "https://tidybill.app/api/expenses/bulk-action" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"ids\": [
        16
    ],
    \"action\": \"unarchive\"
}"
const url = new URL(
    "https://tidybill.app/api/expenses/bulk-action"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "ids": [
        16
    ],
    "action": "unarchive"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "affected": 3
    }
}
 

Request      

POST api/expenses/bulk-action

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

ids   integer[]  optional    
action   string     

Example: unarchive

Must be one of:
  • archive
  • unarchive
  • delete

List expenses.

requires authentication

Supports filtering by category, client_id, project_id, is_billable, is_billed, and date range. Meta includes aggregate totals for active expenses.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/expenses" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/expenses"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 1,
            "user_id": 20,
            "client_id": null,
            "project_id": null,
            "invoice_id": null,
            "category": "other",
            "vendor": null,
            "description": null,
            "amount": 5000,
            "currency": "USD",
            "tax_amount": 0,
            "tax_name": null,
            "tax_rate": 0,
            "expense_date": "2026-04-05",
            "status": "pending",
            "is_archived": false,
            "is_billable": false,
            "is_billed": false,
            "receipt_path": null,
            "reference": null,
            "created_at": "2026-04-05T19:38:20.000000Z",
            "updated_at": "2026-04-05T19:38:20.000000Z"
        },
        {
            "id": 2,
            "user_id": 21,
            "client_id": null,
            "project_id": null,
            "invoice_id": null,
            "category": "other",
            "vendor": null,
            "description": null,
            "amount": 5000,
            "currency": "USD",
            "tax_amount": 0,
            "tax_name": null,
            "tax_rate": 0,
            "expense_date": "2026-04-05",
            "status": "pending",
            "is_archived": false,
            "is_billable": false,
            "is_billed": false,
            "receipt_path": null,
            "reference": null,
            "created_at": "2026-04-05T19:38:21.000000Z",
            "updated_at": "2026-04-05T19:38:21.000000Z"
        }
    ]
}
 

Request      

GET api/expenses

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Create an expense.

requires authentication

Creates an expense attributed to the authenticated user. Currency defaults to the company currency if not provided.

Example request:
curl --request POST \
    "https://tidybill.app/api/expenses" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"category\": \"meals\",
    \"vendor\": \"b\",
    \"description\": \"Et animi quos velit et fugiat.\",
    \"amount\": 26,
    \"currency\": \"l\",
    \"tax_amount\": 9,
    \"tax_name\": \"n\",
    \"tax_rate\": 5,
    \"expense_date\": \"2026-04-05T19:38:21\",
    \"is_billable\": false,
    \"reference\": \"k\"
}"
const url = new URL(
    "https://tidybill.app/api/expenses"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "category": "meals",
    "vendor": "b",
    "description": "Et animi quos velit et fugiat.",
    "amount": 26,
    "currency": "l",
    "tax_amount": 9,
    "tax_name": "n",
    "tax_rate": 5,
    "expense_date": "2026-04-05T19:38:21",
    "is_billable": false,
    "reference": "k"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 3,
        "user_id": 22,
        "client_id": null,
        "project_id": null,
        "invoice_id": null,
        "category": "other",
        "vendor": null,
        "description": null,
        "amount": 5000,
        "currency": "USD",
        "tax_amount": 0,
        "tax_name": null,
        "tax_rate": 0,
        "expense_date": "2026-04-05",
        "status": "pending",
        "is_archived": false,
        "is_billable": false,
        "is_billed": false,
        "receipt_path": null,
        "reference": null,
        "created_at": "2026-04-05T19:38:21.000000Z",
        "updated_at": "2026-04-05T19:38:21.000000Z"
    }
}
 

Request      

POST api/expenses

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

category   string     

Example: meals

Must be one of:
  • advertising
  • bank_fees
  • contractor_fees
  • education
  • equipment
  • insurance
  • meals
  • office_supplies
  • rent
  • software
  • travel_transport
  • utilities
  • other
vendor   string  optional    

Must not be greater than 255 characters. Example: b

description   string  optional    

Must not be greater than 2000 characters. Example: Et animi quos velit et fugiat.

amount   integer     

Must be at least 1. Example: 26

currency   string  optional    

Must not be greater than 3 characters. Example: l

tax_amount   integer  optional    

Must be at least 0. Example: 9

tax_name   string  optional    

Must not be greater than 50 characters. Example: n

tax_rate   integer  optional    

Must be at least 0. Must not be greater than 10000. Example: 5

expense_date   string     

Must be a valid date. Example: 2026-04-05T19:38:21

is_billable   boolean  optional    

Example: false

client_id   string  optional    

The id of an existing record in the clients table.

project_id   string  optional    

The id of an existing record in the projects table.

reference   string  optional    

Must not be greater than 255 characters. Example: k

Get an expense.

requires authentication

Example request:
curl --request GET \
    --get "https://tidybill.app/api/expenses/16" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/expenses/16"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 4,
        "user_id": 23,
        "client_id": null,
        "project_id": null,
        "invoice_id": null,
        "category": "other",
        "vendor": null,
        "description": null,
        "amount": 5000,
        "currency": "USD",
        "tax_amount": 0,
        "tax_name": null,
        "tax_rate": 0,
        "expense_date": "2026-04-05",
        "status": "pending",
        "is_archived": false,
        "is_billable": false,
        "is_billed": false,
        "receipt_path": null,
        "reference": null,
        "created_at": "2026-04-05T19:38:21.000000Z",
        "updated_at": "2026-04-05T19:38:21.000000Z"
    }
}
 

Request      

GET api/expenses/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the expense. Example: 16

Update an expense.

requires authentication

Example request:
curl --request PUT \
    "https://tidybill.app/api/expenses/16" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"category\": \"advertising\",
    \"vendor\": \"b\",
    \"description\": \"Et animi quos velit et fugiat.\",
    \"amount\": 26,
    \"currency\": \"l\",
    \"tax_amount\": 9,
    \"tax_name\": \"n\",
    \"tax_rate\": 5,
    \"expense_date\": \"2026-04-05T19:38:21\",
    \"is_billable\": true,
    \"reference\": \"k\"
}"
const url = new URL(
    "https://tidybill.app/api/expenses/16"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "category": "advertising",
    "vendor": "b",
    "description": "Et animi quos velit et fugiat.",
    "amount": 26,
    "currency": "l",
    "tax_amount": 9,
    "tax_name": "n",
    "tax_rate": 5,
    "expense_date": "2026-04-05T19:38:21",
    "is_billable": true,
    "reference": "k"
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 5,
        "user_id": 24,
        "client_id": null,
        "project_id": null,
        "invoice_id": null,
        "category": "other",
        "vendor": null,
        "description": null,
        "amount": 5000,
        "currency": "USD",
        "tax_amount": 0,
        "tax_name": null,
        "tax_rate": 0,
        "expense_date": "2026-04-05",
        "status": "pending",
        "is_archived": false,
        "is_billable": false,
        "is_billed": false,
        "receipt_path": null,
        "reference": null,
        "created_at": "2026-04-05T19:38:21.000000Z",
        "updated_at": "2026-04-05T19:38:21.000000Z"
    }
}
 

Request      

PUT api/expenses/{id}

PATCH api/expenses/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the expense. Example: 16

Body Parameters

category   string  optional    

Example: advertising

Must be one of:
  • advertising
  • bank_fees
  • contractor_fees
  • education
  • equipment
  • insurance
  • meals
  • office_supplies
  • rent
  • software
  • travel_transport
  • utilities
  • other
vendor   string  optional    

Must not be greater than 255 characters. Example: b

description   string  optional    

Must not be greater than 2000 characters. Example: Et animi quos velit et fugiat.

amount   integer     

Must be at least 1. Example: 26

currency   string  optional    

Must not be greater than 3 characters. Example: l

tax_amount   integer  optional    

Must be at least 0. Example: 9

tax_name   string  optional    

Must not be greater than 50 characters. Example: n

tax_rate   integer  optional    

Must be at least 0. Must not be greater than 10000. Example: 5

expense_date   string     

Must be a valid date. Example: 2026-04-05T19:38:21

is_billable   boolean  optional    

Example: true

client_id   string  optional    

The id of an existing record in the clients table.

project_id   string  optional    

The id of an existing record in the projects table.

reference   string  optional    

Must not be greater than 255 characters. Example: k

Delete an expense.

requires authentication

Example request:
curl --request DELETE \
    "https://tidybill.app/api/expenses/16" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/expenses/16"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

DELETE api/expenses/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the expense. Example: 16

Archive an expense.

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/expenses/16/archive" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/expenses/16/archive"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

POST api/expenses/{expense_id}/archive

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

expense_id   integer     

The ID of the expense. Example: 16

Unarchive an expense.

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/expenses/16/unarchive" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/expenses/16/unarchive"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

POST api/expenses/{expense_id}/unarchive

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

expense_id   integer     

The ID of the expense. Example: 16

Upload a receipt for an expense.

requires authentication

Accepts JPEG, PNG, GIF, WebP, or PDF files up to 5 MB.

Example request:
curl --request POST \
    "https://tidybill.app/api/expenses/16/receipt" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"receipt\": \"b\"
}"
const url = new URL(
    "https://tidybill.app/api/expenses/16/receipt"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "receipt": "b"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 6,
        "user_id": 25,
        "client_id": null,
        "project_id": null,
        "invoice_id": null,
        "category": "other",
        "vendor": null,
        "description": null,
        "amount": 5000,
        "currency": "USD",
        "tax_amount": 0,
        "tax_name": null,
        "tax_rate": 0,
        "expense_date": "2026-04-05",
        "status": "pending",
        "is_archived": false,
        "is_billable": false,
        "is_billed": false,
        "receipt_path": null,
        "reference": null,
        "created_at": "2026-04-05T19:38:22.000000Z",
        "updated_at": "2026-04-05T19:38:22.000000Z"
    }
}
 

Request      

POST api/expenses/{expense_id}/receipt

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

expense_id   integer     

The ID of the expense. Example: 16

Body Parameters

receipt   string     

Must not be greater than 5120 characters. Example: b

Download an expense receipt.

requires authentication

Streams the uploaded receipt file as a download. Returns 404 if no receipt is attached.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/expenses/16/receipt" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/expenses/16/receipt"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


file
 

Request      

GET api/expenses/{expense_id}/receipt

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

expense_id   integer     

The ID of the expense. Example: 16

Credits

Bulk action on credits.

requires authentication

Supported actions: archive, unarchive, delete. Returns the count of affected records.

Example request:
curl --request POST \
    "https://tidybill.app/api/credits/bulk-action" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"ids\": [
        16
    ],
    \"action\": \"unarchive\"
}"
const url = new URL(
    "https://tidybill.app/api/credits/bulk-action"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "ids": [
        16
    ],
    "action": "unarchive"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "affected": 3
    }
}
 

Request      

POST api/credits/bulk-action

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

ids   integer[]  optional    
action   string     

Example: unarchive

Must be one of:
  • archive
  • unarchive
  • delete

List credits.

requires authentication

Supports filtering by client_id, type, and has_balance. Results are paginated with active/archived counts in the meta.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/credits" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/credits"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 1,
            "client_id": 186,
            "invoice_id": null,
            "type": "credit_note",
            "is_archived": false,
            "amount": 26768,
            "balance": 26768,
            "currency": "USD",
            "description": "Quidem nostrum qui commodi incidunt iure odit.",
            "date": "2026-04-05T00:00:00.000000Z",
            "created_at": "2026-04-05T19:38:22.000000Z",
            "updated_at": "2026-04-05T19:38:22.000000Z"
        },
        {
            "id": 2,
            "client_id": 187,
            "invoice_id": null,
            "type": "credit_note",
            "is_archived": false,
            "amount": 13913,
            "balance": 13913,
            "currency": "USD",
            "description": "Ratione nemo voluptate accusamus ut et recusandae modi rerum.",
            "date": "2026-04-05T00:00:00.000000Z",
            "created_at": "2026-04-05T19:38:22.000000Z",
            "updated_at": "2026-04-05T19:38:22.000000Z"
        }
    ]
}
 

Request      

GET api/credits

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Create a credit.

requires authentication

Creates a credit note for a client. The initial balance is automatically set equal to amount. Currency defaults to the company currency if not provided.

Example request:
curl --request POST \
    "https://tidybill.app/api/credits" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"client_id\": \"architecto\",
    \"type\": \"overpayment\",
    \"amount\": 22,
    \"currency\": \"gzm\",
    \"description\": \"Et fugiat sunt nihil accusantium.\",
    \"date\": \"2026-04-05T19:38:22\"
}"
const url = new URL(
    "https://tidybill.app/api/credits"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "client_id": "architecto",
    "type": "overpayment",
    "amount": 22,
    "currency": "gzm",
    "description": "Et fugiat sunt nihil accusantium.",
    "date": "2026-04-05T19:38:22"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 3,
        "client_id": 188,
        "invoice_id": null,
        "type": "credit_note",
        "is_archived": false,
        "amount": 21638,
        "balance": 21638,
        "currency": "USD",
        "description": "Modi deserunt aut ab provident perspiciatis.",
        "date": "2026-04-05T00:00:00.000000Z",
        "created_at": "2026-04-05T19:38:22.000000Z",
        "updated_at": "2026-04-05T19:38:22.000000Z"
    }
}
 

Request      

POST api/credits

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

client_id   string     

The id of an existing record in the clients table. Example: architecto

invoice_id   string  optional    

The id of an existing record in the invoices table.

type   string     

Example: overpayment

Must be one of:
  • credit_note
  • overpayment
  • prepayment
amount   integer     

Must be at least 1. Example: 22

currency   string  optional    

Must be 3 characters. Example: gzm

description   string  optional    

Must not be greater than 2000 characters. Example: Et fugiat sunt nihil accusantium.

date   string     

Must be a valid date. Example: 2026-04-05T19:38:22

Get a credit.

requires authentication

Example request:
curl --request GET \
    --get "https://tidybill.app/api/credits/16" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/credits/16"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 4,
        "client_id": 189,
        "invoice_id": null,
        "type": "credit_note",
        "is_archived": false,
        "amount": 26768,
        "balance": 26768,
        "currency": "USD",
        "description": "Quidem nostrum qui commodi incidunt iure odit.",
        "date": "2026-04-05T00:00:00.000000Z",
        "created_at": "2026-04-05T19:38:22.000000Z",
        "updated_at": "2026-04-05T19:38:22.000000Z"
    }
}
 

Request      

GET api/credits/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the credit. Example: 16

Archive a credit.

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/credits/16/archive" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/credits/16/archive"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

POST api/credits/{credit_id}/archive

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

credit_id   integer     

The ID of the credit. Example: 16

Unarchive a credit.

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/credits/16/unarchive" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/credits/16/unarchive"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

POST api/credits/{credit_id}/unarchive

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

credit_id   integer     

The ID of the credit. Example: 16

Get client credit balance.

requires authentication

Returns the total available credit balance and list of credits with a remaining balance for the given client.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/clients/4/credits" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/clients/4/credits"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "client_id": 1,
        "available_balance": 5000,
        "credits": []
    }
}
 

Request      

GET api/clients/{client_id}/credits

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

client_id   integer     

The ID of the client. Example: 4

Apply credit to an invoice.

requires authentication

Applies part or all of a credit's balance to an outstanding invoice. Both must belong to the same client. The invoice must be in a payable state (sent, viewed, partial, or overdue). Automatically transitions the invoice to paid or partial based on the resulting amount_due.

Example request:
curl --request POST \
    "https://tidybill.app/api/credits/16/apply" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"invoice_id\": \"architecto\",
    \"amount\": 22
}"
const url = new URL(
    "https://tidybill.app/api/credits/16/apply"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "invoice_id": "architecto",
    "amount": 22
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 5,
        "client_id": 190,
        "invoice_id": null,
        "type": "credit_note",
        "is_archived": false,
        "amount": 11278,
        "balance": 11278,
        "currency": "USD",
        "description": "Quos velit et fugiat sunt nihil accusantium harum.",
        "date": "2026-04-05T00:00:00.000000Z",
        "created_at": "2026-04-05T19:38:22.000000Z",
        "updated_at": "2026-04-05T19:38:22.000000Z"
    }
}
 

Request      

POST api/credits/{credit_id}/apply

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

credit_id   integer     

The ID of the credit. Example: 16

Body Parameters

invoice_id   string     

Example: architecto

amount   integer     

Must be at least 1. Example: 22

Recurring Invoices

Bulk action on recurring invoices.

requires authentication

Supported actions: activate, pause, archive, delete. Returns the count of affected records.

Example request:
curl --request POST \
    "https://tidybill.app/api/recurring-invoices/bulk-action" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"ids\": [
        16
    ],
    \"action\": \"architecto\"
}"
const url = new URL(
    "https://tidybill.app/api/recurring-invoices/bulk-action"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "ids": [
        16
    ],
    "action": "architecto"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "affected": 3
    }
}
 

Request      

POST api/recurring-invoices/bulk-action

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

ids   integer[]  optional    
action   string     

Example: architecto

List recurring invoices.

requires authentication

Supports filtering by status, client_id, and frequency. Results are paginated and include per-status counts in the meta.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/recurring-invoices?filter%5Bstatus%5D=active&filter%5Bclient_id%5D=1&filter%5Bfrequency%5D=monthly&sort=next_generate_date" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/recurring-invoices"
);

const params = {
    "filter[status]": "active",
    "filter[client_id]": "1",
    "filter[frequency]": "monthly",
    "sort": "next_generate_date",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 38,
            "client_id": 191,
            "title": "Adipisci quidem nostrum qui.",
            "status": "active",
            "is_archived": false,
            "frequency": "monthly",
            "frequency_label": "Monthly",
            "interval": 1,
            "start_date": "2026-04-05",
            "end_date": null,
            "next_generate_date": "2026-04-05",
            "last_generated_at": null,
            "auto_send": false,
            "currency": "USD",
            "subtotal": 0,
            "tax_total": 0,
            "discount_type": null,
            "discount_value": null,
            "discount_total": 0,
            "total": 0,
            "notes": null,
            "payment_terms": 30,
            "occurrences_generated": 0,
            "max_occurrences": null,
            "created_at": "2026-04-05T19:38:22.000000Z",
            "updated_at": "2026-04-05T19:38:22.000000Z"
        },
        {
            "id": 39,
            "client_id": 192,
            "title": "Qui repudiandae laboriosam.",
            "status": "active",
            "is_archived": false,
            "frequency": "monthly",
            "frequency_label": "Monthly",
            "interval": 1,
            "start_date": "2026-04-05",
            "end_date": null,
            "next_generate_date": "2026-04-05",
            "last_generated_at": null,
            "auto_send": false,
            "currency": "USD",
            "subtotal": 0,
            "tax_total": 0,
            "discount_type": null,
            "discount_value": null,
            "discount_total": 0,
            "total": 0,
            "notes": null,
            "payment_terms": 30,
            "occurrences_generated": 0,
            "max_occurrences": null,
            "created_at": "2026-04-05T19:38:22.000000Z",
            "updated_at": "2026-04-05T19:38:22.000000Z"
        }
    ]
}
 

Request      

GET api/recurring-invoices

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

filter[status]   string  optional    

Filter by status. Example: active

filter[client_id]   integer  optional    

Filter by client ID. Example: 1

filter[frequency]   string  optional    

Filter by frequency. Example: monthly

sort   string  optional    

Sort field (prefix with - for descending). Example: next_generate_date

Create a recurring invoice.

requires authentication

Creates an active recurring invoice template. The next_generate_date is set to start_date and invoices are generated automatically on schedule. Pass line_items to attach them in the same request.

Example request:
curl --request POST \
    "https://tidybill.app/api/recurring-invoices" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"client_id\": \"architecto\",
    \"title\": \"n\",
    \"frequency\": \"quarterly\",
    \"interval\": 67,
    \"start_date\": \"2026-04-05T19:38:22\",
    \"end_date\": \"2052-04-28\",
    \"auto_send\": false,
    \"currency\": \"ngz\",
    \"discount_type\": \"percentage\",
    \"discount_value\": 16,
    \"notes\": \"n\",
    \"payment_terms\": 84,
    \"next_generate_date\": \"2026-04-05T19:38:22\",
    \"max_occurrences\": 66,
    \"line_items\": [
        {
            \"description\": \"Velit et fugiat sunt nihil accusantium.\",
            \"quantity\": 52,
            \"unit_price\": 8,
            \"tax_name\": \"k\",
            \"tax_rate\": 14,
            \"sort_order\": 16
        }
    ]
}"
const url = new URL(
    "https://tidybill.app/api/recurring-invoices"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "client_id": "architecto",
    "title": "n",
    "frequency": "quarterly",
    "interval": 67,
    "start_date": "2026-04-05T19:38:22",
    "end_date": "2052-04-28",
    "auto_send": false,
    "currency": "ngz",
    "discount_type": "percentage",
    "discount_value": 16,
    "notes": "n",
    "payment_terms": 84,
    "next_generate_date": "2026-04-05T19:38:22",
    "max_occurrences": 66,
    "line_items": [
        {
            "description": "Velit et fugiat sunt nihil accusantium.",
            "quantity": 52,
            "unit_price": 8,
            "tax_name": "k",
            "tax_rate": 14,
            "sort_order": 16
        }
    ]
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 40,
        "client_id": 193,
        "title": "Et animi quos.",
        "status": "active",
        "is_archived": false,
        "frequency": "monthly",
        "frequency_label": "Monthly",
        "interval": 1,
        "start_date": "2026-04-05",
        "end_date": null,
        "next_generate_date": "2026-04-05",
        "last_generated_at": null,
        "auto_send": false,
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "notes": null,
        "payment_terms": 30,
        "occurrences_generated": 0,
        "max_occurrences": null,
        "created_at": "2026-04-05T19:38:22.000000Z",
        "updated_at": "2026-04-05T19:38:22.000000Z"
    },
    "status": "201"
}
 

Request      

POST api/recurring-invoices

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

client_id   string     

The id of an existing record in the clients table. Example: architecto

title   string     

Must not be greater than 255 characters. Example: n

frequency   string     

Example: quarterly

Must be one of:
  • weekly
  • monthly
  • quarterly
  • yearly
  • custom
interval   integer  optional    

Must be at least 1. Example: 67

start_date   string     

Must be a valid date. Example: 2026-04-05T19:38:22

end_date   string  optional    

Must be a valid date. Must be a date after start_date. Example: 2052-04-28

auto_send   boolean  optional    

Example: false

currency   string  optional    

Must be 3 characters. Example: ngz

discount_type   string  optional    

Example: percentage

Must be one of:
  • percentage
  • fixed
discount_value   integer  optional    

Example: 16

notes   string  optional    

Must not be greater than 2000 characters. Example: n

payment_terms   integer  optional    

Must be at least 0. Example: 84

next_generate_date   string  optional    

Must be a valid date. Example: 2026-04-05T19:38:22

max_occurrences   integer  optional    

Must be at least 1. Example: 66

line_items   object[]  optional    
description   string     

Must not be greater than 1000 characters. Example: Velit et fugiat sunt nihil accusantium.

quantity   number     

Must be at least 0.0001. Example: 52

unit_price   number     

Must be at least 0. Example: 8

service_id   string  optional    

The id of an existing record in the services table.

tax_name   string  optional    

Must not be greater than 50 characters. Example: k

tax_rate   number  optional    

Must be at least 0. Must not be greater than 100. Example: 14

sort_order   integer  optional    

Example: 16

Get a recurring invoice.

requires authentication

Includes line items, client, and the list of invoices generated from this template.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/recurring-invoices/6" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/recurring-invoices/6"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 41,
        "client_id": 194,
        "title": "Adipisci quidem nostrum qui.",
        "status": "active",
        "is_archived": false,
        "frequency": "monthly",
        "frequency_label": "Monthly",
        "interval": 1,
        "start_date": "2026-04-05",
        "end_date": null,
        "next_generate_date": "2026-04-05",
        "last_generated_at": null,
        "auto_send": false,
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "notes": null,
        "payment_terms": 30,
        "occurrences_generated": 0,
        "max_occurrences": null,
        "created_at": "2026-04-05T19:38:22.000000Z",
        "updated_at": "2026-04-05T19:38:22.000000Z"
    }
}
 

Request      

GET api/recurring-invoices/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the recurring invoice. Example: 6

recurringInvoice   integer     

The recurring invoice ID. Example: 1

Update a recurring invoice.

requires authentication

If line_items is provided, the full set of line items is synced. Omit line_items to update template fields only without touching line items.

Example request:
curl --request PUT \
    "https://tidybill.app/api/recurring-invoices/6" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"title\": \"b\",
    \"frequency\": \"quarterly\",
    \"interval\": 22,
    \"start_date\": \"2026-04-05T19:38:22\",
    \"end_date\": \"2052-04-28\",
    \"auto_send\": false,
    \"currency\": \"ngz\",
    \"discount_type\": \"percentage\",
    \"discount_value\": 16,
    \"notes\": \"n\",
    \"payment_terms\": 84,
    \"next_generate_date\": \"2026-04-05T19:38:22\",
    \"max_occurrences\": 66,
    \"line_items\": [
        {
            \"description\": \"Velit et fugiat sunt nihil accusantium.\",
            \"quantity\": 52,
            \"unit_price\": 8,
            \"tax_name\": \"k\",
            \"tax_rate\": 14,
            \"sort_order\": 16
        }
    ]
}"
const url = new URL(
    "https://tidybill.app/api/recurring-invoices/6"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "title": "b",
    "frequency": "quarterly",
    "interval": 22,
    "start_date": "2026-04-05T19:38:22",
    "end_date": "2052-04-28",
    "auto_send": false,
    "currency": "ngz",
    "discount_type": "percentage",
    "discount_value": 16,
    "notes": "n",
    "payment_terms": 84,
    "next_generate_date": "2026-04-05T19:38:22",
    "max_occurrences": 66,
    "line_items": [
        {
            "description": "Velit et fugiat sunt nihil accusantium.",
            "quantity": 52,
            "unit_price": 8,
            "tax_name": "k",
            "tax_rate": 14,
            "sort_order": 16
        }
    ]
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 42,
        "client_id": 195,
        "title": "Et animi quos.",
        "status": "active",
        "is_archived": false,
        "frequency": "monthly",
        "frequency_label": "Monthly",
        "interval": 1,
        "start_date": "2026-04-05",
        "end_date": null,
        "next_generate_date": "2026-04-05",
        "last_generated_at": null,
        "auto_send": false,
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "notes": null,
        "payment_terms": 30,
        "occurrences_generated": 0,
        "max_occurrences": null,
        "created_at": "2026-04-05T19:38:22.000000Z",
        "updated_at": "2026-04-05T19:38:22.000000Z"
    }
}
 

Request      

PUT api/recurring-invoices/{id}

PATCH api/recurring-invoices/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the recurring invoice. Example: 6

recurringInvoice   integer     

The recurring invoice ID. Example: 1

Body Parameters

client_id   string  optional    

The id of an existing record in the clients table.

title   string  optional    

Must not be greater than 255 characters. Example: b

frequency   string  optional    

Example: quarterly

Must be one of:
  • weekly
  • monthly
  • quarterly
  • yearly
  • custom
interval   integer  optional    

Must be at least 1. Example: 22

start_date   string  optional    

Must be a valid date. Example: 2026-04-05T19:38:22

end_date   string  optional    

Must be a valid date. Must be a date after start_date. Example: 2052-04-28

auto_send   boolean  optional    

Example: false

currency   string  optional    

Must be 3 characters. Example: ngz

discount_type   string  optional    

Example: percentage

Must be one of:
  • percentage
  • fixed
discount_value   integer  optional    

Example: 16

notes   string  optional    

Must not be greater than 2000 characters. Example: n

payment_terms   integer  optional    

Must be at least 0. Example: 84

next_generate_date   string  optional    

Must be a valid date. Example: 2026-04-05T19:38:22

max_occurrences   integer  optional    

Must be at least 1. Example: 66

line_items   object[]  optional    
description   string     

Must not be greater than 1000 characters. Example: Velit et fugiat sunt nihil accusantium.

quantity   number     

Must be at least 0.0001. Example: 52

unit_price   number     

Must be at least 0. Example: 8

service_id   string  optional    

The id of an existing record in the services table.

tax_name   string  optional    

Must not be greater than 50 characters. Example: k

tax_rate   number  optional    

Must be at least 0. Must not be greater than 100. Example: 14

sort_order   integer  optional    

Example: 16

Delete a recurring invoice.

requires authentication

Example request:
curl --request DELETE \
    "https://tidybill.app/api/recurring-invoices/6" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/recurring-invoices/6"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

DELETE api/recurring-invoices/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the recurring invoice. Example: 6

recurringInvoice   integer     

The recurring invoice ID. Example: 1

List line items for a recurring invoice.

requires authentication

Example request:
curl --request GET \
    --get "https://tidybill.app/api/recurring-invoices/6/line-items" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/recurring-invoices/6/line-items"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 87,
            "service_id": null,
            "time_entry_id": null,
            "description": "Nostrum qui commodi incidunt iure.",
            "quantity": 1.02,
            "unit_price": "13053.000000",
            "amount": 13314,
            "tax_name_1": null,
            "tax_rate_1": null,
            "tax_amount_1": 0,
            "tax_name_2": null,
            "tax_rate_2": null,
            "tax_amount_2": 0,
            "sort_order": 0,
            "is_late_fee": false,
            "created_at": "2026-04-05T19:38:22.000000Z",
            "updated_at": "2026-04-05T19:38:22.000000Z"
        },
        {
            "id": 88,
            "service_id": null,
            "time_entry_id": null,
            "description": "Nemo voluptate accusamus ut et.",
            "quantity": 8.73,
            "unit_price": "36405.000000",
            "amount": 317816,
            "tax_name_1": null,
            "tax_rate_1": null,
            "tax_amount_1": 0,
            "tax_name_2": null,
            "tax_rate_2": null,
            "tax_amount_2": 0,
            "sort_order": 0,
            "is_late_fee": false,
            "created_at": "2026-04-05T19:38:22.000000Z",
            "updated_at": "2026-04-05T19:38:22.000000Z"
        }
    ]
}
 

Request      

GET api/recurring-invoices/{recurring_invoice_id}/line-items

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

recurring_invoice_id   integer     

The ID of the recurring invoice. Example: 6

recurringInvoice   integer     

The recurring invoice ID. Example: 1

Add a line item to a recurring invoice.

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/recurring-invoices/6/line-items" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"description\": \"Eius et animi quos velit et.\",
    \"quantity\": 60,
    \"unit_price\": 42,
    \"tax_name_1\": \"l\",
    \"tax_rate_1\": 19,
    \"tax_name_2\": \"n\",
    \"tax_rate_2\": 5,
    \"sort_order\": 16
}"
const url = new URL(
    "https://tidybill.app/api/recurring-invoices/6/line-items"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "description": "Eius et animi quos velit et.",
    "quantity": 60,
    "unit_price": 42,
    "tax_name_1": "l",
    "tax_rate_1": 19,
    "tax_name_2": "n",
    "tax_rate_2": 5,
    "sort_order": 16
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 89,
        "service_id": null,
        "time_entry_id": null,
        "description": "Quos velit et fugiat sunt nihil accusantium harum.",
        "quantity": 9.96,
        "unit_price": "11278.000000",
        "amount": 112329,
        "tax_name_1": null,
        "tax_rate_1": null,
        "tax_amount_1": 0,
        "tax_name_2": null,
        "tax_rate_2": null,
        "tax_amount_2": 0,
        "sort_order": 0,
        "is_late_fee": false,
        "created_at": "2026-04-05T19:38:22.000000Z",
        "updated_at": "2026-04-05T19:38:22.000000Z"
    },
    "status": "201"
}
 

Request      

POST api/recurring-invoices/{recurring_invoice_id}/line-items

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

recurring_invoice_id   integer     

The ID of the recurring invoice. Example: 6

recurringInvoice   integer     

The recurring invoice ID. Example: 1

Body Parameters

service_id   string  optional    

The id of an existing record in the services table.

description   string     

Must not be greater than 1000 characters. Example: Eius et animi quos velit et.

quantity   number     

Must be at least 0.0001. Example: 60

unit_price   number     

Must be at least 0. Example: 42

tax_name_1   string  optional    

Must not be greater than 50 characters. Example: l

tax_rate_1   integer  optional    

Must be at least 0. Must not be greater than 10000. Example: 19

tax_name_2   string  optional    

Must not be greater than 50 characters. Example: n

tax_rate_2   integer  optional    

Must be at least 0. Must not be greater than 10000. Example: 5

sort_order   integer  optional    

Example: 16

Update a line item on a recurring invoice.

requires authentication

Example request:
curl --request PUT \
    "https://tidybill.app/api/recurring-invoices/6/line-items/4" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"description\": \"Eius et animi quos velit et.\",
    \"quantity\": 60,
    \"unit_price\": 42,
    \"tax_name_1\": \"l\",
    \"tax_rate_1\": 19,
    \"tax_name_2\": \"n\",
    \"tax_rate_2\": 5,
    \"sort_order\": 16
}"
const url = new URL(
    "https://tidybill.app/api/recurring-invoices/6/line-items/4"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "description": "Eius et animi quos velit et.",
    "quantity": 60,
    "unit_price": 42,
    "tax_name_1": "l",
    "tax_rate_1": 19,
    "tax_name_2": "n",
    "tax_rate_2": 5,
    "sort_order": 16
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 90,
        "service_id": null,
        "time_entry_id": null,
        "description": "Quos velit et fugiat sunt nihil accusantium harum.",
        "quantity": 9.96,
        "unit_price": "11278.000000",
        "amount": 112329,
        "tax_name_1": null,
        "tax_rate_1": null,
        "tax_amount_1": 0,
        "tax_name_2": null,
        "tax_rate_2": null,
        "tax_amount_2": 0,
        "sort_order": 0,
        "is_late_fee": false,
        "created_at": "2026-04-05T19:38:22.000000Z",
        "updated_at": "2026-04-05T19:38:22.000000Z"
    }
}
 

Request      

PUT api/recurring-invoices/{recurring_invoice_id}/line-items/{lineItem_id}

PATCH api/recurring-invoices/{recurring_invoice_id}/line-items/{lineItem_id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

recurring_invoice_id   integer     

The ID of the recurring invoice. Example: 6

lineItem_id   integer     

The ID of the lineItem. Example: 4

recurringInvoice   integer     

The recurring invoice ID. Example: 1

lineItem   integer     

The line item ID. Example: 1

Body Parameters

service_id   string  optional    

The id of an existing record in the services table.

description   string     

Must not be greater than 1000 characters. Example: Eius et animi quos velit et.

quantity   number     

Must be at least 0.0001. Example: 60

unit_price   number     

Must be at least 0. Example: 42

tax_name_1   string  optional    

Must not be greater than 50 characters. Example: l

tax_rate_1   integer  optional    

Must be at least 0. Must not be greater than 10000. Example: 19

tax_name_2   string  optional    

Must not be greater than 50 characters. Example: n

tax_rate_2   integer  optional    

Must be at least 0. Must not be greater than 10000. Example: 5

sort_order   integer  optional    

Example: 16

Delete a line item from a recurring invoice.

requires authentication

Example request:
curl --request DELETE \
    "https://tidybill.app/api/recurring-invoices/6/line-items/4" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/recurring-invoices/6/line-items/4"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

DELETE api/recurring-invoices/{recurring_invoice_id}/line-items/{lineItem_id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

recurring_invoice_id   integer     

The ID of the recurring invoice. Example: 6

lineItem_id   integer     

The ID of the lineItem. Example: 4

recurringInvoice   integer     

The recurring invoice ID. Example: 1

lineItem   integer     

The line item ID. Example: 1

Move a recurring invoice to another company.

requires authentication

Reassigns the template to the target company. Clears the client association, nullifies service references on line items, and unlinks previously generated invoices in the source company.

Example request:
curl --request POST \
    "https://tidybill.app/api/recurring-invoices/6/move" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"target_company_id\": 16
}"
const url = new URL(
    "https://tidybill.app/api/recurring-invoices/6/move"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "target_company_id": 16
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 47,
        "client_id": 200,
        "title": "Et animi quos.",
        "status": "active",
        "is_archived": false,
        "frequency": "monthly",
        "frequency_label": "Monthly",
        "interval": 1,
        "start_date": "2026-04-05",
        "end_date": null,
        "next_generate_date": "2026-04-05",
        "last_generated_at": null,
        "auto_send": false,
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "notes": null,
        "payment_terms": 30,
        "occurrences_generated": 0,
        "max_occurrences": null,
        "created_at": "2026-04-05T19:38:22.000000Z",
        "updated_at": "2026-04-05T19:38:22.000000Z"
    }
}
 

Request      

POST api/recurring-invoices/{recurringInvoice_id}/move

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

recurringInvoice_id   integer     

The ID of the recurringInvoice. Example: 6

recurringInvoice   integer     

The recurring invoice ID. Example: 1

Body Parameters

target_company_id   integer     

Example: 16

Archive a recurring invoice.

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/recurring-invoices/6/archive" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/recurring-invoices/6/archive"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

POST api/recurring-invoices/{recurringInvoice_id}/archive

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

recurringInvoice_id   integer     

The ID of the recurringInvoice. Example: 6

recurringInvoice   integer     

The recurring invoice ID. Example: 1

Unarchive a recurring invoice.

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/recurring-invoices/6/unarchive" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/recurring-invoices/6/unarchive"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

POST api/recurring-invoices/{recurringInvoice_id}/unarchive

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

recurringInvoice_id   integer     

The ID of the recurringInvoice. Example: 6

recurringInvoice   integer     

The recurring invoice ID. Example: 1

Pause a recurring invoice.

requires authentication

Stops automatic invoice generation. Only active recurring invoices can be paused.

Example request:
curl --request POST \
    "https://tidybill.app/api/recurring-invoices/6/pause" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/recurring-invoices/6/pause"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 48,
        "client_id": 201,
        "title": "Adipisci quidem nostrum qui.",
        "status": "active",
        "is_archived": false,
        "frequency": "monthly",
        "frequency_label": "Monthly",
        "interval": 1,
        "start_date": "2026-04-05",
        "end_date": null,
        "next_generate_date": "2026-04-05",
        "last_generated_at": null,
        "auto_send": false,
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "notes": null,
        "payment_terms": 30,
        "occurrences_generated": 0,
        "max_occurrences": null,
        "created_at": "2026-04-05T19:38:22.000000Z",
        "updated_at": "2026-04-05T19:38:22.000000Z"
    }
}
 

Request      

POST api/recurring-invoices/{recurringInvoice_id}/pause

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

recurringInvoice_id   integer     

The ID of the recurringInvoice. Example: 6

recurringInvoice   integer     

The recurring invoice ID. Example: 1

Resume a paused recurring invoice.

requires authentication

Resumes automatic invoice generation by setting the status back to active. Only paused recurring invoices can be resumed.

Example request:
curl --request POST \
    "https://tidybill.app/api/recurring-invoices/6/resume" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/recurring-invoices/6/resume"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 49,
        "client_id": 202,
        "title": "Adipisci quidem nostrum qui.",
        "status": "active",
        "is_archived": false,
        "frequency": "monthly",
        "frequency_label": "Monthly",
        "interval": 1,
        "start_date": "2026-04-05",
        "end_date": null,
        "next_generate_date": "2026-04-05",
        "last_generated_at": null,
        "auto_send": false,
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "notes": null,
        "payment_terms": 30,
        "occurrences_generated": 0,
        "max_occurrences": null,
        "created_at": "2026-04-05T19:38:22.000000Z",
        "updated_at": "2026-04-05T19:38:22.000000Z"
    }
}
 

Request      

POST api/recurring-invoices/{recurringInvoice_id}/resume

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

recurringInvoice_id   integer     

The ID of the recurringInvoice. Example: 6

recurringInvoice   integer     

The recurring invoice ID. Example: 1

Manually generate an invoice from a recurring invoice.

requires authentication

Immediately generates the next invoice regardless of the scheduled date. Returns 422 if the maximum occurrence limit has been reached.

Example request:
curl --request POST \
    "https://tidybill.app/api/recurring-invoices/6/generate" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/recurring-invoices/6/generate"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 1617,
        "uuid": "5134a4d7-1b9b-43c7-bc68-9dbc2f15bb1f",
        "client_id": 203,
        "recurring_invoice_id": null,
        "invoice_number": "INV-52976",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "due_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "amount_paid": 0,
        "amount_due": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "paid_at": null,
        "late_fee_amount": 0,
        "late_fee_applied_at": null,
        "reminders_sent": 0,
        "created_at": "2026-04-05T19:38:23.000000Z",
        "updated_at": "2026-04-05T19:38:23.000000Z"
    },
    "status": "201"
}
 

Request      

POST api/recurring-invoices/{recurringInvoice_id}/generate

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

recurringInvoice_id   integer     

The ID of the recurringInvoice. Example: 6

recurringInvoice   integer     

The recurring invoice ID. Example: 1

Billing

List Stripe prices for all plans.

requires authentication

Returns active recurring prices per plan keyed by plan name. Cached for one hour.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/billing/prices" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/billing/prices"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "starter": [
            {
                "price_id": "price_starter_monthly",
                "amount": 900,
                "currency": "usd",
                "interval": "month",
                "interval_count": 1
            },
            {
                "price_id": "price_starter_annual",
                "amount": 700,
                "currency": "usd",
                "interval": "month",
                "interval_count": 1
            }
        ],
        "pro": [
            {
                "price_id": "price_pro_monthly",
                "amount": 1800,
                "currency": "usd",
                "interval": "month",
                "interval_count": 1
            }
        ]
    }
}
 

Request      

GET api/billing/prices

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Get current subscription.

requires authentication

Returns the authenticated user's active subscription or a default free plan object.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/billing/subscription" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/billing/subscription"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "plan": "starter",
        "status": "active",
        "trial_ends_at": null,
        "current_period_end": "2026-05-01T00:00:00Z"
    }
}
 

Request      

GET api/billing/subscription

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

List available plans.

requires authentication

Returns all plan tiers and their feature limits.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/billing/plans" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/billing/plans"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 1,
            "plan": "free",
            "max_clients": 5,
            "max_invoices_per_month": 5,
            "max_projects": 3,
            "max_members": 1,
            "max_companies": 1,
            "feature_api_access": false,
            "feature_recurring_invoices": false,
            "feature_time_tracking": false,
            "feature_quotes": false,
            "feature_client_portal": false
        },
        {
            "id": 2,
            "plan": "starter",
            "max_clients": 25,
            "max_invoices_per_month": 50,
            "max_projects": 10,
            "max_members": 3,
            "max_companies": 2,
            "feature_api_access": false,
            "feature_recurring_invoices": true,
            "feature_time_tracking": true,
            "feature_quotes": true,
            "feature_client_portal": true
        },
        {
            "id": 3,
            "plan": "pro",
            "max_clients": null,
            "max_invoices_per_month": null,
            "max_projects": null,
            "max_members": 25,
            "max_companies": 5,
            "feature_api_access": true,
            "feature_recurring_invoices": true,
            "feature_time_tracking": true,
            "feature_quotes": true,
            "feature_client_portal": true
        }
    ]
}
 

Request      

GET api/billing/plans

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Get current plan usage.

requires authentication

Returns usage counts for the authenticated user's companies against their plan limits.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/billing/usage" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/billing/usage"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "clients": 12,
        "invoices_this_month": 8,
        "projects": 4,
        "team_members": 2,
        "companies": 1,
        "max_companies": 2
    }
}
 

Request      

GET api/billing/usage

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

List Stripe billing invoices.

requires authentication

Returns the last 12 Stripe invoices for the authenticated user's subscription. Returns an empty array if the user has no Stripe customer ID.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/billing/invoices" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/billing/invoices"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": "in_xxx",
            "number": "INV-0001",
            "amount": 900,
            "currency": "USD",
            "status": "paid",
            "date": "2026-03-01",
            "pdf_url": "https://pay.stripe.com/invoice/xxx/pdf"
        }
    ]
}
 

Example response (200, No Stripe customer):


{
    "data": []
}
 

Request      

GET api/billing/invoices

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Create a Stripe Checkout session.

requires authentication

Creates a Stripe Checkout session for the given plan and price. Returns the hosted checkout URL.

Example request:
curl --request POST \
    "https://tidybill.app/api/billing/checkout" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"plan\": \"starter\",
    \"price_id\": \"price_abc123\"
}"
const url = new URL(
    "https://tidybill.app/api/billing/checkout"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "plan": "starter",
    "price_id": "price_abc123"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "checkout_url": "https://checkout.stripe.com/pay/cs_live_xxx",
        "plan": "starter"
    }
}
 

Example response (422):


{
    "message": "Plan not configured."
}
 

Request      

POST api/billing/checkout

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

plan   string     

The plan to subscribe to. Example: starter

price_id   string     

The Stripe price ID. Must start with price_. Example: price_abc123

Get Stripe Billing Portal URL.

requires authentication

Returns a URL to the Stripe Customer Portal where the user can manage their subscription. Returns null if the user has no Stripe customer ID.

Example request:
curl --request POST \
    "https://tidybill.app/api/billing/portal" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/billing/portal"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "portal_url": "https://billing.stripe.com/session/xxx"
    }
}
 

Example response (200, No Stripe customer):


{
    "data": {
        "portal_url": null
    }
}
 

Request      

POST api/billing/portal

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Settings

Get company settings.

requires authentication

Example request:
curl --request GET \
    --get "https://tidybill.app/api/settings" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/settings"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 178,
        "uuid": "53a3ef0f-7120-4769-9543-8498f8da2a40",
        "name": "Price Ltd",
        "legal_name": null,
        "email": "[email protected]",
        "phone": null,
        "address_line_1": null,
        "address_line_2": null,
        "city": null,
        "state": null,
        "postal_code": null,
        "country": null,
        "currency": "USD",
        "tax_number": null,
        "logo_path": null,
        "invoice_prefix": "INV-",
        "quote_prefix": "QUO-",
        "invoice_layout": "classic",
        "default_payment_terms": 30,
        "default_hourly_rate": null,
        "default_tax_name_1": null,
        "default_tax_rate_1": null,
        "default_tax_name_2": null,
        "default_tax_rate_2": null,
        "late_fee_type": null,
        "late_fee_value": null,
        "late_fee_days": null,
        "created_at": "2026-04-05T19:38:23.000000Z",
        "updated_at": "2026-04-05T19:38:23.000000Z"
    }
}
 

Request      

GET api/settings

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Update company settings.

requires authentication

Updates general company settings including address, currency, invoice/quote prefix, default tax rates, default payment terms, and hourly rate. All fields are optional (PATCH semantics).

Example request:
curl --request PUT \
    "https://tidybill.app/api/settings" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"b\",
    \"legal_name\": \"n\",
    \"email\": \"[email protected]\",
    \"phone\": \"v\",
    \"address_line_1\": \"d\",
    \"address_line_2\": \"l\",
    \"city\": \"j\",
    \"state\": \"n\",
    \"postal_code\": \"ikhwaykcmyuwpwlv\",
    \"country\": \"qw\",
    \"currency\": \"rsi\",
    \"tax_number\": \"t\",
    \"invoice_prefix\": \"cpscql\",
    \"quote_prefix\": \"dzsnrw\",
    \"default_payment_terms\": 19,
    \"default_hourly_rate\": 33,
    \"default_tax_name_1\": \"j\",
    \"default_tax_rate_1\": 17,
    \"default_tax_name_2\": \"v\",
    \"default_tax_rate_2\": 24,
    \"invoice_layout\": \"classic\"
}"
const url = new URL(
    "https://tidybill.app/api/settings"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "b",
    "legal_name": "n",
    "email": "[email protected]",
    "phone": "v",
    "address_line_1": "d",
    "address_line_2": "l",
    "city": "j",
    "state": "n",
    "postal_code": "ikhwaykcmyuwpwlv",
    "country": "qw",
    "currency": "rsi",
    "tax_number": "t",
    "invoice_prefix": "cpscql",
    "quote_prefix": "dzsnrw",
    "default_payment_terms": 19,
    "default_hourly_rate": 33,
    "default_tax_name_1": "j",
    "default_tax_rate_1": 17,
    "default_tax_name_2": "v",
    "default_tax_rate_2": 24,
    "invoice_layout": "classic"
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 179,
        "uuid": "303015f8-8550-415a-a3b8-eee3d4976f0e",
        "name": "Considine LLC",
        "legal_name": null,
        "email": "[email protected]",
        "phone": null,
        "address_line_1": null,
        "address_line_2": null,
        "city": null,
        "state": null,
        "postal_code": null,
        "country": null,
        "currency": "USD",
        "tax_number": null,
        "logo_path": null,
        "invoice_prefix": "INV-",
        "quote_prefix": "QUO-",
        "invoice_layout": "classic",
        "default_payment_terms": 30,
        "default_hourly_rate": null,
        "default_tax_name_1": null,
        "default_tax_rate_1": null,
        "default_tax_name_2": null,
        "default_tax_rate_2": null,
        "late_fee_type": null,
        "late_fee_value": null,
        "late_fee_days": null,
        "created_at": "2026-04-05T19:38:23.000000Z",
        "updated_at": "2026-04-05T19:38:23.000000Z"
    }
}
 

Request      

PUT api/settings

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

name   string  optional    

Must not be greater than 255 characters. Example: b

legal_name   string  optional    

Must not be greater than 255 characters. Example: n

email   string  optional    

Must be a valid email address. Must not be greater than 255 characters. Example: [email protected]

phone   string  optional    

Must not be greater than 50 characters. Example: v

address_line_1   string  optional    

Must not be greater than 255 characters. Example: d

address_line_2   string  optional    

Must not be greater than 255 characters. Example: l

city   string  optional    

Must not be greater than 255 characters. Example: j

state   string  optional    

Must not be greater than 255 characters. Example: n

postal_code   string  optional    

Must not be greater than 20 characters. Example: ikhwaykcmyuwpwlv

country   string  optional    

Must be 2 characters. Example: qw

currency   string  optional    

Must be 3 characters. Example: rsi

tax_number   string  optional    

Must not be greater than 255 characters. Example: t

invoice_prefix   string  optional    

Must not be greater than 10 characters. Example: cpscql

quote_prefix   string  optional    

Must not be greater than 10 characters. Example: dzsnrw

default_payment_terms   integer  optional    

Must be at least 0. Must not be greater than 365. Example: 19

default_hourly_rate   integer  optional    

Must be at least 0. Example: 33

default_tax_name_1   string  optional    

Must not be greater than 255 characters. Example: j

default_tax_rate_1   integer  optional    

Must be at least 0. Must not be greater than 10000. Example: 17

default_tax_name_2   string  optional    

Must not be greater than 255 characters. Example: v

default_tax_rate_2   integer  optional    

Must be at least 0. Must not be greater than 10000. Example: 24

invoice_layout   string  optional    

Example: classic

Must be one of:
  • classic
  • modern
  • compact

Get branding settings.

requires authentication

Returns the company logo path and default invoice template branding settings.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/settings/branding" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/settings/branding"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "logo_path": "logos/1/logo.png",
        "template": {
            "id": 1,
            "layout": "modern",
            "primary_color": "#1a1a2e",
            "secondary_color": "#1e40af",
            "accent_color": "#3b82f6",
            "font_family": "Helvetica",
            "show_logo": true,
            "header_html": null,
            "footer_html": null,
            "custom_css": null
        }
    }
}
 

Request      

GET api/settings/branding

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Update branding settings.

requires authentication

Creates or updates the default invoice template for the company. HTML fields are sanitized.

Example request:
curl --request PUT \
    "https://tidybill.app/api/settings/branding" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"layout\": \"modern\",
    \"primary_color\": \"bngzmiy\",
    \"secondary_color\": \"vdljnik\",
    \"accent_color\": \"hwaykcm\",
    \"font_family\": \"y\",
    \"show_logo\": false,
    \"header_html\": \"u\",
    \"footer_html\": \"w\",
    \"custom_css\": \"p\"
}"
const url = new URL(
    "https://tidybill.app/api/settings/branding"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "layout": "modern",
    "primary_color": "bngzmiy",
    "secondary_color": "vdljnik",
    "accent_color": "hwaykcm",
    "font_family": "y",
    "show_logo": false,
    "header_html": "u",
    "footer_html": "w",
    "custom_css": "p"
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 1,
        "layout": "modern",
        "primary_color": "#1a1a2e",
        "secondary_color": "#1e40af",
        "accent_color": "#3b82f6",
        "font_family": "Helvetica",
        "show_logo": true,
        "header_html": null,
        "footer_html": null,
        "custom_css": null
    }
}
 

Request      

PUT api/settings/branding

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

layout   string  optional    

Example: modern

Must be one of:
  • classic
  • modern
  • compact
primary_color   string  optional    

Must not be greater than 7 characters. Example: bngzmiy

secondary_color   string  optional    

Must not be greater than 7 characters. Example: vdljnik

accent_color   string  optional    

Must not be greater than 7 characters. Example: hwaykcm

font_family   string  optional    

Must not be greater than 255 characters. Example: y

show_logo   boolean  optional    

Example: false

header_html   string  optional    

Must not be greater than 5000 characters. Example: u

footer_html   string  optional    

Must not be greater than 5000 characters. Example: w

custom_css   string  optional    

Must not be greater than 5000 characters. Example: p

requires authentication

Accepts a JPEG, PNG, GIF, or WebP image up to 2 MB. Replaces any existing logo.

Get late fee settings.

requires authentication

Example request:
curl --request GET \
    --get "https://tidybill.app/api/settings/late-fees" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/settings/late-fees"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "late_fee_type": "percentage",
        "late_fee_value": 150,
        "late_fee_days": 30
    }
}
 

Request      

GET api/settings/late-fees

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Update late fee settings.

requires authentication

late_fee_value is stored in cents (flat) or basis points (percentage). late_fee_days is the number of days past due before the fee applies.

Example request:
curl --request PUT \
    "https://tidybill.app/api/settings/late-fees" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"late_fee_type\": \"flat\",
    \"late_fee_value\": 27,
    \"late_fee_days\": 22
}"
const url = new URL(
    "https://tidybill.app/api/settings/late-fees"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "late_fee_type": "flat",
    "late_fee_value": 27,
    "late_fee_days": 22
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "late_fee_type": "percentage",
        "late_fee_value": 150,
        "late_fee_days": 30
    }
}
 

Request      

PUT api/settings/late-fees

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

late_fee_type   string  optional    

Example: flat

Must be one of:
  • percentage
  • flat
late_fee_value   integer  optional    

Must be at least 0. Example: 27

late_fee_days   integer  optional    

Must be at least 0. Must not be greater than 90. Example: 22

List payment reminders.

requires authentication

Returns all payment reminders for the current company ordered by reminder number.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/settings/reminders" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/settings/reminders"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 1,
            "reminder_number": 1,
            "days_offset": -3,
            "subject": "Invoice due soon",
            "body": "Your invoice is due in 3 days.",
            "is_active": true
        }
    ]
}
 

Request      

GET api/settings/reminders

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Create a payment reminder.

requires authentication

days_offset is relative to the due date: negative values send before due, positive values send after due.

Example request:
curl --request POST \
    "https://tidybill.app/api/settings/reminders" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"reminder_number\": 1,
    \"days_offset\": 22,
    \"subject\": \"g\",
    \"body\": \"z\",
    \"is_active\": false
}"
const url = new URL(
    "https://tidybill.app/api/settings/reminders"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "reminder_number": 1,
    "days_offset": 22,
    "subject": "g",
    "body": "z",
    "is_active": false
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 2,
        "reminder_number": 2,
        "days_offset": 7,
        "subject": "Invoice overdue",
        "body": "Your invoice is 7 days overdue.",
        "is_active": true
    }
}
 

Request      

POST api/settings/reminders

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

reminder_number   integer     

Must be at least 1. Must not be greater than 3. Example: 1

days_offset   integer     

Must be at least -30. Must not be greater than 90. Example: 22

subject   string  optional    

Must not be greater than 255 characters. Example: g

body   string  optional    

Must not be greater than 2000 characters. Example: z

is_active   boolean  optional    

Example: false

Update a payment reminder.

requires authentication

Example request:
curl --request PUT \
    "https://tidybill.app/api/settings/reminders/16" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"reminder_number\": 1,
    \"days_offset\": 22,
    \"subject\": \"g\",
    \"body\": \"z\",
    \"is_active\": true
}"
const url = new URL(
    "https://tidybill.app/api/settings/reminders/16"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "reminder_number": 1,
    "days_offset": 22,
    "subject": "g",
    "body": "z",
    "is_active": true
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 1,
        "reminder_number": 1,
        "days_offset": -3,
        "subject": "Invoice due soon",
        "body": "Your invoice is due in 3 days.",
        "is_active": true
    }
}
 

Request      

PUT api/settings/reminders/{reminder_id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

reminder_id   integer     

The ID of the reminder. Example: 16

Body Parameters

reminder_number   integer  optional    

Must be at least 1. Must not be greater than 3. Example: 1

days_offset   integer  optional    

Must be at least -30. Must not be greater than 90. Example: 22

subject   string  optional    

Must not be greater than 255 characters. Example: g

body   string  optional    

Must not be greater than 2000 characters. Example: z

is_active   boolean  optional    

Example: true

Delete a payment reminder.

requires authentication

Example request:
curl --request DELETE \
    "https://tidybill.app/api/settings/reminders/16" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/settings/reminders/16"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

DELETE api/settings/reminders/{reminder_id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

reminder_id   integer     

The ID of the reminder. Example: 16

Bank Accounts

List bank accounts.

requires authentication

Returns all bank accounts for the current company ordered by sort_order. Not paginated.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/settings/bank-accounts" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/settings/bank-accounts"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 4,
            "name": "Primary",
            "details": "Quidem nostrum qui commodi incidunt iure odit. Et modi ipsum nostrum omnis autem et consequatur. Dolores enim non facere tempora. Voluptatem laboriosam praesentium quis adipisci.",
            "sort_order": 0,
            "created_at": "2026-04-05T19:38:23.000000Z",
            "updated_at": "2026-04-05T19:38:23.000000Z"
        },
        {
            "id": 5,
            "name": "Primary",
            "details": "Corporis dolorem mollitia deleniti nemo odit quia officia. Dignissimos neque blanditiis odio.",
            "sort_order": 0,
            "created_at": "2026-04-05T19:38:23.000000Z",
            "updated_at": "2026-04-05T19:38:23.000000Z"
        }
    ]
}
 

Request      

GET api/settings/bank-accounts

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Create a bank account.

requires authentication

Example request:
curl --request POST \
    "https://tidybill.app/api/settings/bank-accounts" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"b\",
    \"details\": \"n\",
    \"sort_order\": 84
}"
const url = new URL(
    "https://tidybill.app/api/settings/bank-accounts"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "b",
    "details": "n",
    "sort_order": 84
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 6,
        "name": "Primary",
        "details": "Velit et fugiat sunt nihil accusantium. Mollitia modi deserunt aut ab provident perspiciatis quo. Nostrum aut adipisci quidem nostrum. Commodi incidunt iure odit.",
        "sort_order": 0,
        "created_at": "2026-04-05T19:38:23.000000Z",
        "updated_at": "2026-04-05T19:38:23.000000Z"
    }
}
 

Example response (201):


{}
 

Request      

POST api/settings/bank-accounts

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

name   string     

Must not be greater than 100 characters. Example: b

details   string  optional    

Must not be greater than 2000 characters. Example: n

sort_order   integer  optional    

Must be at least 0. Example: 84

Show a bank account.

requires authentication

Example request:
curl --request GET \
    --get "https://tidybill.app/api/settings/bank-accounts/1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/settings/bank-accounts/1"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 7,
        "name": "Primary",
        "details": "Quidem nostrum qui commodi incidunt iure odit. Et modi ipsum nostrum omnis autem et consequatur. Dolores enim non facere tempora. Voluptatem laboriosam praesentium quis adipisci.",
        "sort_order": 0,
        "created_at": "2026-04-05T19:38:23.000000Z",
        "updated_at": "2026-04-05T19:38:23.000000Z"
    }
}
 

Request      

GET api/settings/bank-accounts/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the bank account. Example: 1

Update a bank account.

requires authentication

Example request:
curl --request PUT \
    "https://tidybill.app/api/settings/bank-accounts/1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"b\",
    \"details\": \"n\",
    \"sort_order\": 84
}"
const url = new URL(
    "https://tidybill.app/api/settings/bank-accounts/1"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "b",
    "details": "n",
    "sort_order": 84
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 8,
        "name": "Primary",
        "details": "Velit et fugiat sunt nihil accusantium. Mollitia modi deserunt aut ab provident perspiciatis quo. Nostrum aut adipisci quidem nostrum. Commodi incidunt iure odit.",
        "sort_order": 0,
        "created_at": "2026-04-05T19:38:23.000000Z",
        "updated_at": "2026-04-05T19:38:23.000000Z"
    }
}
 

Request      

PUT api/settings/bank-accounts/{id}

PATCH api/settings/bank-accounts/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the bank account. Example: 1

Body Parameters

name   string     

Must not be greater than 100 characters. Example: b

details   string  optional    

Must not be greater than 2000 characters. Example: n

sort_order   integer  optional    

Must be at least 0. Example: 84

Delete a bank account.

requires authentication

Example request:
curl --request DELETE \
    "https://tidybill.app/api/settings/bank-accounts/1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/settings/bank-accounts/1"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

DELETE api/settings/bank-accounts/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The ID of the bank account. Example: 1

Imports

FreshBooks OAuth callback.

Handles the authorization code callback from FreshBooks. Exchanges the code for access tokens, fetches the FreshBooks identity, and syncs company settings. Redirects to the frontend settings page on success.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/settings/freshbooks/callback?code=authcode123" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/settings/freshbooks/callback"
);

const params = {
    "code": "authcode123",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (302, Redirect to settings on success):



 

Example response (400):


{
    "message": "Missing authorization code or session cookie. Please try connecting again."
}
 

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: https://tidybill.app
access-control-allow-credentials: true
 

{
    "message": "Missing authorization code or session cookie. Please try connecting again."
}
 

Example response (502):


{
    "message": "Failed to exchange code for tokens."
}
 

Request      

GET api/settings/freshbooks/callback

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

code   string     

Authorization code from FreshBooks. Example: authcode123

Initiate FreshBooks OAuth.

requires authentication

Redirects the user to FreshBooks' OAuth authorization page. Credentials must be saved first. Identity is preserved via a signed cookie (FreshBooks does not support state parameter).

Example request:
curl --request GET \
    --get "https://tidybill.app/api/settings/freshbooks/connect?company_id=1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/settings/freshbooks/connect"
);

const params = {
    "company_id": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (302, Redirect to FreshBooks):



 

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: https://tidybill.app
access-control-allow-credentials: true
 

{
    "message": "Unauthenticated."
}
 

Request      

GET api/settings/freshbooks/connect

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

company_id   integer     

The company to import into. Example: 1

Save FreshBooks OAuth credentials.

requires authentication

Stores the FreshBooks app client ID and secret for the current user/company. Required before initiating the OAuth flow.

Example request:
curl --request POST \
    "https://tidybill.app/api/settings/freshbooks/credentials" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"client_id\": \"abc123\",
    \"client_secret\": \"secret456\"
}"
const url = new URL(
    "https://tidybill.app/api/settings/freshbooks/credentials"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "client_id": "abc123",
    "client_secret": "secret456"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "message": "Credentials saved"
    }
}
 

Request      

POST api/settings/freshbooks/credentials

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

client_id   string     

The FreshBooks app client ID. Example: abc123

client_secret   string     

The FreshBooks app client secret. Example: secret456

Preview FreshBooks import counts.

requires authentication

Fetches resource counts from FreshBooks (clients, invoices, etc.) without importing. Updates the import status to previewing.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/settings/freshbooks/preview" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/settings/freshbooks/preview"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "clients": 42,
        "invoices": 210,
        "payments": 185,
        "expenses": 67
    }
}
 

Example response (422):


{
    "data": {
        "message": "FreshBooks is not connected."
    }
}
 

Request      

GET api/settings/freshbooks/preview

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Start FreshBooks import.

requires authentication

Queues the import job for the connected FreshBooks account. Optionally restrict to specific resource types. Returns 409 if an import is already in progress.

Example request:
curl --request POST \
    "https://tidybill.app/api/settings/freshbooks/import" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"selected_resources\": [
        \"clients\",
        \"invoices\"
    ],
    \"reimport_existing\": false
}"
const url = new URL(
    "https://tidybill.app/api/settings/freshbooks/import"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "selected_resources": [
        "clients",
        "invoices"
    ],
    "reimport_existing": false
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (202):


{
    "data": {
        "message": "Import started"
    }
}
 

Example response (409):


{
    "data": {
        "message": "Import is already in progress."
    }
}
 

Example response (422):


{
    "data": {
        "message": "FreshBooks is not connected."
    }
}
 

Request      

POST api/settings/freshbooks/import

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

selected_resources   string[]  optional    

Optional list of resource types to import. Valid values depend on FreshBooksImportContext.

reimport_existing   boolean  optional    

Whether to overwrite previously imported records. Example: false

Get FreshBooks import status.

requires authentication

Returns the current state of the import record for the authenticated user and company. Returns null data if no import record exists.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/settings/freshbooks/status" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/settings/freshbooks/status"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "status": "importing",
        "progress": {
            "clients": 42,
            "invoices": 0
        },
        "preview_data": null,
        "error_message": null,
        "started_at": "2026-04-05T08:00:00+00:00",
        "completed_at": null,
        "has_credentials": true,
        "has_tokens": true,
        "freshbooks_account_id": "ABC123",
        "selected_resources": [
            "clients",
            "invoices"
        ]
    }
}
 

Example response (200, No import record):


{
    "data": null
}
 

Request      

GET api/settings/freshbooks/status

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Reset import to preview state.

requires authentication

Clears any in-progress or errored import so a new import can be started from the preview step.

Example request:
curl --request POST \
    "https://tidybill.app/api/settings/freshbooks/reset" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/settings/freshbooks/reset"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "message": "Ready for new import"
    }
}
 

Example response (422):


{
    "data": {
        "message": "FreshBooks is not connected."
    }
}
 

Request      

POST api/settings/freshbooks/reset

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Disconnect FreshBooks.

requires authentication

Clears stored OAuth tokens and account identifiers from the import record. The credentials (client ID/secret) are preserved.

Example request:
curl --request POST \
    "https://tidybill.app/api/settings/freshbooks/disconnect" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/settings/freshbooks/disconnect"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "message": "Disconnected from FreshBooks."
    }
}
 

Request      

POST api/settings/freshbooks/disconnect

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Stripe Connect

Stripe Connect OAuth callback.

Handles the OAuth callback from Stripe. Exchanges the authorization code for tokens, stores the connected account ID on the company, then redirects to the frontend settings page.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/settings/stripe/callback?code=ac_xxx&state=base64encodedstate" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/settings/stripe/callback"
);

const params = {
    "code": "ac_xxx",
    "state": "base64encodedstate",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (302, Redirect to settings on success):



 

Example response (400):


{
    "message": "Missing authorization code or state. Please try connecting again."
}
 

Example response (400):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: https://tidybill.app
access-control-allow-credentials: true
 

{
    "message": "Invalid session. Please try connecting again."
}
 

Example response (502):


{
    "message": "Failed to connect Stripe account."
}
 

Request      

GET api/settings/stripe/callback

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

code   string     

Authorization code from Stripe. Example: ac_xxx

state   string     

Signed state parameter issued during connect. Example: base64encodedstate

Initiate Stripe Connect OAuth.

requires authentication

Redirects the user to Stripe's OAuth authorization page. Requires company_id as a query parameter. The user must have access to the specified company.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/settings/stripe/connect?company_id=1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/settings/stripe/connect"
);

const params = {
    "company_id": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (302, Redirect to Stripe):



 

Example response (401):

Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: https://tidybill.app
access-control-allow-credentials: true
 

{
    "message": "Unauthenticated."
}
 

Request      

GET api/settings/stripe/connect

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

company_id   integer     

The company to connect Stripe to. Example: 1

Get Stripe Connect status.

requires authentication

Returns the connection status of the company's Stripe Connect account. Fetches live status from Stripe and syncs it locally; falls back to cached values if Stripe is unreachable.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/settings/stripe/status" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/settings/stripe/status"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, Connected):


{
    "data": {
        "connected": true,
        "account_name": "Acme Corp",
        "payouts_enabled": true,
        "charges_enabled": true,
        "connected_at": "2026-01-15T10:00:00+00:00"
    }
}
 

Example response (200, Not connected):


{
    "data": {
        "connected": false
    }
}
 

Request      

GET api/settings/stripe/status

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Disconnect Stripe Connect.

requires authentication

Deauthorizes the connected Stripe account and clears the connection from the company record. Proceeds even if the Stripe deauthorization API call fails.

Example request:
curl --request POST \
    "https://tidybill.app/api/settings/stripe/disconnect" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/settings/stripe/disconnect"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "message": "Stripe account disconnected."
    }
}
 

Request      

POST api/settings/stripe/disconnect

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

API Tokens

List API tokens.

requires authentication

Returns all personal access tokens for the authenticated user. The token value is never returned after creation.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/api-tokens" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/api-tokens"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 1,
            "name": "My Token",
            "last_used_at": "2026-04-01T10:00:00Z",
            "created_at": "2026-03-01T10:00:00Z"
        }
    ]
}
 

Request      

GET api/api-tokens

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Create an API token.

requires authentication

Requires a Pro plan. The token field is only returned on creation and cannot be retrieved again.

Example request:
curl --request POST \
    "https://tidybill.app/api/api-tokens" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"CI Deploy Token\"
}"
const url = new URL(
    "https://tidybill.app/api/api-tokens"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "CI Deploy Token"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 2,
        "name": "CI Deploy Token",
        "token": "1|abc123plaintext",
        "created_at": "2026-04-05T08:00:00Z"
    }
}
 

Example response (403):


{
    "message": "API access requires a Pro plan. Please upgrade."
}
 

Request      

POST api/api-tokens

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

name   string     

A descriptive name for the token. Example: CI Deploy Token

Revoke an API token.

requires authentication

Example request:
curl --request DELETE \
    "https://tidybill.app/api/api-tokens/architecto" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/api-tokens/architecto"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());

Example response (204, No content):

Empty response
 

Request      

DELETE api/api-tokens/{tokenId}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

tokenId   string     

Example: architecto

Invitations

Get invitation details.

Returns the company name, role, invitee email, and inviter name for the given invitation token. Returns 404 if the token is not found, 410 if expired, and 422 if already accepted.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/invitations/abc123token/details" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/invitations/abc123token/details"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "company_name": "Acme Corp",
        "role": "member",
        "email": "[email protected]",
        "inviter": "John Smith"
    }
}
 

Example response (410):


{
    "message": "This invitation has expired."
}
 

Example response (422):


{
    "message": "This invitation has already been accepted."
}
 

Request      

GET api/invitations/{token}/details

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

token   string     

The invitation token. Example: abc123token

Accept an invitation.

requires authentication

Adds the authenticated user to the company with the invited role and marks the invitation as accepted. Runs inside a transaction. Returns the company ID on success.

Example request:
curl --request POST \
    "https://tidybill.app/api/invitations/abc123token/accept" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/invitations/abc123token/accept"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "message": "Invitation accepted.",
    "data": {
        "company_id": 5
    }
}
 

Example response (200, Already a member):


{
    "message": "You are already a member of this company.",
    "data": {
        "company_id": 5
    }
}
 

Example response (410):


{
    "message": "This invitation has expired."
}
 

Example response (422):


{
    "message": "This invitation has already been accepted."
}
 

Request      

POST api/invitations/{token}/accept

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

token   string     

The invitation token. Example: abc123token

Client Portal

Get invoice for client portal.

Public endpoint accessed via invoice UUID. Marks the invoice as viewed on first access. Also returns Stripe payment availability and bank account details if configured.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/portal/invoices/550e8400-e29b-41d4-a716-446655440000" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/portal/invoices/550e8400-e29b-41d4-a716-446655440000"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 42,
        "invoice_number": "INV-0042",
        "status": "sent",
        "total": 150000,
        "amount_due": 150000,
        "currency": "USD"
    },
    "payment_available": true,
    "bank_details": null
}
 

Request      

GET api/portal/invoices/{uuid}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

uuid   string     

The invoice UUID. Example: 550e8400-e29b-41d4-a716-446655440000

Download invoice PDF.

Returns the invoice as a PDF file. Public endpoint accessed via UUID.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/portal/invoices/550e8400-e29b-41d4-a716-446655440000/pdf" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/portal/invoices/550e8400-e29b-41d4-a716-446655440000/pdf"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, PDF file):


{
    "Content-Type": "application/pdf"
}
 

Request      

GET api/portal/invoices/{uuid}/pdf

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

uuid   string     

The invoice UUID. Example: 550e8400-e29b-41d4-a716-446655440000

Get quote for client portal.

Public endpoint accessed via quote UUID. Marks the quote as viewed on first access.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/portal/quotes/550e8400-e29b-41d4-a716-446655440001" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/portal/quotes/550e8400-e29b-41d4-a716-446655440001"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 151,
        "uuid": "13fb884a-24bf-4790-9731-a88368d2068a",
        "client_id": 125,
        "quote_number": "QUO-66650",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "expiry_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "accepted_at": null,
        "declined_at": null,
        "converted_to_invoice_id": null,
        "converted_to_project_id": null,
        "pdf_path": null,
        "created_at": "2026-04-05T19:38:13.000000Z",
        "updated_at": "2026-04-05T19:38:13.000000Z"
    }
}
 

Request      

GET api/portal/quotes/{uuid}

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

uuid   string     

The quote UUID. Example: 550e8400-e29b-41d4-a716-446655440001

Download quote PDF.

Returns the quote as a PDF file. Public endpoint accessed via UUID.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/portal/quotes/550e8400-e29b-41d4-a716-446655440001/pdf" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/portal/quotes/550e8400-e29b-41d4-a716-446655440001/pdf"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200, PDF file):


{
    "Content-Type": "application/pdf"
}
 

Request      

GET api/portal/quotes/{uuid}/pdf

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

uuid   string     

The quote UUID. Example: 550e8400-e29b-41d4-a716-446655440001

Accept a quote.

Transitions the quote to the accepted status. Public endpoint accessed via UUID.

Example request:
curl --request POST \
    "https://tidybill.app/api/portal/quotes/550e8400-e29b-41d4-a716-446655440001/accept" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/portal/quotes/550e8400-e29b-41d4-a716-446655440001/accept"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 152,
        "uuid": "2398d4ba-97d1-45dc-9fa1-fa2af7b601cc",
        "client_id": 126,
        "quote_number": "QUO-77300",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "expiry_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "accepted_at": null,
        "declined_at": null,
        "converted_to_invoice_id": null,
        "converted_to_project_id": null,
        "pdf_path": null,
        "created_at": "2026-04-05T19:38:13.000000Z",
        "updated_at": "2026-04-05T19:38:13.000000Z"
    }
}
 

Example response (422):


{
    "message": "Quote cannot be accepted in its current status."
}
 

Request      

POST api/portal/quotes/{uuid}/accept

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

uuid   string     

The quote UUID. Example: 550e8400-e29b-41d4-a716-446655440001

Decline a quote.

Transitions the quote to the declined status. Public endpoint accessed via UUID.

Example request:
curl --request POST \
    "https://tidybill.app/api/portal/quotes/550e8400-e29b-41d4-a716-446655440001/decline" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/portal/quotes/550e8400-e29b-41d4-a716-446655440001/decline"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 153,
        "uuid": "94349ce5-6058-456d-9288-03f3dc8e70cc",
        "client_id": 127,
        "quote_number": "QUO-10778",
        "status": "draft",
        "is_archived": false,
        "status_label": "Draft",
        "status_color": "gray",
        "issue_date": "2026-04-05",
        "expiry_date": "2026-05-05",
        "currency": "USD",
        "subtotal": 0,
        "tax_total": 0,
        "discount_type": null,
        "discount_value": null,
        "discount_total": 0,
        "total": 0,
        "notes": null,
        "terms": null,
        "footer": null,
        "sent_at": null,
        "viewed_at": null,
        "accepted_at": null,
        "declined_at": null,
        "converted_to_invoice_id": null,
        "converted_to_project_id": null,
        "pdf_path": null,
        "created_at": "2026-04-05T19:38:13.000000Z",
        "updated_at": "2026-04-05T19:38:13.000000Z"
    }
}
 

Example response (422):


{
    "message": "Quote cannot be declined in its current status."
}
 

Request      

POST api/portal/quotes/{uuid}/decline

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

uuid   string     

The quote UUID. Example: 550e8400-e29b-41d4-a716-446655440001

Initiate portal payment.

Creates a Stripe Checkout session for the invoice (using the company's Connect account) or returns bank transfer details if Stripe is not connected. Public endpoint accessed via UUID.

Example request:
curl --request POST \
    "https://tidybill.app/api/portal/invoices/550e8400-e29b-41d4-a716-446655440000/checkout" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/portal/invoices/550e8400-e29b-41d4-a716-446655440000/checkout"
);

const headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Example response (200, Stripe checkout):


{
    "data": {
        "checkout_url": "https://checkout.stripe.com/pay/cs_live_xxx"
    }
}
 

Example response (200, Bank transfer):


{
    "data": {
        "payment_method": "bank_details",
        "bank_accounts": [
            {
                "name": "Main Account",
                "details": "BSB 123-456, Account 987654321"
            }
        ],
        "reference": "INV-0042",
        "amount": 150000,
        "currency": "USD"
    }
}
 

Example response (422):


{
    "message": "Invoice is not payable."
}
 

Example response (502):


{
    "message": "Payment service unavailable. Please try again later."
}
 

Request      

POST api/portal/invoices/{uuid}/checkout

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

uuid   string     

The invoice UUID. Example: 550e8400-e29b-41d4-a716-446655440000

Dashboard

Get dashboard stats.

requires authentication

Returns outstanding and overdue invoice totals (by currency), plus unbilled time entry hours and estimated value for the current company.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/dashboard/stats" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/dashboard/stats"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "outstanding_by_currency": {
            "USD": 250000
        },
        "overdue_by_currency": {
            "USD": 75000
        },
        "unbilled_hours": 12.5,
        "unbilled_amount": 187500
    }
}
 

Request      

GET api/dashboard/stats

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Get revenue chart data.

requires authentication

Returns monthly revenue grouped by currency for the last 12 months.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/dashboard/revenue-chart" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/dashboard/revenue-chart"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "month": "May 2025",
            "revenue_by_currency": {
                "USD": 120000
            }
        },
        {
            "month": "Apr 2026",
            "revenue_by_currency": {
                "USD": 180000
            }
        }
    ]
}
 

Request      

GET api/dashboard/revenue-chart

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Get recent activity.

requires authentication

Returns the 10 most recently updated non-cancelled, non-archived invoices.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/dashboard/recent-activity" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/dashboard/recent-activity"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "type": "invoice",
            "id": 42,
            "description": "Invoice INV-0042 to Acme Corp",
            "status": "sent",
            "amount": 150000,
            "date": "2026-04-04T09:00:00Z"
        }
    ]
}
 

Request      

GET api/dashboard/recent-activity

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Get accounts receivable aging.

requires authentication

Returns outstanding invoice totals bucketed by age (0-30, 31-60, 61-90, 91+ days), grouped by currency.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/dashboard/aging" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/dashboard/aging"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "USD": {
            "overdue_total": 75000,
            "outstanding_total": 175000,
            "total": 250000,
            "buckets": [
                {
                    "label": "0-30 Days",
                    "amount": 100000
                },
                {
                    "label": "31-60 Days",
                    "amount": 75000
                },
                {
                    "label": "61-90 Days",
                    "amount": 50000
                },
                {
                    "label": "91+ Days",
                    "amount": 25000
                }
            ]
        }
    }
}
 

Request      

GET api/dashboard/aging

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Get unbilled hours by client.

requires authentication

Returns the top 10 clients with unbilled billable time entries, ordered by total unbilled hours.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/dashboard/unbilled-by-client" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/dashboard/unbilled-by-client"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "total_hours": 34.5,
        "clients": [
            {
                "client_id": 7,
                "client_name": "Acme Corp",
                "hours": 18,
                "seconds": 64800
            },
            {
                "client_id": 3,
                "client_name": "Globex",
                "hours": 16.5,
                "seconds": 59400
            }
        ]
    }
}
 

Request      

GET api/dashboard/unbilled-by-client

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Get revenue by client.

requires authentication

Returns the top 10 clients by total invoiced amount (year to date), grouped by currency.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/dashboard/revenue-by-client" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/dashboard/revenue-by-client"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "year": 2026,
        "clients": [
            {
                "client_id": 7,
                "client_name": "Acme Corp",
                "currency": "USD",
                "total": 480000
            },
            {
                "client_id": 3,
                "client_name": "Globex",
                "currency": "USD",
                "total": 210000
            }
        ]
    }
}
 

Request      

GET api/dashboard/revenue-by-client

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Reports

Profit and loss report.

requires authentication

Returns monthly invoiced/collected revenue and expenses for the given date range. All monetary values are in cents.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/reports/profit-loss?start_date=2026-01-01&end_date=2026-12-31" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"start_date\": \"2026-04-05T19:38:23\",
    \"end_date\": \"2052-04-28\"
}"
const url = new URL(
    "https://tidybill.app/api/reports/profit-loss"
);

const params = {
    "start_date": "2026-01-01",
    "end_date": "2026-12-31",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "start_date": "2026-04-05T19:38:23",
    "end_date": "2052-04-28"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "period": {
        "start": "2026-01-01",
        "end": "2026-12-31"
    },
    "revenue": {
        "invoiced": 600000,
        "collected": 480000,
        "by_month": [
            {
                "month": "Jan 2026",
                "month_key": "2026-01",
                "invoiced": 50000,
                "collected": 40000,
                "expenses": 10000,
                "profit": 30000
            }
        ]
    },
    "summary": {
        "total_invoiced": 600000,
        "total_collected": 480000,
        "outstanding": 120000,
        "total_expenses": 60000,
        "net_profit": 420000
    }
}
 

Request      

GET api/reports/profit-loss

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

start_date   string  optional    

Date in YYYY-MM-DD format. Defaults to start of current year. Example: 2026-01-01

end_date   string  optional    

Date in YYYY-MM-DD format. Defaults to end of current year. Example: 2026-12-31

Body Parameters

start_date   string  optional    

Must be a valid date. Example: 2026-04-05T19:38:23

end_date   string  optional    

Must be a valid date. Must be a date after or equal to start_date. Example: 2052-04-28

Accounts receivable aging report.

requires authentication

Returns outstanding invoice amounts bucketed by age across all payable invoices, with a per-client breakdown.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/reports/account-aging" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "https://tidybill.app/api/reports/account-aging"
);

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "buckets": [
        {
            "label": "Current",
            "range": "0-30 days",
            "amount": 100000,
            "count": 5
        },
        {
            "label": "31-60 days",
            "range": "31-60 days",
            "amount": 50000,
            "count": 2
        },
        {
            "label": "61-90 days",
            "range": "61-90 days",
            "amount": 25000,
            "count": 1
        },
        {
            "label": "90+ days",
            "range": "90+ days",
            "amount": 10000,
            "count": 1
        }
    ],
    "total_outstanding": 185000,
    "clients": [
        {
            "client_id": 7,
            "client_name": "Acme Corp",
            "current": 100000,
            "days_31_60": 50000,
            "days_61_90": 0,
            "days_90_plus": 0,
            "total": 150000
        }
    ]
}
 

Request      

GET api/reports/account-aging

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Tax summary report.

requires authentication

Returns tax collected per tax name/rate combination for the given date range. Amounts in cents, tax rates in basis points (e.g. 1500 = 15%).

Example request:
curl --request GET \
    --get "https://tidybill.app/api/reports/tax-summary?start_date=2026-01-01&end_date=2026-12-31" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"start_date\": \"2026-04-05T19:38:23\",
    \"end_date\": \"2052-04-28\"
}"
const url = new URL(
    "https://tidybill.app/api/reports/tax-summary"
);

const params = {
    "start_date": "2026-01-01",
    "end_date": "2026-12-31",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "start_date": "2026-04-05T19:38:23",
    "end_date": "2052-04-28"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "period": {
        "start": "2026-01-01",
        "end": "2026-12-31"
    },
    "taxes": [
        {
            "tax_name": "GST",
            "tax_rate": 1500,
            "taxable_amount": 400000,
            "tax_collected": 60000,
            "invoice_count": 12
        }
    ],
    "total_tax_collected": 60000
}
 

Request      

GET api/reports/tax-summary

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

start_date   string  optional    

Date in YYYY-MM-DD format. Defaults to start of current year. Example: 2026-01-01

end_date   string  optional    

Date in YYYY-MM-DD format. Defaults to end of current year. Example: 2026-12-31

Body Parameters

start_date   string  optional    

Must be a valid date. Example: 2026-04-05T19:38:23

end_date   string  optional    

Must be a valid date. Must be a date after or equal to start_date. Example: 2052-04-28

Team utilization report.

requires authentication

Returns billable vs non-billable hours and utilization rate per team member for the given date range.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/reports/team-utilization?start_date=2026-01-01&end_date=2026-12-31" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"start_date\": \"2026-04-05T19:38:23\",
    \"end_date\": \"2052-04-28\"
}"
const url = new URL(
    "https://tidybill.app/api/reports/team-utilization"
);

const params = {
    "start_date": "2026-01-01",
    "end_date": "2026-12-31",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "start_date": "2026-04-05T19:38:23",
    "end_date": "2052-04-28"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "period": {
        "start": "2026-01-01",
        "end": "2026-12-31"
    },
    "members": [
        {
            "user_id": 1,
            "name": "Jane Smith",
            "total_hours": 160,
            "billable_hours": 128,
            "non_billable_hours": 32,
            "utilization_rate": 80,
            "billable_amount": 1920000,
            "by_client": [
                {
                    "client_id": 7,
                    "client_name": "Acme Corp",
                    "hours": 80,
                    "amount": 1200000
                }
            ]
        }
    ]
}
 

Request      

GET api/reports/team-utilization

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

start_date   string  optional    

Date in YYYY-MM-DD format. Defaults to start of current year. Example: 2026-01-01

end_date   string  optional    

Date in YYYY-MM-DD format. Defaults to end of current year. Example: 2026-12-31

Body Parameters

start_date   string  optional    

Must be a valid date. Example: 2026-04-05T19:38:23

end_date   string  optional    

Must be a valid date. Must be a date after or equal to start_date. Example: 2052-04-28

Expense report.

requires authentication

Returns total expenses with breakdowns by category and by client (billable only) for the given date range. All amounts in cents.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/reports/expenses?from=2026-01-01&to=2026-12-31" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"from\": \"2026-04-05T19:38:23\",
    \"to\": \"2052-04-28\"
}"
const url = new URL(
    "https://tidybill.app/api/reports/expenses"
);

const params = {
    "from": "2026-01-01",
    "to": "2026-12-31",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "from": "2026-04-05T19:38:23",
    "to": "2052-04-28"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": {
        "total": 85000,
        "total_tax": 12750,
        "billable_total": 60000,
        "by_category": [
            {
                "category": "travel",
                "total": 30000,
                "count": 3
            }
        ],
        "by_client": [
            {
                "client": "Acme Corp",
                "client_id": 7,
                "total": 60000,
                "count": 5
            }
        ]
    }
}
 

Request      

GET api/reports/expenses

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

from   string  optional    

Start date in YYYY-MM-DD format. Example: 2026-01-01

to   string  optional    

End date in YYYY-MM-DD format. Example: 2026-12-31

Body Parameters

from   string  optional    

Must be a valid date. Example: 2026-04-05T19:38:23

to   string  optional    

Must be a valid date. Must be a date after or equal to from. Example: 2052-04-28

Invoice summary report.

requires authentication

Returns invoice counts and totals grouped by currency, status, and client for the given date range. All amounts in cents.

Example request:
curl --request GET \
    --get "https://tidybill.app/api/reports/invoice-summary?start_date=2026-01-01&end_date=2026-12-31&client_id=7" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"start_date\": \"2026-04-05T19:38:23\",
    \"end_date\": \"2052-04-28\",
    \"client_id\": 16
}"
const url = new URL(
    "https://tidybill.app/api/reports/invoice-summary"
);

const params = {
    "start_date": "2026-01-01",
    "end_date": "2026-12-31",
    "client_id": "7",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "start_date": "2026-04-05T19:38:23",
    "end_date": "2052-04-28",
    "client_id": 16
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "totals_by_currency": {
        "USD": {
            "total_invoiced": 600000,
            "total_paid": 480000,
            "total_outstanding": 95000,
            "total_overdue": 25000,
            "invoice_count": 24
        }
    },
    "by_status": {
        "paid": {
            "count": 18,
            "total": 480000
        },
        "sent": {
            "count": 4,
            "total": 95000
        },
        "overdue": {
            "count": 2,
            "total": 25000
        }
    },
    "by_client": [
        {
            "client_id": 7,
            "client_name": "Acme Corp",
            "currency": "USD",
            "invoiced": 300000,
            "paid": 240000,
            "outstanding": 60000
        }
    ]
}
 

Request      

GET api/reports/invoice-summary

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

start_date   string  optional    

Date in YYYY-MM-DD format. Defaults to start of current year. Example: 2026-01-01

end_date   string  optional    

Date in YYYY-MM-DD format. Defaults to end of current year. Example: 2026-12-31

client_id   integer  optional    

Filter to a specific client. Example: 7

Body Parameters

start_date   string  optional    

Must be a valid date. Example: 2026-04-05T19:38:23

end_date   string  optional    

Must be a valid date. Must be a date after or equal to start_date. Example: 2052-04-28

client_id   integer  optional    

Example: 16

Activity Log

List activity log entries

requires authentication

Example request:
curl --request GET \
    --get "https://tidybill.app/api/activity-log?subject_type=invoice&subject_id=1" \
    --header "Authorization: Bearer {YOUR_AUTH_KEY}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"subject_type\": \"architecto\",
    \"subject_id\": 16
}"
const url = new URL(
    "https://tidybill.app/api/activity-log"
);

const params = {
    "subject_type": "invoice",
    "subject_id": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {YOUR_AUTH_KEY}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "subject_type": "architecto",
    "subject_id": 16
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 1,
            "description": "created",
            "event": "created",
            "causer_name": "John",
            "properties": {},
            "created_at": "2026-04-05T00:00:00+00:00"
        }
    ],
    "meta": {
        "current_page": 1,
        "last_page": 1,
        "total": 1
    }
}
 

Example response (422, Invalid subject type):



 

Request      

GET api/activity-log

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

subject_type   string     

The subject type (invoice, quote, credit, recurring-invoice). Example: invoice

subject_id   integer     

The subject ID. Example: 1

Body Parameters

subject_type   string     

Example: architecto

subject_id   integer     

Example: 16