> ## Documentation Index
> Fetch the complete documentation index at: https://developers.everhour.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 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

<Steps>
  <Step title="Register a webhook">
    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.
  </Step>

  <Step title="Handle the handshake">
    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: <value>
    ```

    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.
  </Step>

  <Step title="Receive events">
    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.
  </Step>
</Steps>

## Registering a webhook

<CodeGroup>
  ```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}
  <?php
  $ch = curl_init('https://api.everhour.com/hooks');
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_POST, true);
  curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-Api-Key: YOUR_API_KEY',
      'Content-Type: application/json',
  ]);
  curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
      'targetUrl' => '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<String> 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());
  ```
</CodeGroup>

## 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.

<Note>
  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.
</Note>

## 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

<Warning>
  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.
</Warning>

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.
