> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developer.jelliu.co/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developer.jelliu.co/_mcp/server.

# List call metrics by hour of day

GET https://api.jelliu.co/api/analytics/calls-by-hour

Returns one row per hour of the day (0-23) that had at least one call in the window,
with volume, completion rate, success rate and average sentiment. Use it to find the
hours when people pick up and convert, for example to tune a campaign's calling
schedule.

**Filters.** `days` sets the window (rolling, ending now; missing or out-of-range values
become 30). `direction`, `campaignId` and `agentId` narrow every row. `timezone` decides
the hour a call falls in (pass the zone your contacts live in); an unknown zone name
silently falls back to UTC.

**Ordering.** Ascending by `hour`, at most 24 rows.

**No data.** Hours without calls are omitted. `completionRate` and `successRate` are `0`
when their denominator is zero; `avgSentiment` is `null` when no call in that hour was
scored.

**Idempotency.** Safe to retry: this is a read.

**Consistency.** Cached for 5 minutes, then served stale for up to 30 minutes while a
background refresh runs.

**Access**
- **Required scope:** `read`. Signed-in users need the owner, admin, member or viewer role.
- **Rate limit:** Analytics — 30 requests/min per workspace, in addition to the general API budget of your plan. See [Rate limits](/rate-limits).
- **Plan:** Available on every plan. A workspace without an active subscription (active, trialing or past due) gets `403 BILLING_ERROR`.


Reference: https://developer.jelliu.co/api-reference/analytics/get-analytics-calls-by-hour

## Authentication

- `Authorization` header (bearer token, required) — Workspace API key: `jl_` followed by 64 lowercase hex characters, created by the workspace owner in the dashboard (**Settings → API Keys**) and sent as `Authorization: Bearer jl_...`. The plaintext is shown once, at creation; Jelliu stores only a SHA-256 hash. A workspace can hold up to 25 active keys. | Scope | GET / HEAD | POST / PUT / PATCH / DELETE | Admin-only routes | | --- | --- | --- | --- | | `read` | Yes | No | No | | `write` | Yes | Yes | No | | `full` | Yes | Yes | Yes | Operations restricted to admins or owners reject keys without the `full` scope with `403`, and say so in their description. No key, whatever its scope, can mint or revoke API keys or rotate a webhook secret — that requires a signed-in owner session. A revoked key stops authenticating within about 10 seconds. See [Authentication](/authentication).

## Request

### Query parameters

- `days` (integer, optional, default: 30) — Size of the reporting window in days, ending now. On most analytics routes a missing or out-of-range value falls back to `30` instead of being rejected; the operation says when it is strict.
- `direction` (enum, optional, default: all) — Which calls to count: `inbound` (calls the agent answered), `outbound` (calls the agent placed) or `all` (both).
  - Allowed values: `inbound`, `outbound`, `all`
- `campaignId` (string, optional) — Scope every number to one campaign of the workspace. Omit it to aggregate across all campaigns and manual calls.
- `agentId` (string, optional) — Scope every number to one agent — the agent that served each call, so manual (campaign-less) calls count. Omit it to aggregate across agents.
- `timezone` (string, optional) — IANA time zone the day and hour buckets are computed in (default `UTC`). An unknown zone falls back to UTC; a value longer than 50 characters is rejected with `400`.

## Response

### 200

Hourly rows in ascending hour order.

- `data` (list of object, optional)
  - `hour` (integer, optional) — Hour of the day, 0-23, in the requested timezone (UTC by default).
  - `total` (integer, optional) — Calls created in that hour across all days of the window, any status.
  - `completed` (integer, optional) — Calls with status `completed`.
  - `successes` (integer, optional) — Completed calls whose outcome is a success outcome of any category.
  - `completionRate` (double, optional) — Percentage 0-100, `completed / total`, two decimals.
  - `successRate` (double, optional) — Percentage 0-100, `successes / completed`, two decimals. `0` when nothing completed.
  - `avgSentiment` (double, optional, nullable) — Mean sentiment (-1 to 1) of scored calls, two decimals. `null` when none were scored.

## Errors

### 400 Bad Request Error

`direction` is not `inbound`, `outbound` or `all`, `campaignId` or `agentId` is not a UUID, or `timezone` is longer than 50 characters.

- `error` (object, required) — The error object. Always has `code` and `message`.
  - `code` (string, required) — Stable machine-readable error code (for example `VALIDATION_FAILED`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `BILLING_ERROR`, `COMPLIANCE_BLOCKED`, `RATE_LIMIT_EXCEEDED`, `INTERNAL_ERROR`). Switch on this, not on `message`. See [Errors](/errors).
  - `message` (string, required) — Human-readable explanation. English or Spanish depending on the route; may change without notice.
  - `details` (object or list of object, optional) — Present on `VALIDATION_FAILED` only. Its shape depends on how the route validates: * a field map, either Zod's `flatten()` output (`{ "formErrors": [], "fieldErrors": { "name": ["..."] } }`) or just its `fieldErrors` part (`{ "name": ["..."] }`); * an issue list, where each issue has at least `path`, `message` and `code`. `path` is a dot-separated string on routes that let the schema throw, and an array of keys on routes that forward Zod's raw issues (those also carry Zod's extra issue fields).
    - Field map
      - `formErrors` (list of string, optional)
      - `fieldErrors` (map from string to list of string, optional)
  - `metadata` (map from string to any, optional) — Structured detail exposed for a small allowlist of codes only — for example `BILLING_ERROR` carries `limit`, `current` and `tier` (resource caps) or `tier` and `feature` (feature gates).

### 401 Unauthorized Error

No usable credential. Either the `Authorization` header is missing or is not a well-formed `Bearer jl_…` key, or the key is unknown, revoked or expired. Do not retry with the same key. See [Authentication](/authentication#401-unauthorized).

- `error` (object, required) — The error object. Always has `code` and `message`.
  - `code` (string, required) — Stable machine-readable error code (for example `VALIDATION_FAILED`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `BILLING_ERROR`, `COMPLIANCE_BLOCKED`, `RATE_LIMIT_EXCEEDED`, `INTERNAL_ERROR`). Switch on this, not on `message`. See [Errors](/errors).
  - `message` (string, required) — Human-readable explanation. English or Spanish depending on the route; may change without notice.
  - `details` (object or list of object, optional) — Present on `VALIDATION_FAILED` only. Its shape depends on how the route validates: * a field map, either Zod's `flatten()` output (`{ "formErrors": [], "fieldErrors": { "name": ["..."] } }`) or just its `fieldErrors` part (`{ "name": ["..."] }`); * an issue list, where each issue has at least `path`, `message` and `code`. `path` is a dot-separated string on routes that let the schema throw, and an array of keys on routes that forward Zod's raw issues (those also carry Zod's extra issue fields).
    - Field map
      - `formErrors` (list of string, optional)
      - `fieldErrors` (map from string to list of string, optional)
  - `metadata` (map from string to any, optional) — Structured detail exposed for a small allowlist of codes only — for example `BILLING_ERROR` carries `limit`, `current` and `tier` (resource caps) or `tier` and `feature` (feature gates).

### 403 Forbidden Error

No active subscription, a signed-in role outside owner/admin/member/viewer, an API key without the `read` scope, or a suspended workspace.

- `error` (object, required) — The error object. Always has `code` and `message`.
  - `code` (string, required) — Stable machine-readable error code (for example `VALIDATION_FAILED`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `BILLING_ERROR`, `COMPLIANCE_BLOCKED`, `RATE_LIMIT_EXCEEDED`, `INTERNAL_ERROR`). Switch on this, not on `message`. See [Errors](/errors).
  - `message` (string, required) — Human-readable explanation. English or Spanish depending on the route; may change without notice.
  - `details` (object or list of object, optional) — Present on `VALIDATION_FAILED` only. Its shape depends on how the route validates: * a field map, either Zod's `flatten()` output (`{ "formErrors": [], "fieldErrors": { "name": ["..."] } }`) or just its `fieldErrors` part (`{ "name": ["..."] }`); * an issue list, where each issue has at least `path`, `message` and `code`. `path` is a dot-separated string on routes that let the schema throw, and an array of keys on routes that forward Zod's raw issues (those also carry Zod's extra issue fields).
    - Field map
      - `formErrors` (list of string, optional)
      - `fieldErrors` (map from string to list of string, optional)
  - `metadata` (map from string to any, optional) — Structured detail exposed for a small allowlist of codes only — for example `BILLING_ERROR` carries `limit`, `current` and `tier` (resource caps) or `tier` and `feature` (feature gates).

### 429 Too Many Requests Error

The analytics budget (30 requests/min) or the plan's general API budget is exhausted.

- `error` (object, required) — The error object. Always has `code` and `message`.
  - `code` (string, required) — Stable machine-readable error code (for example `VALIDATION_FAILED`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `BILLING_ERROR`, `COMPLIANCE_BLOCKED`, `RATE_LIMIT_EXCEEDED`, `INTERNAL_ERROR`). Switch on this, not on `message`. See [Errors](/errors).
  - `message` (string, required) — Human-readable explanation. English or Spanish depending on the route; may change without notice.
  - `details` (object or list of object, optional) — Present on `VALIDATION_FAILED` only. Its shape depends on how the route validates: * a field map, either Zod's `flatten()` output (`{ "formErrors": [], "fieldErrors": { "name": ["..."] } }`) or just its `fieldErrors` part (`{ "name": ["..."] }`); * an issue list, where each issue has at least `path`, `message` and `code`. `path` is a dot-separated string on routes that let the schema throw, and an array of keys on routes that forward Zod's raw issues (those also carry Zod's extra issue fields).
    - Field map
      - `formErrors` (list of string, optional)
      - `fieldErrors` (map from string to list of string, optional)
  - `metadata` (map from string to any, optional) — Structured detail exposed for a small allowlist of codes only — for example `BILLING_ERROR` carries `limit`, `current` and `tier` (resource caps) or `tier` and `feature` (feature gates).

## Examples

**Response**

```json
{
  "data": [
    {
      "hour": 9,
      "total": 38,
      "completed": 33,
      "successes": 10,
      "completionRate": 86.84,
      "successRate": 30.3,
      "avgSentiment": 0.36
    },
    {
      "hour": 10,
      "total": 52,
      "completed": 47,
      "successes": 15,
      "completionRate": 90.38,
      "successRate": 31.91,
      "avgSentiment": 0.33
    },
    {
      "hour": 15,
      "total": 41,
      "completed": 34,
      "successes": 8,
      "completionRate": 82.93,
      "successRate": 23.53,
      "avgSentiment": null
    }
  ]
}
```

**SDK Code**

```python Analytics_getAnalyticsCallsByHour_example
import requests

url = "https://api.jelliu.co/api/analytics/calls-by-hour"

querystring = {"agentId":"2f2d8b4d-aa6f-41e0-9d3e-4d61e1b07a11","campaignId":"0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90","days":"30","direction":"outbound","timezone":"America/Bogota"}

headers = {"Authorization": "Bearer <token>"}

response = requests.get(url, headers=headers, params=querystring)

print(response.json())
```

```javascript Analytics_getAnalyticsCallsByHour_example
const url = 'https://api.jelliu.co/api/analytics/calls-by-hour?agentId=2f2d8b4d-aa6f-41e0-9d3e-4d61e1b07a11&campaignId=0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90&days=30&direction=outbound&timezone=America%2FBogota';
const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go Analytics_getAnalyticsCallsByHour_example
package main

import (
	"fmt"
	"net/http"
	"io"
)

func main() {

	url := "https://api.jelliu.co/api/analytics/calls-by-hour?agentId=2f2d8b4d-aa6f-41e0-9d3e-4d61e1b07a11&campaignId=0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90&days=30&direction=outbound&timezone=America%2FBogota"

	req, _ := http.NewRequest("GET", url, nil)

	req.Header.Add("Authorization", "Bearer <token>")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby Analytics_getAnalyticsCallsByHour_example
require 'uri'
require 'net/http'

url = URI("https://api.jelliu.co/api/analytics/calls-by-hour?agentId=2f2d8b4d-aa6f-41e0-9d3e-4d61e1b07a11&campaignId=0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90&days=30&direction=outbound&timezone=America%2FBogota")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'

response = http.request(request)
puts response.read_body
```

```java Analytics_getAnalyticsCallsByHour_example
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.get("https://api.jelliu.co/api/analytics/calls-by-hour?agentId=2f2d8b4d-aa6f-41e0-9d3e-4d61e1b07a11&campaignId=0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90&days=30&direction=outbound&timezone=America%2FBogota")
  .header("Authorization", "Bearer <token>")
  .asString();
```

```php Analytics_getAnalyticsCallsByHour_example
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('GET', 'https://api.jelliu.co/api/analytics/calls-by-hour?agentId=2f2d8b4d-aa6f-41e0-9d3e-4d61e1b07a11&campaignId=0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90&days=30&direction=outbound&timezone=America%2FBogota', [
  'headers' => [
    'Authorization' => 'Bearer <token>',
  ],
]);

echo $response->getBody();
```

```csharp Analytics_getAnalyticsCallsByHour_example
using RestSharp;

var client = new RestClient("https://api.jelliu.co/api/analytics/calls-by-hour?agentId=2f2d8b4d-aa6f-41e0-9d3e-4d61e1b07a11&campaignId=0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90&days=30&direction=outbound&timezone=America%2FBogota");
var request = new RestRequest(Method.GET);
request.AddHeader("Authorization", "Bearer <token>");
IRestResponse response = client.Execute(request);
```

```swift Analytics_getAnalyticsCallsByHour_example
import Foundation

let headers = ["Authorization": "Bearer <token>"]

let request = NSMutableURLRequest(url: NSURL(string: "https://api.jelliu.co/api/analytics/calls-by-hour?agentId=2f2d8b4d-aa6f-41e0-9d3e-4d61e1b07a11&campaignId=0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90&days=30&direction=outbound&timezone=America%2FBogota")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```