---
title: "Kpi"
method: GET
path: "/api/v1/dashboard/kpi"
tags: ["dashboard"]
---

# Kpi

`GET /api/v1/dashboard/kpi`

## 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/kpi`
- Auth: HTTP bearer

## Response `200`

Successful Response

- DashboardKPI
  - `leads_total` integer, required
  - `leads_in_outreach` integer, required
  - `leads_replied` integer, required
  - `accounts_active` integer, required
  - `accounts_warming` integer, required
  - `sequences_active` integer, required
  - `sequences_paused` integer, required
  - `messages_pending_preview` integer, required
  - `qualified_replies_this_month` integer, required
  - `accept_rate_pct` number, nullable, required
  - `avg_relevance_score` integer, nullable, required
  - `est_pipeline_usd` number, required
  - `invites_sent_today` integer, required
  - `invites_cap_today` integer, required
  - `messages_sent_7d` integer, required
  - `replies_7d` integer, required
  - `sends_by_day` SendsByDay[], required
    - `date` string, required
    - `sent` integer, required
    - `replies` integer, required
  - `stats` OutreachStats, 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
    - `replies` ReplyStats: Messages sent and what came back. ``contacted`` counts SEQUENCES (people) reached by any substantive step, not messages — an invite plus two follow-ups to one lead is one person. ``silent`` is the complement, so positive + neutral + negative + unclassified + silent == contacted.
      - `dms_sent` integer
      - `emails_sent` integer
      - `contacted` integer
      - `replied` integer
      - `reply_rate_pct` number, nullable
      - `positive` integer
      - `neutral` integer
      - `negative` integer
      - `unclassified` integer
      - `silent` integer
    - `comments_sent` integer

---

[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)
