Export Clockify to JSON: Time Entries API, Detailed Reports, and OCR for PDF Exports
If you want to export Clockify to JSON, the answer depends on what you can reach. With an API key and access to the workspace, Clockify already speaks JSON: GET /v1/workspaces/{workspaceId}/user/{userId}/time-entries returns one user’s entries, and POST /v1/workspaces/{workspaceId}/reports/detailed on the Reports API returns entries across the workspace with project, client, task, and user names already resolved. The product UI is different. Its export button gives you PDF on the Free plan and CSV or Excel on paid plans, and there is no JSON download there. When the only copy of the data is one of those PDFs, a screenshot, or a printed timesheet, OCR is the step that turns it back into records.
This guide covers all three routes and how to land them in one schema. It follows the same split as the Harvest to JSON and Typeform to JSON guides, so if you run more than one tool, the patterns carry over.
Pick the route by what you actually have
| What you have | Route | What comes back |
|---|---|---|
| API key, need one person’s entries | GET .../user/{userId}/time-entries |
Array of entries with IDs |
| API key, need the whole workspace or filtered by client/project | POST .../reports/detailed with "exportType": "JSON" |
timeEntries plus totals |
| Paid plan, need a spreadsheet | Reports, then Export, then CSV or Excel | Tabular file |
| Free plan UI only, or a PDF someone emailed | Detailed report PDF, then OCR | Markdown or typed JSON |
| Screenshot or paper timesheet | OCR | Markdown or typed JSON |
The first two rows are the clean path. Use OCR only for files that never had an API behind them, or whose workspace you can no longer reach.
Route 1: time entries for a single user
Clockify authenticates with an X-Api-Key header, generated in your profile settings. The Clockify API docs note that workspaces on a subdomain need a key generated specifically for that workspace, and regional workspaces use a prefixed host such as euc1.clockify.me for the EU.
You need two IDs. GET /v1/user returns your own user, including id and activeWorkspace. Then list entries:
curl "https://api.clockify.me/api/v1/workspaces/$WORKSPACE_ID/user/$USER_ID/time-entries?start=2026-10-01T00:00:00Z&end=2026-10-31T23:59:59Z&page=1&page-size=50&hydrated=true" \
-H "X-Api-Key: $CLOCKIFY_API_KEY"
What to expect from this endpoint, based on the published OpenAPI spec:
- Each entry carries
id,description,billable,projectId,taskId,tagIds,type(REGULARorBREAK),isLocked, and atimeIntervalwithstart,end, andduration. durationhere is an ISO 8601 string such asPT1H30M, not a number of hours. Convert it before you sum anything.- The base response uses IDs (
projectId,taskId,tagIds), not names. Thehydrated=trueflag asks for additional information on each entry; check what your workspace returns before you drop your own project and tag lookups. - Paging uses
pageandpage-size(default 50). The docs say paginated responses include aLast-Pageheader set totrueorfalse, so loop until it readstrueinstead of guessing a page count. - Filters include
start,end,project,task,tags,description, andin-progress.
A small loop that writes everything to one JSON file:
import os, json, requests
KEY = os.environ["CLOCKIFY_API_KEY"]
WS, USER = os.environ["WORKSPACE_ID"], os.environ["USER_ID"]
url = f"https://api.clockify.me/api/v1/workspaces/{WS}/user/{USER}/time-entries"
entries, page = [], 1
while True:
r = requests.get(url, headers={"X-Api-Key": KEY}, params={
"start": "2026-10-01T00:00:00Z", "end": "2026-10-31T23:59:59Z",
"page": page, "page-size": 50, "hydrated": "true"})
r.raise_for_status()
entries += r.json()
if r.headers.get("Last-Page", "true") == "true":
break
page += 1
json.dump(entries, open("clockify-entries.json", "w"), indent=2)
This is the right tool when someone wants their own history, or when you sync one user at a time. For a team export it gets slow, which leads to the Reports API.
Route 2: the detailed report as JSON
The Reports API lives on a separate host, reports.api.clockify.me, and takes a POST body instead of query parameters. Setting exportType to JSON returns data your code can read directly. The same field also accepts CSV, XLSX, and PDF, as Clockify’s export via API article shows.
curl -X POST "https://reports.api.clockify.me/v1/workspaces/$WORKSPACE_ID/reports/detailed" \
-H "X-Api-Key: $CLOCKIFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"dateRangeStart": "2026-10-01T00:00:00.000",
"dateRangeEnd": "2026-10-31T23:59:59.000",
"exportType": "JSON",
"detailedFilter": { "page": 1, "pageSize": 1000 }
}'
The response has a timeEntries array and a totals block. Report entries are friendlier than raw entries: they include projectName, clientName, taskName, userName, userEmail, tags, billable, and approvalRequestId, so you rarely need extra lookups. Note that timeInterval.duration in the report schema is an integer, not the ISO string from Route 1. Keep that difference in your parser, or you will add "PT1H30M" to 5400 and get nonsense.
You can narrow the report with users, projects, clients, and tags filters, each shaped like { "ids": [...], "contains": "CONTAINS", "status": "ALL" }. approvalState (APPROVED, UNAPPROVED, ALL) and invoicingState are useful for payroll and billing runs.
Clockify’s help on large detailed reports recommends pageSize: 1000 and stepping page up until you run out of entries.
Limits that change the plan
- Free plan date window. The API reference says detailed, summary, and weekly report data on the Free plan is limited to 31 days per request. Split a year into monthly calls.
- Free plan rate limit. API keys on a Free workspace get 30 requests per hour. Paid plans get 20 per second. A year of monthly reports with a few pages each fits inside an hour on Free, but a careless retry loop will not.
- Time zones.
dateRangeStartanddateRangeEndare interpreted in the user’s time zone, either from the request’stimeZoneor the profile. Pin it if your users span zones, or entries near midnight will move between days. - Permissions. Regular members see their own entries. Admins see everyone’s. A report pulled with a member’s key will quietly be missing other people’s time.
Route 3: the UI export and why it is not JSON
In the app, you open a report, click Export, and pick a format. According to Clockify’s export reports help, Free workspaces can export PDF only, while CSV and Excel need the Basic plan or higher. A PDF holds up to 3,000 rows, and Free exports cover up to 31 days per report. Summary exports drop timestamps, so use the Detailed report when you need start and end times.
CSV is easy to convert in a few lines of code if a paid plan is available. The awkward case is the Free workspace where someone already downloaded months of PDFs, or a former contractor sends you their Clockify PDFs after the workspace is gone. That is pixel data, and it is where OCR fits.
When OCR is the right step
Use OCRskill for Clockify data only when there is no API to call:
- Detailed or Summary report PDFs exported from a Free workspace or attached to an invoice
- Screenshots of the Timesheet or Calendar view sent in chat
- Printed weekly timesheets that someone filled in or signed by hand
- Client PDFs that look like Clockify reports but come from a workspace you cannot access
For a report table, Markdown is usually the better first output. Every row stays as a table row, so you can parse date, project, description, and duration in your own code and check the sum against the report total:
curl https://api.ocrskill.com/get-key.json
curl https://api.ocrskill.com/ocr \
-H "Authorization: Bearer sk-your-key-here" \
-F "file=@clockify-detailed-report-oct.pdf"
When the page is more like a header plus a few values, such as an invoice built from Clockify time, structured extraction with POST /ocr.json?fields=... returns named JSON keys and fails closed with a 400 if a required field is missing. Mark optional keys with a trailing ?. The published field catalog does not promise hours, project, or task keys for every timesheet layout, so do not assume them. Check the OCR JSON API reference for the current list, and see structured OCR JSON for how fields map to output.
Two checks make OCR’d timesheets trustworthy. First, re-add the parsed durations and compare them with the report’s printed total. Second, flag any row where the date falls outside the report’s printed range. A mismatch usually means a row was split across a page break or a duration like 1:05:00 was read as 1:50:00.
One schema for both sources
Pick one record shape and adapt everything into it:
{
"source": "clockify_api | clockify_report | ocr",
"user": "ana@example.com",
"date": "2026-10-14",
"start": "2026-10-14T09:00:00Z",
"end": "2026-10-14T10:30:00Z",
"duration_seconds": 5400,
"project": "Website redesign",
"client": "Acme",
"task": "QA",
"description": "Regression pass on checkout",
"billable": true,
"tags": ["sprint-42"]
}
Map Route 1’s ISO duration and Route 2’s integer duration into duration_seconds. Keep source on every row so reviewers know which values came from OCR and may need a second look. Validate the record with whatever your stack already uses (Pydantic, Zod, JSON Schema) before it reaches payroll or invoicing.
Conclusion
The fastest way to export Clockify to JSON is almost always the Reports API: one POST with exportType: "JSON" gets workspace-wide entries with names resolved, and monthly windows keep you inside the Free plan’s 31-day limit. The per-user time entries endpoint fits smaller syncs, as long as you handle its ISO durations and follow the Last-Page header. The part Clockify does not solve is the backlog of PDF exports that Free workspaces produce, plus screenshots and paper sheets. For those, run OCR, check the parsed rows against the printed totals, and feed them into the same schema as your API data. Get a free OCRskill key when you reach that pile, and keep the Clockify API docs open for everything that still lives in a workspace.
