TimeFlov API

API reference · v1

Read your TimeFlov data from your own tools

The TimeFlov API gives your scripts, BI tools and integrations read access to projects, clients, hours, customer invoices and expenses. It is read-only, answers in JSON and only ever sees your own company.

  1. Create a key. An admin creates one in TimeFlov under Admin, API keys, and picks what it may read. The key is shown once, so store it right away.
  2. Send it with every request as a bearer token in the Authorization header.
  3. Page through the results by sending next_cursor back as cursor until it is null.
Base URL
https://api.timeflov.com/v1
Your first request
curl https://api.timeflov.com/v1/projects \
  -H "Authorization: Bearer $TIMEFLOV_API_KEY"

Authentication#

Every request carries a key in the Authorization header. Keys start with tf_live_.

A key reads only what its scopes allow, such as read:projects. A request for anything else is refused with scope_missing.

A key stops working when an admin revokes it, when it passes its expiry date, or when the admin who created it is deactivated or is no longer an admin.

Keep keys on a server. Anyone holding a key can read what it allows, so never put one in a web page or in an app people download.

The API is included in the Enterprise plan and during the free trial.

Header
Authorization: Bearer tf_live_…

Pagination#

Lists come back in id order, 100 rows at a time. Pass limit for up to 200. While next_cursor is not null, send it back as cursor to get the next page. The last page can be empty.

To pick up only what changed, pass updated_since with an ISO 8601 time. Projects, time entries, customer invoices and expenses accept it. Clients do not, so read that list in full.

A deleted row simply stops appearing, so a full read now and then is how a sync notices deletions.

const api = "https://api.timeflov.com/v1";
const key = process.env.TIMEFLOV_API_KEY;
const headers = { Authorization: `Bearer ${key}` };

const rows = [];
let cursor = null;
do {
  const query = new URLSearchParams({ limit: "200" });
  if (cursor) query.set("cursor", cursor);
  const url = `${api}/time_entries?${query}`;
  const res = await fetch(url, { headers });
  if (!res.ok) throw new Error((await res.json()).code);
  const page = await res.json();
  rows.push(...page.data);
  cursor = page.next_cursor;
} while (cursor);

Data formats#

Ids are UUIDs. Dates are YYYY-MM-DD. Times are ISO 8601 in UTC, such as 2026-10-13T14:22:07.512930+00:00.

Amounts are plain numbers in the currency named on the same record, and each amount field says whether it includes VAT. An expense report's total_amount adds its lines without converting them, so read each line's own currency.

Every field listed for a resource is always present. A field without a value is null. New fields may be added to v1, so ignore any you do not recognise.

Rate limits#

Each key may make 600 requests per clock minute. Every answer to a valid key carries X-RateLimit-Limit and X-RateLimit-Remaining.

Past the limit the API answers 429 with a Retry-After header: the seconds until the next minute starts.

Response headers
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 587

Errors#

Every refusal is JSON with a stable code and an error sentence for people. Build on the code, since the sentence may change.

StatusCodeMeaning
400bad_cursorcursor is not an id from next_cursor.
400bad_limitlimit is not a whole number between 1 and 200.
400bad_updated_sinceupdated_since is not an ISO 8601 time, or the resource does not support it.
401invalid_keyThe key is missing, malformed, unknown, revoked or expired.
402plan_requiredThe company's plan does not include the API.
402subscription_inactiveThe company's subscription has lapsed.
403scope_missingThe key may not read this resource.
404unknown_resourceThere is no such resource in v1.
405method_not_allowedOnly GET is supported.
429rate_limitedToo many requests this minute. Wait the seconds in Retry-After.
500read_failedThe read failed. Retry shortly.
Example refusal · 400
{
  "error": "limit must be an integer between 1 and 200.",
  "code": "bad_limit"
}

List projects#

GET/v1/projects

Every project in your company with its billing terms. Rates and prices exclude VAT.

Needs a key with read:projects

Query parameters

ParameterDescription
limitintegerRows per page, 1 to 200. 100 when left out.
cursoruuidThe next_cursor from the previous page.
updated_sincedate-timeOnly projects changed at or after this time.

Fields

FieldDescription
iduuidThe project's id.
client_iduuidThe client the project belongs to, an id from clients.
namestringThe project's name.
descriptionstring or nullFree text about the project.
statusstringWhere the project is in its life.One of active, on_hold, completed or archived.
project_typestringHow the project is billed: by the hour, at a fixed price, or as a recurring fee.One of time_material, fixed_price or retainer.
approval_statestringWhether the project has been approved.One of approved, pending or rejected.
budget_hoursnumberHours budgeted for the project.
used_hoursnumberHours reported on the project so far.
hourly_ratenumberThe project's default hourly billing rate. A price list or a team member's own rate takes precedence when set.
fixed_pricenumber or nullThe agreed price, on a fixed-price project.
retainer_amountnumber or nullThe fee per period, on a retainer project.
retainer_periodstring or nullHow often a retainer is billed.Either monthly or quarterly.
retainer_start_datedate or nullThe first day of the first retainer period.
is_billablebooleanWhether time on the project is billed to the client.
start_datedateThe project's first day.
end_datedate or nullThe project's last day, when one is set.
created_atdate-timeWhen the project was created.
updated_atdate-timeWhen the project last changed. updated_since compares with this.
curl -G https://api.timeflov.com/v1/projects \
  -H "Authorization: Bearer $TIMEFLOV_API_KEY" \
  -d limit=100 \
  -d updated_since=2026-10-01T00:00:00Z
Example response · 200
{
  "data": [
    {
      "id": "5f0c2a9e-41d7-4b8e-9a51-0d6c3e2f7b10",
      "client_id": "a3e91f04-6c2b-4f55-8d0e-7b19c4d2e6a8",
      "name": "Data platform rollout",
      "description": "Phase 2: ingestion and reporting",
      "status": "active",
      "project_type": "time_material",
      "approval_state": "approved",
      "budget_hours": 640,
      "used_hours": 412.5,
      "hourly_rate": 1250,
      "fixed_price": null,
      "retainer_amount": null,
      "retainer_period": null,
      "retainer_start_date": null,
      "is_billable": true,
      "start_date": "2026-02-02",
      "end_date": "2026-12-18",
      "created_at": "2026-01-20T09:14:02.381104+00:00",
      "updated_at": "2026-10-13T14:22:07.512930+00:00"
    }
  ],
  "next_cursor": null
}

List clients#

GET/v1/clients

Your clients and how they are invoiced. Clients have no updated_at, so read the whole list each time.

Needs a key with read:clients

Query parameters

ParameterDescription
limitintegerRows per page, 1 to 200. 100 when left out.
cursoruuidThe next_cursor from the previous page.

Fields

FieldDescription
iduuidThe client's id.
namestringThe client's name.
org_numberstring or nullThe client's organisation number.
contact_namestring or nullThe contact person.
contact_emailstring or nullThe contact person's email address.
addressstring or nullStreet address.
citystring or nullCity.
zip_codestring or nullPostal code.
countrystringCountry code, such as SE.
payment_termsnumberDays until an invoice to this client is due.
currencystringThe currency the client is invoiced in.One of SEK, EUR, USD, GBP, NOK, DKK, BAM, CHF, PLN or CZK.
invoice_emailstring or nullWhere invoices to the client are emailed.
vat_treatmentstringHow VAT applies to the client: Swedish VAT, EU reverse charge, or a buyer outside the EU.One of domestic, eu_reverse_charge or outside_eu.
vat_numberstring or nullThe client's VAT number.
is_activebooleanFalse once the client has been deactivated.
created_atdate-timeWhen the client was created.
curl -G https://api.timeflov.com/v1/clients \
  -H "Authorization: Bearer $TIMEFLOV_API_KEY" \
  -d limit=100
Example response · 200
{
  "data": [
    {
      "id": "a3e91f04-6c2b-4f55-8d0e-7b19c4d2e6a8",
      "name": "Nordvik Energi AB",
      "org_number": "556677-8899",
      "contact_name": "Karin Lund",
      "contact_email": "karin.lund@nordvik.example",
      "address": "Hamngatan 12",
      "city": "Stockholm",
      "zip_code": "111 47",
      "country": "SE",
      "payment_terms": 30,
      "currency": "SEK",
      "invoice_email": "invoices@nordvik.example",
      "vat_treatment": "domestic",
      "vat_number": "SE556677889901",
      "is_active": true,
      "created_at": "2025-11-04T13:40:51.006217+00:00"
    }
  ],
  "next_cursor": null
}

List time entries#

GET/v1/time_entries

Hours reported on your projects, by your team and by subcontractors. Hours appear as soon as they are saved, before the week is submitted or approved.

Needs a key with read:time_entries

Query parameters

ParameterDescription
limitintegerRows per page, 1 to 200. 100 when left out.
cursoruuidThe next_cursor from the previous page.
updated_sincedate-timeOnly time entries changed at or after this time.

Fields

FieldDescription
iduuidThe entry's id.
timesheet_iduuid or nullThe weekly timesheet the hours belong to. Null for hours bought from a subcontractor.
user_iduuid or nullThe person who reported the hours. Null for hours bought from a subcontractor.
project_iduuidThe project, an id from projects.
vendor_iduuid or nullThe subcontractor the hours were bought from. Null for your own team.
datedateThe day the hours were worked.
hoursnumberHours as a decimal: 7.5 is seven and a half hours.
billablebooleanWhether the hours are billed to the client.
descriptionstring or nullWhat was done.
taskstring or nullThe task the hours were reported on, when one was chosen.
invoiced_line_iduuid or nullThe customer invoice line that billed these hours. Null until they are invoiced.
created_atdate-timeWhen the entry was created.
updated_atdate-timeWhen the entry last changed. updated_since compares with this.
curl -G https://api.timeflov.com/v1/time_entries \
  -H "Authorization: Bearer $TIMEFLOV_API_KEY" \
  -d limit=100 \
  -d updated_since=2026-10-01T00:00:00Z
Example response · 200
{
  "data": [
    {
      "id": "e41a7c92-3b5d-4f08-96c1-8a2d4e6f0b73",
      "timesheet_id": "c7d2e5b1-9f34-4a60-b8e2-1d5f7a3c9e04",
      "user_id": "2b8f6d13-0e7a-4c95-a1d4-6f3e9b2c8a57",
      "project_id": "5f0c2a9e-41d7-4b8e-9a51-0d6c3e2f7b10",
      "vendor_id": null,
      "date": "2026-10-12",
      "hours": 7.5,
      "billable": true,
      "description": "Pipeline tests for the meter feed",
      "task": "Development",
      "invoiced_line_id": null,
      "created_at": "2026-10-12T16:05:33.719402+00:00",
      "updated_at": "2026-10-12T16:05:33.719402+00:00"
    }
  ],
  "next_cursor": null
}

List customer invoices#

GET/v1/customer_invoices

Invoices and credit notes to your clients. total_amount includes VAT.

Needs a key with read:customer_invoices

Query parameters

ParameterDescription
limitintegerRows per page, 1 to 200. 100 when left out.
cursoruuidThe next_cursor from the previous page.
updated_sincedate-timeOnly customer invoices changed at or after this time.

Fields

FieldDescription
iduuidThe invoice's id.
client_iduuidThe client invoiced, an id from clients.
invoice_numberstringThe invoice number.
statusstringWhere the invoice is. An unpaid invoice past its due date usually still reads sent, so compare due_date with today to find overdue invoices.One of draft, review, sent, overdue, paid, credited or voided.
invoice_datedateThe invoice date.
due_datedateThe day payment is due.
total_amountnumberThe total including VAT, in the invoice's currency. Negative on a credit note.
vat_amountnumberThe VAT included in total_amount.
currencystringThe invoice's currency.One of SEK, EUR, USD, GBP, NOK, DKK, BAM, CHF, PLN or CZK.
exchange_ratenumberMultiply an amount by this to get it in your company's base currency. 1 when the invoice is in the base currency.
payment_termsnumberDays from the invoice date to the due date.
sent_atdate-time or nullWhen the invoice was issued and booked. Whether the email or e-invoice reached the client is in delivery_status.
paid_atdate-time or nullWhen the invoice was recorded as paid.
credited_invoice_iduuid or nullOn a credit note, the invoice it credits.
vat_treatmentstringHow VAT was applied, fixed when the invoice was issued.One of domestic, eu_reverse_charge or outside_eu.
delivery_statusstringWhere the email or e-invoice delivery is.One of unsent, queued, sending, delivered, pending_provider or failed.
created_atdate-timeWhen the invoice was created.
updated_atdate-timeWhen the invoice last changed. updated_since compares with this.
curl -G https://api.timeflov.com/v1/customer_invoices \
  -H "Authorization: Bearer $TIMEFLOV_API_KEY" \
  -d limit=100 \
  -d updated_since=2026-10-01T00:00:00Z
Example response · 200
{
  "data": [
    {
      "id": "9d3b1f68-27c4-4e9a-b05d-3c8e1a7f6d42",
      "client_id": "a3e91f04-6c2b-4f55-8d0e-7b19c4d2e6a8",
      "invoice_number": "INV-2026-0142",
      "status": "sent",
      "invoice_date": "2026-10-01",
      "due_date": "2026-10-31",
      "total_amount": 156250,
      "vat_amount": 31250,
      "currency": "SEK",
      "exchange_rate": 1,
      "payment_terms": 30,
      "sent_at": "2026-10-01T08:02:11.204918+00:00",
      "paid_at": null,
      "credited_invoice_id": null,
      "vat_treatment": "domestic",
      "delivery_status": "delivered",
      "created_at": "2026-09-30T15:48:20.118352+00:00",
      "updated_at": "2026-10-01T08:02:11.204918+00:00"
    }
  ],
  "next_cursor": null
}

List expense reports#

GET/v1/expenses

Expense reports with their lines. Amounts include VAT.

Needs a key with read:expenses

Query parameters

ParameterDescription
limitintegerRows per page, 1 to 200. 100 when left out.
cursoruuidThe next_cursor from the previous page.
updated_sincedate-timeOnly expense reports changed at or after this time.

Fields

FieldDescription
iduuidThe report's id.
user_iduuidThe person the report belongs to.
titlestringThe report's title.
statusstringWhere the report is. paid means the money has been paid out.One of draft, submitted, approved, rejected or paid.
total_amountnumberThe sum of the lines' amount, including VAT, added without converting between currencies. For a report with lines in more than one currency, total the lines by their own currency instead.
currencystringThe currency set on the report. Each line has its own currency, which can differ.One of SEK, EUR, USD, GBP, NOK, DKK, BAM, CHF, PLN or CZK.
submitted_atdate-time or nullWhen the report was submitted for approval.
approved_atdate-time or nullWhen the report was approved.
paid_atdate-time or nullWhen the money was paid out.
created_atdate-timeWhen the report was created.
updated_atdate-timeWhen the report last changed. updated_since compares with this.

Fields on each line

Each report carries every one of its lines in lines, in id order.

FieldDescription
iduuidThe line's id.
project_iduuidThe project the cost belongs to.
expense_typestringThe kind of expense.One of receipt, mileage, per_diem, credit_card or wellness.
descriptionstringWhat the expense was for.
amountnumberThe amount including VAT, in the line's currency.
vat_amountnumberThe deductible VAT included in amount. Always 0 on mileage and per diem lines.
currencystringThe line's currency.One of SEK, EUR, USD, GBP, NOK, DKK, BAM, CHF, PLN or CZK.
datedateThe day of the expense.
is_billablebooleanWhether the cost is billed on to the client.
is_wellnessbooleanWhether the line counts against the wellness allowance.
mileage_kmnumber or nullKilometres driven, on a mileage line.
invoiced_line_iduuid or nullThe customer invoice line that billed the cost on. Null until it is billed.
curl -G https://api.timeflov.com/v1/expenses \
  -H "Authorization: Bearer $TIMEFLOV_API_KEY" \
  -d limit=100 \
  -d updated_since=2026-10-01T00:00:00Z
Example response · 200
{
  "data": [
    {
      "id": "0a6e4c2f-8b1d-4f39-a7e5-2c9d1b3f6e84",
      "user_id": "2b8f6d13-0e7a-4c95-a1d4-6f3e9b2c8a57",
      "title": "Client workshop in Gothenburg",
      "status": "approved",
      "total_amount": 1840,
      "currency": "SEK",
      "submitted_at": "2026-10-09T07:31:45.660128+00:00",
      "approved_at": "2026-10-10T10:12:03.090551+00:00",
      "paid_at": null,
      "created_at": "2026-10-08T19:20:14.538770+00:00",
      "updated_at": "2026-10-10T10:12:03.090551+00:00",
      "lines": [
        {
          "id": "f2c9b7e4-1a6d-4e05-b8c3-7d4a2f9e1b65",
          "project_id": "5f0c2a9e-41d7-4b8e-9a51-0d6c3e2f7b10",
          "expense_type": "receipt",
          "description": "Train Stockholm to Gothenburg",
          "amount": 1840,
          "vat_amount": 104.15,
          "currency": "SEK",
          "date": "2026-10-08",
          "is_billable": true,
          "is_wellness": false,
          "mileage_km": null,
          "invoiced_line_id": null
        }
      ]
    }
  ],
  "next_cursor": null
}