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

# Posthook Metrics

> AI agent sessions, prompts, files and attributed commits captured by posthook, as aggregates or row by row

## What posthook data looks like

[Posthook](/posthook/overview) runs on each engineer's machine and records what Claude Code, Cursor and Codex CLI wrote, what the engineer prompted, and which of those lines were committed. Bilanc models it into four tables, all reachable through the metrics endpoint:

| Table                       | Grain                                       | Count metric                      |
| --------------------------- | ------------------------------------------- | --------------------------------- |
| `posthook_sessions`         | One row per agent session                   | `posthook-sessions-count`         |
| `posthook_session_messages` | One row per prompt or assistant reply       | `posthook-session-message-counts` |
| `posthook_session_files`    | One row per file touched in a session       | `posthook-session-file-counts`    |
| `posthook_session_commits`  | One row per commit a session contributed to | `posthook-session-commit-counts`  |

Two more posthook metrics live on the `pull_requests` table because they measure AI lines that reached merged PRs: `posthook-merged-ai-lines` and `posthook-ai-pct-of-code`.

All four posthook tables carry the session owner's Bilanc identity (`merged_user_id`, `team_ids`, squad and manager details), so every people filter and the same row-level security apply as elsewhere.

<Note>
  Only sessions from engineers whose posthook identity is mapped to a Bilanc user are returned. Sessions from unmapped emails are excluded, matching the dashboard.
</Note>

## Metrics

### Session summary metrics (`posthook_sessions`)

<ParamField path="posthook-sessions-count" type="count">
  Distinct sessions that generated or committed at least one AI line. Response column `posthook_sessions_count`. Matches the dashboard's Active Sessions card. Sessions with no captured AI activity exist in the table but are not counted.
</ParamField>

<ParamField path="posthook-active-users" type="count">
  Distinct engineers with at least one session in the window. Response column `posthook_active_users`.
</ParamField>

<ParamField path="posthook-active-time" type="aggregation required">
  AI working time in **seconds**, derived from hook events: each session is active for every 5-minute window containing at least one event. Response column `posthook_active_time`. Pass `"aggregation": "SUM"`; the value is always a sum regardless.
</ParamField>

<ParamField path="posthook-total-tokens" type="aggregation required">
  Input + output tokens. Response column `{agg}_total_tokens`. Cache-read and cache-creation tokens are excluded, matching the posthook dashboard. The raw per-session `total_tokens` column includes them.
</ParamField>

<ParamField path="posthook-output-tokens" type="aggregation required">
  Output tokens only. Response column `{agg}_output_tokens`.
</ParamField>

<ParamField path="posthook-generated-lines" type="aggregation required">
  Lines the agent wrote, from edit events. Response column `{agg}_generated_lines`.
</ParamField>

<ParamField path="posthook-committed-lines" type="aggregation required">
  Lines the agent wrote that posthook attributed into commits. Response column `{agg}_committed_lines`.
</ParamField>

<ParamField path="posthook-ai-code-ratio" type="ratio">
  Committed ÷ generated lines. Response column `posthook_ai_code_ratio`.
</ParamField>

<ParamField path="posthook-top-model" type="string">
  Most-used model among sessions that committed AI code, ignoring placeholder slugs like `default` and `unknown`. Response column `posthook_top_model`.
</ParamField>

### Detail-table counts

<ParamField path="posthook-session-message-counts" type="count">
  Distinct messages in `posthook_session_messages`. Response column `posthook_session_message_counts`.
</ParamField>

<ParamField path="posthook-session-file-counts" type="count">
  Rows in `posthook_session_files`. Response column `posthook_session_file_counts`.
</ParamField>

<ParamField path="posthook-session-commit-counts" type="count">
  Distinct commits in `posthook_session_commits`. Response column `posthook_session_commit_counts`.
</ParamField>

### PR-level attribution (`pull_requests`)

<ParamField path="posthook-merged-ai-lines" type="aggregation required">
  AI-attributed lines that landed in **merged** PRs. Response column `{agg}_merged_ai_lines`. Uses PR date fields (`pr_merged_at`, `pr_created_at`, …) and PR filters.
</ParamField>

<ParamField path="posthook-ai-pct-of-code" type="ratio">
  AI committed lines ÷ lines added across the selected PRs, capped at 1. Response column `posthook_ai_pct_of_code`. The dashboard's "AI % of Code" card requests this with `"pr_states": ["merged"]` and `"date_field": "pr_merged_at"`.
</ParamField>

## Columns

Any column can be used in `group_by` and `order_by`; date-typed columns can be used in `date_field`.

<AccordionGroup>
  <Accordion title="posthook_sessions" defaultOpen>
    | Column                                  | Type      | Description                                                                                |
    | --------------------------------------- | --------- | ------------------------------------------------------------------------------------------ |
    | `session_id`                            | text      | Unique session id                                                                          |
    | `engineer_email`                        | text      | Git email posthook captured for the session                                                |
    | `engineer_name`                         | text      | Engineer name posthook captured                                                            |
    | `agent_slug`                            | text      | Agent used: `claude-code`, `cursor`, `codex`, …                                            |
    | `model_slug`                            | text      | Model used, as reported by the agent                                                       |
    | `model_provider`                        | text      | Vendor derived from `model_slug`: Anthropic, OpenAI, Google, xAI, DeepSeek, Alibaba, Other |
    | `repo_id`                               | text      | Posthook repository id                                                                     |
    | `repo_name`                             | text      | Repository name                                                                            |
    | `remote_url`                            | text      | Repository remote URL                                                                      |
    | `branch`                                | text      | Branch the session worked on                                                               |
    | `started_at`                            | timestamp | Session start                                                                              |
    | `ended_at`                              | timestamp | Session end (null for Cursor, which has no stop hook)                                      |
    | `date`                                  | date      | Date of `started_at`. Default `date_field`                                                 |
    | `input_tokens`                          | integer   | Input tokens                                                                               |
    | `output_tokens`                         | integer   | Output tokens                                                                              |
    | `cache_read_tokens`                     | integer   | Cache-read tokens                                                                          |
    | `cache_creation_tokens`                 | integer   | Cache-creation tokens                                                                      |
    | `total_tokens`                          | integer   | Sum of all four token columns                                                              |
    | `active_minutes`                        | integer   | Event-derived working time (distinct 5-minute windows × 5)                                 |
    | `generated_lines`                       | numeric   | AI-written lines captured from edit events                                                 |
    | `files_edited`                          | integer   | Distinct files the agent edited                                                            |
    | `committed_lines`                       | integer   | AI lines attributed into commits                                                           |
    | `commits_contributed`                   | integer   | Commits the session contributed to                                                         |
    | `user_id`                               | text      | Posthook raw user id                                                                       |
    | `merged_user_id`                        | text      | Resolved Bilanc user id                                                                    |
    | `propelauth_user_id`                    | text      | Bilanc auth user id                                                                        |
    | `name`                                  | text      | Resolved Bilanc user name                                                                  |
    | `role`, `location`, `level`             | text      | From the Bilanc user profile                                                               |
    | `departments`, `team_names`, `team_ids` | array     | Org membership of the user                                                                 |
  </Accordion>

  <Accordion title="posthook_session_messages">
    | Column                                                       | Type      | Description                                                                                                                  |
    | ------------------------------------------------------------ | --------- | ---------------------------------------------------------------------------------------------------------------------------- |
    | `message_id`                                                 | text      | Prompt id for user rows; Stop event id for assistant rows                                                                    |
    | `session_id`                                                 | text      | Owning session                                                                                                               |
    | `message_seq`                                                | integer   | 1-based chronological position within the session                                                                            |
    | `role`                                                       | text      | `user` or `assistant`                                                                                                        |
    | `source`                                                     | text      | `transcript` or `hook` for user rows, `stop_hook` for assistant rows                                                         |
    | `agent_slug`                                                 | text      | Agent the message belongs to                                                                                                 |
    | `message_text`                                               | text      | Full prompt as typed, or the final assistant message of the turn                                                             |
    | `message_ts`                                                 | timestamp | When the message was sent                                                                                                    |
    | `tool_calls`                                                 | integer   | User rows only: tool-use events fired during the turn. Null for assistant rows and for agents without event capture (Cursor) |
    | `turn_minutes`                                               | integer   | User rows only: minutes from the prompt to the turn's final Stop event                                                       |
    | `date`                                                       | date      | Date of `message_ts`, falling back to the session date. Default `date_field`                                                 |
    | `engineer_email`, `merged_user_id`, `team_names`, `team_ids` |           | Owner identity, copied from the session                                                                                      |
  </Accordion>

  <Accordion title="posthook_session_files">
    | Column                                                       | Type      | Description                                                                            |
    | ------------------------------------------------------------ | --------- | -------------------------------------------------------------------------------------- |
    | `session_id`                                                 | text      | Owning session                                                                         |
    | `file_path`                                                  | text      | Repo-relative path                                                                     |
    | `edits`                                                      | integer   | AI edit events on this file in this session                                            |
    | `lines_generated`                                            | numeric   | AI lines generated on this file                                                        |
    | `lines_committed`                                            | integer   | AI lines from this session attributed into commits touching this file                  |
    | `commits_touched`                                            | integer   | Distinct commits this session contributed to on this file                              |
    | `first_edit_ts`                                              | timestamp | First AI edit (null when attribution came without a captured edit)                     |
    | `last_edit_ts`                                               | timestamp | Last AI edit                                                                           |
    | `date`                                                       | date      | Date of first edit, falling back to last edit, then session date. Default `date_field` |
    | `engineer_email`, `merged_user_id`, `team_names`, `team_ids` |           | Owner identity, copied from the session                                                |
  </Accordion>

  <Accordion title="posthook_session_commits">
    | Column                                                       | Type      | Description                                                                |
    | ------------------------------------------------------------ | --------- | -------------------------------------------------------------------------- |
    | `session_id`                                                 | text      | Owning session                                                             |
    | `commit_id`                                                  | text      | Posthook commit id                                                         |
    | `sha`                                                        | text      | Git SHA                                                                    |
    | `short_sha`                                                  | text      | First 7 characters of the SHA                                              |
    | `branch`                                                     | text      | Branch the commit was made on                                              |
    | `message`                                                    | text      | Commit message                                                             |
    | `committed_at`                                               | timestamp | Commit timestamp                                                           |
    | `author_email`                                               | text      | Commit author email                                                        |
    | `lines_added`                                                | integer   | Total lines the commit added                                               |
    | `lines_removed`                                              | integer   | Total lines the commit removed                                             |
    | `files_changed`                                              | integer   | Files the commit changed                                                   |
    | `lines_attributed`                                           | integer   | AI lines posthook attributed from this session into this commit            |
    | `attribution_source`                                         | text      | How posthook matched the session to the commit                             |
    | `confidence`                                                 | text      | Attribution confidence                                                     |
    | `files_touched`                                              | integer   | Files this session touched within the commit                               |
    | `pr_id`                                                      | text      | PR the commit landed in, when Bilanc has one (one PR kept per commit)      |
    | `pr_source`                                                  | text      | VCS of the linked PR: github, gitlab, bitbucket, azure\_devops             |
    | `pr_title`, `pr_url`, `pr_state`                             | text      | Linked PR details                                                          |
    | `date`                                                       | date      | Date of `committed_at`, falling back to session date. Default `date_field` |
    | `engineer_email`, `merged_user_id`, `team_names`, `team_ids` |           | Owner identity, copied from the session                                    |
  </Accordion>
</AccordionGroup>

## Example: team summary by agent, week over week

This is the request behind the Posthook team table in the dashboard. Note `posthook-ai-pct-of-code` is a PR-table metric, so it gets `pr_merged_at` as its date field and the `pr_states` filter; the session-table metrics ignore `pr_states`.

<CodeGroup>
  ```bash Request theme={null}
  curl -X POST 'https://api.bilanc.co/metrics/get-multiple-metrics' \
    -H 'Authorization: YOUR_API_KEY' \
    -H 'Content-Type: application/json' \
    -d '{
      "filters": {
        "start_date": "2026-08-24",
        "end_date": "2026-09-06",
        "pr_states": ["merged"]
      },
      "group_by": ["agent_slug"],
      "date_level": "week",
      "metrics": [
        "posthook-sessions-count",
        "posthook-active-users",
        "posthook-total-tokens",
        "posthook-generated-lines",
        "posthook-committed-lines",
        "posthook-ai-code-ratio",
        "posthook-top-model",
        "posthook-ai-pct-of-code"
      ],
      "date_fields": {
        "posthook-sessions-count": "date",
        "posthook-active-users": "date",
        "posthook-total-tokens": "date",
        "posthook-generated-lines": "date",
        "posthook-committed-lines": "date",
        "posthook-ai-code-ratio": "date",
        "posthook-top-model": "date",
        "posthook-ai-pct-of-code": "pr_merged_at"
      },
      "aggregations": {
        "posthook-total-tokens": "SUM",
        "posthook-generated-lines": "SUM",
        "posthook-committed-lines": "SUM"
      },
      "include_previous_period": true
    }'
  ```

  ```json Response theme={null}
  [
    {
      "week_date": "2026-08-24",
      "agent_slug": "claude-code",
      "posthook_sessions_count": 48,
      "previous_posthook_sessions_count": 41,
      "posthook_active_users": 6,
      "previous_posthook_active_users": 6,
      "sum_total_tokens": 1834211,
      "previous_sum_total_tokens": 1520987,
      "sum_generated_lines": 9120,
      "previous_sum_generated_lines": 7744,
      "sum_committed_lines": 5388,
      "previous_sum_committed_lines": 4130,
      "posthook_ai_code_ratio": 0.59,
      "previous_posthook_ai_code_ratio": 0.53,
      "posthook_top_model": "claude-opus-5",
      "previous_posthook_top_model": "claude-opus-5",
      "posthook_ai_pct_of_code": 0.47,
      "previous_posthook_ai_pct_of_code": 0.39
    },
    {
      "week_date": "2026-08-24",
      "agent_slug": "cursor",
      "posthook_sessions_count": 12,
      "previous_posthook_sessions_count": 15,
      "posthook_active_users": 3,
      "previous_posthook_active_users": 3,
      "sum_total_tokens": 0,
      "previous_sum_total_tokens": 0,
      "sum_generated_lines": 1402,
      "previous_sum_generated_lines": 1880,
      "sum_committed_lines": 611,
      "previous_sum_committed_lines": 902,
      "posthook_ai_code_ratio": 0.44,
      "previous_posthook_ai_code_ratio": 0.48,
      "posthook_top_model": "gpt-5",
      "previous_posthook_top_model": "gpt-5",
      "posthook_ai_pct_of_code": null,
      "previous_posthook_ai_pct_of_code": null
    }
  ]
  ```
</CodeGroup>

<Tip>
  Cursor reports no token counts through its hooks, so token metrics for `agent_slug = cursor` are zero by design.
</Tip>

## Example: every session as a row

Group by `session_id` and the columns you want. This is the request behind the dashboard's session list. `metric_min_value: 1` keeps only sessions that generated or committed AI code; drop it to include every captured session.

<CodeGroup>
  ```bash Request theme={null}
  curl -X POST 'https://api.bilanc.co/metrics/posthook-sessions-count' \
    -H 'Authorization: YOUR_API_KEY' \
    -H 'Content-Type: application/json' \
    -d '{
      "filters": {
        "start_date": "2026-09-01",
        "end_date": "2026-09-07"
      },
      "group_by": [
        "session_id", "name", "engineer_email", "agent_slug", "model_slug",
        "model_provider", "repo_name", "branch", "started_at", "ended_at",
        "active_minutes", "generated_lines", "committed_lines", "total_tokens",
        "input_tokens", "output_tokens", "files_edited", "commits_contributed"
      ],
      "date_field": "date",
      "metric_min_value": 1,
      "order_by": "started_at",
      "order_direction": "DESC",
      "limit": 200
    }'
  ```

  ```json Response theme={null}
  [
    {
      "date_level_not_set": true,
      "session_id": "7f0c3b8e-2d1a-4b6f-9e21-0a5c6d4e8b90",
      "name": "Jordan Lee",
      "engineer_email": "jordan@example.com",
      "agent_slug": "claude-code",
      "model_slug": "claude-opus-5",
      "model_provider": "Anthropic",
      "repo_name": "acme/api",
      "branch": "feat/webhook-retry",
      "started_at": "2026-09-05T08:41:03",
      "ended_at": "2026-09-05T10:12:47",
      "active_minutes": 55,
      "generated_lines": 412,
      "committed_lines": 268,
      "total_tokens": 2418822,
      "input_tokens": 61231,
      "output_tokens": 18904,
      "files_edited": 9,
      "commits_contributed": 3,
      "posthook_sessions_count": 1
    }
  ]
  ```
</CodeGroup>

For more than 200 sessions, window by day or use `get-multiple-metrics`. See [Paging through large tables](/api-reference/endpoint/data-export#paging-through-large-tables).

## Example: a session's prompts and replies

Pin the query to one session with `filters.session_id`. Set `start_date` a day or two before the session started: the `date` column of a message falls back to the session date, but commits attributed later can land after the session's own day.

<CodeGroup>
  ```bash Request theme={null}
  curl -X POST 'https://api.bilanc.co/metrics/posthook-session-message-counts' \
    -H 'Authorization: YOUR_API_KEY' \
    -H 'Content-Type: application/json' \
    -d '{
      "filters": {
        "start_date": "2026-09-03",
        "session_id": "7f0c3b8e-2d1a-4b6f-9e21-0a5c6d4e8b90"
      },
      "group_by": ["message_id", "message_seq", "message_ts", "role", "source", "message_text", "tool_calls", "turn_minutes"],
      "date_field": "date",
      "order_by": "message_seq",
      "order_direction": "ASC"
    }'
  ```

  ```json Response theme={null}
  [
    {
      "date_level_not_set": true,
      "message_id": "p_01J8Q0X3M4",
      "message_seq": 1,
      "message_ts": "2026-09-05T08:41:09",
      "role": "user",
      "source": "hook",
      "message_text": "Add exponential backoff with jitter to the webhook dispatcher. Max 5 attempts, cap at 60s.",
      "tool_calls": 14,
      "turn_minutes": 9,
      "posthook_session_message_counts": 1
    },
    {
      "date_level_not_set": true,
      "message_id": "s_01J8Q0Y7PZ",
      "message_seq": 2,
      "message_ts": "2026-09-05T08:50:31",
      "role": "assistant",
      "source": "stop_hook",
      "message_text": "Added RetryPolicy in dispatcher/retry.py with decorrelated jitter and wired it into WebhookDispatcher.send. Tests in tests/test_retry.py cover the cap and attempt count.",
      "tool_calls": null,
      "turn_minutes": null,
      "posthook_session_message_counts": 1
    }
  ]
  ```
</CodeGroup>

To export prompts in bulk rather than per session, drop `session_id` and window by date instead, adding `session_id` and `engineer_email` to `group_by` so each message keeps its context.

## Example: files touched and commits attributed

The same shape, against the other two detail tables. These are the dashboard's "Files" and "Commits" panels.

<CodeGroup>
  ```bash Files theme={null}
  curl -X POST 'https://api.bilanc.co/metrics/posthook-session-file-counts' \
    -H 'Authorization: YOUR_API_KEY' \
    -H 'Content-Type: application/json' \
    -d '{
      "filters": {
        "start_date": "2026-09-03",
        "session_id": "7f0c3b8e-2d1a-4b6f-9e21-0a5c6d4e8b90"
      },
      "group_by": ["file_path", "edits", "lines_generated", "lines_committed", "commits_touched", "first_edit_ts", "last_edit_ts"],
      "date_field": "date",
      "order_by": "lines_generated",
      "order_direction": "DESC"
    }'
  ```

  ```bash Commits theme={null}
  curl -X POST 'https://api.bilanc.co/metrics/posthook-session-commit-counts' \
    -H 'Authorization: YOUR_API_KEY' \
    -H 'Content-Type: application/json' \
    -d '{
      "filters": {
        "start_date": "2026-09-03",
        "session_id": "7f0c3b8e-2d1a-4b6f-9e21-0a5c6d4e8b90"
      },
      "group_by": ["commit_id", "sha", "short_sha", "branch", "message", "committed_at", "lines_added", "lines_removed", "files_changed", "lines_attributed", "attribution_source", "confidence", "pr_id", "pr_source", "pr_title", "pr_url", "pr_state"],
      "date_field": "date",
      "order_by": "committed_at",
      "order_direction": "ASC"
    }'
  ```

  ```json Commits response theme={null}
  [
    {
      "date_level_not_set": true,
      "commit_id": "c_01J8Q1A2B3",
      "sha": "9e4f1c7a2b8d0e5f6a1b2c3d4e5f60718293a4b5",
      "short_sha": "9e4f1c7",
      "branch": "feat/webhook-retry",
      "message": "Add retry policy to webhook dispatcher",
      "committed_at": "2026-09-05T09:58:12",
      "lines_added": 231,
      "lines_removed": 18,
      "files_changed": 4,
      "lines_attributed": 196,
      "attribution_source": "edit_range",
      "confidence": "high",
      "pr_id": "github:acme/api:4812",
      "pr_source": "github",
      "pr_title": "Add retry to webhook dispatcher",
      "pr_url": "https://github.com/acme/api/pull/4812",
      "pr_state": "merged",
      "posthook_session_commit_counts": 1
    }
  ]
  ```
</CodeGroup>

## Example: daily usage per engineer

A conventional aggregate: tokens, active time and committed lines per engineer per day.

```bash theme={null}
curl -X POST 'https://api.bilanc.co/metrics/get-multiple-metrics' \
  -H 'Authorization: YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "filters": {"start_date": "2026-09-01", "end_date": "2026-09-30"},
    "group_by": ["name", "engineer_email"],
    "date_level": "day",
    "metrics": ["posthook-sessions-count", "posthook-active-time", "posthook-total-tokens", "posthook-committed-lines"],
    "date_fields": {
      "posthook-sessions-count": "date",
      "posthook-active-time": "date",
      "posthook-total-tokens": "date",
      "posthook-committed-lines": "date"
    },
    "aggregations": {
      "posthook-active-time": "SUM",
      "posthook-total-tokens": "SUM",
      "posthook-committed-lines": "SUM"
    }
  }'
```

## Parameter notes

<ResponseField name="date_field" type="string">
  All four posthook tables default to `date`. Sessions also accept `started_at` and `ended_at`; messages `message_ts`; files `first_edit_ts` and `last_edit_ts`; commits `committed_at`. The two PR-table metrics take PR date fields (`pr_merged_at`, `pr_created_at`, `pr_closed_at`, …).
</ResponseField>

<ResponseField name="filters" type="object">
  Posthook tables honour `member`, `team`, `departments`, `locations`, `levels`, `squad_ids`, `squad_levels`, `manager_ids` and `session_id`. They have no repository or PR-state columns, so `repositories` and `pr_states` are ignored for them (filter on the `repo_name` column via `group_by` instead). `posthook-merged-ai-lines` and `posthook-ai-pct-of-code` are PR-table metrics and honour every PR filter.
</ResponseField>

<ResponseField name="aggregation" type="string">
  Required for `posthook-total-tokens`, `posthook-output-tokens`, `posthook-generated-lines`, `posthook-committed-lines`, `posthook-active-time` and `posthook-merged-ai-lines`. Use `SUM` for totals and `AVG` for per-session averages. Count, ratio and top-model metrics take none.
</ResponseField>
