# 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, including removed ones. Each member comes back with profile, group, permission, and time tracking policy fields. What you see depends on your own access: `cost` returns `0` unless you have the `costs` permission, and `permissions` returns an empty array (`[]`) for members with no elevated access.
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. Returns every field of the team member object plus personal settings: timezone, date and time formats, time off permissions, connected integration accounts, and the full team object.
# 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.
# Everhour in Claude
Source: https://developers.everhour.com/claude
Connect Everhour to Claude to start timers, log time, check your timesheet, and find tasks by asking in plain language.
Everhour in Claude lets you track time by asking for it. Start a timer, log an hour to yesterday, check what you tracked this week, or find the task you're about to work on — in the same conversation where you're already working, without opening Everhour.
Claude reads and writes your Everhour data as you, so it sees exactly what you'd see in the web app and nothing more.
## What you can ask for
* **Your timer** — start one on a task, start one without a task, stop it, or check what's running.
* **Your time** — log time to a task, fix a duration you got wrong, or clear an entry you put in the wrong place.
* **Your timesheet** — this week with daily totals, or any month of raw entries.
* **Your tasks** — search by keyword, see what you tracked recently, open one task in detail, or pin the ones you return to.
* **Your projects, clients, and team** — look up a project, a client, or who's on the team.
* **Team reports** — hours by project or member, project budgets, who's on time off, and weekly capacity, as far as your role allows.
## Set up Everhour in Claude
You need an active Everhour account. There's nothing to set up in Everhour first — no app to create and no key to copy, and nothing to pay: connecting an assistant is included in every Everhour plan at no extra cost.
Add Everhour as a custom connector, sign in when your browser opens, and switch it on in your chat. It works the same in Claude on the web, in the desktop app, and in the mobile apps.
**On a Team or Enterprise plan, only an Owner can add a connector.** If that's not you, you don't need to do the setup — ask your Owner to add Everhour once, then open **Customize → Connectors** and click **Connect**. On Free, Pro, and Max you add it yourself.
The setup, click by click — including what to do if you're not an admin.
One command, then authorize in the browser.
To check it worked, ask *"What's my Everhour timesheet this week?"* A real answer means you're connected.
## Interactive cards
In Claude, some answers come back as a card you can actually use, not just text. Ask about your timer and you get a running clock with a **Stop** button. Ask for your tasks and you get a list where each row starts a timer.
There are four:
* **Timer** — the running task, elapsed time, and a **Stop** button. Appears when you ask what you're tracking, or when you start or stop a timer.
* **Task list** — recent tasks, search results, or your favorites. Every row has a start button and a chevron that opens the full task; rows in the favorites list also carry a star.
* **Task card** — one task with its project, assignees, estimate, status, tags, and your time on it. You can start the timer, add time, or star it without leaving the card.
* **Timesheet** — a month grid or a week list, switchable, with daily and weekly totals. In the month grid, click a day to see the entries behind it.
The task card, up close:
The timesheet card in each of those states:
The cards talk to Everhour directly, so a timer you stop in a card is stopped everywhere — the web app, the browser extension, the mobile apps.
Cards appear in the Claude apps that can draw them. Anywhere they can't, the same answer arrives as plain text with nothing left out.
## Example prompts
Phrase things however you like — these are just starting points.
**Tracking right now**
* *"Start a timer on the login bug."*
* *"What am I tracking?"*
* *"Stop my timer."*
* *"Start a timer for the standup — I'm not on a task."*
**Filling in time**
* *"Log 2 hours to the API docs task."*
* *"I forgot to track yesterday — put 3 hours on the checkout redesign for Tuesday."*
* *"Change yesterday's design entry to 90 minutes."*
* *"I logged that to the wrong task, clear it."*
Clearing sets the time to zero rather than deleting the row — the task keeps a 0-hour line on your timesheet. To move the time somewhere else, ask Claude to change the entry to the right task instead of clearing it. [Why it works that way](/mcp-tools#time-records).
**Looking back**
* *"Show my timesheet for this week."*
* *"How much did I track in April?"*
* *"Which project got most of my time last month?"*
**Finding work**
* *"Find tasks about onboarding."*
* *"What have I worked on recently?"*
* *"Track time on this Asana link."* (paste the URL)
* *"Pin the checkout task to my favorites."*
**Reporting on the team**
* *"How many hours did the team log by project last month?"*
* *"Which projects are over budget?"*
* *"Who's out on time off next week?"*
## Permissions and privacy
* **You approve the connection once.** Signing in happens on Everhour, in your browser. Claude never sees your password.
* **Claude acts as you, and only as you.** It can do exactly what your Everhour role allows — a member can't read the team's payroll through Claude any more than through the web app.
* **Claude asks before it changes anything.** The tools that write to your account are marked as such, and Claude prompts you to confirm each time.
* **Everhour doesn't see your conversation.** We receive the details of the actions Claude takes — a task, a duration, a date, a comment, a search term — and nothing else. What you type to Claude stays between you and Claude; it never reaches Everhour.
[Data, privacy, and permissions](/mcp-privacy) covers all of this in full, including how long Claude's access lasts and which links a card can open.
## If something isn't working
Check that Everhour is switched on for this conversation — click **+** at the lower left of the chat box and look under **Connectors**. Connectors are enabled per chat, so one you added earlier may be off in a new conversation.
That's a permissions boundary, not a figure. Cost and billing columns you're not allowed to see come back as `0` — see [Data, privacy, and permissions](/mcp-privacy#what-the-assistant-can-do).
That's the *destructive* hint doing its job — the tools that change your data always prompt. The rest read only and run without asking. [Which ones](/mcp-privacy#actions-that-ask-before-they-run).
If signing in never finishes, Claude lists fewer tools than it should, or the connection needs resetting, see [Troubleshooting](/mcp-connect#troubleshooting). To disconnect Everhour entirely, see [Disconnecting](/mcp-connect#disconnecting).
# 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, Claude Desktop, 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.
This page covers the mechanics. For what to ask Claude once it's connected — example prompts and the interactive cards — see [Everhour in Claude](/claude).
## 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.
***
## Claude Desktop
The Claude desktop app and [claude.ai](https://claude.ai) add Everhour as a **custom connector**. The steps are the same in both, and in the mobile apps.
### Prerequisites
Complete the [prerequisites](#prerequisites). No API key or app registration needed. Which path you take depends on your Claude plan:
| Your plan | What you do |
| - | - |
| Free, Pro, or Max | Add the connector yourself — follow everything below. |
| Team or Enterprise, and you're an Owner | Add it once for the organization: **Organization settings → Connectors → Add → Custom**, choosing **Web** if you're asked for a type. Then tell your team to use the short path. |
| Team or Enterprise, and you're not an Owner | You can't add a custom connector, and you don't need to. Skip to [If someone already added it](#if-someone-already-added-it). |
### Add the connector
1. Open **Customize → Connectors** in the left sidebar, then click **+** next to **Connectors**.
2. Give it a name — `Everhour` — and enter the server URL:
```
https://api.everhour.com/mcp
```
3. Click **Continue**. Claude checks the URL and fills in the authentication it finds, marked **Detected** — for Everhour that's **Sign in now** and **Register automatically**. Leave both as detected; there's no client ID or secret to paste, because Everhour registers your client itself.
4. Click **Add**.
Older builds label this **Settings → Connectors**.
The two-step dialog is still rolling out. If yours puts the name, URL, and **Advanced settings** on a single screen, leave **Advanced settings** alone and click **Add** — there's nothing to change there either way.
### If someone already added it
On a Team or Enterprise plan, your Owner adds Everhour once for everyone. When they have, open **Customize → Connectors**, find Everhour in the list, and click **Connect** — there's no URL to paste and no name to pick. Then carry on with **Authorize** below.
### Authorize
1. Click **Connect** next to Everhour.
2. Your browser opens Everhour's sign-in. Sign in, review the consent screen, and click **Allow**.
3. Everhour shows as connected.
### Enable it in a chat
Connectors are switched on per conversation. Click **+** at the lower left of the chat box, choose **Connectors**, and turn Everhour on.
### Verify
Ask: *"What's my Everhour timesheet this week?"*
***
## 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](/claude) — 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 28 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 Desktop and claude.ai:** open **Customize → Connectors**, remove Everhour, then add it again. On Team and Enterprise an Owner has to do this under **Organization settings → Connectors**.
* **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 it refreshing them.
* 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 Desktop and claude.ai — **Customize → 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 — within an hour, not instantly. Everhour has no revocation endpoint yet; see [Data, privacy, and permissions](/mcp-privacy#approving-and-ending-access).
## 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.
## What you can do
Once connected, your assistant has access to 28 tools covering everyday time tracking:
* **Track time in real time** — start, stop, and check your running timer, with or without a task attached.
* **Log and edit time** — add entries (in seconds), fix a duration, clear a wrong entry, or log time retroactively.
* **Review timesheets** — read your weekly timesheet with daily totals and a per-task breakdown, or raw entries over any month.
* **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.
* **Keep a shortlist** — pin and unpin favorite tasks, and read the pinned list back.
* **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.
## Interactive cards
Some tools return an interactive card alongside their text answer: a timer with a **Stop** button, a task list where every row starts tracking, a full task card, and a timesheet you can switch between a month grid and a week list. Actions inside a card call Everhour directly, so a timer you stop there is stopped everywhere.
Cards render in clients that support [MCP Apps](https://modelcontextprotocol.io). Everywhere else the same answer arrives as text, with nothing missing. See [the card reference](/mcp-tools#interactive-cards) for which tool returns which card.
## 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. See [Data, privacy, and permissions](/mcp-privacy) for what Everhour receives, how long access lasts, and which tools prompt before they run.
## Requirements
* An active Everhour account, on any plan. The MCP server is included in every plan at no extra cost.
* An MCP-capable AI client (see [supported clients](/mcp-connect)).
## Server details
`POST https://api.everhour.com/mcp`
Streamable HTTP
MCP `2025-11-25` and `2025-06-18`, JSON-RPC 2.0
OAuth 2.1 (or `X-Api-Key` for manual setups)
## Next steps
Set up Claude, ChatGPT, Cursor, and other clients.
Ask for your time in plain language, and see what the cards do.
Every tool the assistant can call, and the cards they return.
What Everhour receives, what your role allows, and how access ends.
# Data, privacy, and permissions
Source: https://developers.everhour.com/mcp-privacy
What Everhour receives when an assistant calls a tool, what your role lets the assistant do, how long its access lasts, and which links its cards can open.
Connecting an AI assistant to Everhour gives it a door into your account. This page says exactly how wide that door is: what crosses it, who decides, and how it closes.
Landed here first? [Everhour in Claude](/claude) covers what the assistant can do for you, and [Connect an MCP client](/mcp-connect) covers setting it up.
## What Everhour receives
**Everhour does not store your conversation, and does not store personal data.** The messages between you and the assistant never reach Everhour — we do not receive them, store them, or process them. We have no access to your chat history, your assistant's memory, or your files.
What does reach Everhour is the arguments of the tool calls the assistant makes: a task ID, a duration, a date, a search term, and any comment text you asked it to save. Everhour handles these as ordinary API requests — they're what the tool needs in order to do the thing you asked for.
We keep a log of those calls for **30 days**, for one purpose: so we can debug a call that behaved incorrectly. After 30 days it's discarded. Nothing beyond the call arguments themselves goes into it.
One thing worth stating plainly: free text you ask the assistant to save — a timer comment, a search keyword — *is* a call argument. It travels with the request and sits in that 30-day debug log like any other API parameter. Everything else you type stays with your AI client.
Everhour's handling of this data is governed by the [privacy policy](https://everhour.com/privacy).
## What the assistant can do
Two different permission systems are in play, and it's worth keeping them apart:
* **Who may connect Everhour at all** is decided by your AI client, not by Everhour. In Claude, for example, only an Owner can add a connector on a Team or Enterprise plan. Everhour has no say in that and cannot see it.
* **What the assistant may do once connected** is decided entirely by Everhour, and it's your existing role — nothing new to grant or configure.
The assistant acts as you, and only as you. It can do exactly what your Everhour role allows — a member can't read the team's payroll through the assistant any more than through the web app. There's no separate permission model for MCP: the tools go through the same authorization as the [REST API](/authentication).
Some tools sit behind roles:
| Tool | Requires |
| - | - |
| `everhour_get_project_budgets` | Billing access |
| `everhour_get_time_off` | Supervisor access |
| `everhour_get_users_capacity` | Admin access |
| `everhour_get_client` | Admin access |
| `everhour_get_dashboard` with `payroll` | Cost access |
| `everhour_get_dashboard` with `clients` | Admin access |
| `everhour_get_dashboard` with `timecards` | Supervisor access |
Everything else is open to any member, including `everhour_list_clients` — the list is readable by
everyone, a single client isn't.
When a tool isn't available to you, the assistant is told why rather than shown an empty report.
One exception is worth knowing about when you read a report back. In `everhour_get_dashboard`, money columns you're not allowed to see come back as `0` rather than being left out — costs, billable amount, profit, expenses, and their sub-columns. A `0` there means "not visible to this caller", not "no money". An assistant that reports it as an amount is reporting a permission boundary, not a figure.
## Approving and ending access
You approve the connection once, in your browser. Adding Everhour from your client sends you to Everhour to sign in and approve access — the assistant never sees your password. The flow is standard [OAuth 2.1](/oauth) with PKCE, and the token your client receives is audience-bound to `https://api.everhour.com/mcp`.
Access is short-lived by design. The access token expires after an hour and is renewed silently while the connection is active. Removing the connector in your client stops the renewal, so access ends within the hour.
Disconnecting always ends access, and always within the hour. What Everhour doesn't have yet is a way to cut it off *instantly* — there's no **Revoke** button in your profile and no revocation endpoint, so the current token runs out its remaining time. If you need access stopped sooner than that, contact support.
If you connected with an `X-Api-Key` header instead of OAuth, that key never expires on its own. Regenerate it in [My Profile → API key](https://app.everhour.com/#/account/profile) to cut off every client using it at once.
## Actions that ask before they run
Most of the tools are annotated read-only and change nothing. Seven carry the *destructive* hint, and a well-behaved client prompts you to confirm before running any of them:
* `everhour_start_timer`
* `everhour_start_timer_without_task`
* `everhour_stop_timer`
* `everhour_log_time`
* `everhour_update_time_entry`
* `everhour_delete_time_entry`
* `everhour_favorite_task`
The hint covers anything that changes state you'd notice, not only deletion. Starting a timer stops whichever timer was already running, so it prompts too. Note that `everhour_delete_time_entry` doesn't remove an entry — it clears the duration to 0 and drops the comment, leaving the entry on its task and date.
## Links the cards open
Everhour's [interactive cards](/mcp-tools#interactive-cards) open two kinds of link.
Links to Everhour itself are built by the card and always resolve to `https://app.everhour.com` — the only origin we own and the only one we declare. There are two shapes: `https://app.everhour.com/#/time(view:)` for a task, and `https://app.everhour.com/#/projects//tasks` for a project.
Links to a task in an external tool — GitHub, Asana, Jira, Linear, ClickUp, and the other platforms Everhour syncs with — are **not constructed by the card**. They're stored on the task record, written there by your own integration when the task was synced, and the card passes that stored URL through unchanged. We deliberately don't declare those origins: they differ per customer, we don't own them, and we'd rather each one keep prompting you for confirmation than list domains that aren't ours. The card accepts such a URL only when its scheme is `http` or `https`, and falls back to the Everhour address otherwise.
## Rate limits
The published [API rate limits](/rate-limits) apply to requests authenticated with an API key, including MCP calls made that way. Requests authorized over OAuth aren't counted against that quota today, which is a limit we may add later rather than a guarantee — assistant traffic is a handful of calls per question, so normal use stays well clear of it either way.
## Privacy policy
Everhour's handling of your data is governed by the [Everhour privacy policy](https://everhour.com/privacy) and the [terms of service](https://everhour.com/terms). Connecting an assistant doesn't change either.
# MCP tools reference
Source: https://developers.everhour.com/mcp-tools
The 28 tools the Everhour MCP server exposes, grouped by area, with example prompts.
The Everhour MCP server registers **28 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. Most are marked read-only and never change data — clients can run them without asking you first.
Seven tools carry the *destructive* hint, so Claude, Cursor, and ChatGPT prompt you to confirm before running them:
* `everhour_start_timer`
* `everhour_start_timer_without_task`
* `everhour_stop_timer`
* `everhour_log_time`
* `everhour_update_time_entry`
* `everhour_delete_time_entry`
* `everhour_favorite_task`
The hint marks a tool that changes state you can notice, not just one that deletes. Starting a timer stops whichever timer was already running, so it earns the prompt too.
This page describes what each tool does, not its full call signature. Parameters appear below only where the behaviour is non-obvious — everything else is in the JSON Schema the server publishes. Call the MCP `tools/list` method for the authoritative, live set of names, schemas, and annotations; this page mirrors it rather than replacing it.
## Timer
| Tool | Description | Example |
| - | - | - |
| `everhour_get_current_timer` | Get the running timer and its elapsed time, or none if nothing is running. | *"Is my timer running?"* |
| `everhour_start_timer` | Start the timer on a task. Switches from any timer already running — that one stops and keeps its elapsed time. Starting on the task already running keeps the elapsed time but replaces the comment with the one sent now, so omitting `comment` clears the existing one rather than leaving it. | *"Start a timer on the login bug."* |
| `everhour_start_timer_without_task` | Start a timer that isn't tied to a task, identified by its comment alone. Teams can forbid tracking without a task, in which case the call is rejected with that reason. | *"Start a timer for the standup."* |
| `everhour_stop_timer` | Stop the currently running timer. | *"Stop my timer."* |
Timer state lives on the server and changes outside the conversation — the web app, browser extension, and mobile apps all start and stop the same timer.
## Time records
These tools carry `time_entry` in their names; the record they act on is what `concepts.mdx` calls a [time record](/concepts#time-record).
| Tool | Description | Example |
| - | - | - |
| `everhour_log_time` | Log manual time on a task. When exactly one record already exists for the same task, date, and user, this **adds** to that record instead of creating a second one — logging 30m twice on one task and date leaves 1h. | *"Log 2 hours to the API docs task."* |
| `everhour_update_time_entry` | **Set** an existing record to a new duration, date, or comment. Unlike `everhour_log_time`, this replaces the duration rather than adding to it. | *"Change yesterday's design entry to 90 minutes."* |
| `everhour_delete_time_entry` | Clear a time record: its duration becomes 0 and its comment is removed. The record itself is **not** deleted — it stays on the same task and date and still appears in timesheets with 0 time. | *"Clear the time I logged to the wrong task."* |
| `everhour_get_user_time` | Raw time records for one user over a date range, set with `from` and `to`. `user_id` picks whose — it defaults to `me`, so pass a team member's ID to read theirs. One row per record with its date, duration, comment, and task. No totals, no pagination — the range is capped at 31 days. | *"How much did Alex track last week?"* |
| `everhour_get_my_timesheet` | One week of a timesheet with server-computed daily totals and a per-task breakdown. `week` takes `YYYY-MM-DD` for any date inside your own week, `:YYYY-MM-DD` for another member's, or a numeric week ID. | *"Show my timesheet for this week."* |
**`everhour_update_time_entry` with `time: 0` clears the record instead of updating it.** The duration goes to 0, the comment is removed, and a `date` or task sent in the same call is silently ignored — the same outcome as `everhour_delete_time_entry`. To move a record to another date, send a non-zero `time`.
Pick by shape of the answer: `everhour_get_my_timesheet` for one week with totals, `everhour_get_user_time` for raw entries over any month, and `everhour_get_dashboard` with `dashboard="months"` for totals across a longer period.
## Tasks
| Tool | Description | Example |
| - | - | - |
| `everhour_search_tasks` | Search tasks by keyword across all projects or within one project. Use to find a task before logging time. | *"Find tasks about onboarding."* |
| `everhour_recent_tasks` | Tasks you tracked most recently, newest record first. When there isn't enough history to fill the limit, the tail is padded — first your favorites, then your timesheet tasks, then the newest tasks of your projects. A task listed here wasn't necessarily tracked at all. Default `limit` 15, max 30. With `project_id`, padding stays inside that project and only the last month of tracking counts. | *"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` | Every task of one project, filtered by name when `query` is given. There's no sort order and no sorting parameter, so a `limit` smaller than the project returns an arbitrary subset — not the newest tasks — and stable paging isn't guaranteed. Default `limit` is 50. | *"List the tasks in the Website project."* |
| `everhour_resolve_platform_ids` | Convert platform-specific task IDs (Asana, Jira, GitHub, and others) to Everhour IDs. Use when someone 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.
## Favorites
| Tool | Description | Example |
| - | - | - |
| `everhour_list_favorite_tasks` | Your personal shortlist of pinned tasks. Rows come back in storage order rather than sorted by `position`, so sort on it when the pinned order matters. Rows are typed: `task` carries the task, `comment` is a saved timer comment for tracking without a task, and `section` is a folder header. | *"What are my starred tasks?"* |
| `everhour_favorite_task` | Pin a task to your favorites, or unpin it when `favorite` is false. Passing the state it already has leaves it unchanged. | *"Pin the checkout task to my favorites."* |
## Projects & clients
| Tool | Description | Example |
| - | - | - |
| `everhour_list_projects` | Projects you have access to, one page at a time. Teams run to thousands of projects, so narrow with `query` or page through rather than expecting the whole list. Default `limit` is 50. | *"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. Requires admin access — `everhour_list_clients` is open to every member, but a single client isn't. | *"Show details for the Acme account."* |
## Team
| Tool | Description | Example |
| - | - | - |
| `everhour_get_me` | Get the current authenticated user's 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. Current status, so no date range. Filter by `project_id`, `client_id`, `billing`, `budget_type`, or `budget_period`. `project_status` defaults to `open`, so pass `archived` to see closed projects. Requires billing/budgets access. | *"Which projects are over budget?"* |
| `everhour_get_time_off` | Team time-off assignments that overlap a date range. Only **approved** assignments are returned — pending and declined requests aren't visible. Requires supervisor access. | *"Who's out on time off next week?"* |
| `everhour_get_users_capacity` | Weekly capacity per team member: capacity against logged, billable, and remaining time. 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, the optional `member_id` / `project_id` / `client_id` filters, and `columns` to request a subset of fields.
| `dashboard` | Shows | Example |
| - | - | - |
| `users` | Time, billing, cost, and profit 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. Limited members are narrowed to themselves in every view except `team-hours`.
**A money column you can't see comes back as `0`, not missing.** Costs, billable amount, profit, expenses, and their sub-columns are zeroed for callers without the matching role. A `0` there means "not visible to this caller", never "no money" — don't report it as an amount. Columns gated by role alone (billable amount, costs, and profit for non-admins; payroll without cost access) are absent from the response entirely, which is the case you *can* safely detect.
## 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?"* |
## Guide
| Tool | Description | Example |
| - | - | - |
| `everhour_get_instructions` | The Everhour usage guide, written for the assistant rather than for you: units and date rules, which timesheet tool answers which question, how permissions shape a report, how to format the result. Pass `section` for one part of it, or omit `section` for the whole guide. | — |
This is the one tool nobody asks for by name. The server sends a short set of instructions when your client connects, and hosts truncate that at 2 KB — so the rest of the guide lives here, and a client fetches the part it needs while working.
`section` takes one of six values:
| `section` | Covers |
| - | - |
| `basics` | Domain model, units, date and week rules |
| `workflows` | Timers, logging time, finding tasks, favorites, platform IDs |
| `timesheet` | Reading a timesheet and raw entries without wrong totals |
| `reporting` | Team dashboards, money columns, permissions |
| `formatting` | Output format, chart choice, brand palette |
| `troubleshooting` | Best practices and what error answers mean |
The same guide is also published as an MCP resource, for hosts that read resources instead of calling tools. `everhour://instructions` returns the whole guide, and the `everhour://instructions/{section}` template returns one section — the same six names as above.
## Interactive cards
Thirteen of the 28 tools return an interactive card alongside their text result, so the answer is something you can act on rather than read. The server publishes four card templates as MCP resources under `ui://everhour/`:
| Card | Shows | Returned by |
| - | - | - |
| `timer` | Running task, elapsed time, and a **Stop** button | `everhour_get_current_timer`, `everhour_start_timer`, `everhour_start_timer_without_task`, `everhour_stop_timer` |
| `tasks` | Task list where every row starts a timer | `everhour_search_tasks`, `everhour_recent_tasks`, `everhour_list_tasks`, `everhour_list_favorite_tasks` |
| `task` | One task with project, assignees, estimate, status, tags, and your time on it | `everhour_get_task`, `everhour_log_time`, `everhour_favorite_task` |
| `timesheet` | Month grid or week list, switchable, with daily and weekly totals | `everhour_get_my_timesheet`, `everhour_get_user_time` |
Cards are served with the `text/html;profile=mcp-app` MIME type and render in clients that support MCP Apps. Everywhere else the same answer arrives as text, with nothing missing. Actions inside a card call Everhour directly, so a timer you stop in a card is stopped everywhere — the web app, the browser extension, and the mobile apps.
See [Everhour in Claude](/claude) for what the cards look like in use, and [Data, privacy, and permissions](/mcp-privacy) for the links a card is allowed to open.
# 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.