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:
?repository=owner/name— the team tracking that repository. A bare repository name works too when it is unambiguous.?team=— a team name (case-insensitive) or numeric id.- The caller's only team.
- 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
| label | Human name for the window — "May 2026". Safe to print. |
| type | Which 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, end | ISO-8601 bounds of the window, inclusive. |
| previous_start, previous_end | Bounds of the comparison window every previous_* figure describes. |
| days | Length of the window in days. Use it to turn counts into per-day rates yourself. |
| is_complete | false while the window is still running. Cache freely when true; expect movement when false. |
| comparison | Ready-made delta caption — "vs previous month". |
pull_requests
| merged | PRs whose merge landed inside the window. Respects include_bots. This is the "PRs merged" number for a monthly report. |
| merged_by_humans | Of those, authored by a person. |
| merged_by_bots | Authored 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_dependabot | Dependabot specifically — a subset of merged_by_bots, not additional to it. |
| previous_merged | Same count for the comparison window. |
| merged_delta_percentage | Change against the previous window. From zero it reports 100, not infinity. |
| opened | PRs opened in the window by team members. Not the same as merged — do not use one where you mean the other. |
| reviews | Code reviews left by team members in the window. |
| average_quality_score | Mean 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_distribution | Counts by size band: Tiny, Small, Medium, Large, Extra Large, Oversized. |
| by_repository | Per-repository merge counts with the human/bot split, biggest first. |
| by_author | Per-author merge counts with an is_bot flag, biggest first. Filter on that flag rather than pattern-matching names. |
dora
| rating | Composite band across all four metrics: Elite, High, Medium, Low, or No Data. |
| score | The same thing as a number for charting: Elite 4, High 3, Medium 2, Low 1, No Data 0. |
| previous_rating, previous_score | The comparison window's band. No Data when nothing deployed then — say "no comparison" rather than drawing a fall to zero. |
| deployments | Production deployments in the window: total, successful, failed, and previous_total. These are the raw counts behind the frequency figure. |
| metrics | The four DORA metrics, keyed deployment_frequency, lead_time_for_changes, change_failure_rate, mean_time_to_recovery. |
Each metric carries the same shape:
| value | The measurement, already rounded for display. |
| unit | deployments_per_day, hours or percent. Read this rather than hardcoding units per metric. |
| category | That metric's own band — Elite/High/Medium/Low, or No Data. |
| benchmark | The thresholds the band came from, as display text. Good tooltip content. |
| description | One line explaining what the metric measures. |
| previous_value | Same metric over the comparison window. |
| delta_percentage | Signed change against that window. |
| is_improvement | Whether 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, reason | false 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. |
| committed | Issues 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_completed | Of those, the ones finished by the time the cycle closed. |
| committed_canceled | Of 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_start | Issues that joined the cycle once it was already running — scope creep, for context. Cycles routinely more than double. |
| final_scope, completed | Whole-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_percentage | The 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. |
| cycles | The 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_at | When the cycle data behind this was last read from the tracker. |
people
| active_contributors | Team members who shipped something in the window. |
| team_members | People 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_repositoryinstead. - •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_completeis 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 Datarather than a flattering zero. If a month readsNo 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/teamflags 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.