# Create Client
Source: https://developers.everhour.com/api-reference/clients/create-client
/openapi.json post /clients
Create a new client. Names are not deduplicated — posting the same name creates another client.
# Delete Client
Source: https://developers.everhour.com/api-reference/clients/delete-client
/openapi.json delete /clients/{client_id}
Delete a client.
# Delete Client Budget
Source: https://developers.everhour.com/api-reference/clients/delete-client-budget
/openapi.json delete /clients/{client_id}/budget
Remove the budget attached to a client. The client itself is kept.
# Get All Clients
Source: https://developers.everhour.com/api-reference/clients/get-all-clients
/openapi.json get /clients
Search Clients by Name using the query parameter.
# Get Client
Source: https://developers.everhour.com/api-reference/clients/get-client
/openapi.json get /clients/{client_id}
Retrieve a single client by ID.
# Update Client
Source: https://developers.everhour.com/api-reference/clients/update-client
/openapi.json put /clients/{client_id}
Update an existing client. Only the fields supplied in the body are modified.
# Update Client Budget
Source: https://developers.everhour.com/api-reference/clients/update-client-budget
/openapi.json put /clients/{client_id}/budget
Create or replace the budget attached to a client. Send a complete budget object; partial updates are not supported.
# Create Field in Project
Source: https://developers.everhour.com/api-reference/custom-fields/create-field-in-project
/openapi.json post /projects/{project_id}/fields
Create a new custom field on a project.
# Delete Field
Source: https://developers.everhour.com/api-reference/custom-fields/delete-field
/openapi.json delete /fields/{field_id}
Delete a custom field. Existing task values for that field are discarded.
# Get Project Fields Configuration
Source: https://developers.everhour.com/api-reference/custom-fields/get-project-fields-configuration
/openapi.json get /projects/{project_id}/fields
List the custom fields configured on a project, in display order.
# Reorder Fields
Source: https://developers.everhour.com/api-reference/custom-fields/reorder-fields
/openapi.json put /projects/{project_id}/fields-order
Reorder a project's custom fields. Send the full list of field IDs in the desired order.
# Update Field
Source: https://developers.everhour.com/api-reference/custom-fields/update-field
/openapi.json put /fields/{field_id}
Update a custom field's name, type-specific options, or visibility.
# Add Attachment To Expense
Source: https://developers.everhour.com/api-reference/expenses/add-attachment-to-expense
/openapi.json post /expenses/{expense_id}/attachments
Attach a previously uploaded file (by its token from `POST /attachments`) to an existing expense.
# Create Attachment
Source: https://developers.everhour.com/api-reference/expenses/create-attachment
/openapi.json post /attachments
Upload a file and receive a one-time attachment token. The token can then be attached to an expense via `POST /expenses/{expense_id}/attachments` or included in `POST /expenses`.
# Create Category
Source: https://developers.everhour.com/api-reference/expenses/create-category
/openapi.json post /expenses/categories
Create a new expense category.
# Create Expense
Source: https://developers.everhour.com/api-reference/expenses/create-expense
/openapi.json post /expenses
Log a new expense. Amounts are in cents (see [Concepts](/concepts)); attach files first via `POST /attachments` and reference their IDs in the request body.
# Delete Attachment
Source: https://developers.everhour.com/api-reference/expenses/delete-attachment
/openapi.json delete /attachments/{attachment_id}
Delete an attachment and its underlying file.
# Delete Category
Source: https://developers.everhour.com/api-reference/expenses/delete-category
/openapi.json delete /expenses/categories/{category_id}
Delete an expense category. Expenses already assigned to it are kept but become uncategorized.
# Delete Expense
Source: https://developers.everhour.com/api-reference/expenses/delete-expense
/openapi.json delete /expenses/{expense_id}
Delete an expense entry. Attached files are also removed.
# Download Attachment
Source: https://developers.everhour.com/api-reference/expenses/download-attachment
/openapi.json get /attachments/{attachment_token}/download
Download the binary contents of an attachment by its token. The response is the raw file, not JSON.
# Get All Categories
Source: https://developers.everhour.com/api-reference/expenses/get-all-categories
/openapi.json get /expenses/categories
List expense categories available in the team.
# Get All Expenses
Source: https://developers.everhour.com/api-reference/expenses/get-all-expenses
/openapi.json get /expenses
List expenses logged by the authenticated user.
# Update Category
Source: https://developers.everhour.com/api-reference/expenses/update-category
/openapi.json put /expenses/categories/{category_id}
Update an expense category.
# Update Expense
Source: https://developers.everhour.com/api-reference/expenses/update-expense
/openapi.json put /expenses/{expense_id}
Update an existing expense entry.
# Create Invoice
Source: https://developers.everhour.com/api-reference/invoices/create-invoice
/openapi.json post /clients/{client_id}/invoices
Create an invoice for a client. The invoice is generated from the client's tracked time over the requested date range — see [Dates](/dates-and-timezones).
# Delete Invoice
Source: https://developers.everhour.com/api-reference/invoices/delete-invoice
/openapi.json delete /invoices/{invoice_id}
Delete an invoice. Time records attached to the invoice are released and become billable again.
# Export Invoice to Xero/QB/FB
Source: https://developers.everhour.com/api-reference/invoices/export-invoice-to-xeroqbfb
/openapi.json post /invoices/{invoice_id}/export
Export an invoice to a connected external accounting system.
Supports Xero, QuickBooks, and FreshBooks integrations — the destination must be linked to the team beforehand. An invoice can only be exported once; subsequent calls return an error.
# Get All Invoices
Source: https://developers.everhour.com/api-reference/invoices/get-all-invoices
/openapi.json get /invoices
List invoices in the team. Supports filtering by date and client.
# Get Invoice
Source: https://developers.everhour.com/api-reference/invoices/get-invoice
/openapi.json get /invoices/{invoice_id}
Retrieve a single invoice with its full line-item breakdown.
# Refresh Invoice Line Items
Source: https://developers.everhour.com/api-reference/invoices/refresh-invoice-line-items
/openapi.json post /invoices/{invoice_id}/reset-time
Refresh an invoice's line items from current time records.
The invoice's billable items are rebuilt from the underlying time tracked against the invoice's client over its date range. Useful when time records have been added, edited, or deleted after the invoice was first generated.
# Update Invoice
Source: https://developers.everhour.com/api-reference/invoices/update-invoice
/openapi.json put /invoices/{invoice_id}
Update invoice metadata (notes, dates, custom totals). To change the lifecycle state, set `manualStatus` to `draft`, `sent`, or `paid`.
# Archive/Unarchive Project
Source: https://developers.everhour.com/api-reference/projects/archiveunarchive-project
/openapi.json patch /projects/{project_id}/archive
Archive or unarchive a project. Archived projects are hidden from default listings but their data is preserved.
# Create Project
Source: https://developers.everhour.com/api-reference/projects/create-project
/openapi.json post /projects
Create a new project. To copy from an existing template use the `template` or `publicTemplate` body field. To sync a project from a connected integration use `POST /projects/{project_id}/sync` instead.
# Create Section
Source: https://developers.everhour.com/api-reference/projects/create-section
/openapi.json post /projects/{project_id}/sections
Create a new section in a project.
# Delete Project
Source: https://developers.everhour.com/api-reference/projects/delete-project
/openapi.json delete /projects/{project_id}
Delete a project, its tasks, and all associated time records. This is irreversible.
# Delete Section
Source: https://developers.everhour.com/api-reference/projects/delete-section
/openapi.json delete /sections/{section_id}
Delete a section. Tasks in the section are kept but become section-less.
# Get All Projects
Source: https://developers.everhour.com/api-reference/projects/get-all-projects
/openapi.json get /projects
List projects the caller has access to. Use the `query` parameter to search by name and `limit`/`page` for pagination — see [Pagination](/pagination).
# Get Project
Source: https://developers.everhour.com/api-reference/projects/get-project
/openapi.json get /projects/{project_id}
Retrieve a single project. See [Concepts](/concepts) for the project model.
# Get Project Sections
Source: https://developers.everhour.com/api-reference/projects/get-project-sections
/openapi.json get /projects/{project_id}/sections
List sections (groupings of tasks) within a project.
# Get Section
Source: https://developers.everhour.com/api-reference/projects/get-section
/openapi.json get /sections/{section_id}
Retrieve a single section by ID.
# Sync Integration Project
Source: https://developers.everhour.com/api-reference/projects/sync-integration-project
/openapi.json post /projects/{project_id}/sync
Sync new integration projects to Everhour.
Traditionally, project sync relied on background jobs which could delay access to Everhour functionality.
This endpoint allows instant synchronization of a project from a connected integration (like Trello, Asana, ClickUp, etc.) into Everhour.
Useful for automation flows where you want to start working with a project immediately after creating it in your external tool.
Safe to call multiple times — if the project already exists in Everhour, it simply returns it without creating duplicates.
**Workflow**
- Create a project in your external tool (Asana, Trello, etc.) and retrieve its ID.
- Sync it to Everhour using this endpoint in Everhour API.
- Proceed with other Everhour API actions — assign a budget, link a client, or set task estimates.
Supported platform codes:
as, b2, b3, bb, cl, gh, gl, in, li, mo, no, td, tw, tr, wr.
# Update Project
Source: https://developers.everhour.com/api-reference/projects/update-project
/openapi.json put /projects/{project_id}
Update project settings. To change budget or billable rate, use `PUT /projects/{project_id}/billing` instead.
# Update Project Billing/Budget
Source: https://developers.everhour.com/api-reference/projects/update-project-billingbudget
/openapi.json put /projects/{project_id}/billing
Set or update the project's budget, billable rate, and billing type in a single call. Amounts are in cents (see [Concepts](/concepts)).
# Update Section
Source: https://developers.everhour.com/api-reference/projects/update-section
/openapi.json put /sections/{section_id}
Update a section's name or position.
# Clients Report
Source: https://developers.everhour.com/api-reference/reports/clients-report
/openapi.json get /dashboards/clients
Aggregated client report. Same filters and units as `GET /dashboards/projects` — see [Concepts](/concepts).
# Estimates Report (deprecated)
Source: https://developers.everhour.com/api-reference/reports/estimates-report-deprecated
/openapi.json get /team/estimate/export
Legacy export of team estimates, returned as JSON. Requires admin access. Kept for backward compatibility; new integrations should use the dashboard reports.
# Projects Report
Source: https://developers.everhour.com/api-reference/reports/projects-report
/openapi.json get /dashboards/projects
Aggregated project report.
Returns time, billing, and budget figures grouped by project. Filter the result with `date.gte`, `date.lte`, `projectId`, `clientId`, and `memberId` query parameters — see [Dates](/dates-and-timezones) for parameter formats. Time columns are in seconds, amounts in cents (see [Concepts](/concepts)).
# Time Report (deprecated)
Source: https://developers.everhour.com/api-reference/reports/time-report-deprecated
/openapi.json get /team/time/export
Legacy export of team time records, returned as JSON. Kept for backward compatibility; new integrations should use `GET /team/time` or the dashboard reports.
# Users Report
Source: https://developers.everhour.com/api-reference/reports/users-report
/openapi.json get /dashboards/users
Aggregated member report. Same filters and units as `GET /dashboards/projects` — see [Concepts](/concepts).
# Create Assignment
Source: https://developers.everhour.com/api-reference/schedule/create-assignment
/openapi.json post /resource-planner/assignments
Create a schedule assignment. To create time-off use `POST /resource-planner/assignments/time-off` instead.
# Delete Assignment
Source: https://developers.everhour.com/api-reference/schedule/delete-assignment
/openapi.json delete /resource-planner/assignments/{assignment_id}
Delete a schedule assignment. You can include an optional `reason` (max 1000 characters) in the request body.
# Get All Assignments
Source: https://developers.everhour.com/api-reference/schedule/get-all-assignments
/openapi.json get /resource-planner/assignments
List schedule assignments across the team. Supports filtering by date range, project, and member.
# Update Assignment
Source: https://developers.everhour.com/api-reference/schedule/update-assignment
/openapi.json put /resource-planner/assignments/{assignment_id}
Update an existing schedule assignment.
# Create Task
Source: https://developers.everhour.com/api-reference/tasks/create-task
/openapi.json post /projects/{project_id}/tasks
Create a task in a project. For tasks coming from connected integrations, sync them via `POST /projects/{project_id}/sync` instead.
# Delete Task
Source: https://developers.everhour.com/api-reference/tasks/delete-task
/openapi.json delete /tasks/{task_id}
Delete a task and all of its time records.
# Delete Task Estimate
Source: https://developers.everhour.com/api-reference/tasks/delete-task-estimate
/openapi.json delete /tasks/{task_id}/estimate
Remove a task's estimate.
# Get Project Tasks
Source: https://developers.everhour.com/api-reference/tasks/get-project-tasks
/openapi.json get /projects/{project_id}/tasks
List tasks in a project. By default all tasks are returned; set `exclude-closed=true` to hide closed/completed tasks. See [Pagination](/pagination) for `limit`/`page` rules.
# Get Task
Source: https://developers.everhour.com/api-reference/tasks/get-task
/openapi.json get /tasks/{task_id}
Retrieve a single task with its time totals, estimate, and custom fields.
# Search Project Tasks
Source: https://developers.everhour.com/api-reference/tasks/search-project-tasks
/openapi.json get /projects/{project_id}/tasks/search
Search tasks within a single project. Same query semantics as `GET /tasks/search`; `limit` must be between 1 and 100.
# Search Tasks
Source: https://developers.everhour.com/api-reference/tasks/search-tasks
/openapi.json get /tasks/search
Search tasks across all projects by name. The `limit` parameter must be between 1 and 100.
# Update Task
Source: https://developers.everhour.com/api-reference/tasks/update-task
/openapi.json put /tasks/{task_id}
Update task fields (name, status, due date, section, labels).
# Update Task Billing
Source: https://developers.everhour.com/api-reference/tasks/update-task-billing
/openapi.json put /tasks/{task_id}/billing
Set a task's billing rate or mark it non-billable. Requires admin access. The same values are returned on task responses when you pass `opts_include_billing=1`.
# Update Task Estimate
Source: https://developers.everhour.com/api-reference/tasks/update-task-estimate
/openapi.json put /tasks/{task_id}/estimate
Set or replace a task's estimate (in seconds). Send a complete estimate object; partial updates are not supported.
# Create Allocation
Source: https://developers.everhour.com/api-reference/time-off/create-allocation
/openapi.json post /allocations
Create a time-off allocation for a user and time-off type.
# Create Time Off Type
Source: https://developers.everhour.com/api-reference/time-off/create-time-off-type
/openapi.json post /resource-planner/time-off-types
Create a new time-off type.
# Delete Allocation
Source: https://developers.everhour.com/api-reference/time-off/delete-allocation
/openapi.json delete /allocations/{allocation_id}
Delete a time-off allocation.
# Delete Time Off Type
Source: https://developers.everhour.com/api-reference/time-off/delete-time-off-type
/openapi.json delete /resource-planner/time-off-types/{type_id}
Delete a time-off type. Existing time-off entries that reference it are kept.
# Get All Allocations
Source: https://developers.everhour.com/api-reference/time-off/get-all-allocations
/openapi.json get /allocations
List per-user time-off allocations (e.g. annual vacation balances).
# Get Time Off Types
Source: https://developers.everhour.com/api-reference/time-off/get-time-off-types
/openapi.json get /resource-planner/time-off-types
List the time-off types defined for the team (e.g. Vacation, Sick Leave).
# Update Allocation
Source: https://developers.everhour.com/api-reference/time-off/update-allocation
/openapi.json put /allocations/{allocation_id}
Update a time-off allocation's amount or period.
# Update Time Off Type
Source: https://developers.everhour.com/api-reference/time-off/update-time-off-type
/openapi.json put /resource-planner/time-off-types/{type_id}
Update a time-off type's name, color, or settings.
# Add Time
Source: https://developers.everhour.com/api-reference/time-records/add-time
/openapi.json post /time
Add a time record (in seconds) for a user on a given task and date.
**Upsert:** at most one time record exists per `(user, date, task)`. Posting again with the same combination updates the existing record's duration instead of creating a duplicate. To update a specific record by ID, use `PUT /time/{time_id}`.
See [Dates](/dates-and-timezones) for accepted date and duration formats.
# Delete Time Record
Source: https://developers.everhour.com/api-reference/time-records/delete-time-record
/openapi.json delete /time/{time_id}
Remove a time record by setting its duration to zero.
The history trail is preserved; the row no longer appears in time listings, which filter for `time > 0`.
# Get All Time Records
Source: https://developers.everhour.com/api-reference/time-records/get-all-time-records
/openapi.json get /team/time
List time records across the entire team for a date range. When `from`/`to` are omitted, only the current day is returned. See [Dates](/dates-and-timezones).
# Get Project Time Records
Source: https://developers.everhour.com/api-reference/time-records/get-project-time-records
/openapi.json get /projects/{project_id}/time
List time records logged against a single project, across all users and tasks.
# Get Task Time Records
Source: https://developers.everhour.com/api-reference/time-records/get-task-time-records
/openapi.json get /tasks/{task_id}/time
List time records logged against a single task, across all users.
# Get User Time Records
Source: https://developers.everhour.com/api-reference/time-records/get-user-time-records
/openapi.json get /users/{user_id}/time
List time records for a single user. Supports the same filters as `GET /team/time`.
# Update Time Record
Source: https://developers.everhour.com/api-reference/time-records/update-time-record
/openapi.json put /time/{time_id}
Update a specific time record by ID. To add or upsert time, use `POST /time` instead.
# Clock In
Source: https://developers.everhour.com/api-reference/timecards/clock-in
/openapi.json post /users/{user_id}/timecards/clock-in
Clock a user in. Starts a new timecard segment at the current time.
# Clock Out
Source: https://developers.everhour.com/api-reference/timecards/clock-out
/openapi.json post /users/{user_id}/timecards/clock-out
Clock a user out. Closes the current timecard segment.
# Delete Timecard
Source: https://developers.everhour.com/api-reference/timecards/delete-timecard
/openapi.json delete /users/{user_id}/timecards/{date}
Delete a user's timecard entry for a specific date.
# Get All Timecards
Source: https://developers.everhour.com/api-reference/timecards/get-all-timecards
/openapi.json get /timecards
List timecards across the team. Defaults to the last two weeks when no date range is provided — see [Dates](/dates-and-timezones).
# Get Timecard
Source: https://developers.everhour.com/api-reference/timecards/get-timecard
/openapi.json get /users/{user_id}/timecards/{date}
Retrieve a user's timecard for a specific date.
# Get User Timecards
Source: https://developers.everhour.com/api-reference/timecards/get-user-timecards
/openapi.json get /users/{user_id}/timecards
List timecards for a single user. Defaults to the last two weeks when no date range is provided.
# Update Timecard
Source: https://developers.everhour.com/api-reference/timecards/update-timecard
/openapi.json put /users/{user_id}/timecards/{date}
Update (or create, if missing) a user's timecard for a specific date. Suitable for manual edits — clock-in and clock-out have dedicated endpoints.
# Get All Team Timers
Source: https://developers.everhour.com/api-reference/timers/get-all-team-timers
/openapi.json get /team/timers
List the timers currently running across the team — one entry per active user.
# Get Running Timer
Source: https://developers.everhour.com/api-reference/timers/get-running-timer
/openapi.json get /timers/current
Retrieve the authenticated user's currently running timer, or an empty timer payload if none is running.
# Start Timer
Source: https://developers.everhour.com/api-reference/timers/start-timer
/openapi.json post /timers
Start a timer for the authenticated user on a given task.
Only one timer can run per user at a time. If the user already has a timer running on another task, that timer is stopped automatically before the new one starts; the stopped run is committed to a time record on its own date (the date the timer was started, which may not be today for cross-midnight runs).
See [Concepts](/concepts) for timer semantics.
# Stop Timer
Source: https://developers.everhour.com/api-reference/timers/stop-timer
/openapi.json delete /timers/current
Stop the authenticated user's currently running timer.
The accumulated duration is committed to a time record on the timer's task for the current date and returned in the response.
# Approve or Reject Approval Request
Source: https://developers.everhour.com/api-reference/timesheets/approve-or-reject-approval-request
/openapi.json put /timesheets/{timesheet_id}/approval
Approve or reject an existing timesheet approval request.
Use this endpoint to act on a request previously created with `POST /timesheets/{timesheet_id}/approval`. The response reflects the resulting approval state.
# Approve Week/Request for Approval
Source: https://developers.everhour.com/api-reference/timesheets/approve-weekrequest-for-approval
/openapi.json post /timesheets/{timesheet_id}/approval
Submit a timesheet week for approval.
The week's owner uses this endpoint to request review; an approver acts on the request via `PUT /timesheets/{timesheet_id}/approval`.
The `timesheet_id` is the concatenation of `user_id` and a 4-digit `week_id` in `YYWW` format (no separator). Example: user `14856` for week `2535` (week 35 of 2025) → `timesheet_id = 148562535`. See the Timesheets overview for the full Week ID definition.
# Discard Your Approval Request
Source: https://developers.everhour.com/api-reference/timesheets/discard-your-approval-request
/openapi.json put /timesheets/{timesheet_id}/discard-approval
Retract the caller's previously submitted approval request for a week. The week returns to draft state.
# Get Team Timesheets
Source: https://developers.everhour.com/api-reference/timesheets/get-team-timesheets
/openapi.json get /timesheets
List timesheet weeks across the team, grouped by user.
# Get User Timesheets
Source: https://developers.everhour.com/api-reference/timesheets/get-user-timesheets
/openapi.json get /users/{user_id}/timesheets
List timesheet weeks for a single user. The week id is the Monday date of the week (see [Dates](/dates-and-timezones)).
# Get All Users
Source: https://developers.everhour.com/api-reference/users/get-all-users
/openapi.json get /team/users
List all members of the team. Supports `query` for name search and `limit` for pagination — see [Pagination](/pagination).
# Get Current User
Source: https://developers.everhour.com/api-reference/users/get-current-user
/openapi.json get /users/me
Retrieve the user that owns the API key, including personal settings (timezone, formats, API key).
# Create Webhook
Source: https://developers.everhour.com/api-reference/webhooks/create-webhook
/openapi.json post /hooks
Register a new webhook subscription.
**Handshake:** on create, Everhour sends a POST to the supplied `targetUrl` with an `X-Hook-Secret` header (HMAC-SHA512) and an empty body. The receiver must respond with a 2xx status to complete verification. Send `X-Skip-Handshake: 1` to skip verification when the receiver cannot participate.
**Scope:** a webhook can listen across the whole team or be scoped to a single project. See [Webhooks](/webhooks) for event types, payload structure, and delivery semantics.
# Delete Webhook
Source: https://developers.everhour.com/api-reference/webhooks/delete-webhook
/openapi.json delete /hooks/{hook_id}
Delete a webhook subscription. Stops further deliveries; no event is sent for the deletion itself.
# Get All Webhooks
Source: https://developers.everhour.com/api-reference/webhooks/get-all-webhooks
/openapi.json get /hooks
List all webhook subscriptions created with your API key.
# Get Webhook
Source: https://developers.everhour.com/api-reference/webhooks/get-webhook
/openapi.json get /hooks/{hook_id}
Retrieve a single webhook subscription.
# Update Webhook
Source: https://developers.everhour.com/api-reference/webhooks/update-webhook
/openapi.json put /hooks/{hook_id}
Update a webhook's events or scope. The `targetUrl` cannot be changed and is ignored if sent; the handshake is repeated against the existing URL to confirm it is still reachable. Send `X-Skip-Handshake: 1` to skip it.
# Authentication
Source: https://developers.everhour.com/authentication
How to authenticate requests to the Everhour API using an API key.
All API requests must be authenticated. Everhour supports two methods: an **API key** for your own scripts and server-to-server jobs, and **[OAuth 2.1](/oauth)** for apps that act on behalf of another user. This page covers the API key — pass it in the `X-Api-Key` request header.
## Getting your API key
1. Sign in to your Everhour account.
2. Go to your [profile page](https://app.everhour.com/#/account/profile).
3. Scroll to the bottom — your API key is shown there.
## Using the API key
Include the key in every request:
```http theme={null}
X-Api-Key: YOUR_API_KEY
```
You can also pass the key as an `api_key` query parameter (for example, `?api_key=YOUR_API_KEY`). The `X-Api-Key` header is recommended — query strings can be recorded in server logs and browser history.
**Example:**
```bash Bash/cURL theme={null}
curl https://api.everhour.com/users/me \
-H "X-Api-Key: YOUR_API_KEY"
```
```ruby Ruby theme={null}
require "net/http"
uri = URI("https://api.everhour.com/users/me")
req = Net::HTTP::Get.new(uri)
req["X-Api-Key"] = "YOUR_API_KEY"
resp = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
puts resp.body
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.everhour.com/users/me",
headers={"X-Api-Key": "YOUR_API_KEY"}
)
print(response.json())
```
```php PHP theme={null}
$ch = curl_init("https://api.everhour.com/users/me");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-Api-Key: YOUR_API_KEY"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
echo curl_exec($ch);
curl_close($ch);
```
```java Java theme={null}
import java.net.URI;
import java.net.http.*;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.everhour.com/users/me"))
.header("X-Api-Key", "YOUR_API_KEY")
.GET()
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```
```javascript JavaScript theme={null}
const response = await fetch("https://api.everhour.com/users/me", {
headers: { "X-Api-Key": "YOUR_API_KEY" }
});
console.log(await response.json());
```
```go Go theme={null}
// import "fmt"; "io"; "net/http"
req, _ := http.NewRequest("GET", "https://api.everhour.com/users/me", nil)
req.Header.Set("X-Api-Key", "YOUR_API_KEY")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
```
```csharp .NET theme={null}
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-Api-Key", "YOUR_API_KEY");
var body = await client.GetStringAsync("https://api.everhour.com/users/me");
Console.WriteLine(body);
```
## What happens without a valid key
If the `X-Api-Key` header is missing or the key is invalid, the API returns:
```http theme={null}
HTTP/1.1 403 Forbidden
```
```json theme={null}
{
"code": 403,
"message": "Access denied"
}
```
## Security recommendations
* Store the API key in environment variables, not in source code.
* Rotate the key if you suspect it has been compromised — you can regenerate it from your profile page.
* Each API key is tied to a specific user account and inherits that user's permissions.
Everhour issues one API key per user account. For apps that act on behalf of other users, use [OAuth 2.1](/oauth) instead of sharing a key. Fine-grained scopes aren't available with either method — access follows the user's role.
# Key concepts
Source: https://developers.everhour.com/concepts
Core resources in the Everhour data model — what they are and how they relate.
Before diving into individual endpoints, it helps to understand the vocabulary used throughout the API and these docs.
## Team
The top-level tenant boundary. Every user belongs to exactly one team. Resources such as projects, clients, and invoices are scoped to a team. Your API key inherits the team context of the user account it belongs to.
## Project
A container for work. Projects have members, tasks, budget settings, and billing configuration. A project may be linked to a client. The `GET /projects` endpoint lists all projects accessible to the authenticated user.
## Task
The primary unit of work inside a project. Tasks have assignees, an estimate, a status, and they receive time records. Tasks may be organized into sections.
## Section
A grouping of tasks within a project — equivalent to a column or swimlane in a board view.
## Time record
A logged time entry. Each time record is attached to a task and carries a `date` (date-only) and a `time` value in **seconds**. It may also hold a comment and a billable flag.
## Timer
A running, in-progress time record. Starting a timer creates an active entry; stopping it finalizes the `time` duration and saves the record. Only one timer can be active per user at a time.
## Client
A billing-side entity. Projects may be linked to a client for reporting and invoicing purposes.
## Invoice
A billing document generated from time records and expenses for a client. Invoices are created, updated, and tracked through the API.
## Expense
A non-time cost attached to a project (for example, a software subscription or travel cost). Expenses have an `amount` (integer, in the smallest currency unit) and a `date`.
## Webhook
An outbound HTTP callback. Users can subscribe their own endpoint to receive event notifications when resources change — for example, when a task is created or a timer is stopped. See [Webhooks](/webhooks) for setup and event reference.
# Dates and timezones
Source: https://developers.everhour.com/dates-and-timezones
Date and time formats used in Everhour API requests and responses, including timezone handling and duration encoding.
The Everhour API uses a small set of fixed string formats for all date and time values. None of these formats are ISO 8601 extended format, and Unix timestamps are not accepted or returned.
Do not use ISO 8601 extended format or Unix timestamps — the API will reject or misparse the value.
## Accepted formats
| Format | Field type | Example |
| --------------------- | ------------------------------------------------------------------ | --------------------- |
| `YYYY-MM-DD` | Date-only (`date`, `from`, `to`, expense `date`, assignment dates) | `2026-05-04` |
| `YYYY-MM-DD HH:MM:SS` | Datetime (`createdAt`, `updatedAt`, screenshot timestamps) | `2026-05-04 09:30:00` |
| `HH:MM` | Time of day (timecard `startTime`, `endTime`, clock-in) | `09:30` |
## Request fields
Use `YYYY-MM-DD` for all date parameters such as `date`, `from`, and `to`. Use `YYYY-MM-DD HH:MM:SS` for datetime fields. Use `HH:MM` for time-of-day fields.
Date range parameters are **inclusive on both ends**. A request with `from=2026-05-01&to=2026-05-31` returns records for every day from May 1 through May 31 inclusive.
## Response fields
Datetime fields such as `createdAt` and `updatedAt` are returned as `YYYY-MM-DD HH:MM:SS` with no timezone suffix. Treat all such values as UTC.
Date-only fields are returned as `YYYY-MM-DD`.
## Timezones
The user object includes a `timezone` field that contains the user's UTC offset as a **float representing hours** — for example, `-5`, `5.5`, or `0`. Values are restricted to whole and half hours. This is not an IANA timezone name.
No timezone conversion is applied to datetime values in API responses. All datetimes are UTC-naive; apply the user's `timezone` offset locally if you need to display times in the user's local time.
## Time durations
The `time` field on time records and timer responses is an **integer in seconds**. To convert to hours, divide by `3600`.
```
3600 → 1 h
5400 → 1 h 30 m
90 → 1 m 30 s
```
## Money amounts
`amount` fields on expenses and invoices are **integers in the smallest currency unit** (cents for USD/EUR). For example, `1500` represents \$15.00. `quantity` fields are floats.
## Clearing a date field
Fields that accept the `DateTimeResettable` type treat an empty string `""` as a signal to clear the stored value rather than a parse error. This is the only way to unset such a field via the API.
## Code examples
The examples below build a request to `GET /team/time` with a date range, then convert the `time` integer in each record to hours.
```bash Bash/cURL theme={null}
curl -G "https://api.everhour.com/team/time" \
-H "X-Api-Key: YOUR_API_KEY" \
--data-urlencode "from=2026-05-01" \
--data-urlencode "to=2026-05-31" \
| jq '[.[] | {id: .id, hours: (.time / 3600)}]'
```
```ruby Ruby theme={null}
require "net/http"
require "json"
require "uri"
uri = URI("https://api.everhour.com/team/time")
uri.query = URI.encode_www_form(from: "2026-05-01", to: "2026-05-31")
req = Net::HTTP::Get.new(uri)
req["X-Api-Key"] = "YOUR_API_KEY"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
records = JSON.parse(response.body)
records.each do |record|
hours = record["time"] / 3600.0
puts "#{record["id"]}: #{hours} h"
end
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.everhour.com/team/time",
headers={"X-Api-Key": "YOUR_API_KEY"},
params={"from": "2026-05-01", "to": "2026-05-31"},
)
response.raise_for_status()
for record in response.json():
hours = record["time"] / 3600
print(f"{record['id']}: {hours:.2f} h")
```
```php PHP theme={null}
"2026-05-01", "to" => "2026-05-31"]);
$url = "https://api.everhour.com/team/time?" . $query;
$ctx = stream_context_create([
"http" => [
"header" => "X-Api-Key: YOUR_API_KEY\r\n",
],
]);
$body = file_get_contents($url, false, $ctx);
$records = json_decode($body, true);
foreach ($records as $record) {
$hours = $record["time"] / 3600;
printf("%s: %.2f h\n", $record["id"], $hours);
}
```
```java Java theme={null}
import java.net.URI;
import java.net.http.*;
import com.fasterxml.jackson.databind.*;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.everhour.com/team/time?from=2026-05-01&to=2026-05-31"))
.header("X-Api-Key", "YOUR_API_KEY")
.GET()
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
ObjectMapper mapper = new ObjectMapper();
JsonNode records = mapper.readTree(response.body());
for (JsonNode record : records) {
double hours = record.get("time").asDouble() / 3600;
System.out.printf("%s: %.2f h%n", record.get("id").asText(), hours);
}
```
```javascript JavaScript theme={null}
import fetch from "node-fetch";
const params = new URLSearchParams({ from: "2026-05-01", to: "2026-05-31" });
const response = await fetch(`https://api.everhour.com/team/time?${params}`, {
headers: { "X-Api-Key": "YOUR_API_KEY" },
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const records = await response.json();
for (const record of records) {
const hours = record.time / 3600;
console.log(`${record.id}: ${hours.toFixed(2)} h`);
}
```
```go Go theme={null}
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
params := url.Values{"from": {"2026-05-01"}, "to": {"2026-05-31"}}
req, _ := http.NewRequest("GET", "https://api.everhour.com/team/time?"+params.Encode(), nil)
req.Header.Set("X-Api-Key", "YOUR_API_KEY")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var records []struct {
ID string `json:"id"`
Time int `json:"time"`
}
json.NewDecoder(resp.Body).Decode(&records)
for _, r := range records {
hours := float64(r.Time) / 3600
fmt.Printf("%s: %.2f h\n", r.ID, hours)
}
}
```
```csharp .NET theme={null}
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-Api-Key", "YOUR_API_KEY");
var url = "https://api.everhour.com/team/time?from=2026-05-01&to=2026-05-31";
var records = await client.GetFromJsonAsync(url)
?? Array.Empty();
foreach (var record in records)
{
double hours = record.GetProperty("time").GetDouble() / 3600;
Console.WriteLine($"{record.GetProperty("id").GetString()}: {hours:F2} h");
}
```
# Errors
Source: https://developers.everhour.com/errors
HTTP status codes and error response format used by the Everhour API.
The Everhour API uses standard HTTP status codes. All error responses include a JSON body with a `code` and `message` field.
## Error response format
```json theme={null}
{
"code": 404,
"message": "Resource not found"
}
```
Some errors include an additional `errors` field with per-field validation details:
```json theme={null}
{
"code": 422,
"message": "Validation failed",
"errors": {
"time": ["must be a positive integer"]
}
}
```
## Status codes
| Code | Meaning |
| ----- | ---------------------------------------------------------------------------------------- |
| `200` | Success |
| `201` | Resource created |
| `204` | Success, no content returned |
| `400` | Bad request — malformed JSON or missing required parameter |
| `403` | Forbidden — missing or invalid API key, or your account lacks permission for this action |
| `404` | Not found — resource does not exist or is not accessible to your account |
| `422` | Unprocessable entity — request was valid JSON but failed validation |
| `429` | Too many requests — rate limit exceeded. See [Rate limits](/rate-limits) |
| `503` | Service unavailable — something went wrong on our end |
## Handling errors
Check the HTTP status code before parsing the response body. A `2xx` status indicates success. For anything else, read the `message` field for a human-readable explanation.
```bash Bash/cURL theme={null}
response=$(curl -s -w "\n%{http_code}" https://api.everhour.com/projects \
-H "X-Api-Key: YOUR_API_KEY")
body=$(echo "$response" | head -1)
status=$(echo "$response" | tail -1)
if [ "$status" -ge 400 ]; then
echo "Error: $body"
fi
```
```ruby Ruby theme={null}
require "net/http"
require "json"
uri = URI("https://api.everhour.com/projects")
req = Net::HTTP::Get.new(uri, { "X-Api-Key" => "YOUR_API_KEY" })
resp = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
unless resp.is_a?(Net::HTTPSuccess)
error = JSON.parse(resp.body)
puts "Error #{error['code']}: #{error['message']}"
end
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.everhour.com/projects",
headers={"X-Api-Key": "YOUR_API_KEY"}
)
if not response.ok:
error = response.json()
print(f"Error {error['code']}: {error['message']}")
```
```php PHP theme={null}
$ch = curl_init("https://api.everhour.com/projects");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-Api-Key: YOUR_API_KEY"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) {
$error = json_decode($body, true);
echo "Error {$error['code']}: {$error['message']}";
}
```
```java Java theme={null}
import java.net.URI;
import java.net.http.*;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.everhour.com/projects"))
.header("X-Api-Key", "YOUR_API_KEY")
.GET()
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) {
System.out.println("Error: " + response.body());
}
```
```javascript JavaScript theme={null}
const response = await fetch("https://api.everhour.com/projects", {
headers: { "X-Api-Key": "YOUR_API_KEY" }
});
if (!response.ok) {
const error = await response.json();
console.error(`Error ${error.code}: ${error.message}`);
}
```
```go Go theme={null}
// import "fmt"; "io"; "net/http"
req, _ := http.NewRequest("GET", "https://api.everhour.com/projects", nil)
req.Header.Set("X-Api-Key", "YOUR_API_KEY")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Error: %s\n", body)
}
```
```csharp .NET theme={null}
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-Api-Key", "YOUR_API_KEY");
var response = await client.GetAsync("https://api.everhour.com/projects");
if (!response.IsSuccessStatusCode) {
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Error: {body}");
}
```
If you encounter a `503` error that persists, contact support via chat inside your Everhour account or at [ask@everhour.com](mailto:ask@everhour.com) with the request details.
# Everhour API
Source: https://developers.everhour.com/introduction
A REST API for programmatic access to your time tracking data, projects, users, and reports.
The Everhour API lets you read and write your account data: time records, projects, tasks, users, clients, invoices, and reports. It uses standard REST conventions — predictable URLs, HTTP methods, and JSON for all requests and responses.
## Base URL
```
https://api.everhour.com
```
## Request format
The API accepts **JSON only**. Set the `Content-Type` header on all requests with a body:
```http theme={null}
Content-Type: application/json
```
All text must be UTF-8 encoded. The API always returns JSON, including error responses.
## Explore the documentation
Get your API key and make your first request in minutes.
Understand the Everhour data model before diving into endpoints.
How API key authentication works.
How to page through list results.
Date formats, time durations, and timezone handling.
Receive real-time event notifications.
HTTP status codes and error response format.
# Connect an MCP client
Source: https://developers.everhour.com/mcp-connect
Connect Claude Code, Cursor, VS Code, Codex, ChatGPT, and Devin to the Everhour MCP server over OAuth.
The Everhour MCP server works with any client that speaks the MCP Streamable HTTP transport. The server URL is the same everywhere:
```
https://api.everhour.com/mcp
```
Clients connect with OAuth 2.1. You don't register an application or manage a client ID and secret — the first time a client connects, Everhour registers it automatically ([Dynamic Client Registration](/oauth)) and opens your browser to approve access. Pick your client below.
## Prerequisites
* An active Everhour account. You can sign in with Google, SAML SSO, or email and password.
There's nothing to set up in Everhour ahead of time — no app to create, no credentials to copy, no redirect URL to register. Authorization happens in your browser when you connect.
***
## Claude Code
[Claude Code](https://claude.com/claude-code) supports remote MCP servers with OAuth natively.
### Prerequisites
Complete the [prerequisites](#prerequisites). No API key or app registration needed — Claude Code handles the OAuth flow for you.
### Add the server
Add the server with one command:
```bash theme={null}
claude mcp add --transport http everhour https://api.everhour.com/mcp
```
### Authorize
1. Run `/mcp` in Claude Code and select **everhour**.
2. Choose **Authenticate**. Your browser opens Everhour's sign-in.
3. Sign in to Everhour, review the consent screen, and click **Allow**.
4. Return to Claude Code — `/mcp` now shows `everhour` as **connected**.
### Verify
Ask Claude: *"What's my Everhour timesheet this week?"* A correct answer means the tools are wired up.
***
## Cursor
[Cursor](https://cursor.com) connects to the Everhour MCP server using its built-in OAuth support.
### Prerequisites
Complete the [prerequisites](#prerequisites). Cursor stores the connection globally or per project — no API key needed.
### Configure Cursor
1. Open **Cursor Settings** (`⌘,` on Mac, `Ctrl+,` on Windows/Linux) → **Tools & MCP**.
2. Click **+ Add new global MCP server** and add the following (this edits `~/.cursor/mcp.json`):
```json theme={null}
{
"mcpServers": {
"everhour": {
"url": "https://api.everhour.com/mcp"
}
}
}
```
### Connect
1. Back in **Tools & MCP**, find `everhour` in the list and click **Connect**.
2. Your browser opens Everhour's sign-in. Sign in, review the consent screen, and click **Allow**.
3. The browser redirects back to Cursor via the `cursor://` protocol handler.
4. Return to Cursor — the server status changes to **connected**.
### Test
Open a new agent window and ask: *"Show my Everhour tasks I tracked this week."*
***
## VS Code
VS Code supports MCP servers with OAuth natively (VS Code 1.102 or later).
### Prerequisites
Complete the [prerequisites](#prerequisites). Requires VS Code 1.102 or later.
### Register the server
1. Open the Command Palette (`⌘⇧P` on Mac, `Ctrl+Shift+P` on Windows/Linux) and run **MCP: Add Server**.
2. Choose **HTTP**, enter the URL `https://api.everhour.com/mcp`, and name it `everhour`.
You can also add it directly to `.vscode/mcp.json` — note VS Code uses the `servers` key:
```json theme={null}
{
"servers": {
"everhour": {
"type": "http",
"url": "https://api.everhour.com/mcp"
}
}
}
```
### Authorize
1. Start the server — use the **Start** action shown above the entry in `mcp.json`, or accept the prompt when it appears.
2. Your browser opens Everhour's sign-in. Sign in, review the consent screen, and click **Allow**.
3. Return to VS Code — the server shows as **running**.
### Test
Open Chat in **Agent** mode and ask: *"What Everhour projects do I have?"*
***
## Codex CLI
[Codex CLI](https://developers.openai.com/codex/cli) supports remote MCP servers with OAuth natively.
### Prerequisites
Complete the [prerequisites](#prerequisites). No API key needed — Codex handles the OAuth flow for you.
### Install the CLI
Install the Codex CLI, then verify it's on your PATH:
```bash theme={null}
curl -fsSL https://chatgpt.com/codex/install.sh | sh # macOS/Linux
# or, with npm:
npm install -g @openai/codex
codex --version
```
### Add the server
```bash theme={null}
codex mcp add everhour --url https://api.everhour.com/mcp
```
### Sign in
Log in to start the OAuth flow:
```bash theme={null}
codex mcp login everhour
```
Your browser opens Everhour's sign-in. Sign in, review the consent screen, and click **Allow** — then return to the terminal.
### Test
Run `codex`, then `/mcp` — confirm `everhour` is listed and authenticated. Ask: *"What did I track in Everhour today?"*
***
## ChatGPT / Codex Desktop
OpenAI's desktop app — which combines ChatGPT and Codex — supports custom MCP servers over OAuth.
### Prerequisites
Complete the [prerequisites](#prerequisites). No API key needed — the app handles the OAuth flow for you.
### Add a custom MCP
1. Open **Settings → Plugins → MCPs** and click **Add server**.
2. In the **Connect to a custom MCP** dialog, set:
* **Type:** Streamable HTTP
* **Name:** `everhour`
* **URL:** `https://api.everhour.com/mcp`
3. Click **Save**. `everhour` now appears in the MCP list.
### Authenticate
1. Next to `everhour` in the MCP list, click **Authenticate**.
2. Your browser opens Everhour's sign-in. Sign in, review the consent screen, and click **Allow**.
3. Return to the app — `everhour` shows as connected.
### Test
Ask: *"What's my Everhour timesheet this week?"*
***
## Devin (Windsurf)
[Devin](https://devin.ai/desktop) supports remote MCP servers with OAuth natively.
### Prerequisites
Complete the [prerequisites](#prerequisites). No API key or bridge needed — Devin handles the OAuth flow for you.
### Configure Devin
1. Open the Command Palette (`⌘⇧P` on Mac, `Ctrl+Shift+P` on Windows/Linux) and run **Devin MCP Registry**.
2. On the **MCP Marketplace**, click the gear icon in the top-right of the **Installed MCP** area to open `mcp.json`, then add:
```json theme={null}
{
"mcpServers": {
"everhour": {
"type": "http",
"url": "https://api.everhour.com/mcp"
}
},
"version": 1
}
```
### Authorize
1. Save the config. Devin loads the server and opens your browser to Everhour's sign-in.
2. Sign in, review the consent screen, and click **Allow**.
3. Return to Devin — `everhour` shows as **connected**.
### Verify
Ask: *"What's my Everhour timesheet this week?"*
***
## Other clients
Any MCP client that supports remote servers with OAuth — including Claude connectors — works with the same URL. Add `https://api.everhour.com/mcp` as a remote/HTTP server and complete the sign-in and click **Allow** when prompted. Clients that only speak stdio can use the [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) bridge — run `npx -y mcp-remote@latest https://api.everhour.com/mcp` (needs Node.js).
## Troubleshooting
Make sure pop-ups aren't blocked and that you finish the **Allow** step in the browser tab your client opened. If the consent page reports an expired request, start the connection again from your client.
The server isn't authenticated yet. Run `codex mcp login everhour` to (re)authorize, then check again with `/mcp`.
Restart your client. The server registers 24 tools; a lower count usually means your client cached an older list.
Your client cached a registration from an earlier connection that Everhour no longer recognizes, so authorization keeps failing. Clear the cached client, then add the server again:
* **Claude Code:** `claude mcp logout everhour`, then `claude mcp remove everhour`, then re-run `claude mcp add --transport http everhour https://api.everhour.com/mcp`.
* **Codex CLI:** `codex mcp logout everhour`, then `codex mcp remove everhour`, then `codex mcp add everhour --url https://api.everhour.com/mcp`.
* **Cursor / VS Code / Devin:** remove the `everhour` block from the config (and clear any stored authorization), then add it again to force a fresh registration.
## Disconnecting
* **Remove the server in your client.** This discards its stored tokens and stops access immediately.
* Claude Code: `claude mcp logout everhour`, then `claude mcp remove everhour`.
* Cursor / VS Code / Devin: delete the `everhour` block from the config and restart.
* Codex CLI: `codex mcp logout everhour`, then `codex mcp remove everhour`.
* **In a desktop or chat app:** ChatGPT / Codex Desktop — **Settings → Plugins → MCPs** and remove `everhour`; Claude — **Settings → Connectors** and remove the Everhour connector.
Access tokens are short-lived and refresh automatically while a client stays connected. Removing the server stops the refresh, so access ends once the current token expires.
## Connect with an API key
If a client can't do OAuth — or you're wiring up automation and want a headless connection — you can authenticate with an Everhour API key in an `X-Api-Key` header instead.
An API key never expires on its own and grants full access to your account. Prefer OAuth wherever your client supports it, and never commit the key to a repository.
1. Copy your key from [My Profile → API key](https://app.everhour.com/#/account/profile).
2. Add the server with the header (replace `YOUR_API_KEY`):
```json theme={null}
{
"mcpServers": {
"everhour": {
"type": "streamable-http",
"url": "https://api.everhour.com/mcp",
"headers": {
"X-Api-Key": "YOUR_API_KEY"
}
}
}
}
```
Codex uses TOML instead:
```toml theme={null}
[mcp_servers.everhour]
url = "https://api.everhour.com/mcp"
http_headers = { X-Api-Key = "YOUR_API_KEY" }
```
3. Restart the client. To revoke access, regenerate the key in your profile — every client using the old key loses access at once.
## How authorization works
You don't need this to connect, but if you're building or debugging a client, the [OAuth 2.1](/oauth) section walks through the whole flow — discovery, Dynamic Client Registration, the authorization code + PKCE exchange, and token refresh — with a sequence diagram. The [OAuth 2.1 reference](/oauth-reference) lists every endpoint and parameter. MCP clients request the `https://api.everhour.com/mcp` resource, so their token is audience-bound to this server.
# Everhour MCP server
Source: https://developers.everhour.com/mcp-overview
Connect AI assistants like Claude and ChatGPT to your Everhour account to track time, manage tasks, and read timesheets in plain language.
The Everhour MCP server lets an AI assistant read and write your Everhour data on your behalf — start a timer, log time, check your timesheet, or find a task — without you switching tabs or memorizing IDs. You describe what you want in plain language and the assistant calls the right tool.
It implements the [Model Context Protocol](https://modelcontextprotocol.io) (MCP), an open standard for connecting AI clients to external tools and data. Any MCP-capable client can connect.
## Server details
`POST https://api.everhour.com/mcp`
Streamable HTTP
MCP `2025-06-18`, JSON-RPC 2.0
OAuth 2.1 (or `X-Api-Key` for manual setups)
## What you can do
Once connected, your assistant has access to 24 tools covering everyday time tracking:
* **Track time in real time** — start, stop, and check your running timer.
* **Log and edit time** — add entries (in seconds), fix a duration, delete a wrong entry, or log time retroactively.
* **Review timesheets** — read your weekly timesheet with daily totals and a per-task breakdown.
* **Find work across tools** — search tasks, list recently tracked tasks, and resolve a task ID or URL from Asana, Jira, GitHub, Linear, and other integrations to the matching Everhour task.
* **Read projects, clients, and team** — list and fetch projects, clients, and team members.
* **Report on the team** — pull hours by project or member, project budgets, time off, and weekly capacity over any date range (subject to your role).
See the [tools reference](/mcp-tools) for the full list with parameters.
## Authentication
Clients connect with [**OAuth 2.1**](/oauth). You add the server URL and approve access in your browser — no app to create and no credentials to copy, because Everhour registers your client automatically. The access token is short-lived and refreshes on its own. See [Connect an MCP client](/mcp-connect).
For clients that can't do OAuth, or for headless automation, you can authenticate with an Everhour API key in an `X-Api-Key` header instead. The key is long-lived and grants full account access, so prefer OAuth where it's available.
Whichever method you use, the assistant can only do what your Everhour role allows — the same permissions that govern the REST API.
## Requirements
* An active Everhour account.
* An MCP-capable AI client (see [supported clients](/mcp-connect)).
* To authenticate: either a client that supports OAuth (no setup — it registers itself), or your [Everhour API key](https://app.everhour.com/#/account/profile) for the header-based method.
## Next steps
Set up Claude, ChatGPT, Cursor, and other clients.
Every tool the assistant can call, with parameters.
# MCP tools reference
Source: https://developers.everhour.com/mcp-tools
The 24 tools the Everhour MCP server exposes, grouped by area, with example prompts.
The Everhour MCP server registers **24 tools**, all prefixed `everhour_`. You don't call them by name — your AI client picks the right tool from your request and fills in the details. This reference is handy when you're debugging a connection or want to see exactly what an assistant can do.
The example prompts below are just that — examples. Phrase requests however you like; the client maps them to the right tool. Durations are always in seconds, and dates use `YYYY-MM-DD` ([more](/dates-and-timezones)).
### Behavior hints
Each tool is annotated so well-behaved clients know how to treat it. Read-only tools (most `get_`/`list_`/`search_` tools) never change data. Write tools are marked accordingly, and `everhour_delete_time_entry` carries the *destructive* hint — Claude, Cursor, and ChatGPT prompt you to confirm before running it.
The tool set can change over time. For the authoritative, live list, use the MCP `tools/list` method — this page mirrors the current set.
## Timer
| Tool | Description | Example |
| ---------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------- |
| `everhour_get_current_timer` | Get the currently running timer for the authenticated user, or null if no timer is running. | *"Is my timer running?"* |
| `everhour_start_timer` | Start a timer on a task. Search for the task first to get its ID. | *"Start a timer on the login bug."* |
| `everhour_stop_timer` | Stop the currently running timer. | *"Stop my timer."* |
## Time entries
| Tool | Description | Example |
| ---------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------- |
| `everhour_log_time` | Log a manual time entry for a task. Time is in seconds. Date defaults to today. | *"Log 2 hours to the API docs task."* |
| `everhour_update_time_entry` | Update an existing time entry (duration, date, or comment). | *"Change yesterday's design entry to 90 minutes."* |
| `everhour_delete_time_entry` | Delete a time entry. | *"Delete the time I logged to the wrong task."* |
| `everhour_get_user_time` | Get time entries for a user within a date range. Max 31 days per request. | *"How much did Alex track last week?"* |
| `everhour_get_my_timesheet` | Get the current user's weekly timesheet with daily totals and task breakdown. | *"Show my timesheet for this week."* |
## Tasks
| Tool | Description | Example |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------ |
| `everhour_search_tasks` | Search tasks by keyword across all projects or within a specific project. Use to find tasks before logging time. | *"Find tasks about onboarding."* |
| `everhour_recent_tasks` | Get recently tracked tasks. Shows tasks you worked on recently — use before searching by keyword. | *"What have I worked on recently?"* |
| `everhour_get_task` | Get a single task by ID including time tracked and estimate data. | *"Show details for the checkout task."* |
| `everhour_list_tasks` | List tasks within a specific project. Requires project\_id. | *"List the tasks in the Website project."* |
| `everhour_resolve_platform_ids` | Convert platform-specific task IDs (Asana, Jira, GitHub, etc.) to Everhour IDs. Use when the user provides a task URL or ID from an integration. | *"Track time on this pasted Asana link."* |
Call `everhour_list_platforms` to validate a platform code before `everhour_resolve_platform_ids`. This pair turns a pasted Asana or Jira URL into the right Everhour task to log against.
## Projects & clients
| Tool | Description | Example |
| ------------------------ | ---------------------------------------------------------------------------------- | -------------------------------------- |
| `everhour_list_projects` | List projects accessible to the user. Use to find project IDs before logging time. | *"What projects do I have?"* |
| `everhour_get_project` | Get a single project by ID including client association and billing info. | *"Show the Website Redesign project."* |
| `everhour_list_clients` | List clients, optionally filtered by name. | *"List my clients."* |
| `everhour_get_client` | Get a single client by ID. | *"Show details for the Acme account."* |
## Team
| Tool | Description | Example |
| -------------------------- | ------------------------------------------------------------ | ----------------------------- |
| `everhour_get_me` | Get current authenticated user profile, role, and team info. | *"What's my role and team?"* |
| `everhour_list_team_users` | List all team members with their roles and capacity. | *"List everyone on my team."* |
## Reporting
| Tool | Description | Example |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `everhour_get_dashboard` | Aggregated team report for a date range — one row per member, project, client, team group, week, or month. Money columns appear only for roles allowed to see them. | *"How many hours did the team log by project last month?"* |
| `everhour_get_project_budgets` | Budget report — budget, spent, remaining, and progress per project. Requires budget access. | *"Which projects are over budget?"* |
| `everhour_get_time_off` | Team time-off assignments that overlap a date range. Requires admin access. | *"Who's out on time off next week?"* |
| `everhour_get_users_capacity` | Weekly capacity per team member. Requires admin access. | *"What's each teammate's capacity this week?"* |
### Dashboard views
`everhour_get_dashboard` is one tool with a `dashboard` selector, so it covers reports the Everhour app splits across several screens. Every view shares the same date range and optional `member_id` / `project_id` / `client_id` filters.
| `dashboard` | Shows | Example |
| ------------- | --------------------------------------------------------------- | --------------------------------------------------------------- |
| `users` | Time, billing, and cost per member | *"How much did each teammate track in May?"* |
| `team-hours` | Tracked time vs. time off, overtime, and capacity per member | *"Show tracked hours against capacity this month."* |
| `payroll` | Payroll, overtime, and gross pay per member (needs cost access) | *"Show payroll for May — overtime and gross pay."* |
| `projects` | Time and cost per project | *"How many hours went to each project in April?"* |
| `clients` | Time and cost per client (needs admin) | *"Break down the team's time by client for Q2."* |
| `team-groups` | Totals per team group | *"Compare tracked hours across team groups this quarter."* |
| `months` | Totals per calendar month | *"Show tracked time month by month this year."* |
| `weeks` | Totals per calendar week | *"How did weekly tracked time trend over the last two months?"* |
| `timecards` | Clock-in/out summary per member (needs supervisor) | *"Summarize the team's clock-in and clock-out last week."* |
Omit `member_id` for the whole team, or name a person to scope to one member. Money views (`payroll` and cost columns elsewhere) and the `clients` and `timecards` views need the matching cost, admin, or supervisor role — without it, those columns or views are left out.
## Platforms
| Tool | Description | Example |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| `everhour_list_platforms` | List supported integration platforms and their 2-letter codes. Use to validate platform codes before calling `everhour_resolve_platform_ids`. | *"Which integrations can I resolve task IDs from?"* |
# OAuth 2.1
Source: https://developers.everhour.com/oauth
Authorize an app to call the Everhour API on a user's behalf using OAuth 2.1 with PKCE and Dynamic Client Registration — no app to create and no client secret.
OAuth 2.1 lets an app call the Everhour API **on behalf of a user** without holding that user's API key. The user signs in and approves access in their browser, and your app receives a short-lived access token instead of a long-lived credential.
Everhour implements OAuth 2.1 with the authorization code grant, PKCE, and [Dynamic Client Registration](https://datatracker.ietf.org/doc/html/rfc7591). Your app registers itself and discovers every endpoint from the server's metadata — there's no application to create in a dashboard and no client secret to manage.
There's nothing to set up in Everhour ahead of time — no app to create, no client ID or secret to copy, no redirect URL to pre-register. Your app registers itself the first time it connects, and the user approves access in the browser.
## OAuth or an API key?
Everhour supports two ways to authenticate. Pick by who the caller is.
| | [API key](/authentication) | OAuth 2.1 |
| -------------- | ------------------------------------------ | ------------------------------------------------------------- |
| **Best for** | Your own scripts and server-to-server jobs | Apps that act for other users (MCP clients, third-party apps) |
| **Credential** | One long-lived key per user | Short-lived access token + rotating refresh token |
| **Setup** | Copy the key from your profile | User signs in and approves in the browser |
| **Sent as** | `X-Api-Key` header | `Authorization: Bearer` header |
Both methods act as the signed-in user and are bound by that user's Everhour role — neither grants more than the person behind it can do.
## What a token can access
An access token is a JSON Web Token (JWT) that is **audience-bound** to a resource:
* By default a token is issued for the whole API (`https://api.everhour.com`) and works on every REST endpoint.
* A client can narrow the token to a single resource by passing the [`resource`](https://datatracker.ietf.org/doc/html/rfc8707) parameter at authorization. [MCP clients](/mcp-connect) request `https://api.everhour.com/mcp`, so their token is accepted only on `/mcp`.
Everhour has **no OAuth scopes**. Access is governed entirely by the signed-in user's role, the same as the API key. The consent screen lists what the app will be able to do so the user can make an informed decision, but there are no per-scope toggles.
## How the flow works
Compatible clients run this end to end on their own — you configure your app with the server URL and it discovers the rest.
An unauthenticated request to a protected resource returns `401` with a `WWW-Authenticate` header pointing to the protected-resource metadata. The client reads two documents:
* `GET /.well-known/oauth-protected-resource` ([RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)) — names the authorization server.
* `GET /.well-known/oauth-authorization-server` ([RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414)) — lists the authorize, token, and registration endpoints.
The client registers itself at `POST /oauth/register` ([RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591)) with a `client_name` and its `redirect_uris`, and receives a `client_id`. Clients are **public** — PKCE-protected, with no secret.
The client sends the user to `/oauth/authorize` with a PKCE `S256` challenge. The user signs in, reviews the consent screen, and clicks **Allow**. Everhour redirects back to the client with a one-time `code`.
The client exchanges the `code` (with its PKCE verifier) at `POST /oauth/token` for an access token (a JWT, valid \~1 hour) and a rotating refresh token.
The client calls the API with `Authorization: Bearer ` and uses the refresh token to get a new access token when the old one expires.
```mermaid theme={null}
sequenceDiagram
participant App as Your app
participant API as api.everhour.com
participant User as User's browser
App->>API: Request without a token
API-->>App: 401 + WWW-Authenticate (metadata URL)
App->>API: GET discovery documents
App->>API: POST /oauth/register (client_name, redirect_uris)
API-->>App: client_id (public client)
App->>User: Open /oauth/authorize (PKCE S256 challenge)
User->>API: Sign in and click Allow
API-->>App: Redirect with authorization code
App->>API: POST /oauth/token (code + PKCE verifier)
API-->>App: access_token (JWT, ~1h) + refresh_token
App->>API: Request with Authorization: Bearer
API-->>App: 200 OK
```
## Next steps
Every endpoint, parameter, token lifetime, and security rule.
The most common OAuth client — connect Claude, ChatGPT, Cursor, and more.
# OAuth 2.1 reference
Source: https://developers.everhour.com/oauth-reference
Endpoints, parameters, token lifetimes, and security rules for Everhour's OAuth 2.1 authorization flow.
This page documents every OAuth endpoint and its parameters. For a walkthrough of how the pieces fit together, see [OAuth 2.1](/oauth). All endpoints are served from the API base URL:
```
https://api.everhour.com
```
## Discovery
Clients don't hardcode endpoints — they read them from the server's metadata. An unauthenticated request to a protected resource returns `401` with a `WWW-Authenticate` header naming the protected-resource metadata URL.
| Method | Path | Standard | Returns |
| ------ | ------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------ |
| `GET` | `/.well-known/oauth-protected-resource` | [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) | The authorization server for the API |
| `GET` | `/.well-known/oauth-protected-resource/mcp` | [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) | The authorization server for the `/mcp` resource |
| `GET` | `/.well-known/oauth-authorization-server` | [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) | Endpoint URLs and supported capabilities |
The authorization-server metadata tells the client everything it needs:
```json theme={null}
{
"issuer": "https://api.everhour.com",
"authorization_endpoint": "https://api.everhour.com/oauth/authorize",
"token_endpoint": "https://api.everhour.com/oauth/token",
"registration_endpoint": "https://api.everhour.com/oauth/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"]
}
```
## Register a client
Register your app with Dynamic Client Registration ([RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591)). The request needs no authentication, and there is nothing to create in Everhour beforehand.
```http theme={null}
POST /oauth/register
Content-Type: application/json
```
| Field | Type | Required | Description |
| --------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `client_name` | string | Yes | A human-readable name shown on the consent screen. |
| `redirect_uris` | array | Yes | One or more redirect URIs. Each must use `https`, `http` on a loopback host (`127.0.0.1` / `localhost`), or a private-use scheme (for example `myapp://callback`). |
The response returns a `client_id`. Everhour issues **public** clients only — there is no `client_secret`.
```json theme={null}
{
"client_id": "a1b2c3d4-...",
"client_name": "My integration",
"redirect_uris": ["https://myapp.example.com/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "none"
}
```
## Authorize
Send the user to the authorization endpoint to sign in and approve access. Generate a PKCE code verifier and its `S256` challenge first — `plain` challenges are rejected.
```http theme={null}
GET /oauth/authorize
```
| Parameter | Required | Description |
| ----------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `response_type` | Yes | Must be `code`. |
| `client_id` | Yes | The `client_id` from registration. |
| `redirect_uri` | Yes | One of the URIs registered for the client. |
| `code_challenge` | Yes | The PKCE challenge derived from your verifier. |
| `code_challenge_method` | Yes | Must be `S256`. |
| `state` | Recommended | An opaque value echoed back on the redirect. Verify it to protect against CSRF. |
| `resource` | Optional | Narrows the token's audience ([RFC 8707](https://datatracker.ietf.org/doc/html/rfc8707)). Omit for a full-API token; pass `https://api.everhour.com/mcp` for an MCP-only token. |
After the user clicks **Allow**, Everhour redirects to your `redirect_uri` with a one-time `code` (and your `state`). If the user declines, it returns an `access_denied` error.
## Exchange the code for tokens
Exchange the `code` for tokens at the token endpoint. Because clients are public, no client authentication is sent — the PKCE `code_verifier` proves the request comes from the app that started the flow.
```bash cURL theme={null}
curl https://api.everhour.com/oauth/token \
-d grant_type=authorization_code \
-d code=AUTHORIZATION_CODE \
-d redirect_uri=https://myapp.example.com/callback \
-d client_id=YOUR_CLIENT_ID \
-d code_verifier=YOUR_PKCE_VERIFIER
```
```http HTTP theme={null}
POST /oauth/token HTTP/1.1
Host: api.everhour.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https://myapp.example.com/callback
&client_id=YOUR_CLIENT_ID
&code_verifier=YOUR_PKCE_VERIFIER
```
The response contains the access token and a refresh token:
```json theme={null}
{
"token_type": "Bearer",
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": 3600,
"refresh_token": "def50200a1b2c3d4..."
}
```
## Refresh a token
Access tokens last about an hour. When one expires, use the refresh token to get a new pair. Refresh tokens **rotate** — each refresh returns a new refresh token and invalidates the old one, so always store the latest.
```bash theme={null}
curl https://api.everhour.com/oauth/token \
-d grant_type=refresh_token \
-d refresh_token=YOUR_REFRESH_TOKEN \
-d client_id=YOUR_CLIENT_ID
```
The token keeps the same audience it was first issued for — a refresh can't widen an `/mcp` token to the full API.
## Token lifetimes
| Token | Lifetime | Notes |
| ------------------ | ------------ | --------------------------------------------------------------------------------- |
| Authorization code | \~10 minutes | Single use, exchanged once at the token endpoint. |
| Access token | \~1 hour | Signed JWT (`RS256`). Stateless — validated by signature, not stored server-side. |
| Refresh token | \~1 month | Rotates on every use. |
Because access tokens are stateless, there is no per-token revocation endpoint. To end access, stop using and refreshing the tokens — the access token expires within the hour, and the refresh token lapses once it's no longer used.
## Use a token with the REST API
Send the access token in an `Authorization: Bearer` header. A full-API token works on every REST endpoint, exactly where you'd otherwise send `X-Api-Key`:
```bash theme={null}
curl https://api.everhour.com/users/me \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```
When the token expires the API returns `401`; refresh it and retry. A token that was narrowed to `https://api.everhour.com/mcp` is accepted only on `/mcp` — other paths return `401`.
## Security
* **PKCE is required.** Only the `S256` method is accepted; `plain` is rejected.
* **Clients are public.** There is no client secret — never expect or store one.
* **Redirect URIs are matched exactly.** Register only URIs you control, and use `https` (or `http` on loopback for local development).
* **Verify `state`.** Send a random `state` on authorize and check it on the redirect to prevent CSRF.
* **Tokens are short-lived.** Store them securely, prefer memory or an OS keychain over disk, and never commit them.
* **Everything is over HTTPS.** All endpoints require TLS in production.
# Pagination
Source: https://developers.everhour.com/pagination
How pagination works in the Everhour API — per-endpoint parameters, defaults, and how to detect the last page.
Pagination in the Everhour API is per-endpoint. Each endpoint that supports pagination defines its own `page` and `limit` parameters, defaults, and maximum values. Some endpoints return the full collection in a single response and accept no pagination parameters at all.
## How it works
Paginated endpoints accept one or both of these query parameters:
| Parameter | Type | Description |
| --------- | ------- | ------------------------------------------------------------------- |
| `page` | integer | Page number, 1-based. Default is `1`. |
| `limit` | integer | Number of items per page. Defaults and maximums differ by endpoint. |
Responses are bare JSON arrays. There are no response headers, no JSON envelope, and no total count field. To detect the last page, check whether the number of items returned is less than `limit`.
Some endpoints — including `GET /clients` and `GET /invoices` — return the full collection with no pagination support. Accounts with large collections should narrow requests using available filters rather than fetching everything at once.
# Quickstart
Source: https://developers.everhour.com/quickstart
Get your API key and make your first Everhour API request in minutes.
This guide walks you through getting your API key and making your first authenticated request.
Sign in to your Everhour account and open your [profile settings](https://app.everhour.com/#/account/profile). Scroll to the bottom — your API key is listed there.
Treat your API key like a password. Anyone with the key has full access to your Everhour account via the API.
Pass the key in the `X-Api-Key` header. A good first request is fetching your own user profile:
```bash Bash/cURL theme={null}
curl https://api.everhour.com/users/me \
-H "X-Api-Key: YOUR_API_KEY"
```
```ruby Ruby theme={null}
require "net/http"
require "json"
uri = URI("https://api.everhour.com/users/me")
req = Net::HTTP::Get.new(uri)
req["X-Api-Key"] = "YOUR_API_KEY"
resp = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(resp.body)
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.everhour.com/users/me",
headers={"X-Api-Key": "YOUR_API_KEY"}
)
print(response.json())
```
```php PHP theme={null}
$ch = curl_init("https://api.everhour.com/users/me");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-Api-Key: YOUR_API_KEY"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$body = curl_exec($ch);
curl_close($ch);
print_r(json_decode($body, true));
```
```java Java theme={null}
import java.net.URI;
import java.net.http.*;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.everhour.com/users/me"))
.header("X-Api-Key", "YOUR_API_KEY")
.GET()
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```
```javascript JavaScript theme={null}
const response = await fetch("https://api.everhour.com/users/me", {
headers: { "X-Api-Key": "YOUR_API_KEY" }
});
const user = await response.json();
console.log(user);
```
```go Go theme={null}
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.everhour.com/users/me", nil)
req.Header.Set("X-Api-Key", "YOUR_API_KEY")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
```
```csharp .NET theme={null}
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-Api-Key", "YOUR_API_KEY");
var body = await client.GetStringAsync("https://api.everhour.com/users/me");
Console.WriteLine(body);
```
A successful response returns `200` with a JSON object representing your user:
```json theme={null}
{
"id": 12345,
"name": "Jane Smith",
"email": "jane@example.com",
"role": "admin"
}
```
If you see a `401 Unauthorized` response, double-check your API key.
## Next steps
Details on how the X-Api-Key header works.
Understand request limits and how to handle 429 responses.
HTTP status codes and error response format.
Learn the core objects: teams, projects, tasks, time records, and more.
# Rate limits
Source: https://developers.everhour.com/rate-limits
Request rate limits for the Everhour API and how to handle 429 responses.
The Everhour API enforces rate limits to ensure stability for all users.
## Current limits
The limit is **100 requests per 10 seconds** per API key. This value may vary with server load and is subject to change.
## Exceeded limit response
When you exceed the rate limit, the API returns:
```http theme={null}
HTTP/1.1 429 Too Many Requests
Retry-After: 10
```
The `Retry-After` header specifies how many seconds to wait before retrying.
## Handling 429 responses
Implement exponential backoff in your integration:
```bash Bash/cURL theme={null}
for attempt in 1 2 3 4 5; do
response=$(curl -s -w "\n%{http_code}" https://api.everhour.com/projects \
-H "X-Api-Key: YOUR_API_KEY")
status=$(echo "$response" | tail -1)
if [ "$status" != "429" ]; then break; fi
sleep $(( 2 ** attempt ))
done
```
```ruby Ruby theme={null}
require "net/http"
def request_with_retry(uri, headers, max_retries: 5)
max_retries.times do |attempt|
req = Net::HTTP::Get.new(uri, headers)
resp = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
return resp unless resp.code == "429"
sleep(resp["Retry-After"]&.to_i || 2**attempt)
end
raise "Max retries exceeded"
end
```
```python Python theme={null}
import time
import requests
def request_with_retry(url, headers, max_retries=5):
for attempt in range(max_retries):
response = requests.get(url, headers=headers)
if response.status_code == 429:
wait = int(response.headers.get("Retry-After", 2 ** attempt))
time.sleep(wait)
continue
return response
raise Exception("Max retries exceeded")
```
```php PHP theme={null}
function request_with_retry(string $url, array $headers, int $max_retries = 5): string {
for ($attempt = 0; $attempt < $max_retries; $attempt++) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 429) return $body;
sleep(pow(2, $attempt));
}
throw new RuntimeException("Max retries exceeded");
}
```
```java Java theme={null}
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
HttpClient client = HttpClient.newHttpClient();
HttpResponse requestWithRetry(String url, int maxRetries) throws Exception {
for (int attempt = 0; attempt < maxRetries; attempt++) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("X-Api-Key", "YOUR_API_KEY")
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 429) return response;
long wait = response.headers().firstValue("Retry-After")
.map(Long::parseLong).orElse((long) Math.pow(2, attempt));
Thread.sleep(wait * 1000);
}
throw new RuntimeException("Max retries exceeded");
}
```
```javascript JavaScript theme={null}
async function requestWithRetry(url, headers, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, { headers });
if (response.status !== 429) return response;
const wait = parseInt(response.headers.get("Retry-After") ?? String(2 ** attempt), 10);
await new Promise(resolve => setTimeout(resolve, wait * 1000));
}
throw new Error("Max retries exceeded");
}
```
```go Go theme={null}
// import "fmt"; "math"; "net/http"; "time"
func requestWithRetry(url string, apiKey string, maxRetries int) (*http.Response, error) {
for attempt := 0; attempt < maxRetries; attempt++ {
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("X-Api-Key", apiKey)
resp, err := http.DefaultClient.Do(req)
if err != nil || resp.StatusCode != 429 {
return resp, err
}
time.Sleep(time.Duration(math.Pow(2, float64(attempt))) * time.Second)
}
return nil, fmt.Errorf("max retries exceeded")
}
```
```csharp .NET theme={null}
async Task RequestWithRetry(HttpClient client, string url, int maxRetries = 5) {
for (int attempt = 0; attempt < maxRetries; attempt++) {
var response = await client.GetAsync(url);
if (response.StatusCode != System.Net.HttpStatusCode.TooManyRequests) return response;
var wait = response.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(Math.Pow(2, attempt));
await Task.Delay(wait);
}
throw new Exception("Max retries exceeded");
}
```
## Batch operations
If your use case requires a large volume of requests — such as a one-time data migration or bulk export — contact us before running it. We may be able to provide a more efficient data access method.
Reach us via chat inside your Everhour account or at [ask@everhour.com](mailto:ask@everhour.com).
# Webhooks
Source: https://developers.everhour.com/webhooks
Receive real-time event notifications when resources change in your Everhour account.
Webhooks let your application receive HTTP POST notifications whenever something changes in Everhour — a task is created, a timer is stopped, an invoice is updated. Instead of polling the API, you register a URL and Everhour delivers events to it as they happen.
## How it works
1. You register a `targetUrl` via `POST /hooks`.
2. Everhour performs a handshake to verify the endpoint is reachable.
3. When a subscribed event fires, Everhour POSTs the event payload to your URL.
## Getting started with webhooks
Send a `POST /hooks` request with your `targetUrl` and the list of events you want to receive.
Your endpoint must be publicly reachable. Everhour will immediately perform a handshake to confirm it is live.
A successful registration returns `201 Created`. If the handshake fails, you will receive an `Invalid callback` error. If you have already registered an **active** webhook for the same URL, you will receive `409 Conflict`. If a previous webhook for that URL was disabled, it will be restored instead.
When you create (or update) a webhook, Everhour sends a POST request to your `targetUrl` with the following headers:
```
User-Agent: Everhour (everhour.com)
X-Hook-Secret:
```
Your endpoint must respond with any `2xx` status code. No specific response body is required. If you do not respond with `2xx`, the subscription request is rejected.
The `X-Hook-Secret` value is sent once during the handshake only. It is not repeated on event deliveries.
For automated testing or CI environments where you cannot serve a live endpoint, include the `X-Skip-Handshake: true` header on your subscription request to bypass the handshake check.
After a successful handshake, Everhour will POST event payloads to your URL whenever a subscribed event fires. Events are delivered asynchronously via an internal message queue.
Respond with a `2xx` status to acknowledge receipt. Sustained delivery failures may cause Everhour to set the webhook `active=false`. To reactivate a disabled webhook, send a `PUT /hooks/{id}` request with the desired events list.
## Registering a webhook
```bash Bash/cURL theme={null}
curl -X POST https://api.everhour.com/hooks \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"targetUrl": "https://example.com/everhour-events",
"events": ["api:task:created", "api:timer:started"]
}'
```
```ruby Ruby theme={null}
require 'net/http'
require 'json'
uri = URI('https://api.everhour.com/hooks')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['X-Api-Key'] = 'YOUR_API_KEY'
request['Content-Type'] = 'application/json'
request.body = JSON.generate({
targetUrl: 'https://example.com/everhour-events',
events: ['api:task:created', 'api:timer:started']
})
response = http.request(request)
puts response.body
```
```python Python theme={null}
import requests
response = requests.post(
'https://api.everhour.com/hooks',
headers={
'X-Api-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
json={
'targetUrl': 'https://example.com/everhour-events',
'events': ['api:task:created', 'api:timer:started'],
}
)
print(response.json())
```
```php PHP theme={null}
'https://example.com/everhour-events',
'events' => ['api:task:created', 'api:timer:started'],
]));
$response = curl_exec($ch);
curl_close($ch);
echo $response;
```
```java Java theme={null}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
HttpClient client = HttpClient.newHttpClient();
String body = """
{
"targetUrl": "https://example.com/everhour-events",
"events": ["api:task:created", "api:timer:started"]
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.everhour.com/hooks"))
.header("X-Api-Key", "YOUR_API_KEY")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.everhour.com/hooks', {
method: 'POST',
headers: {
'X-Api-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
targetUrl: 'https://example.com/everhour-events',
events: ['api:task:created', 'api:timer:started'],
}),
});
const data = await response.json();
console.log(data);
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
payload, _ := json.Marshal(map[string]interface{}{
"targetUrl": "https://example.com/everhour-events",
"events": []string{"api:task:created", "api:timer:started"},
})
req, _ := http.NewRequest("POST", "https://api.everhour.com/hooks", bytes.NewBuffer(payload))
req.Header.Set("X-Api-Key", "YOUR_API_KEY")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status)
}
```
```csharp .NET theme={null}
using System.Net.Http;
using System.Text;
using System.Text.Json;
var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-Api-Key", "YOUR_API_KEY");
var payload = JsonSerializer.Serialize(new {
targetUrl = "https://example.com/everhour-events",
events = new[] { "api:task:created", "api:timer:started" }
});
var content = new StringContent(payload, Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://api.everhour.com/hooks", content);
Console.WriteLine(await response.Content.ReadAsStringAsync());
```
## Managing webhooks
| Method | Path | Description |
| -------- | ------------- | -------------------------------------------- |
| `GET` | `/hooks` | List all webhooks for the authenticated user |
| `GET` | `/hooks/{id}` | Get a single webhook |
| `POST` | `/hooks` | Create a webhook (returns `201 Created`) |
| `PUT` | `/hooks/{id}` | Update subscribed events or project filter |
| `DELETE` | `/hooks/{id}` | Delete a webhook (returns `204 No Content`) |
Webhooks are scoped to the authenticated user — each API key sees only the webhooks it created. A user cannot register the same `targetUrl` more than once.
When updating a webhook with `PUT /hooks/{id}`, the `targetUrl` field is ignored. You can change only the subscribed events and the project filter. A handshake is performed again on update to confirm the endpoint is still reachable.
## Project-scoped webhooks
A webhook can be scoped to a single project by including a `project` field on creation. Project-scoped webhooks receive events only for that project.
Project-scoped webhooks cannot subscribe to `api:project:created`, `api:client:*`, or `api:invoice:*` events. Those events are account-level and are only available on unscoped webhooks.
## Event types
At least one event must be selected when creating a webhook.
| Event | Triggered when |
| ----------------------- | ---------------------------------------- |
| `api:project:created` | A project is created |
| `api:project:updated` | A project's details are updated |
| `api:project:removed` | A project is deleted |
| `api:task:created` | A task is created |
| `api:task:updated` | A task's details are updated |
| `api:task:removed` | A task is deleted |
| `api:task:recovered` | A previously deleted task is restored |
| `api:timer:started` | A timer is started |
| `api:timer:stopped` | A running timer is stopped |
| `api:time:updated` | A time record is created or modified |
| `api:section:created` | A section is created |
| `api:section:updated` | A section's details are updated |
| `api:section:removed` | A section is deleted |
| `api:section:recovered` | A previously deleted section is restored |
| `api:client:created` | A client is created |
| `api:client:updated` | A client's details are updated |
| `api:estimate:updated` | A task estimate is changed |
| `api:invoice:created` | An invoice is created |
| `api:invoice:updated` | An invoice is updated |
| `api:invoice:deleted` | An invoice is deleted |
## Event payload structure
Everhour sends a POST request to your `targetUrl` with a JSON body in the following shape:
```json theme={null}
{
"event": "api:task:created",
"createdAt": "2026-05-04 12:00:00",
"data": {
"id": "gh:123456",
"data": {
// full serialized resource object — same shape as the corresponding REST endpoint
}
},
"attributes": {
"projects": ["ev:1"],
"users": [123]
},
"user": {
"id": 123,
"teamId": 456
}
}
```
* `event` — the event type that fired
* `createdAt` — timestamp of the event in `YYYY-MM-DD HH:MM:SS` format (UTC)
* `data.data` — the full resource object, identical in shape to the corresponding REST API response
* `attributes.projects` — list of project IDs related to the event
* `attributes.users` — list of user IDs related to the event
* `user` — the Everhour user whose action triggered the event
## Security
Everhour does not sign event delivery payloads. There is no HMAC or signature header on the POST requests sent to your `targetUrl`. The `X-Hook-Secret` header is only sent once during the subscription handshake and is not repeated on event deliveries.
To secure your endpoint, use TLS (HTTPS) and treat the `targetUrl` itself as a secret. Anyone who knows the URL can POST arbitrary data to it, so avoid publishing or logging the URL.
The handshake on subscription creation verifies that your endpoint is reachable, but it does not establish an ongoing trust mechanism. Design your endpoint to validate the structure and content of incoming payloads as appropriate for your use case.
## Delivery and reliability
* Deliveries are asynchronous. There is no guaranteed maximum latency between an event firing and your endpoint being called.
* If your endpoint returns a non-`2xx` response, Everhour will retry delivery. Sustained failures may cause the webhook to be automatically disabled (`active=false`).
* To reactivate a disabled webhook, send a `PUT /hooks/{id}` request with the desired events list. This triggers a new handshake.
* Specific retry counts and timeout windows are managed internally and are not configurable via the API.