REST API

Overview

The GitVelocity v1 REST API exposes read-only endpoints under /api/v1/ so you can pull velocity data into your warehouse, BI tool, or any other system that speaks HTTPS. Authentication uses Personal Access Tokens (PATs) minted from your organization's settings page.

Quick start

  1. Open Settings → API Tokens in the dashboard.
  2. Click Create token, give it a name (e.g. "Snowflake ETL"), pick an expiry (90 days, 1 year, or never), and copy the value that appears in the modal. You will only see it once.
  3. Use the token as a Bearer credential against any of the endpoints below.
curl -H "Authorization: Bearer gv_pat_..." \
  "https://api.gitvelocity.dev/api/v1/pull-requests?limit=50"

Endpoints

Method Path Returns
GET /api/v1/pull-requests Pull requests with metadata. JSON list with cursor pagination, or CSV via Accept: text/csv.
GET /api/v1/pull-requests/{id} Single PR with the full six-dimension score breakdown.
GET /api/v1/contributors List of contributors in the organization, cursor-paginated.
GET /api/v1/contributors/{username} Single contributor with aggregated current-state scores.
GET /api/v1/contributors/{username}/trends Per-contributor time series, bucketed by day / week / month / quarter.
GET /api/v1/trends/contributors Fleet-wide trend matrix — every contributor's scores over time.
GET /api/v1/organization The org the token is currently scoped to (id, name, slug, plan).

All response keys are snake_case (contributor_username, external_id, next_cursor).

Tokens

  • Tokens are scoped to exactly one organization at mint time. You cannot use a token to read data from a different organization.
  • Tokens carry the read scope only. Write endpoints (achievement reactions, benchmark annotations, manual rescores, etc.) reject PATs with 403.
  • Tokens expire on the schedule you choose: 90 days, 1 year (recommended default), or never. Mint a new one to rotate when an expiring token reaches its cutoff. A "Never expires" token never auto-expires but can still be revoked manually.
  • Tokens can be revoked from Settings → API Tokens. Revocation takes effect immediately.
  • The raw token is shown once at mint time and is never persisted server-side in plaintext — only a SHA-256 hash is stored.

Endpoint reference

GET /api/v1/pull-requests

Pull requests with metadata. Default response is JSON with cursor pagination; Accept: text/csv returns CSV.

Query parameters (all optional):

Param Type Description
repos string Comma-separated repo filter, e.g. acme/api,acme/web.
authors string Comma-separated author login filter.
merged_after ISO date Only return PRs merged on or after this date.
merged_before ISO date Only return PRs merged on or before this date.
min_score number Only return PRs with a score >= this value.
max_score number Only return PRs with a score <= this value.
search string Free-text search across PR title and description.
days integer Convenience window — last N days. Ignored when merged_after / merged_before are set.
cursor string Opaque pagination token from the previous response's next_cursor. Omit on the first page.
limit integer Page size (default 50, max 200; values above 200 are clamped silently).
fields string Comma-separated list of fields to include, e.g. id,title,score. Optional — full payload is the default.
format string csv to force the CSV branch from clients that can't set Accept. See content negotiation.

JSON response shape (Accept: application/json, the default):

{
  "items": [
    {
      "id": "12345",
      "repo": "acme/api",
      "external_id": 87,
      "title": "Add throttle to PAT endpoints",
      "html_url": "https://github.com/acme/api/pull/87",
      "score": 91,
      "contributor_username": "alice",
      "contributor_display_name": "Alice Example",
      "state": "closed",
      "merged": true,
      "merged_at": "2026-05-01T17:24:00.000Z",
      "updated_at": "2026-05-01T17:24:00.000Z",
      "closed_at": null,
      "source_type": "pr"
    }
  ],
  "next_cursor": "eyJtZXJnZWRfYXQiOiIyMDI2LTA1LTAxVDE3OjI0OjAwLjAwMFoiLCJpZCI6ImFjbWUvYXBpIzg3In0"
}

next_cursor is null on the last page. To fetch the next page, pass it back verbatim as ?cursor=.... The cursor is opaque — don't try to parse it; the format is allowed to change without notice.

CSV response (Accept: text/csv or ?format=csv):

HTTP/1.1 200 OK
Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename="gitvelocity-pull-requests-2026-05-15.csv"

Repository,Type,Reference,Title,Author Login,...
acme/api,pr,87,Add throttle to PAT endpoints,alice,...

The CSV columns match the dashboard's Activity export exactly.

GET /api/v1/pull-requests/{id}

Single PR with the full six-dimension score breakdown. id is the opaque identifier returned by the list endpoint.

The detail endpoint is JSON-only — Accept: text/csv returns 406 Not Acceptable.

Example response:

{
  "id": "12345",
  "repo": "acme/api",
  "external_id": 87,
  "title": "Add throttle to PAT endpoints",
  "score": 91,
  "contributor_username": "alice",
  "rubric": {
    "scope": { "score": 18, "max_score": 20, "factors": "..." },
    "architecture": { "score": 19, "max_score": 20, "factors": "..." },
    "implementation": { "score": 18, "max_score": 20, "factors": "..." },
    "risk": { "score": 17, "max_score": 20, "factors": "..." },
    "quality": { "score": 14, "max_score": 15, "factors": "..." },
    "perf_security": { "score": 5, "max_score": 5, "factors": "..." }
  },
  "schema_version": "1.0",
  "created_at": "2026-05-01T17:24:30.000Z"
}

GET /api/v1/contributors

Cursor-paginated list of contributors. Same cursor / limit semantics as /pull-requests.

{
  "items": [
    {
      "username": "alice",
      "display_name": "Alice Example",
      "profile_url": "https://github.com/alice",
      "avatar_url": "https://avatars.example.com/alice.png",
      "is_bot": false,
      "first_seen_at": "2025-11-04T12:00:00.000Z",
      "last_seen_at": "2026-05-12T09:15:00.000Z"
    }
  ],
  "next_cursor": null
}

GET /api/v1/contributors/{username}

Single contributor with current-state aggregated scores. 404 on unknown username.

Optional ?days=N controls the lookback window (default: all-time).

{
  "username": "alice",
  "display_name": "Alice Example",
  "profile_url": "https://github.com/alice",
  "avatar_url": null,
  "repositories": ["acme/api", "acme/web"],
  "current_scores": {
    "average_score": 87,
    "total_prs": 30,
    "max_score": 100,
    "min_score": 50,
    "score_distribution": { "high": 20, "medium": 8, "low": 2 }
  }
}

GET /api/v1/contributors/{username}/trends

Per-contributor time series.

Param Type Description
interval string day | week | month | quarter. Default week.
periods integer How many intervals to return (default 12, max 52 — values above 52 are clamped silently).
{
  "username": "alice",
  "display_name": "Alice Example",
  "interval": "week",
  "periods": ["2026-W17", "2026-W18", "2026-W19"],
  "points": [
    { "period": "2026-W17", "average_score": 82, "total_score": 820 },
    { "period": "2026-W18", "average_score": 85, "total_score": 850 },
    { "period": "2026-W19", "average_score": 87, "total_score": 870 }
  ]
}

GET /api/v1/trends/contributors

Fleet-wide trend matrix. Same interval / periods parameters as the per-contributor endpoint, plus cursor pagination over contributors (cursor, limit).

{
  "interval": "week",
  "periods": ["2026-W17", "2026-W18", "2026-W19"],
  "contributors": [
    {
      "username": "alice",
      "display_name": "Alice Example",
      "points": [
        { "period": "2026-W17", "average_score": 82, "total_score": 820 },
        { "period": "2026-W18", "average_score": 85, "total_score": 850 },
        { "period": "2026-W19", "average_score": 87, "total_score": 870 }
      ]
    }
  ],
  "next_cursor": null
}

GET /api/v1/organization

Returns the organization the token is currently scoped to. Useful for debugging "what am I authed against?" from an ETL host without round-tripping the Settings UI.

{
  "id": "11111111-2222-3333-4444-555555555555",
  "slug": "acme",
  "name": "Acme Co",
  "plan": null
}

plan is reserved for future use and is always null today.

Cursor pagination

All list endpoints (/pull-requests, /contributors, /trends/contributors) return next_cursor: string | null. Pass the value back as ?cursor=... to fetch the next page. null indicates the last page.

The cursor is opaque — do not assume any structure. We may change the encoding without notice; clients that pass it back verbatim will continue to work, clients that try to parse it may break.

Inserts during pagination don't cause skipped or duplicated rows, because the cursor encodes the ordering key of the last seen row.

Field selection

?fields=id,title,score is accepted on every list endpoint. The current release returns the full DTO regardless; the parameter is reserved so clients can opt in to partial responses when we wire field projection in a future release. Adopt the parameter now if you want to be forward-compatible.

Status codes

Status When you'll see it
200 Successful read.
401 Token missing, malformed, expired, or revoked. Check WWW-Authenticate header.
403 Token authenticated but lacks the read scope, the user who minted the token has lost membership in the token's organization (error: "membership_revoked"), or you hit a non-v1 endpoint with a PAT.
404 Unknown id / username on a detail endpoint, or the org the token was minted for has been deleted.
406 You sent Accept: text/csv to a detail endpoint. CSV is only supported on the list endpoint.
429 Rate limit exceeded. Honor the Retry-After header (or Retry-After-minute / Retry-After-day for granular per-window values) and try again.
5xx GitVelocity is having a bad day. Retry with exponential backoff.

Rate limits

PAT-authenticated requests share two budgets per organization:

  • 60 requests per minute
  • 10,000 requests per day

If you exceed either, the API responds 429 Too Many Requests. The CSV mode is designed to return everything in one call — prefer it over paginating /pull-requests if you can fit the result in memory.

A 429 response looks like:

{
  "statusCode": 429,
  "message": "ThrottlerException: Too Many Requests"
}

with a numeric Retry-After header giving the seconds to wait. For finer-grained back-off control, two per-window headers ship alongside it: Retry-After-minute (seconds until the 60 / minute window resets) and Retry-After-day (seconds until the 10,000 / day window resets). The unsuffixed Retry-After mirrors Retry-After-minute, since the minute window resets first.

Snowflake load example

A minimal pattern to land the CSV into Snowflake:

#!/usr/bin/env bash
# Download CSV and upload to S3 for COPY INTO.
set -euo pipefail

TOKEN="$GITVELOCITY_PAT"
DATE="$(date -u +%Y-%m-%d)"

curl -fsSL \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: text/csv" \
  "https://api.gitvelocity.dev/api/v1/pull-requests?days=7" \
  | aws s3 cp - "s3://my-bucket/gitvelocity/${DATE}.csv"
-- Snowflake side: load via COPY INTO.
CREATE OR REPLACE TABLE raw_gitvelocity_prs (
  organization     VARCHAR,
  repo             VARCHAR,
  pr_number        NUMBER,
  title            VARCHAR,
  author_login     VARCHAR,
  created_at       TIMESTAMP_TZ,
  merged_at        TIMESTAMP_TZ,
  score            NUMBER,
  -- ...add the rest of the columns you care about.
  raw_csv          VARIANT
);

COPY INTO raw_gitvelocity_prs
FROM @my_s3_stage/gitvelocity/
FILE_FORMAT = (TYPE = CSV SKIP_HEADER = 1 FIELD_OPTIONALLY_ENCLOSED_BY = '"');

Known limits

A few things to plan around:

  • Rate limits are per endpoint per org, not a shared budget. The 60 / minute and 10,000 / day quotas apply to each endpoint independently. One ETL job hitting /api/v1/pull-requests cannot 429 a different ETL job hitting /api/v1/contributors, but two clients hitting the same endpoint share that endpoint's bucket.
  • No per-token fairness within an org. Multiple PATs in the same org draw from the same per-endpoint bucket — there is no per-token sub-allocation. If one job goes hot, it can starve another on the same endpoint.
  • PATs with a finite expiry don't auto-refresh. Rotate them via Settings → API Tokens before they expire. The API will start returning 401 invalid_token the moment a token crosses its expiry. Pick "Never expires" if your platform can't accommodate periodic rotation.
  • ?fields= is accepted but not yet honored. The current implementation always returns the full DTO. Reserved for a future release.

Stability

  • New optional fields and new endpoints may appear at any time and are not considered breaking changes.
  • We will give 30 days notice to every active token owner before any breaking change to a documented field or endpoint under /api/v1/.

If anything is unclear, missing, or broken, reach out via the in-app chat or support@headline.com.