Skip to content

Clients API

All endpoints require authentication and clients tool permissions.

GET /api/clients

Query parameters:

ParamTypeDescription
statusstringFilter by status (comma-separated: prospect,active,dormant,alumni)
tierstringFilter by tier (comma-separated: platinum,core,standard,review)
assigned_dm_idstringFilter by assigned delivery manager user ID
health_score_minnumberMinimum health score
health_score_maxnumberMaximum health score
searchstringSearch by company name
sortstringSort field: health_score (default), name, tier, last_qbr_date
pagenumberPage number (default: 1)
per_pagenumberItems per page (default: 50)

Response: { clients, total, page, pages }

Each client includes computed is_qbr_overdue, qbr_overdue_days, is_score_stale, tier_track, tier_score, and tier_scored_at fields.

GET /api/clients/stats

Returns badge counts for the sidebar: { overdue_qbr_count, low_health_count }

POST /api/clients

Body:

{
"company_id": "string (required)",
"status": "string (default: prospect)",
"assigned_dm_id": "string (optional)"
}
GET /api/clients/:companyId

Returns { client, stats, contacts, group, parent_client } where:

  • stats includes active_projects, open_deals, total_revenue, meetings_count, contacts_count, last_meeting_date
  • client includes all tier scoring fields: tier_track, tier_score, tier_revenue_score, tier_profitability_score, tier_strategic_score, tier_relationship_score, tier_growth_score, tier_adoption_score, tier_revenue_override, tier_override_reason, tier_scored_at, tier_scored_by, tier_scored_by_name, is_score_stale
  • group contains children and aggregated stats for parent companies
  • parent_client is set if this company is a child in a group
PATCH /api/clients/:companyId

Body: Any combination of status, assigned_dm_id, next_qbr_date, last_qbr_date, onboarding_started_at, onboarding_completed_at, offboarding_started_at, offboarding_completed_at.

Writes appropriate history events for each changed field.

PUT /api/clients/:companyId/tier-score

Submits human dimension scores, auto-calculates revenue, computes weighted tier score, and derives tier assignment.

Body:

{
"tier_track": "delivery | product (required)",
"profitability_score": "number 1-5 (Track A only)",
"strategic_score": "number 1-5 (Track A only)",
"relationship_score": "number 1-5 (both tracks)",
"growth_score": "number 1-5 (Track B only)",
"revenue_override": "number 1-5 or null",
"override_reason": "string (required if override is set)"
}

Response:

{
"tier": "platinum | core | standard | review",
"tier_score": 4.25,
"tier_track": "delivery",
"revenue": {
"annual_total": 285000,
"score": 4,
"source": "nucleus | productive | none",
"override": null
},
"dimensions": {
"revenue": 4,
"profitability": 3,
"strategic": 4,
"relationship": 5
}
}

On tier change: auto-sets next_qbr_date based on tier cadence, creates a notification for the assigned DM, and logs a history entry.

GET /api/clients/:companyId/tier-history

Returns { history } — array of scoring events for trend charts. Each entry includes all dimension scores, tier, source (manual or auto), and scorer name.

GET /api/clients/:companyId/revenue-summary

Returns { annual_total, revenue_score, source, monthly } where monthly is an array of { month, total } for the last 12 months.

POST /api/clients/recalculate-revenue

Requires clients:manage permission. Recalculates revenue scores for all active clients with a tier track set. Returns { updated, tier_changes }.

Also runs automatically via cron on the 1st of each month at 2am UTC.

POST /api/clients/:companyId/health-scores

Body: { score: number (0-100), factors?: string[], assessed_by?: string }

GET /api/clients/:companyId/qbrs

Returns { qbrs } sorted by date descending.

POST /api/clients/:companyId/qbrs

Body: { date, attendees?, summary?, outcomes?, next_qbr_date?, granola_meeting_id? }

GET /api/clients/:companyId/goals
POST /api/clients/:companyId/goals

Body: { metric, target, baseline?, unit?, period? }

PATCH /api/clients/:companyId/goals/:goalId

Body: { current_value: number }

GET /api/clients/:companyId/history

Returns history events with resource_type = "client".

POST /api/clients/:companyId/ai-summary

Generates a relationship summary using Workers AI (Kimi K2.5) and caches it. Returns { ai_summary, ai_summary_generated_at }.


GET /api/clients/:companyId/qbr/:qbrId

Returns { qbr } with parsed JSON fields: content, source_data, portal_visibility.

PATCH /api/clients/:companyId/qbr/:qbrId

Body: { summary?, outcomes?, content?, portal_visibility? }

Cannot edit a QBR with status presented.

POST /api/clients/:companyId/qbr/:qbrId/present

Sets QBR status to presented, updates client_profiles.last_qbr_date and optionally next_qbr_date.

POST /api/clients/:companyId/qbr/prepare

Generates a draft QBR using AI from context since the last QBR (or 90 days). Creates a QBR record with status draft, including structured content and source data. Returns { qbr }.


GET /api/clients/:companyId/stripe/search?q=query

Searches connected Stripe account for customers matching the query. Returns { customers }.

POST /api/clients/:companyId/stripe/link

Body: { stripe_customer_id: string }

Links a Stripe customer to the company for revenue tracking. Verifies customer exists first.

DELETE /api/clients/:companyId/stripe/link

Removes the Stripe customer link from the company.


GET /api/clients/:companyId/success-profile

Returns { profile, questionnaire_completed }.

PATCH /api/clients/:companyId/success-profile

Body: { financial_year_start?, financial_year_end?, key_trading_periods?, channel_online_pct?, channel_instore_pct?, channel_wholesale_pct?, celebration_vision?, growth_priorities_text?, company_direction_text? }

Creates or updates the success profile. On first completion, notifies the assigned DM.


GET /api/clients/:companyId/wins

Query parameters:

ParamTypeDescription
sincestringFilter wins after this date
untilstringFilter wins before this date
statusstringFilter by status (default: excludes dismissed)

Returns { wins } with linked project and goal info.

POST /api/clients/:companyId/wins

Body: { title, description?, outcome_text?, project_id?, metric_key?, metric_delta?, metric_delta_unit?, won_at?, linked_goal_id?, source?, status? }

Source defaults to manual, status defaults to confirmed.

PATCH /api/clients/:companyId/wins/:winId

Body: { title?, description?, outcome_text?, metric_delta?, metric_delta_unit?, linked_goal_id?, status?, snoozed_until? }

DELETE /api/clients/:companyId/wins/:winId

Requires clients:manage permission.


GET /api/clients/:companyId/results

Query parameters:

ParamTypeDescription
period_typestringweek, month (default), quarter, year
period_endstringEnd date (ISO, default: today)

Response: { period, prior_period, metrics[], insight, connected_platforms[] }

Each metric includes value, avg_value, delta_pct, target, tracking_status (on_track/at_risk/off_track).

The insight object includes content (free text), content_json (structured analysis JSON), health_score, and overall_health when available.

GET /api/clients/:companyId/results/campaigns

Query parameters:

ParamTypeDescription
period_typestringweek, month (default), quarter, year
period_endstringEnd date (ISO, default: today)
platformstringFilter: meta_ads, google_ads, tiktok_ads
sort_bystringSort column (default: spend)
sort_dirstringasc or desc (default)

Response: { period, campaigns[], totals }

Each campaign includes: campaign_id, campaign_name, platform, campaign_status, campaign_objective, spend, roas, cpa, conversions, conversion_value, reach, frequency, ctr, cpm, cpc, top_creative_title, top_creative_body, spend_delta_pct.

GET /api/clients/:companyId/results/campaigns/daily

Same query parameters as campaign breakdown. Returns { daily } — a map of campaign_id to Array<{ date, spend }> for sparkline rendering.

POST /api/clients/:companyId/results/ask

Body: { question: string }

Response: { answer: string }

POST /api/clients/:companyId/results/prepare-qbr

Gathers current quarter metrics and latest AI insight, creates a QBR record. Requires clients:update.

Response: { qbr: { id, date, summary } }

POST /api/clients/:companyId/connections/backfill

Body: { days: 30 | 60 | 90 }

Triggers async backfill of campaign-level and account-level metrics for all connected ad platforms. Rate-limited to one backfill per company at a time.

Response: 202 Accepted with { message }

POST /api/clients/:companyId/connections/sync

Triggers async sync of all connected platforms for the company. Returns 202 Accepted.