---
title: "Stats Monthly"
method: GET
path: "/api/v1/dashboard/stats/monthly"
tags: ["dashboard"]
---

# Stats Monthly

`GET /api/v1/dashboard/stats/monthly`

## Request

- Base URL: `https://api.nudgelink.co` (the document declares no server; this is the origin it was published from)
- URL: `https://api.nudgelink.co/api/v1/dashboard/stats/monthly`
- Auth: HTTP bearer

Per-month invite/accept breakdown for the current client. Scoped to the
caller — same client-scoping as ``/kpi`` — so it never leaks another
client's numbers.

## Query parameters

- `months` integer

## Response `200`

Successful Response

- MonthlyStatsResponse
  - `months` MonthlyStats[], required
    - `month` string, required
    - `invites` InviteStats: Connection requests. ``*_pct`` are None (never 0.0) on an empty base. THREE outcomes, not two (added 2026-08-07 after the first production backfill). An invite we sent is in exactly one of: * **accepted** — ``invite_accepted_at`` is stamped. * **declined** — we looked AFTER inviting and they are still not a connection. "Declined" is shorthand: it also covers ignored. * **unknown** — we have no observation since the invite went out. That third bucket is not a technicality. Relationship distance is only refreshed by the DM gate, which runs when a sequence reaches ``linkedin_followup_1``; ``warmup_view``'s blob is written BEFORE the invite and carries no timestamp at all. So a lead whose follow-up is still sitting in the preview queue has never been re-checked, and folding those into the denominator reports them as rejections. The first prod run showed exactly that: 379 invites, 376 "not 1st-degree", an 0.8% accept rate. Real cold-invite accept rates are 15-40%. Nearly all of that 376 was "we never looked", and dividing by it would have put a catastrophic-looking number in front of clients. So ``accept_rate_pct`` divides by **accepted + declined** — what we actually know — and ``unknown`` is reported alongside so the number can be read with its own confidence attached. The bucket shrinks toward zero once PR 2's ``relation.new`` webhook lands, which is the real fix.
      - `sent` integer
      - `with_note` integer
      - `without_note` integer
      - `accepted` integer
      - `accepted_with_note` integer
      - `accepted_without_note` integer
      - `declined` integer
      - `unknown` integer
      - `judged` integer
      - `judged_with_note` integer
      - `judged_without_note` integer
      - `accept_rate_pct` number, nullable
      - `accept_rate_with_note_pct` number, nullable
      - `accept_rate_without_note_pct` number, nullable

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/nudgelink/apis/nudgelink-api.md) · [All operations](https://skmtc.dev/nudgelink/apis/nudgelink-api/llms.txt) · [OpenAPI document](https://skmtc.dev/nudgelink/apis/nudgelink-api/revisions/baa46e6db986?raw)
