REST API

Pull your delivery numbers into your own dashboard

What this API is for

Read-only JSON for the numbers Coderbuds computes about your delivery: pull requests merged, DORA performance, review activity and contributor counts — for any calendar month, with the preceding month alongside it so you can show movement without a second call.

Build a board-deck slide, a monthly report, an internal dashboard, a spreadsheet job. Every figure is the same figure Coderbuds shows in its own UI — the API composes the identical calculations rather than re-deriving them.

REST API or MCP server?

Coderbuds has two machine-readable surfaces. They serve different jobs, and picking the wrong one costs you a week.

Use When
This REST API Your own software is doing the reading: a dashboard backend, a scheduled job, a warehouse loader, a report generator. Plain HTTP, cacheable, no handshake.
MCP server A coding agent is doing the reading, mid-task, and needs to act as well as look — request a review, trigger a deploy, check whether a change fits before opening a PR.

Quick start

Step 1 — get a token

Go to API Tokens, name the token after the thing that will use it (metrics-dashboard), and leave read checked. Copy the token — it is shown once.

The read permission is required and sufficient. A token without it is rejected with 403. Granting more permissions does not unlock anything here — every endpoint on this page is a GET.

Step 2 — confirm what the token can see

curl -H "Authorization: Bearer $CODERBUDS_TOKEN" \
     -H "Accept: application/json" \
     https://coderbuds.com/api/v1/team

Returns the team the token resolved to and the repository slugs you can scope later calls to. If you are on more than one team, this call tells you so — see team scoping.

Step 3 — fetch a month

curl -H "Authorization: Bearer $CODERBUDS_TOKEN" \
     -H "Accept: application/json" \
     "https://coderbuds.com/api/v1/metrics/monthly?month=2026-05"

That is the whole integration. Everything below is reference.

Authentication

Every request carries Authorization: Bearer <token>. Send Accept: application/json too, so validation failures come back as JSON rather than a redirect.

  • A token belongs to a person, and sees exactly what that person's team memberships allow. If they leave, the token dies with their account — for anything long-lived, mint it on an account that will outlast the integration.
  • Tokens do not expire on a timer. Revoke one at any time from API Tokens; the next request gets a 401.
  • Call this from your server, not from a browser. Cross-origin requests are technically permitted, but a token in front-end code is a token you have published. Proxy it through your own backend.
  • Never commit a token. Read it from an environment variable or your secret manager.

Which team the numbers describe

Coderbuds does not guess. If the token's owner belongs to one team, that team is used and you can ignore this section. Otherwise resolution runs in this order, and a request it cannot resolve fails loudly rather than returning some other team's data:

  1. ?repository=owner/name — the team tracking that repository. A bare repository name works too when it is unambiguous.
  2. ?team= — a team name (case-insensitive) or numeric id.
  3. The caller's only team.
  4. Otherwise 422, listing the teams to choose from.
HTTP/1.1 422 Unprocessable Content

{
  "message": "You belong to multiple Coderbuds teams. Pass ?team=<name-or-id> or ?repository=<owner/name> to say which one.",
  "teams": [
    { "id": 1, "name": "Platform" },
    { "id": 7, "name": "Mobile" }
  ]
}

Endpoints

Endpoint Returns
GET /api/v1 The endpoint list, parameters and auth rules, as JSON. Needs a token but no team.
GET /api/v1/team The resolved team, plus every repository slug on it with its provider and archived flag.
GET /api/v1/metrics/monthly One period of delivery metrics, with the preceding period for comparison.

All responses wrap their body in a data key. Errors do not — they return message at the top level.

Choosing a period

/api/v1/metrics/monthly takes exactly one period. Pass month, or a named period, or a from/to pair. month wins if you pass several.

Parameter Meaning
month YYYY-MM. Any month, however far back your history goes. Compared against the month before it. Ask for the current month and the window stops at now.
period One of rolling_7d, rolling_30d, previous_week, current_week, previous_month, current_month.
from, to A custom range, both required together. Compared against the equally long range immediately before it.
repositories[] Repeat to scope to specific owner/name slugs. Omit for every repository on the team. Slugs that match nothing come back in unmatched_repositories instead of failing the request.
include_bots Default true. Set false to drop bot-authored PRs from the totals. The human/bot/dependabot split is reported either way, so you rarely need this.
team, repository Team scoping, described above.

Pass nothing and you get last month

The default is the last complete calendar month, deliberately: a monthly report of a month still in progress is a number that changes under the reader. When you do report an in-flight period, period.is_complete is false — label it as partial in your UI.

Response

A complete GET /api/v1/metrics/monthly?month=2026-05 body, abbreviated only where a list repeats:

{
  "data": {
    "team": { "id": 1, "name": "Platform" },
    "period": {
      "label": "May 2026",
      "type": "specific_month",
      "start": "2026-05-01T00:00:00+00:00",
      "end": "2026-05-31T23:59:59+00:00",
      "previous_start": "2026-04-01T00:00:00+00:00",
      "previous_end": "2026-04-30T23:59:59+00:00",
      "days": 31,
      "is_complete": true,
      "comparison": "vs previous month"
    },
    "repositories": ["acme/saas", "acme/hub"],
    "unmatched_repositories": [],
    "pull_requests": {
      "merged": 63,
      "merged_by_humans": 48,
      "merged_by_bots": 15,
      "merged_by_dependabot": 11,
      "previous_merged": 54,
      "merged_delta_percentage": 16.7,
      "opened": 71,
      "previous_opened": 60,
      "reviews": 84,
      "previous_reviews": 77,
      "average_quality_score": 78,
      "previous_average_quality_score": 74,
      "size_distribution": {
        "Tiny": 12, "Small": 24, "Medium": 15,
        "Large": 8, "Extra Large": 3, "Oversized": 1
      },
      "by_repository": [
        { "repository": "acme/saas", "total": 41, "human": 33, "bot": 8 },
        { "repository": "acme/hub", "total": 22, "human": 15, "bot": 7 }
      ],
      "by_author": [
        { "author": "Ada Lovelace", "total": 19, "is_bot": false },
        { "author": "dependabot[bot]", "total": 11, "is_bot": true }
      ]
    },
    "dora": {
      "rating": "High",
      "score": 3,
      "previous_rating": "Medium",
      "previous_score": 2,
      "deployments": {
        "total": 27,
        "successful": 25,
        "failed": 2,
        "previous_total": 19
      },
      "metrics": {
        "deployment_frequency": {
          "value": 0.87,
          "unit": "deployments_per_day",
          "category": "High",
          "benchmark": "Elite: >1/day • High: ~1/week • Medium: ~1/month",
          "description": "How often deployments to production happen",
          "previous_value": 0.63,
          "delta_percentage": 38.1,
          "is_improvement": true
        },
        "lead_time_for_changes": { "...": "same shape, unit: hours" },
        "change_failure_rate": { "...": "same shape, unit: percent" },
        "mean_time_to_recovery": { "...": "same shape, unit: hours" }
      }
    },
    "tickets": {
      "available": true,
      "reason": null,
      "scope": "team",
      "completed": 48,
      "previous_completed": 41,
      "completed_delta_percentage": 17.1,
      "bugs": 11,
      "features": 19,
      "by_label": { "Feature": 19, "Bug": 11, "Improvement": 8 },
      "last_synced_at": "2026-06-01T08:00:04+00:00"
    },
    "commitment": {
      "available": true,
      "reason": null,
      "scope": "team",
      "committed": 83,
      "committed_completed": 47,
      "committed_canceled": 3,
      "added_after_start": 87,
      "final_scope": 170,
      "completed": 112,
      "reliability_percentage": 56.6,
      "cycles": [
        {
          "number": 9,
          "starts_at": "2026-05-24T23:00:00+00:00",
          "ends_at": "2026-06-07T23:00:00+00:00",
          "committed": 40,
          "committed_completed": 11,
          "committed_canceled": 1,
          "added_after_start": 11,
          "final_scope": 51,
          "completed": 16
        }
      ],
      "last_synced_at": "2026-06-08T02:00:04+00:00"
    },
    "people": {
      "active_contributors": 7,
      "team_members": 9
    },
    "generated_at": "2026-06-01T09:14:22+00:00"
  }
}

Field reference

period

labelHuman name for the window — "May 2026". Safe to print.
typeWhich kind of period you got: specific_month, previous_month, current_month, rolling_7d, rolling_30d, previous_week, current_week, custom_range. Echo this back when caching, so you never mistake one window for another.
start, endISO-8601 bounds of the window, inclusive.
previous_start, previous_endBounds of the comparison window every previous_* figure describes.
daysLength of the window in days. Use it to turn counts into per-day rates yourself.
is_completefalse while the window is still running. Cache freely when true; expect movement when false.
comparisonReady-made delta caption — "vs previous month".

pull_requests

mergedPRs whose merge landed inside the window. Respects include_bots. This is the "PRs merged" number for a monthly report.
merged_by_humansOf those, authored by a person.
merged_by_botsAuthored by a bot: any login containing [bot] or dependabot. A bot that follows neither convention counts as human — check by_author if a figure looks off.
merged_by_dependabotDependabot specifically — a subset of merged_by_bots, not additional to it.
previous_mergedSame count for the comparison window.
merged_delta_percentageChange against the previous window. From zero it reports 100, not infinity.
openedPRs opened in the window by team members. Not the same as merged — do not use one where you mean the other.
reviewsCode reviews left by team members in the window.
average_quality_scoreMean Coderbuds quality score, 0–100, over PRs in the window. 0 means nothing was scored, not that quality was zero — check merged before showing it.
size_distributionCounts by size band: Tiny, Small, Medium, Large, Extra Large, Oversized.
by_repositoryPer-repository merge counts with the human/bot split, biggest first.
by_authorPer-author merge counts with an is_bot flag, biggest first. Filter on that flag rather than pattern-matching names.

dora

ratingComposite band across all four metrics: Elite, High, Medium, Low, or No Data.
scoreThe same thing as a number for charting: Elite 4, High 3, Medium 2, Low 1, No Data 0.
previous_rating, previous_scoreThe comparison window's band. No Data when nothing deployed then — say "no comparison" rather than drawing a fall to zero.
deploymentsProduction deployments in the window: total, successful, failed, and previous_total. These are the raw counts behind the frequency figure.
metricsThe four DORA metrics, keyed deployment_frequency, lead_time_for_changes, change_failure_rate, mean_time_to_recovery.

Each metric carries the same shape:

valueThe measurement, already rounded for display.
unitdeployments_per_day, hours or percent. Read this rather than hardcoding units per metric.
categoryThat metric's own band — Elite/High/Medium/Low, or No Data.
benchmarkThe thresholds the band came from, as display text. Good tooltip content.
descriptionOne line explaining what the metric measures.
previous_valueSame metric over the comparison window.
delta_percentageSigned change against that window.
is_improvementWhether the move is the good direction — already accounts for metrics where lower is better. Colour your arrows from this, not from the sign of the delta.

commitment

Of the work committed to in the cycles that closed in this window, how much finished. Team-scoped, like tickets, and ignores repositories.

available, reasonfalse with a sentence when there is nothing to measure — no cycle closed in the window, the team does not run cycles, or the tracker needs reconnecting. Every figure is null then, never 0. A month with no cycle in it is unmeasured, not 0% reliable.
committedIssues that were in the cycle when it started — the promise. Work rolled over from the previous cycle counts, because at the start of this one the team committed to it again.
committed_completedOf those, the ones finished by the time the cycle closed.
committed_canceledOf those, the ones cancelled. Reported separately and not folded into the headline: whether a cancelled commitment is a miss or a legitimate descope is your call, not ours.
added_after_startIssues that joined the cycle once it was already running — scope creep, for context. Cycles routinely more than double.
final_scope, completedWhole-cycle totals at close, as the tracker's own burndown reports them, so the throughput view is available too. completed ÷ final_scope is throughput, not reliability — it rewards adding work late.
reliability_percentageThe headline: committed_completed ÷ committed, so it cannot exceed 100. Several cycles in a month are summed numerator over summed denominator, not averaged — a nine-issue cycle should not sway the month like a hundred-issue one. null if a cycle began with nothing in it.
cyclesThe same fields per cycle, oldest first, with number, starts_at and ends_at — so a month's figure is auditable. A cycle counts towards the month it ends in in the tracker team's own timezone: a cycle ending 30 June 23:00Z closed on 1 July in London and belongs to July. Cycles still running are excluded until they close.
last_synced_atWhen the cycle data behind this was last read from the tracker.

people

active_contributorsTeam members who shipped something in the window.
team_membersPeople on the team today. It is a current figure against a historical window, so treat "X of Y" ratios for old months with care.

Errors

Status Meaning and fix
401 Missing, malformed or revoked token. Check the header is literally Bearer <token> and that the token still exists on the API Tokens page.
403 The token authenticated but lacks the read permission. Mint a new one with read checked; permissions cannot be added to an existing token.
422 Either the team could not be resolved (body carries a teams array to pick from) or a parameter is invalid (body carries errors keyed by field). Both are permanent for that request — do not retry unchanged.
429 Over 60 requests per minute for that account. Honour Retry-After and back off.
HTTP/1.1 422 Unprocessable Content

{
  "message": "The month must be in YYYY-MM format, for example 2026-05.",
  "errors": {
    "month": ["The month must be in YYYY-MM format, for example 2026-05."]
  }
}

Rate limits and caching

  • 60 requests per minute, counted per Coderbuds account rather than per token — two tokens on the same account share one budget. Generous for reporting; easy to blow through by fanning out per repository. Fetch the team-wide payload once and read by_repository instead.
  • Metrics are computed on demand and cached server-side for about a minute, so two calls a second apart can return identical numbers. That is the cache, not a stalled pipeline.
  • Cache completed months in your own storage. Once period.is_complete is true the figures only change if history is backfilled — refetch monthly, not hourly.
  • All timestamps are ISO-8601 in UTC. Month boundaries are UTC too — a deploy at 23:00 on the 31st local time may land in the following month.

Worked examples

TypeScript — one month, typed

type MonthlyMetrics = {
  period: { label: string; days: number; is_complete: boolean };
  pull_requests: { merged: number; merged_by_humans: number; previous_merged: number };
  dora: {
    rating: 'Elite' | 'High' | 'Medium' | 'Low' | 'No Data';
    score: 0 | 1 | 2 | 3 | 4;
    deployments: { total: number; failed: number };
  };
};

export async function fetchMonth(month: string): Promise<MonthlyMetrics> {
  const url = new URL('https://coderbuds.com/api/v1/metrics/monthly');
  url.searchParams.set('month', month);

  const response = await fetch(url, {
    headers: {
      Authorization: `Bearer ${process.env.CODERBUDS_TOKEN}`,
      Accept: 'application/json',
    },
  });

  if (!response.ok) {
    const body = await response.json().catch(() => ({}));
    throw new Error(`Coderbuds ${response.status}: ${body.message ?? 'request failed'}`);
  }

  const { data } = await response.json();

  return data;
}

PHP (Laravel) — build a report row

use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.coderbuds.token'))
    ->acceptJson()
    ->get('https://coderbuds.com/api/v1/metrics/monthly', ['month' => '2026-05']);

$metrics = $response->throw()->json('data');

$row = [
    'month' => $metrics['period']['label'],
    'prs_merged' => $metrics['pull_requests']['merged'],
    'prs_merged_by_people' => $metrics['pull_requests']['merged_by_humans'],
    'dora_rating' => $metrics['dora']['rating'],
    'dora_score' => $metrics['dora']['score'],
    'deploys' => $metrics['dora']['deployments']['total'],
    'failed_deploys' => $metrics['dora']['deployments']['failed'],
    'lead_time_hours' => $metrics['dora']['metrics']['lead_time_for_changes']['value'],
    'partial' => ! $metrics['period']['is_complete'],
];

Backfill a year of history

for month in 2025-08 2025-09 2025-10 2025-11 2025-12 \
             2026-01 2026-02 2026-03 2026-04 2026-05 2026-06 2026-07; do
  curl -s -H "Authorization: Bearer $CODERBUDS_TOKEN" \
          -H "Accept: application/json" \
          "https://coderbuds.com/api/v1/metrics/monthly?month=$month" \
    > "metrics-$month.json"
  sleep 1
done

Each month is an independent call, so a series is a loop. The sleep keeps a long backfill comfortably inside the rate limit.

Reading the numbers honestly

Things worth knowing before a figure ends up on a slide someone argues about:

  • DORA needs deployment data. Lead time, change failure rate and recovery time all derive from production deployments Coderbuds can actually see. A repository with no deployment signal reports No Data rather than a flattering zero. If a month reads No Data, check deployment tracking before concluding nothing shipped.
  • Early history is thinner than recent history. Metrics only exist for the period after a repository was connected, and deployment coverage improves as teams wire up more signals. A rising trend can be rising coverage. Compare like with like.
  • Archived repositories still count. Archiving stops syncing and nudging, but the work that happened there stays in the historical totals — otherwise last year's numbers would shrink every time a repo is retired. /api/v1/team flags which are archived.
  • PR counts are not output. They measure flow, not value. Pair them with something outcome-shaped, and never rank people on by_author.

What this API does not serve

Coderbuds reads code: pull requests, reviews, deployments. It does not own your issue tracker's ledger, so these do not come from here and are not inferred:

  • Roadmap projects and the scope of the cycle that is still running.
  • Cycle time — how long an issue took from start to done.
  • Estimate points. Cycle figures are issue counts. If your team adopts estimates later they will arrive as separate fields rather than quietly changing what these mean.

Read those from Linear or Jira directly and join on the month — a number quietly guessed is worse than a number absent.

Tickets completed and the bug/feature mix are served, in tickets, for teams with Linear connected. Two things to know about that block: it is team-scoped and ignores repositories, because filtering tickets by repository would quietly reduce "completed" to "completed with a linked pull request"; and it reports available: false with a reason — never a zero — when the tracker has not been synced since completion tracking landed. bugs and features match a deliberately short list of label names; anything else your team uses is in by_label, spelled exactly as the tracker spells it.

Commitment reliability is served too, in commitment, for teams that run cycles in Linear. It answers "of what we committed to in the cycles that closed this month, how much did we finish?" — measured against the issues that were in each cycle when it started, which is the only version of the question that cannot be gamed by adding work late. See the field reference for why completed ÷ final_scope is a different measure, and read available: false as unmeasured rather than as a bad month.

Versioning

The path carries the version. Within v1, changes are additive: new fields may appear, existing ones will not be renamed, retyped or removed. Parse defensively — ignore keys you do not recognise rather than failing on them — and anything that must break will arrive as v2 alongside it. GET /api/v1 always describes the live surface, so an integration can check itself rather than trusting a page that might be older than the code.

Condensed spec

Everything above, compressed — paste this into an agent's context when you want it to write the integration.

BASE       https://coderbuds.com/api/v1
AUTH       Authorization: Bearer <token>   (token needs the "read" permission)
HEADERS    Accept: application/json
LIMIT      60 req/min per account (not per token); 429 carries Retry-After
ENVELOPE   success -> { "data": ... };  error -> { "message": ..., "errors"?: ..., "teams"?: ... }

GET /                      endpoint discovery (no team needed)
GET /team                  { id, name, repositories: [{ slug, provider, is_archived }] }
GET /metrics/monthly       one period of metrics + the preceding period

/metrics/monthly query (pick ONE period form; month wins):
  month=YYYY-MM                        any calendar month
  period=rolling_7d|rolling_30d|previous_week|current_week|previous_month|current_month
  from=YYYY-MM-DD&to=YYYY-MM-DD        custom range, both required
  repositories[]=owner/name            repeatable; omit for whole team
  include_bots=true|false              default true
  team=<name|id>                       required only if the caller has 2+ teams
  repository=owner/name                alternative team resolver
  (no period params -> last COMPLETE calendar month)

response.data:
  team              { id, name }
  period            { label, type, start, end, previous_start, previous_end,
                      days, is_complete, comparison }
  repositories      [slug]
  unmatched_repositories [slug]        requested slugs that matched nothing
  pull_requests     { merged, merged_by_humans, merged_by_bots, merged_by_dependabot,
                      previous_merged, merged_delta_percentage,
                      opened, previous_opened, reviews, previous_reviews,
                      average_quality_score, previous_average_quality_score,
                      size_distribution { Tiny..Oversized },
                      by_repository [{ repository, total, human, bot }],
                      by_author [{ author, total, is_bot }] }
  dora              { rating, score, previous_rating, previous_score,
                      deployments { total, successful, failed, previous_total },
                      metrics { deployment_frequency | lead_time_for_changes |
                                change_failure_rate | mean_time_to_recovery } }
  dora.metrics.*    { value, unit, category, benchmark, description,
                      previous_value, delta_percentage, is_improvement }
  tickets           { available, reason, scope, completed, previous_completed,
                      completed_delta_percentage, bugs, features,
                      by_label { label: count }, last_synced_at }
                    team-scoped, ignores `repositories`; available:false + reason
                    (never 0) when the tracker has not been synced
  commitment        { available, reason, scope, committed, committed_completed,
                      committed_canceled, added_after_start, final_scope, completed,
                      reliability_percentage,
                      cycles [{ number, starts_at, ends_at, committed,
                                committed_completed, committed_canceled,
                                added_after_start, final_scope, completed }],
                      last_synced_at }
                    cycles that CLOSED in the window, counted in the tracker team's
                    timezone; reliability = committed_completed / committed, so it
                    cannot exceed 100. available:false (never 0%) when no cycle closed
  people            { active_contributors, team_members }
  generated_at      ISO-8601

rating/category: Elite | High | Medium | Low | No Data      score: 4 | 3 | 2 | 1 | 0
units: deployments_per_day | hours | percent
statuses: 401 bad/absent token · 403 token lacks "read" · 422 bad params or unresolved team · 429 rate limited

RULES
- merged = PRs merged in window. opened = PRs opened. Not interchangeable.
- merged_by_dependabot is a SUBSET of merged_by_bots.
- Colour deltas from is_improvement, not the sign (lower is better for 3 of 4 metrics).
- "No Data" means unmeasured, not zero. Render as "—", never as 0.
- Cache months where is_complete is true; do not poll them.
- commitment.reliability_percentage is the commitment measure. completed / final_scope
  is throughput; do not label it reliability.
- Roadmap projects, the running cycle's scope and issue cycle time are NOT here. Read
  them from the issue tracker; do not derive them from pull requests.

Related

  • Connect your agent — the same data over MCP, plus the tools that act on it.
  • DORA metrics — how each metric is calculated and what the bands mean.
  • Deploy hooks — get deployment data flowing so DORA stops reading No Data.
  • Security — how tokens and team scoping are enforced.