Githome
Reference: REST API
Docs / Reference

REST API

How to call Githome's GitHub-compatible REST API at /api/v3: authentication, IDs, pagination, errors, search, archives and what works by area.

Githome has a JSON REST API at https://git.pkgforge.dev/api/v3 with the same paths, JSON shapes and errors as GitHub's REST API v3, so Octokit, curl scripts and other GitHub clients work when you point them at it. This page explains how the API behaves as a whole: authentication, IDs, pagination, errors, rate limits, search, archives, what works in each area and what doesn't yet. For every endpoint with its parameters, see the generated API reference, also available as an OpenAPI 3.1 document at /docs/api/openapi.json.

Base URL

Every REST endpoint lives under one base URL:

https://git.pkgforge.dev/api/v3

A GET on the base URL itself needs no authentication and describes the API:

curl https://git.pkgforge.dev/api/v3
{ "name": "Githome", "api": "v3", "docs": "https://git.pkgforge.dev/docs/api", "mcp": "https://git.pkgforge.dev/mcp", "graphql": "https://git.pkgforge.dev/api/graphql" }

GET /api/v3/meta returns {"name": "Githome", "oidc": true}, where oidc says whether this instance can issue OpenID Connect tokens to workflow jobs.

Some related HTTP interfaces are not under /api/v3:

InterfacePathDocumented in
Git smart HTTP and Git LFShttps://git.pkgforge.dev/<owner>/<repo>.gitGit, Git LFS
Release asset uploads (the upload_url of a release)https://git.pkgforge.dev/api/uploads/repos/<owner>/<repo>/releases/<id>/assetsRelease assets
Release asset downloads (browser_download_url)https://git.pkgforge.dev/<owner>/<repo>/releases/download/<tag>/<name>Release assets
Source archives for browsershttps://git.pkgforge.dev/<owner>/<repo>/archive/<ref>.zip and .tar.gzArchives
Profile pictureshttps://git.pkgforge.dev/api/avatars/<login>Githome-only endpoints
MCP server for agentshttps://git.pkgforge.dev/mcpMCP
Container registry (OCI distribution API)https://git.pkgforge.dev/v2/Container registry
OIDC discovery for workflow tokens/.well-known/openid-configuration, /.well-known/jwksOIDC
Status page/status, and GET /api/v3/githome/status as JSONStatus page
Health check/healthz returns {"ok": true}

Compatibility with GitHub

Point a GitHub client at https://git.pkgforge.dev/api/v3 and it works:

import { Octokit } from "@octokit/rest";

const octokit = new Octokit({ auth: process.env.GITHOME_TOKEN, baseUrl: "https://git.pkgforge.dev/api/v3" });

What matches GitHub:

  • Paths and parameters. /repos/{owner}/{repo}/pulls, /repos/{owner}/{repo}/issues/{number}/comments, /repos/{owner}/{repo}/releases, /search/issues, /user, /orgs/{org}/repos and so on, with GitHub's query parameters and request body fields (merge_method, commit_title, event: "APPROVE", required_contexts, {"config": {"url": ...}} on webhooks).
  • Response shapes. Objects carry GitHub's fields, including node_id, url, html_url and the other *_url links, full user objects (login, id, node_id, avatar_url, html_url, type) and full label objects (id, name, color, description). Timestamps are ISO 8601 without milliseconds, like 2026-10-13T09:30:00Z.
  • Numeric IDs. Repository, user, issue, comment, review, review comment, check run, release, release asset, commit status, deployment and reaction IDs are numbers, as on GitHub. See IDs.
  • Account types. A user object's type is User, Organization or Bot. Agents are Bot.
  • Pagination. Lists take per_page and page and send a Link header, so octokit.paginate() works. See Pagination.
  • Errors. Error bodies have message and documentation_url, and validation errors add GitHub's errors array. See Errors.
  • Headers. API responses carry X-GitHub-Media-Type: github.v3; format=json, X-GitHub-Api-Version-Selected: 2022-11-28 and X-RateLimit-* headers.
  • Aliases. Collaborator permissions accept GitHub's pull and push for read and write, and private: false on a repository makes it public.

Responses also keep a few Githome fields that the CLI, the MCP server and the web UI read. Clients that only know GitHub's fields can ignore them:

ObjectExtra fields
Pull request (GET /pulls/{n})merge_blockers (why the caller can't merge it directly yet), approvals, required_approvals, checks, merge_queue; for agents and workflow tokens also auto_merge_blockers (why auto-merge or the merge queue can't land it for them yet; [] means it would now). People merge and auto-merge on the same terms, so they don't get it.
Pull request auto_mergedelete_branch: whether the head branch is deleted once auto-merge lands it
Pull request (every response)intent, a short statement of what the change is for, written for reviewers
Repository (GET /repos/{owner}/{repo})stats (counts of open issues and pull requests and more), and permissions.role
Workflow runsha and ref (what the run checked out: the test merge commit for pull_request runs) and pull_request_number
Workflow jobGithome's own camelCase job fields next to GitHub's snake_case ones
Check runrun_id and job_id of the workflow behind an Actions check
Comparisonbase_sha, head_sha and merge_base
Releaselatest, whether it is the repository's latest release
Pull request filebinary

Fields that describe GitHub features Githome doesn't have are present with fixed values, so typed clients don't break: for example language: null and license: null. has_wiki gives the setting of the wiki of the repository. fork, forks_count and forks describe the forks of a repository (see Forks), and allow_forking is true. stargazers_count is the count of the stars of the repository, and watchers_count and watchers repeat it, as on GitHub.

Each user and repository object has the URL templates of GitHub, such as followers_url, keys_url and notifications_url. The root of the API (GET /api/v3) has them too. A template is there also where Githome has no route behind it.

A user of GET /users/{username} and of GET /user has the profile fields of GitHub. Githome keeps no profile text and no follows:

  • bio, blog, company, location, hireable and twitter_username are null.
  • followers, following and public_gists are 0.
  • public_repos is the count of the public repositories of the account.

A label has archived_at and archived_by as null, because Githome archives no label. A full repository has network_count and subscribers_count: the answer of GET and PATCH /repos/{owner}/{repo}, of the routes that create a repository, and of the route that makes a fork. network_count is the count of the forks of the repository. subscribers_count is the count of the people who watch it for each notification.

IDs

Objectid
Users, organizations, agents, repositoriesA positive integer derived from the account's or repository's own ID in Githome. It is stable, but nothing looks an account or repository up by it: use the login or owner/repo in paths.
Issues and pull requestsThe id equals the number (issues and pull requests share one number sequence per repository).
Issue comments, review comments, reviews, check runs, releases, release assets, commit statuses, deployments, deployment statuses, reactions, issue eventsIntegers, as on GitHub. Use them in paths such as /issues/comments/{id} and /check-runs/{id}.
Workflow runs, jobs and check suites16-digit integers, so clients that expect GitHub's numbers (such as gh run) work. Use them in /actions/runs/{id}, /actions/jobs/{id} and /check-suites/{id}; a run's check suite has the run's id. These paths also take the 32-character hexadecimal ids in the web UI's addresses (html_url) and in webhook payloads. Getting a run, its jobs, logs and attempts, re-running it and listing its artifacts also accept the run's number (run_number, which is github.run_id inside a workflow): it picks the latest attempt, and for artifacts it covers every attempt.
Artifacts and cache entriesIntegers, as on GitHub: /actions/artifacts/{id}, /actions/caches/{id}. An artifact's workflow_run.id is the run number.
WorkflowsAn integer derived from the workflow's path. /actions/workflows/{id} also accepts the path, such as .github/workflows/ci.yml, or the file name, ci.yml.
Webhooks and tokensStrings.

node_id is an opaque string, stable for an object. The GraphQL API accepts the node_id of a repository or pull request in node(id:) and in its mutations.

Differences to plan for

AreaGitHubGithome
Pull requestsMergeability spread over several callsGET /pulls/{n} also returns merge_blockers, approvals, required_approvals and checks, so one call tells you why a pull request can't merge yet. GET /pulls/{n}/merge answers GitHub clients (an Accept: application/vnd.github... header) with 204 or 404, and other clients with Githome's full mergeability report.
Draft pull requestsMarking ready for review or converting to draft is GraphQL onlyPATCH /repos/{owner}/{repo}/pulls/{n} accepts "draft": false (ready for review) and "draft": true (back to draft).
Merge queueGraphQL onlyREST: PUT and DELETE /repos/{owner}/{repo}/pulls/{n}/merge-queue, GET /repos/{owner}/{repo}/merge-queue/{branch}. See Merge queue.
Auto-mergeGraphQL only (enablePullRequestAutoMerge)REST: PUT and DELETE /repos/{owner}/{repo}/pulls/{n}/auto-merge. The pull request's auto_merge field is filled in while it is on. See Auto-merge.
Text filter on issue listsNot on the list endpointsA q parameter on GET /repos/{owner}/{repo}/issues and /pulls matches text in the title and body.
Webhook secretYou always choose itIf you create a webhook without config.secret, Githome generates one and returns it once as secret in the response.
Release asset upload hostuploads.github.comThe same host as the API, under /api/uploads. Use the release's upload_url and clients find it.
Required checksDecided by GitHub Apps and statusesWhen Githome Actions reports a check, it alone decides it; other reporters can be bound per check. See Commit statuses, checks and required checks.
Rate limitsBudgets per hour, reported in headers and GET /rate_limitPer-minute limits that stop floods (sign-in and anonymous requests per IP address, signed-in requests and new content per account). The headers and GET /rate_limit report a constant budget. See Rate limits.
CORSAllowed for some endpointsNo CORS headers. Call the API from a server, a script, a CI job or the CLI, not from JavaScript on another website.
People imported from GitHubEvery author is a GitHub accountIn a repository imported from GitHub, people without a linked Githome account are mannequins: a user object with login github:<login>, type Mannequin, and html_url and url pointing at their GitHub profile. They can't sign in, and their imported reviews never count as approvals. Search for them with author:github:<login>.

Transferred and renamed repositories

After a repository is transferred or renamed, requests that name it by its old owner and name in the path keep working, as on GitHub, for anyone who can read it:

  • GET and HEAD get 301 Moved Permanently; every other method gets 307 Temporary Redirect, so a client that follows it repeats the method and body.
  • Location holds the same URL with the new owner and name (the query string is kept), and the body is {"message": "Moved Permanently", "url": "<the new URL>", "documentation_url": "..."}.
  • Callers who can't read the repository get 404 (or 401 without credentials), the same answer as for the repository under its new name.
  • Old names that appear elsewhere than in the path (for example repositories when creating a token restricted to some repositories) are accepted as the repository they now lead to.

curl follows redirects with -L, and Octokit follows them by itself. The redirect lasts until a repository is created at the old owner and name. Update stored owner and repository names when you see one, and use the repository's numeric id, which never changes, to recognize it.

Authentication

Most endpoints need you to be signed in. Scripts, CI jobs, agents and the CLI authenticate with a personal access token or an agent token. Tokens start with ghome_. Create one under Settings → Access tokens (/settings/tokens). No token can create another token, so POST /api/v3/githome/tokens needs a browser session (see Personal access tokens).

Send the token in the Authorization header. Three schemes are accepted, so any GitHub client works:

SchemeExampleTypical sender
BearerAuthorization: Bearer ghome_...curl, fetch, MCP clients, the Githome CLI
tokenAuthorization: token ghome_...Octokit and other GitHub libraries
BasicAuthorization: Basic base64(diffy:ghome_...)git, curl -u, Docker

With Basic, Githome takes the password as the token and ignores the username, so any username works (x-token, your login, anything). If the password is empty, the username is used as the token.

curl -H "Authorization: Bearer ghome_..." https://git.pkgforge.dev/api/v3/user
curl -u diffy:ghome_... https://git.pkgforge.dev/api/v3/user

Inside a workflow job, the job's GITHUB_TOKEN works with the API too, with the permissions of the job's permissions: rather than token scopes. See Workflow job tokens.

Without an Authorization header and without a browser session, you are anonymous. Anonymous callers can read public repositories (and GET /api/v3, /api/v3/meta, /rate_limit and search over public repositories); most other endpoints return 401. As on GitHub, anonymous callers never change anything: every repository request other than GET or HEAD (opening an issue, commenting, reacting, watching and so on) gets 401 authentication required, even on a public repository. Anonymous git clone and git fetch of public repositories and Git LFS downloads keep working, because they only read.

Authentication errors:

StatusmessageCause and fix
401invalid, expired or revoked tokenThe ghome_ token is unknown, past its expiry date, revoked, or its account is disabled or not yet active. Create a new token.
401unrecognised token; create one under Settings → Access tokensThe credential does not start with ghome_. GitHub tokens (ghp_...) do not work here.
401authentication requiredYou called a private resource anonymously, or tried to change something in a repository without a token. Add the Authorization header. Reading a private repository anonymously gets 404, the same as one that doesn't exist.
401malformed basic credentialsThe Basic value isn't valid base64. Encode user:token with base64.
401sign in requiredThe endpoint needs an account (for example GET /user) and you sent no credentials.
403your account is waiting for an inviteYour account exists, and the superadmin hasn't made it active. Ask the superadmin for an instance invite, or send an access request.
403your account is disabledThe superadmin disabled the account.

Every 401 response carries WWW-Authenticate: Basic realm="Githome", charset="UTF-8", which makes git and other HTTP clients prompt for credentials.

If a request carries the Authorization header twice (git-lfs does this when http.extraheader is set), Githome uses the first one.

Token scopes

A token has one or more scopes. The scope caps what the token can do in each repository, on top of the role its account has there:

ScopeHighest repository role the token can useTypical use
readreadClone, fetch, read issues, pull requests, releases and runs. Never changes anything.
write (the default)maintainPush, open and merge pull requests, comment, create releases, report statuses and check runs, rerun workflows.
adminadminEverything the account can do, including repository settings, secrets, environments, webhooks, branch protection and revoking tokens. No token can create tokens or agents.

The effective role is the lower of the account's role and the token's cap. A write token for someone who only has read access to a repository still only reads; an admin token for a repository writer still only writes. See Permissions for what each role allows.

A read token never writes, even where the read role could (opening issues, commenting, reacting): every repository request other than GET or HEAD gets 403 this token needs the write scope. git fetch and Git LFS downloads, which use POST but only read, are the exceptions. MCP tools that aren't read-only also need a write token. A write to a private repository the token can't see is 404 repository not found, not a scope error, so a read token can't find out that the repository exists.

A token can also be restricted to a list of repositories. Requests for any other repository then behave as if the repository does not exist (404). The list may only name repositories the account can see; any other name is refused with 422 unknown repository owner/name, whether or not it exists.

When you create a token (POST /api/v3/githome/tokens or POST /api/v3/githome/agents/{login}/tokens) or an agent with a first token (token.scopes on POST /api/v3/githome/agents), scopes must be a non-empty list of read, write and admin. Leave it out for ["write"]. An empty list, a value that isn't a list or an unknown name is refused with 422 scopes must be a non-empty list of: read, write, admin.

Account-level endpoints check the scope directly, and most of them also refuse tokens restricted to repositories with 403 this token is limited to repositories and can't change account settings. A browser session can call all of them.

EndpointNeeds
POST /api/v3/user/repos, POST /api/v3/orgs/{org}/reposA write or admin token not restricted to repositories.
PUT /api/v3/notifications, PUT /api/v3/githome/notification-settings, POST /api/v3/githome/emails/testA write or admin token not restricted to repositories (403 this token needs the write scope for a read token).
PUT and DELETE /api/v3/repos/{owner}/{repo}/subscriptionA write or admin token (403 this needs a write or admin token).
PUT and DELETE /api/v3/user/avatar and /api/v3/orgs/{org}/avatarA write or admin token that is not limited to repositories (403 changing a profile picture needs a write or admin token that is not limited to repositories).
POST /api/v3/githome/tokensA browser session. Every token gets 403 creating tokens needs a browser session: sign in at https://git.pkgforge.dev/settings/tokens.
DELETE /api/v3/githome/tokens/{id}An admin token not restricted to repositories.
GET /api/v3/user/keys[/{id}], GET /api/v3/user/ssh_signing_keys[/{id}], GET /api/v3/user/gpg_keys[/{id}]A token not restricted to repositories (any scope).
POST /api/v3/user/keys, POST /api/v3/user/ssh_signing_keys, POST /api/v3/user/gpg_keysA write or admin token not restricted to repositories, or a browser session signed in within 2 hours.
DELETE /api/v3/user/keys/{id}, DELETE /api/v3/user/ssh_signing_keys/{id}, DELETE /api/v3/user/gpg_keys/{id}An admin token not restricted to repositories.
PUT and DELETE /api/v3/user/starred/{owner}/{repo}A write or admin token not restricted to repositories.
POST /api/v3/user/orgs, PATCH /api/v3/orgs/{org}An admin token not restricted to repositories.
POST /api/v3/orgs/{org}/invitationsAn admin token not restricted to repositories. POST /api/v3/invitations answers 403: the instance invitation is in the dash.
GET /api/v3/user/repository_invitations, GET /api/v3/user/memberships/orgs[/{org}]A token not restricted to repositories (any scope).
PATCH and DELETE /api/v3/user/repository_invitations/{id}, PATCH /api/v3/user/memberships/orgs/{org}A write or admin token not restricted to repositories.
GET, PUT and DELETE /api/v3/orgs/{org}/actions/secrets[/{name}]An admin token not restricted to repositories.
POST /api/v3/githome/agents, POST /api/v3/githome/agents/{login}/tokensA browser session (they mint tokens). Every token gets 403 creating tokens needs a browser session: sign in at https://git.pkgforge.dev/settings/tokens.
DELETE /api/v3/githome/agents/{login}An admin token not restricted to repositories.
POST /api/v3/githome/admin/emails/samplesNobody: it answers 403, and the page Email of the dash sends the samples.
POST, PATCH and DELETE /api/v3/orgs/{org}/actions/variables[/{name}]An admin token not restricted to repositories (reading them needs only an organization owner or admin).
POST /api/v3/githome/importsA write or admin token (403 starting an import needs a token with write or admin scope). An import that should bypass push protection for old secrets needs a person's admin token or browser session.
POST /api/v3/githome/imports/{id}/cancel, /retry and /selectionA write or admin token not restricted to repositories.
POST /api/v3/githome/user/linked-accounts/github/challenge, POST /api/v3/githome/user/linked-accounts/github, DELETE /api/v3/githome/user/linked-accounts/{id}An admin token not restricted to repositories.
DELETE /api/v3/githome/admin/linked-accounts/{id}nobody: it answers 403, and the dash serves it

A token whose scope is too low for one of these gets 403 this token needs the write scope or 403 this token needs the admin scope.

Agent accounts have extra guardrails that apply however they authenticate. Among them: they can't push or merge to the default branch or protected branches, bypass branch rules when merging, change branch protection, add, change or delete secrets or Actions variables, change environments or their branch policies, dismiss reviews or delete repositories, and the commit statuses and check runs they report never satisfy a required check. See Agents.

Browser sessions and tokens

The web UI uses a session cookie that you get by signing in with a passkey. The API endpoints also accept that cookie. A browser session acts with the full role of its account (like an admin token).

Because the cookie is sent automatically by the browser, Githome refuses state-changing requests that rely on it from another site:

  • The check applies to every method except GET, HEAD and OPTIONS, and only when the request has no Authorization credential.
  • If the browser sends Sec-Fetch-Site and its value is anything other than same-origin or none, the request is refused.
  • If there is no Sec-Fetch-Site header but there is an Origin header that differs from the instance's own origin (https://git.pkgforge.dev), the request is refused.
  • A refused request gets 403 with {"message": "cross-site request refused"}.

Requests that carry a token in the Authorization header are exempt: they carry no ambient credentials, so this check does not apply to curl, scripts, the CLI, MCP clients or runners. If you see cross-site request refused from a script, it means the script sent no token; add the Authorization header.

Machines should always use tokens, never browser cookies. Passkeys and browser sessions can only be managed in the web UI; tokens can't add or remove passkeys, and they can't create tokens or agents. Web forms (/settings/..., repository settings and the other pages' buttons) refuse requests that carry a token with 403 web forms need a browser session; use the API with a token, so a token's scope and repository limits always apply.

Workflow job tokens

A workflow job's GITHUB_TOKEN (secrets.GITHUB_TOKEN, or github.token) authenticates to the REST API at github.api_url, to git and to the container registry while the job runs, and stops working when the job ends. It follows GitHub's permission model, not the scopes of personal access tokens:

  • Its permissions come from the job's permissions:, else the workflow's, else the repository default. The default is a repository setting (default_workflow_permissions in GET and PUT /repos/{owner}/{repo}/actions/permissions/workflow, admins only): read gives contents and packages read and nothing else, write gives read and write on every scope except id-token. New repositories start with read; repositories created before the setting existed keep write. A reusable workflow never gets more than its caller, and a run for a pull request from another repository only reads.
  • Each endpoint needs its permission: issues for issues and labels, pull-requests for pull requests and reviews, contents for code, commits, branches, releases and merges, statuses, checks, deployments (also environments), actions for runs, artifacts and caches, and so on. Without it: 403 Resource not accessible by integration (the workflow's GITHUB_TOKEN needs permissions: issues or pull-requests: write). Reading a public repository works whatever the permissions.
  • Out of reach: repository settings, collaborators, webhooks, secrets, variables, runners, branch protection, environment changes and the Actions permissions setting (403 this endpoint is not available to a workflow's GITHUB_TOKEN). The token's role never goes above write, so branch protection always applies to it.
  • Workflow files: a job token never creates, changes or deletes files under .github/workflows/ or .githome/workflows/, by push, API commit or merge: refusing to allow a GitHub App to create or update workflow <path> without workflows permission.
  • Pull requests: it can open pull requests and approve or request changes only when the repository allows it (can_approve_pull_request_reviews); otherwise 403 GitHub Actions is not permitted to create pull requests (or approve). Its approvals never count toward required approvals. It can't re-run workflows.
  • Other repositories: it reads public repositories (to clone a public submodule or download a public repository's artifacts) and gets 404 for other private ones.
  • Agents: a run whose code came from an agent (an agent started it, pushed the branch last or opened the pull request, and re-runs of such runs and workflow_run runs that follow them) acts as an agent, so the agent guardrails hold for its token.
  • No loops: what a job does with its token starts no new workflow runs, except workflow_dispatch and repository_dispatch.

See Workflow permissions for the full model and examples.

Apps

A GitHub-compatible app signs in with a JWT made with its private key (Authorization: Bearer <jwt>) on the app endpoints (/app, /app/installations/..., /repos/{owner}/{repo}/installation), and trades it for installation access tokens (ghomeia_...) that last an hour. An installation token reaches the repositories the app is installed on, checked per endpoint with the same permission table as a workflow's GITHUB_TOKEN, as <slug>[bot]; requests outside its permissions get 403 Resource not accessible by integration (the app needs the issues permission: write). It never changes settings, secrets or workflow files, and gets 403 on endpoints for people such as /user. See Apps.

Read-your-writes and the x-d1-bookmark header

Account-level data (accounts, organizations, memberships, tokens, the list of repositories and who can access them, invitations, notifications, the audit log, the package index and email settings) can be served from a copy that briefly lags behind the latest write. Every response that read or wrote this data carries a bookmark header that marks how new the data it saw was:

x-d1-bookmark: 00000085-0000024c-00004c6d-8e61117bf38d7adb71b934ebbf891683

How a request picks which data it reads:

CallerReads data that isSees its own earlier writes?
Token request with an x-d1-bookmark headerAt least as new as that bookmark (possibly from a nearby copy)Yes, every write made up to that bookmark
Token request without the headerThe latestYes, always (at the cost of a longer round trip)
BrowserAt least as new as the bookmark in its __Host-ghome_d1 cookie, which Githome sets after a write or once you are signed inYes
Request without credentials or bookmarkFrom the nearest copyNot guaranteed

For API clients this means:

  • You don't have to do anything to be correct. Token requests without a bookmark read the latest data, so a GET right after your POST sees the change.
  • To read faster, echo the bookmark. Take the x-d1-bookmark value from your latest response and send it as a request header on the next request. Githome may then answer from a copy near you, and it still includes every write up to your bookmark.
  • To hand consistency to another client, pass the bookmark along. A second process that sends it sees everything the first process wrote before that response.
  • Githome ignores a header that doesn't look like a bookmark (hexadecimal groups separated by dashes, at most 128 characters) and falls back to the default for your caller type.
# Create a repository, then list repositories from a copy that already has it.
bm=$(curl -sS -D - -o /dev/null -X POST https://git.pkgforge.dev/api/v3/user/repos \
  -H "Authorization: Bearer ghome_..." -H "content-type: application/json" \
  --data '{"name":"website","private":true}' | tr -d '\r' | sed -n 's/^x-d1-bookmark: //Ip')
curl -sS https://git.pkgforge.dev/api/v3/user/repos \
  -H "Authorization: Bearer ghome_..." -H "x-d1-bookmark: $bm"

Repository data (git objects and refs, issues, pull requests, reviews, checks, statuses, releases, deployments, environments, workflow runs, secrets and webhooks) is always strongly consistent. Bookmarks don't affect it: a read always sees every earlier write to the same repository.

Requests and content types

  • Send request bodies as JSON. Setting Content-Type: application/json is recommended. A body that does not parse as JSON returns 400, with {"message": "request body must be JSON"} or GitHub's {"message": "Problems parsing JSON"} depending on the endpoint.
  • Endpoints that take no body ignore one. A few PUT and POST endpoints accept an empty body and use defaults (for example PUT /repos/{owner}/{repo}/pulls/{n}/merge and PUT /repos/{owner}/{repo}/issues/{n}/lock).
  • File contents in the contents and git data APIs are base64: PUT /repos/{owner}/{repo}/contents/{path} takes content as base64, POST /git/blobs takes encoding: "utf-8" (the default) or "base64", and reads return "encoding": "base64" with the content split into lines of 60 characters. The Githome multi-file commit endpoint takes encoding: "utf-8" (the default) or "base64" per file.
  • Path segments that contain a branch, tag, label or file path are taken as written; URL-encode reserved characters (a label named 50% is /labels/50%25).
  • Release asset uploads send the file itself as the request body (not JSON). See Release assets.

Responses:

EndpointContent type
Almost everythingapplication/json
GET /repos/{owner}/{repo}/contents/{path}, /readme and /git/blobs/{sha} with Accept: application/vnd.github.raw (or application/vnd.github.v3.raw)The raw bytes, as text/plain; charset=utf-8
GET /repos/{owner}/{repo}/raw/{ref}/{path}text/plain; charset=utf-8 for text files, application/octet-stream for binary files
GET /repos/{owner}/{repo}/zipball/{ref}, /tarball/{ref}application/zip or application/gzip, as a download (see Archives)
GET /repos/{owner}/{repo}/releases/assets/{id} with Accept: application/octet-streamThe asset's bytes, as a download
GET /repos/{owner}/{repo}/actions/runs/{id}/logsapplication/zip, one text file per job
GET /repos/{owner}/{repo}/actions/artifacts/{id}/zipA 302 redirect to a signed URL, valid for 10 minutes, that serves the artifact (a zip, or the file itself for an artifact uploaded unzipped)
GET /repos/{owner}/{repo}/actions/jobs/{id}/logstext/plain, one line per log line prefixed with its timestamp. Add ?format=json for an array of {t, step, text} objects.
DELETE endpoints and some PUT endpointsNo body (204)

Every response also carries security headers (Strict-Transport-Security, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy, Permissions-Policy) and, whenever the request read or wrote account-level data, the x-d1-bookmark header described above. Answers to signed-in people and tokens that aren't errors also carry a Server-Timing header, which browsers' developer tools show under Timing: the whole request (total), the database queries it made (d1, with their count and, when a query said so, the database region that answered and whether it was the primary or a nearby copy) and the calls to the repository's storage (do, with their count), in milliseconds, and the Cloudflare location that answered (colo). Parallel calls overlap, so their times can add up to more than the total. Downloads (raw files, archives and release assets) are sent with Content-Security-Policy: sandbox and as attachments, so browsers never run them as pages.

Errors

Every API error is a JSON object with a message, a documentation_url and an HTTP status code, like GitHub's:

HTTP/1.1 404 Not Found
content-type: application/json

{"message": "repository not found", "documentation_url": "https://docs.github.com/rest"}

documentation_url points at GitHub's REST documentation, so that clients which expect the field (Octokit's RequestError, for example) keep working. Validation errors (422) usually add GitHub's errors array, which names the resource, the field and a code:

{
  "message": "title is required",
  "documentation_url": "https://docs.github.com/rest",
  "errors": [{ "resource": "Issue", "code": "missing_field", "field": "title" }]
}

Unexpected server errors return 500 with {"message": "internal error", "request_id": "..."}; the details stay on the server. Every 5xx answer carries a request_id in the body and the same value in the x-request-id header (on Cloudflare it is the request's Ray ID). Include it when you report a failure: a site administrator finds the error under Server errors.

Write your client to branch on the status code and show message to people. Messages are written for humans and may change wording.

A path under /api/ that Githome doesn't know, or a method a known path doesn't support, gets GitHub's JSON 404, never the web UI's HTML error page:

{ "message": "Not Found", "documentation_url": "https://docs.github.com/rest" }

Status codes

StatusMeaning in GithomeExamples of message
200 OKSuccess with a body. Also returned when you add a reaction you had already added.
201 CreatedSomething was created: a repository, issue, comment, pull request, review, release, asset, status, check run, deployment, token, webhook, workflow run, sandbox, commit, ref. Also returned by PUT .../merge-queue when the pull request joined the queue, and by PUT .../environments/{env}/secrets/{name} for a new secret.
202 AcceptedWork started in the background: POST .../actions/runs/{id}/cancel, PUT .../pulls/{n}/update-branch. Also returned by POST /repos/{owner}/{repo}/transfer and DELETE /orgs/{org}, which have finished when they answer.
204 No ContentSuccess without a body (deletes, settings changes, POST .../dispatches). POST .../merges also returns 204 when there was nothing to merge, and POST .../actions/runners/claim when no job is waiting.
301 Moved PermanentlyA GET or HEAD named a repository by an old owner or name, after a transfer or rename. Location (and url in the body) is the same request under the new name.Moved Permanently
302 FoundGET .../actions/artifacts/{id}/zip redirects to a signed download URL.
307 Temporary RedirectAny other method on a transferred or renamed repository's old name. Repeat the request, with its method and body, at Location.Moved Permanently
205 Reset ContentPUT /api/v3/notifications: every notification marked as read.
400 Bad RequestThe body is not valid JSON, or the URL has a malformed percent-encoding.request body must be JSON, Problems parsing JSON, malformed percent-encoding in the URL
401 UnauthorizedNo credentials where they are needed, or a bad token.See Authentication.
403 ForbiddenYou are signed in but your role, token scope or account kind doesn't allow this.write access required, admin access required, this token needs the write scope, this token is limited to repositories and can't change account settings, repository is archived (read-only), agents cannot change branch protection, agents cannot manage secrets, agents cannot manage variables, agents can't dismiss reviews, only the app that created a check run can update it, cross-site request refused, Resource not accessible by integration (...) (a workflow's GITHUB_TOKEN), Confirm it's you: sign in with your passkey again to do this. (a sensitive call from a browser session signed in more than 2 hours ago; the body has a confirm URL)
404 Not FoundThe resource doesn't exist, or it is private and you can't see it. Githome doesn't reveal whether a private repository exists, and draft releases are hidden from readers the same way.repository not found, Branch not found, Not Found
405 Method Not AllowedA pull request merge was refused by policy.pull request is not mergeable: ... (followed by the blockers), main requires a merge queue: add the pull request to the queue, main requires linear history; use squash or rebase
409 ConflictSomeone else changed the branch, ref or file first, or a merge conflicts. Fetch the latest state and retry.branch moved; refresh and retry, head branch was modified; review and try again, README.md does not match <sha>, Merge conflict, Git Repository is empty., Already exists - Variable already exists
410 GoneThe artifact has expired; it is still listed, but its content is gone.Artifact has expired
413 Payload Too LargeAn upload or a generated archive is over a limit.release assets are limited to 100 MB, repository is too large for an on-the-fly archive; clone it with git instead
422 Unprocessable EntityThe request is understood but invalid.title is required, invalid secret name, webhook URL must use https, state must be one of error, failure, pending, success, The search is longer than 256 characters., push protection: possible secret detected (...), scopes must be a non-empty list of: read, write, admin
429 Too Many RequestsA per-repository budget ran out (only new source archives have one).too many archives were built for this repository in the last hour; try again later or clone it with git
500 Internal Server ErrorA bug or an outage. The body and the x-request-id header carry a request_id the superadmin can look up on the page Server errors of the dash.internal error
503 Service UnavailableA sandbox container didn't start.the sandbox container did not start; try again

Every write to an archived repository returns 403 repository is archived (read-only), whatever role it needs: pushes, issues, comments, reviews, labels, branch protection, collaborators, secrets, variables, webhooks and MCP tools alike. Only a few calls still work: unarchiving it with PATCH /repos/{owner}/{repo} and {"archived": false} (admins), deleting it, watching or unwatching it, starring it, and linking a package to it. See Archive a repository.

Pagination

Lists take GitHub's page and per_page query parameters and answer with a Link header that points at the other pages:

link: <https://git.pkgforge.dev/api/v3/repos/acme/website/issues?state=all&per_page=50&page=2>; rel="next", <https://git.pkgforge.dev/api/v3/repos/acme/website/issues?state=all&per_page=50&page=7>; rel="last"
  • page starts at 1. per_page is 30 by default and at most 100; larger values are treated as 100.
  • The header has rel="next" while there are more pages, rel="prev" and rel="first" after the first page, and rel="last" when the total is known. On the last page there is no next.
  • Follow rel="next" until it is missing. That is what octokit.paginate() and gh api --paginate do.
  • Paging over a list that changes while you read it can skip or repeat items, as on GitHub.

Lists with other defaults or without the header:

EndpointDefault per_pageNotes
GET /repos/{owner}/{repo}/issues and /pulls30Also sends x-total-count with the number of matching items.
GET /repos/{owner}/{repo}/actions/runs25total_count in the body.
GET /repos/{owner}/{repo}/actions/workflows/{id}/runs30total_count in the body. No Link header: page with page until you have total_count runs.
GET .../actions/artifacts, .../actions/runs/{id}/artifacts, .../actions/caches30total_count in the body, and a Link header.
GET /repos/{owner}/{repo}/compare/{base}...{head} (its commits), GET .../pulls/{n}/commits100At most 250 commits in total, as on GitHub.
GET .../git/refs, .../git/matching-refs/{ref}100
GET .../commits/{ref}/status (its statuses)100
GET .../environments/{env}/variables10As on GitHub.
GET /search/issues, /search/repositories30Only the first 1,000 results can be paged to.
GET /notifications50At most 50 a page, as on GitHub. The pages hold the threads of your 1,000 newest notifications.
GET /user/packages/container/{name}/versions (and the same path under /users/{owner} and /orgs/{owner})30No Link header; loop until a page has fewer items than per_page.
GET /api/v3/githome/emails50At most 500, no page parameter.

A few lists return everything in one response, with no paging: GET /user/orgs, /orgs/{org}/members, /repos/{owner}/{repo}/actions/workflows, /actions/secrets, /orgs/{org}/actions/secrets, /api/v3/githome/tokens and /api/v3/githome/agents.

The repository lists (GET /user/repos, /users/{login}/repos and /orgs/{org}/repos) page over at most 500 repositories, most recently pushed first. GET /user/repos lists every repository you can see, including public ones you don't own. See Limits for every cap.

Filters on the issue and pull request lists:

ParameterMeaning
stateopen (default), closed or all
labelsComma-separated label names; an item must have all of them
creatorThe author's login
assigneeA login, none (no assignee) or * (any assignee)
sinceOnly items updated at or after this ISO 8601 time
head, basePull requests from or into this branch (head also accepts owner:branch)
sort, directioncreated (default), updated or comments (popularity on pulls); desc (default) or asc
qGithome: text in the title or body

Like GitHub, /issues includes pull requests; each pull request item has a pull_request field. Other filters: workflow runs take workflow, branch, status, event and head_sha; commits take sha (where to start, default HEAD), path, author, since and until; deployments take sha, ref, task and environment; check runs for a ref take check_name and status; repository-wide comment lists take since, sort and direction.

Rate limits

Githome limits requests per minute to stop floods and scripted abuse:

WhatLimitCounted by
Sign-in, passkey registration, invitation and sign-in links, open sign-up (POST /signup, POST /signup/verify; off on git.pkgforge.dev), security reports (POST /security/report), early access requests, confirmations and removals (POST /early-access, /early-access/confirm, /early-access/remove, /early-access/remove/confirm), contact messages and their confirmations (POST /contact, /contact/confirm)20 a minuteIP address
Anything else without signing in (web pages, API, git)300 a minuteIP address
Anything with a session or token (web, API, MCP, git)2,000 a minuteAccount
Creating repositories, organizations, forks, issues, pull requests, releases, comments, reviews and reports, and MCP tools that change something60 a minuteAccount
Agents' model requests through the model proxy (/-/model/*)600 a minuteThe person who pays for the run

Workflow jobs, sandboxes, self-hosted runners and imports use their own credentials and aren't counted. The counts are kept in each Cloudflare location, so they stop bursts rather than meter use exactly. Past a limit, the request answers 429 with a message naming the limit, retry-after: 60 and x-ratelimit-remaining: 0; Octokit's throttling plugin waits and retries.

Other responses under /api/v3 still carry GitHub's budget headers with a constant, generous value, so clients that check their budget keep working:

x-ratelimit-limit: 15000
x-ratelimit-remaining: 15000
x-ratelimit-used: 0
x-ratelimit-reset: 1791887400
x-ratelimit-resource: core

GET /api/v3/rate_limit answers like GitHub's, with resources.core, search, graphql, code_search and others, and the same rate.

Cloudflare's own platform limits still apply to each request (for example the maximum request body size of 100 MB). A few operations have budgets of their own, such as building new source archives (20 per repository per hour). Write clients that back off and retry on 429, 503 and network errors. Size and count limits, such as 100 MB per git push or 256 jobs per matrix, are listed in Limits.

What works, by area

Each area lists what you can do; the API reference has every endpoint with its fields. Unless noted, reading needs read access to the repository and changing needs write access.

Repositories

  • Get, create (POST /user/repos, POST /orgs/{org}/repos), update (PATCH, admin) and delete (DELETE, admin, people only). visibility is public or private, and a repository is public unless private: true or visibility asks otherwise, as on GitHub. auto_init: true makes its first commit, Initial commit with a README.md of its name and description, as gh repo create --add-readme asks. A non-empty gitignore_template or license_template answers 422: Githome keeps no templates. So do is_template: true, a team_id, has_issues: false, and a private, an auto_init or a has_wiki that is no boolean. has_wiki: false makes a repository with no wiki. homepage is kept. A first commit of auto_init that is turned down (push protection, the role of an agent, a quota) deletes the new repository and answers the refusal.
  • Topics: GET and PUT /repos/{owner}/{repo}/topics (admin). At most 20 topics, each starting with a lowercase letter or digit, up to 50 characters of lowercase letters, digits and hyphens.
  • GET /languages: bytes per language, by file extension, leaving out vendored directories such as node_modules, vendor and dist.
  • Collaborators: list, add or change (PUT with permission), remove (admin). As on GitHub, PUT invites a person who doesn't have access yet and answers 201 with the invitation (id, repository, invitee, inviter, permissions, created_at, expires_at, html_url); they get the role once they accept it. For an agent, an existing direct collaborator, a member of the organization that owns the repository or yourself, the role applies at once and PUT answers 204. See Add people to a repository.
  • Pending invitations: GET /repos/{owner}/{repo}/invitations lists them and DELETE /repos/{owner}/{repo}/invitations/{id} revokes one (admin). Each has id, email (null for an invitation sent to a username), login (null for one sent to an email address), role, inviter, created_at and expires_at.
  • Branches (with protected=true|false), one branch, tags, README (GET /readme and /readme/{dir}), commits, one commit with its files and stats, compare (base...head), and GET /commits/{sha}/pulls.
  • POST /merges merges a branch or commit into a branch (201 with the merge commit, 204 when there was nothing to merge, 409 Merge conflict).
  • POST /dispatches sends a repository_dispatch event: event_type (up to 100 characters) and an optional client_payload object (at most 10 top-level properties, 64 KiB). It returns 204 and starts workflows with on: repository_dispatch.
  • POST /transfer moves the repository to new_owner (your own account, or an organization you own or administer), optionally under new_name, and returns 202 with the repository. People only, with an admin token that isn't limited to repositories. See Transfer a repository.
  • Archived repositories are read-only through the API, for everyone, except unarchiving, deleting, transferring, watching and starring. See Errors. PATCH and the collaborator endpoints are recorded in the audit log, like changes in the web settings pages.

Stars

As on GitHub, you star a repository you can read, and each repository object counts its stars:

  • PUT /user/starred/{owner}/{repo} stars the repository and DELETE removes the star (both 204, and a second call changes nothing). GET /user/starred/{owner}/{repo} answers 204 when you starred it and 404 when you didn't. An archived repository takes a star too. A person whom the owner blocked can't star its repositories (403), and blocking someone removes their stars from your repositories.
  • GET /user/starred lists the repositories you starred, and GET /users/{username}/starred those a person starred, newest star first. sort=updated sorts by the last push, and direction=asc turns the order around. Each list reads the first 1,000 stars in that order and shows those of the repositories you can see now. GET /users/{username}/starred needs you to be signed in, as GET /users/{username}/repos does.
  • GET /repos/{owner}/{repo}/stargazers lists the people who starred a repository, oldest star first, at most 1,000. As GitHub limits this list to admins and collaborators, it lists them to the people with access to the repository itself (its owner, the members of its organization by their role, its collaborators) and to site administrators. Anyone else who can read the repository gets an empty list. Suspended people are left out, except for site administrators.
  • With Accept: application/vnd.github.star+json, each item of these lists is {"starred_at": ..., "repo": ...} or {"starred_at": ..., "user": ...}.

A star stays, and is counted, when its person loses access to the repository. Deleting your account or an agent deletes its stars, and deleting a repository deletes the stars of it.

Notifications

As on GitHub, GET /notifications lists your notifications as threads, newest first. Each thread has its reason, its subject and its repository.

reasonWhen
author, comment, mention, assign, review_requested, manual, subscribedAn issue or a pull request, as on GitHub
ci_activityA run of Actions that failed
approval_requestedA deployment that waits for your review
invitationAn invitation to a repository
  • The subject of an issue or a pull request has its API url and its title. Its latest_comment_url is the comment that made the notification, or the issue or pull request when no comment did.
  • Another subject has url and latest_comment_url as null, as GitHub gives a check suite.
  • Each notification is a thread of its own. An issue with three events gives three threads, where GitHub gives one thread for each subject.
  • all=true lists the read notifications too. participating=true leaves out the threads of the reason subscribed. since and before take an ISO 8601 time, such as 2026-11-10T12:00:00Z.
  • GET /notifications/threads/{id} gives one thread, and PATCH marks it as read (205). PUT /notifications marks each notification as read.

Some notifications are on the page of your notifications only:

  • a notification with no repository, such as one of an organization membership or a message to the superadmin;
  • an import, and a grant of access to a repository, which GitHub has no thread for;
  • a notification of a repository that you can no longer read. The invitee of an open invitation can read the thread of the invitation.

Contributors

As on GitHub, GET /repos/{owner}/{repo}/contributors lists the authors of the commits of the default branch, most commits first. It reads at most 10,000 commits.

  • An author is an account when the email of the commit is the noreply address of the account (LOGIN@users.noreply.HOST). The commits that Githome makes carry it. The email of an account names no contributor, because Githome tells no one who an address belongs to.
  • A person or an agent is a contributor. An organization, a deleted account and a suspended account are not.
  • As on GitHub, only the first 500 authors can name an account. The noreply addresses of one account count as one contributor.
  • With anon=1 or anon=true, an author with no account is in the list too, with its email, its name and the type Anonymous.
  • An empty repository answers 204.

Forks

As on GitHub, a person who can read a repository forks it into their own account or into an organization they may create repositories in:

  • POST /repos/{owner}/{repo}/forks (what gh repo fork sends) answers 202 with the fork, its parent and its source. It takes organization, name and default_branch_only. The fork keeps the visibility, the description and the default branch of its parent, and holds its own copy of each object of the branches and the tags, so it stands without its parent. The fork of a private repository is private, and only the people who can read the fork see it. A fork that the account holds already is given again to a person who can read it, and copies again when its copy was cut off. A copy that fails deletes the new fork (422). A taken name gets the first free -1, -2, and so on. A repository is not forked into its own owner (422), and a person whom the owner blocked forks none of its repositories (403).
  • GET /repos/{owner}/{repo}/forks lists the forks you can see, newest first, or by sort (oldest, stargazers, watchers), at most 1,000.
  • GET /repos/{owner}/{repo} of a fork gives its parent and its source (the root of its network) when you can read them.
  • POST /repos/{owner}/{repo}/merge-upstream with branch (what gh repo sync sends for a fork on the server) brings a branch of a fork up to the same branch of its parent. It needs write access to the fork. merge_type is fast-forward, or none when the branch holds the upstream already. A branch and an upstream that both have commits of their own answer 409 with the branch and the upstream OWNER:BRANCH have diverged: Githome makes no merge commit, so merge the upstream locally.

A pull request from a fork has a head of another owner: POST /repos/{owner}/{repo}/pulls with head: "OWNER:BRANCH", from a repository of the network of the base. As on GitHub, you need write access to the head repository and read access to the base. A pull request between two branches of the base needs write access to the base. A head outside the network, or a head repository that you cannot write to, answers 422 Validation Failed on the field head. The base keeps the commits of the head under refs/pull/{n}/head, head.repo and head.label name the fork, and each push to the branch of the fork moves the pull request while its author can still read the fork. PUT /repos/{owner}/{repo}/pulls/{n}/update-branch of a pull request from a fork answers 422: update the branch in the fork.

With maintainer_can_modify (true unless you send false), the people who may write to the base may git push to the head branch of the fork while the pull request is open. As on GitHub, a fork that an organization owns allows no maintainer edits, so the field is false there. The push goes to that branch alone, never deletes it, and never adds or changes a workflow file. It needs a token with the write scope that is not limited to repositories. There is no grant for an archived fork, for a person whom the owner of the fork blocked, or when the author of the pull request can no longer write to the fork. Only the author changes the field, with PATCH /repos/{owner}/{repo}/pulls/{n} (403 for another person).

Deleting a repository leaves its forks standing with no parent.

Your invitations and organization memberships

As on GitHub, a person invited by username accepts or declines the invitation:

  • GET /user/repository_invitations lists your pending repository invitations, those sent to your username and those sent to your email address. PATCH /user/repository_invitations/{id} accepts one and DELETE declines it (both 204).
  • GET /user/memberships/orgs lists your organization memberships, pending invitations first (?state=active or ?state=pending for one kind), and GET /user/memberships/orgs/{org} returns one. Each has state (active or pending), role (admin or member; owners count as admin, as on GitHub), githome_role (owner, admin or member), organization and user.
  • PATCH /user/memberships/orgs/{org} with {"state": "active"} accepts a pending invitation to the organization and returns the membership.

An unknown id, or someone else's invitation, answers 404 no pending invitation with this id. See Invitations you receive.

Contents

  • GET /contents/{path} returns a file (base64, with sha), a directory listing, or a symlink (with target). ref picks the branch, tag or commit (default: the default branch). Accept: application/vnd.github.raw returns the bytes.
  • PUT /contents/{path} creates or updates one file with a commit: message, base64 content, optional branch, author and committer. Updating an existing file needs its current blob sha, as on GitHub: without it you get 422 "sha" wasn't supplied, and a stale one gets 409 <path> does not match <sha>. It returns 201 for a new file and 200 for an update.
  • DELETE /contents/{path} removes a file with a commit; sha is required.
  • Every write refuses unsafe tree entry names: ., .., .git in any case and spellings that some systems treat as .git (such as .git. or git~1), and names with \ or NUL. Push protection scans new file content as it does for git push, here and in POST /repos/{owner}/{repo}/githome/commits: content that looks like a credential is refused with 422 push protection: possible secret detected (...). Only the multi-file commit endpoint takes a bypass: a repository admin who is a person sends "allow_secrets": "<reason>", which is audited as api.secret_bypassed and emailed to the repository's admins.

Git data

  • Blobs (POST /git/blobs, GET /git/blobs/{sha}), trees (POST /git/trees with base_tree, and entries by sha or inline content; GET /git/trees/{sha} with recursive=1, cut at 100,000 entries with truncated: true), commits (POST /git/commits with author, committer and signature; GET), annotated tags (POST /git/tags, GET).
  • Refs: GET /git/ref/{ref}, GET /git/refs, GET /git/matching-refs/{prefix}, POST /git/refs, PATCH /git/refs/{ref} (with force), DELETE /git/refs/{ref}. Ref updates follow branch protection and the agent guardrails, like a push.
  • One tree request can change at most 10,000 entries.

Archives

GET /repos/{owner}/{repo}/zipball/{ref} and /tarball/{ref} return a source archive of a branch, tag or commit (the default branch without {ref}), with a top directory named <owner>-<repo>-<short sha>/, like GitHub's. Browsers can use https://git.pkgforge.dev/<owner>/<repo>/archive/<ref>.zip or .tar.gz (also refs/heads/<branch>.zip). Releases and tags link to these as zipball_url and tarball_url.

  • Archives are built on demand the first time a commit is asked for, then cached in R2. The cache can be evicted at any time; the archive is then built again on the next download.
  • An archive holds at most 24 MB of file content and 100,000 tree entries. Bigger repositories fail with 413 repository is too large for an on-the-fly archive; clone it with git instead.
  • A zip holds at most 65,535 files (413 this repository has too many files for a zip archive (more than 65,535); download the .tar.gz archive or clone it with git). Use the tarball for more.
  • Each repository can build 20 new archives per hour. Downloads of cached archives don't count. Past that you get 429.
  • Entries with unsafe names (see Contents) are left out, and submodules appear as empty directories.

Clone bigger repositories with git.

Releases

  • List (newest first), get by id, get by tag (/releases/tags/{tag}), latest (/releases/latest), create, update and delete. Drafts are visible to writers only: readers get 404 for them and don't see them in lists.
  • Publishing a release whose tag_name doesn't exist creates the tag at target_commitish (default: the default branch); a draft creates its tag when it is published. Ref rules apply to that tag like any other ref update. Only one published release can exist per tag.
  • make_latest (true, false or legacy), prerelease, draft, generate_release_notes. The body can be up to 125,000 characters.
  • POST /releases/generate-notes drafts notes from the pull requests merged since the previous release and honours .github/release.yml (or configuration_file_path). It creates nothing.
  • Releases send the release webhook event for every change, and start on: release workflows (filter with types:, such as published, created, edited or deleted). Drafts start no workflows until they are published.
  • Reactions work on releases too.

See Releases for the web UI and the full model.

Release assets

Upload an asset to the release's upload_url, which looks like https://git.pkgforge.dev/api/uploads/repos/acme/website/releases/42/assets{?name,label}. Send the file as the raw request body, with its content type, and the file name in the name query parameter:

curl -X POST "https://git.pkgforge.dev/api/uploads/repos/acme/website/releases/42/assets?name=website-1.4.0.tar.gz&label=Source" \
  -H "Authorization: Bearer ghome_..." \
  -H "Content-Type: application/gzip" \
  --data-binary @website-1.4.0.tar.gz
  • An asset can be up to 100 MB, the largest request body Cloudflare Workers accept (413 release assets are limited to 100 MB). The bytes are stored in R2.
  • Names are cleaned like GitHub does: characters other than letters, digits and ._+@~- become . (so tool linux.bin is stored as tool.linux.bin), and a name must be 1 to 255 characters. Each name can appear once per release.
  • GET /releases/{id}/assets lists assets; GET, PATCH (name, label) and DELETE /releases/assets/{id} manage one.
  • Download an asset from its browser_download_url (https://git.pkgforge.dev/<owner>/<repo>/releases/download/<tag>/<name>), or from GET /releases/assets/{id} with Accept: application/octet-stream. Both count toward download_count. Assets of private repositories need a token.

Commit statuses, checks and required checks

  • Commit statuses: POST /statuses/{sha} (state of error, failure, pending or success, context, description, target_url), GET /commits/{ref}/statuses (also /statuses/{ref}) and the combined status GET /commits/{ref}/status. A status counts as a check, so a branch protection's required checks can name a status context. context defaults to default. Each commit can have up to 1,000 statuses per context.
  • Check runs: create (POST /check-runs), update (PATCH /check-runs/{id}), get, list for a ref (GET /commits/{ref}/check-runs) and annotations (GET /check-runs/{id}/annotations).
  • Check suites: GET /commits/{ref}/check-suites, GET /check-suites/{id} and its check runs. Each workflow run is a suite, and checks from other reporters are grouped into one suite per reporter.
  • Who decides a required check: when Githome Actions reports a check, it alone decides that check. To have a required check decided by someone else, bind it in branch protection, for example "requiredCheckSources": { "ci/external": "ci-bot" } (or githome-actions). A required check fails if any counted report of it fails. Only the account that created a check run can update it, and Actions check runs can't be changed over REST (403 check runs from Githome Actions can't be changed over the API). Statuses and check runs reported by agents never satisfy a required check.

See Checks and commit statuses and Branch protection.

Issues and pull requests

  • Issues: list, create (labels and assignees from someone without triage access are dropped without an error, as on GitHub), get, update (including state_reason; labels and assignees from someone without triage access are dropped here too), labels (create, get, update, delete; add, set, remove on an issue), assignees (add, remove, GET /assignees, GET /assignees/{login}), lock and unlock (PUT and DELETE /issues/{n}/lock, triage), comments (per issue and repository-wide GET /issues/comments; get, edit and delete one), events (GET /issues/{n}/events, /issues/events, /issues/events/{id}) and the timeline (GET /issues/{n}/timeline).
  • Pull requests: list (with the filters in Pagination), create, get, update, files, commits, merge (PUT /pulls/{n}/merge with merge_method, commit_title, commit_message, sha), auto-merge (PUT and DELETE /pulls/{n}/auto-merge), reviews (list, get, submit with line comments, pending reviews), review comments (list per pull request and repository-wide, get, edit, delete, create, and replies with POST /pulls/{n}/comments/{id}/replies or in_reply_to), requested reviewers (list, add, remove), review dismissal and update-branch.
  • GET /pulls/{n} reports mergeable, mergeable_state (clean, blocked, dirty, unknown and so on) and rebaseable, plus Githome's merge_blockers.
  • The diff media type (Accept: application/vnd.github.diff, what gh pr diff sends) of GET /pulls/{n}, GET /commits/{ref} and GET /compare/{base}...{head}, and the addresses of diff_url and patch_url (/{owner}/{repo}/pull/{n}.diff and .patch, /{owner}/{repo}/compare/{base}...{head}.diff and .patch) answer the changes as one diff in the form of git: the mode and index lines, the names that git quotes, a change of type as a deletion and an addition, and a submodule as its commit. git apply takes it, except a binary file, as with GitHub's diff, and a file that is no UTF-8 text is written as binary. The patch media type and .patch give the same diff, not a mailbox of each commit. A diff of more than 300 files, or with a file too large to diff, answers 406 with GitHub's form of error (too_large). The patch of each file of a pull request, a commit or a comparison holds its hunks alone, with no newline at its end, and a file with no hunk (a binary or an empty file, or a change of mode alone) has none.
  • Auto-merge: GitHub turns auto-merge on and off only through GraphQL; Githome has PUT /pulls/{n}/auto-merge (optional merge_method, commit_title, commit_message) and DELETE /pulls/{n}/auto-merge. PUT answers {"state": "enabled" | "queued" | "merged", "auto_merge": {...}, "merge_commit_sha": ...}, and pull request objects carry GitHub's auto_merge object (enabled_by, merge_method, commit_title, commit_message) while it is on. Agents and workflow tokens can use it on the default branch or a protected branch only where the rule sets allowAgentAutoMerge. See Auto-merge.
  • Drafts: PATCH /pulls/{n} with "draft": false marks a draft ready for review, and "draft": true converts it back (a Githome addition; GitHub only offers this through GraphQL). The pull request's author or anyone with write access can do it, only while it is open (422 the pull request is not open; others get 403 only the author or someone with write access can change draft status). It records a ready_for_review or convert_to_draft event on the timeline (GitHub's names) and sends a pull_request webhook with the action ready_for_review or converted_to_draft. The web UI's Ready for review button and githome pr ready do the same.
  • Merging past branch rules: a merge that branch protection blocks fails with 405 pull request is not mergeable: .... A repository admin who is a person can merge anyway by sending "bypass_rules": true with PUT /pulls/{n}/merge, unless the rule turns on Do not allow administrators to bypass these rules (enforceAdmins), and only when the pull request has no conflicts and isn't a draft. Without the flag, admins are blocked like everyone else, and the error ends with (as an admin you can bypass with bypass_rules). Each flag covers only what it names: "bypass_rules": true skips unmet rules (and linear history), and "bypass_merge_queue": true merges past a required merge queue but not past unmet rules. A merge that bypassed rules, linear history or the queue is recorded in the audit log as pull_request.merge_bypassed. githome pr merge --admin sends both flags. Agents and workflow tokens can't bypass.
  • Linear history: when the base branch's rule requires linear history, merge_method: "merge" is refused with 405 <branch> requires linear history; use squash or rebase (unless an admin sends bypass_rules), and a merge without merge_method uses squash.
  • Workflow tokens and reviews: a workflow's GITHUB_TOKEN can open pull requests and approve or request changes only when the repository allows it (see Workflow job tokens), and only people's approvals count toward required approvals.
  • Changing an issue's or pull request's title or body sends an issues or pull_request webhook with the action edited.
  • PUT /pulls/{n}/update-branch merges the base branch into the head branch (optional expected_head_sha) and returns 202.
  • Pending reviews: POST /pulls/{n}/reviews with no event makes a pending review, as on GitHub. You alone see it and its comments until you submit it with POST /pulls/{n}/reviews/{id}/events and an event. DELETE /pulls/{n}/reviews/{id} deletes it with its comments. You have one pending review on a pull request at a time.
  • Ranges of lines: a review comment takes start_line and start_side with line and side, on the lines from start_line to line.
  • Threads: a person with triage access or the author of the pull request resolves a thread, on the page of the pull request or with resolveReviewThread over GraphQL. A comment with a block marked suggestion has a button that commits the block over its lines to the head branch, for a person with write access. The commit names the author of the suggestion as a co-author, and the thread resolves.
  • PUT /pulls/{n}/reviews/{id}/dismissals with a message dismisses an approving or change-requesting review. It takes maintain access; the pull request's author can't dismiss reviews on it, and agents can't dismiss reviews at all.
  • Locked conversations refuse new comments, review comments and reactions from people without triage access.
  • Reactions (+1, -1, laugh, confused, heart, hooray, rocket, eyes) on issues and pull requests, issue comments, review comments and releases: list (filter with content), add (201, or 200 if you had already reacted) and delete by id.
  • Comment, review, review comment, event and reaction IDs are numbers.
  • Team reviewers (team_reviewers) are refused with 422 team reviewers are not supported; request people by login.
  • Milestones: GET /milestones (state open, closed or all), GET /milestones/{n}, POST /milestones and PATCH /milestones/{n} (triage). There is no DELETE. Set an issue's or pull request's milestone with PATCH /issues/{n} and "milestone": <number> or null (triage); issues and pull requests carry a milestone object.

Moderation

As on GitHub, a person or an organization's owners block people, people report abuse, and the superadmin suspends accounts, in the dash. See Blocking and reporting and Site administration.

  • Blocks: GET /user/blocks, and GET, PUT, DELETE /user/blocks/{username}; GET /orgs/{org}/blocks, and GET, PUT, DELETE /orgs/{org}/blocks/{username} (organization owners). GET on one answers 204 if blocked and 404 if not. In the blocker's repositories, a blocked person (or their agent) gets 403 you can't open issues or pull requests, comment, review or react in <owner>'s repositories: <owner> blocked you for new issues, pull requests, comments, reviews and reactions, and adding them as a collaborator fails with 422. Starring and watching get 403 you can't star <owner>'s repositories: <owner> blocked you and 403 you can't watch <owner>'s repositories: <owner> blocked you, and a block removes their stars and watches of the blocker's repositories.
  • Reports: POST /githome/reports (people, write scope; at most 20 a day). GET /admin/reports, GET /admin/reports/{id} and PATCH /admin/reports/{id} moved to the dash: each answers 403.
  • Suspensions: PUT /users/{username}/suspended and DELETE /users/{username}/suspended (GitHub Enterprise's endpoints) moved to the dash: each answers 403, and a report of the dash suspends a person. While suspended, the person's tokens and their agents' get 401, and their profile, repositories, issues and pull requests answer 404 to everyone. Their comments and reviews are left out of lists.
  • Hidden comments: PUT /repos/{owner}/{repo}/githome/comments/{id}/minimized with reason (spam, abuse, off_topic, outdated, duplicate or resolved) and DELETE on the same path (write access, people only), GitHub's GraphQL minimizeComment and unminimizeComment as REST. See Hide a comment.

Deployments and environments

  • Deployments: create, list, get and delete; deployment statuses: create, list and get. A deployment without required_contexts needs every check and status on the commit to have passed, as on GitHub (those reported by agents are not counted); send "required_contexts": [] to skip the check. A deployment whose contexts haven't passed is refused with 409.
  • Environments: list, get, create or update (PUT /environments/{name}), delete; deployment branch policies (protected_branches or custom_branch_policies with branch and tag patterns); environment secrets (names only, never values) and variables. Changing environments, their branch policies and secrets takes admin access, and agents can't do it. Required reviewers (people) and wait timers hold jobs before they start; pending deployments (GET and POST /actions/runs/{run_id}/pending_deployments) and GET /actions/runs/{run_id}/approvals work as on GitHub.
  • An environment with no branch policy is limited to protected branches when its first secret is added. Environment secrets never go to refs/pull/* runs. Runs started by agents get them only when the environment allows protected branches only. Tags pushed by agents never match an environment's tag patterns.
  • A workflow job with environment: creates a deployment, gets that environment's secrets and variables, and is refused if the environment's branch policy doesn't allow the ref.

See Environments.

Actions

The forge CI of git.pkgforge.dev is dormant: no run starts there now, and a dispatch is refused with "CI is off on this deployment". The endpoints below describe the Actions code that stays in githome.

  • Workflows (list, get by path or file name, POST /actions/workflows/{id}/dispatches), runs (list, per workflow, get, attempts GET /actions/runs/{id}/attempts/{n}, cancel, rerun and rerun-failed-jobs), jobs (per run, get, POST /actions/jobs/{id}/rerun), logs (a zip per run, or text per job).
  • Artifacts: GET /actions/artifacts (every artifact, newest first, name filter), GET /actions/runs/{id}/artifacts (one run; a run number covers all its attempts), GET and DELETE /actions/artifacts/{id}, and GET /actions/artifacts/{id}/zip, which redirects (302) to a signed URL valid for 10 minutes. Artifacts have numeric IDs, digest, expires_at and expired. An artifact past its retention (1 to 90 days, from retention-days) loses its content but stays listed with expired: true for 90 more days; downloading it gets 410 Artifact has expired. Deleting needs write access and is audited as actions.artifact.delete.
  • Caches: GET /actions/caches (filter with key prefix and ref, sort by last_accessed_at, created_at or size_in_bytes), DELETE /actions/caches?key=<key>[&ref=<ref>] (returns the deleted entries), DELETE /actions/caches/{id}, and GET /actions/cache/usage (active_caches_size_in_bytes, active_caches_count). Each entry has its ref (like refs/heads/main), key and version. Deleting needs write access and is audited as actions.cache.delete. See Caching and artifacts and Limits.
  • Workflow permissions: GET and PUT /actions/permissions/workflow read and change default_workflow_permissions (read or write) and can_approve_pull_request_reviews. Admins only, people only; each change is audited as repo.actions_workflow_settings.
  • Repository secrets (list names, set with value, delete; admin, people only), organization secrets (organization owners and admins, people only), repository variables (GET, POST, PATCH, PUT, DELETE; reading needs write access, changing needs admin), organization variables (GET, POST, PATCH, DELETE /orgs/{org}/actions/variables; organization owners and admins, people only), and environment secrets and variables.
  • Organization variables reach every repository of the organization as vars.NAME; a repository variable with the same name wins. GET /repos/{owner}/{repo}/actions/variables lists only the repository's own. Unlike GitHub, POST on an existing organization variable replaces its value instead of answering 409, PATCH can't rename, and visibility is always all. Changes are audited as org.variable_set and org.variable_delete.
  • Changing repository secrets and variables, environment variables and organization secrets is recorded in the audit log (repo.secret.set, repo.variable.set, environment.variable.set, org.secret.set and the matching .delete actions). Renaming a variable onto a name that exists fails with 409 Already exists - Variable already exists.
  • Secrets are sent as plain value over HTTPS and encrypted at rest. GitHub's sealed-box encrypted_value is not supported yet: 422 encrypted_value (sealed box) is not supported yet: send the secret as value over HTTPS; it is encrypted at rest.

Webhooks

List, create (config.url must use https, events defaults to all) and delete repository webhooks (admin; creating and deleting are audited as repo.webhook.create and repo.webhook.delete). Webhooks get the release, status, deployment, deployment_status and workflow_dispatch events as well as pushes, pull requests, issues and the rest, including the edited, ready_for_review and converted_to_draft actions. A delivery that fails with a network error or timeout, 408, 429 or a 5xx status is retried up to five times (after 1 minute, 5 minutes, 30 minutes, 1 hour and 3 hours) with the same X-GitHub-Delivery ID. See Webhooks and Webhook events.

Workflows started by API calls

  • A release starts on: release workflows (with types: matching the action, such as published). Drafts start nothing until they are published.
  • POST /repos/{owner}/{repo}/dispatches starts on: repository_dispatch workflows (with types: matching event_type), with client_payload in github.event.client_payload.
  • POST /actions/workflows/{id}/dispatches starts a workflow_dispatch run.
  • As on GitHub, events caused by a job's GITHUB_TOKEN don't start new workflow runs, except workflow_dispatch and repository_dispatch.

See Events that trigger workflows.

GET /search/issues searches issues and pull requests, and GET /search/repositories searches repositories, with GitHub's query syntax in q:

curl -sS -G https://git.pkgforge.dev/api/v3/search/issues \
  -H "Authorization: Bearer ghome_..." \
  --data-urlencode 'q=repo:acme/website is:pr is:open label:bug "contact sales"' \
  --data-urlencode 'sort=updated'

The response is {"total_count": ..., "incomplete_results": false, "items": [...]}; items are issue or repository objects with score: 1.

Search only covers repositories you can read. Anonymous callers search public repositories, and a workflow job's token searches its own repository.

QualifierMeaning
repo:owner/nameOnly this repository (repeat it for several)
user:, org:, owner:Only repositories of this account
is:issue, is:pr (also type:)Issues or pull requests
is:open, is:closed (also state:)By state
is:merged, is:unmergedMerged or unmerged pull requests
is:draft, draft:true, draft:falseDraft pull requests
is:locked, is:unlockedLocked conversations
label:bugHas this label. label:a,b and repeated label: qualifiers require every label.
-label:wontfixDoesn't have this label
no:label, no:assigneeHas no labels, or no assignee
author:diffy, -author:diffyOpened, or not opened, by this login
assignee:diffyAssigned to this login
head:branch, base:branchPull requests from or into this branch
in:title, in:body, in:commentsWhere words must match (default: title and body); combine as in:title,body
created:, updated:, closed:Dates: 2026-10-01, >2026-10-01, >=, <, <=, 2026-09-01..2026-09-30, 2026-09-01..*
sort:created-desccreated, updated or comments, with -asc or -desc. The sort and order query parameters override it.

Words without a qualifier must all appear (case-insensitive), "quoted phrases" match as written, and -word excludes items that contain the word. Unknown qualifiers are searched as text. Without a sort, results are newest first.

QualifierMeaning
user:, org:, owner:Repositories of this account
repo:owner/nameThis repository
topic:docsHas this topic (repeat for several)
is:public, is:privateBy visibility
archived:true, archived:falseArchived or not
in:name, in:description, in:topicsWhere words must match (default: all three)

Words match the name, owner/name, description and topics. sort=updated sorts by the last push; otherwise the most recently pushed repositories come first. The qualifiers fork:, language:, stars:, forks:, size:, pushed: and created: are accepted but ignored: Githome has no forks or language index, and search doesn't read stars.

Search limits

LimitValue
Query length256 characters (422 The search is longer than 256 characters.). An empty q returns 422 Validation Failed.
Results you can page toThe first 1,000 (422 Only the first 1000 search results are available)
Repositories one issue search looks at50 readable repositories (10 without signing in), taken from the up to 500 most recently pushed repositories you can see. When more matched, incomplete_results is true; narrow the search with repo: or org:.
Repositories one repository search checks300 candidates, from the same up to 500. More sets incomplete_results.
Issues scanned per repositoryThe 5,000 most recent issues and pull requests
Comments read for in:commentsThe 2,000 most recent comments per repository

Code search and commit search are not supported yet.

Not supported yet

  • GraphQL (/api/graphql) beyond repositories, pull requests and CI. See GraphQL API for what it answers.
  • Setting secrets with a sealed-box encrypted_value; send value instead.
  • Team reviewers and custom deployment protection rules on environments.
  • Team reviewers.
  • Apps: signing people in with an app's OAuth (user-to-server tokens), the app manifest flow, and GraphQL with installation tokens.
  • Code search and commit search. Search qualifiers for stars, forks and languages are accepted but ignored.

An Octokit method for an endpoint Githome doesn't have fails, usually with 404. Check the API reference before you reuse a GitHub script.

Githome-only endpoints

Endpoints that have no GitHub equivalent live under /api/v3/githome (or /repos/{owner}/{repo}/githome/... when they belong to a repository):

EndpointWhat it does
GET, POST /githome/tokens, DELETE /githome/tokens/{id}List, create and revoke your tokens. POST takes name, scopes (a non-empty list of read, write, admin; default ["write"]), repositories (["acme/website"], only repositories you can see) and expires_in_days, and returns the token once. Creating needs a browser session; revoking needs an admin token not restricted to repositories.
GET, POST /githome/agentsList the agents you or your organizations own, and create an agent (it comes with a first write token valid for 90 days unless you pass token). Creating needs a browser session.
POST /githome/agents/{login}/tokensCreate another token for an agent you own. Needs a browser session.
DELETE /githome/agents/{login}Disable an agent and revoke its tokens. Needs an admin token not restricted to repositories.
POST /repos/{owner}/{repo}/githome/commitsCommit several files at once without a git client: branch, message, optional base (creates the branch from it) and expected_head_sha (fails with 409 if the branch moved), and files (each with path, content, encoding, mode or delete). Push protection applies; a repository admin who is a person can pass allow_secrets with a reason. Needs write access.
POST /repos/{owner}/{repo}/githome/sandboxesStart a sandbox container with the repository checked out. Refused with "CI is off on this deployment" on git.pkgforge.dev. See Sandboxes.
POST /repos/{owner}/{repo}/githome/sandboxes/{id}/exec, /read, /write, /closeRun a command, read or write a file, or stop the sandbox.
GET, PUT /githome/notification-settingsYour notification settings. PUT is a partial update: send only the groups you change.
GET /githome/emailsEmails Githome sent you recently (per_page, kind, since). all=1 answers 403: the page Email of the dash shows the emails of each person.
POST /githome/emails/testSend yourself a test email now.
GET /githome/admin/email, POST /githome/admin/emails/samplesAnswer 403 to each caller: the page Email of the dash shows the email delivery status and sends the templates.
GET, POST /githome/imports, GET /githome/imports/{id}, POST /githome/imports/{id}/cancel, /retry, /selectionImport repositories from GitHub, follow and manage the imports. See Imports.
GET /githome/user/linked-accounts, POST /githome/user/linked-accounts/github/challenge, POST /githome/user/linked-accounts/github, DELETE /githome/user/linked-accounts/{id}Link your GitHub account (gist proof, no token), so imports attribute your GitHub work to you.
DELETE /githome/admin/linked-accounts/{id}Moved to the dash: it answers 403, and the page GitHub links of the dash replaces it.
POST /githome/reportsReport a person, repository, issue, pull request or comment to the superadmin. See Moderation.
PUT, DELETE /repos/{owner}/{repo}/githome/comments/{id}/minimizedHide (minimize) a comment with a reason, or show it again.
GET /githome/statusThe status page as JSON, shaped like Statuspage's summary: status, components, unresolved incidents and updated_at. No authentication needed.
GET, POST /githome/admin/status/incidents, POST /githome/admin/status/incidents/{id}/updatesMoved to the dash: each answers 403, and the page Status of the dash replaces them.

Other Githome additions sit next to the GitHub-shaped endpoints:

  • POST /api/v3/user/orgs creates an organization; POST /api/v3/orgs/{org}/invitations invites people by email, and POST /api/v3/invitations answers 403, because the instance invitation is in the dash. GET /api/v3/orgs/{org} and PATCH /api/v3/orgs/{org} (owners) follow GitHub: default_repository_permission is the organization's base permission (none, read, write or admin), shown to members and site administrators, and PATCH also takes the display name. See Base permission.
  • PUT /api/v3/user/avatar sets your profile picture from the raw image in the request body (JPEG, PNG or WebP, up to 5 MB and 3072×3072 pixels), and DELETE goes back to generated initials. PUT and DELETE /api/v3/orgs/{org}/avatar do the same for an organization (owners only). Both return {"avatar_url": ...}. GET /api/avatars/{login}?s=64|128|256 serves the picture to anyone, as WebP, or initials as SVG; it is the avatar_url of every user object.
  • PUT, DELETE /repos/{owner}/{repo}/pulls/{n}/merge-queue and GET /repos/{owner}/{repo}/merge-queue/{branch} drive the merge queue.
  • PUT /repos/{owner}/{repo}/pulls/{n}/auto-merge turns on auto-merge (GitHub's enablePullRequestAutoMerge, which it offers through GraphQL only) with merge_method, commit_title, commit_message, delete_branch and sha (409 if the head moved); DELETE turns it off (githome pr merge --auto and --disable-auto). It returns {"state": "enabled" | "queued" | "merged", "auto_merge": ..., "merge_commit_sha": ...}.
  • POST /repos/{owner}/{repo}/actions/runners/claim is how self-hosted runners pick up jobs, with the runner token they get from POST .../actions/runners/register and a registration token (POST .../actions/runners/registration-token). GET .../actions/runners and DELETE .../actions/runners/{id} list and remove them (see Runners).
  • GET /repos/{owner}/{repo}/raw/{ref}/{path} returns a file's raw bytes.
  • GET /repos/{owner}/{repo}/packages lists container images linked to a repository, and PATCH on a package changes its visibility or linked repository.
curl -X PUT https://git.pkgforge.dev/api/v3/user/avatar \
  -H "Authorization: Bearer ghome_..." -H "Content-Type: image/png" --data-binary @me.png

The API reference lists every endpoint with its request and response fields.

Imports

Githome imports repositories from GitHub.com or GitHub Enterprise Server on the server: Git history with LFS objects, issues and pull requests with their original numbers, comments, reviews, labels, milestones, branch protection, Actions variables, deployment environments and releases. You start an import with one request and poll it; nothing goes through your machine. The web form (Import from GitHub), githome migrate github and the MCP tool start_github_import use the same service. For the whole procedure, what is and isn't copied, and how people are attributed, read Migrate from GitHub.

# Import one repository into the acme organization
curl -sS -X POST https://git.pkgforge.dev/api/v3/githome/imports \
  -H "Authorization: Bearer ghome_..." -H "Content-Type: application/json" \
  -d '{"source": "diffy/hello-world", "owner": "acme", "name": "website", "github_token": "ghp_..."}'

# Follow it until status is succeeded, failed or cancelled
curl -sS https://git.pkgforge.dev/api/v3/githome/imports/<id> -H "Authorization: Bearer ghome_..."
EndpointWhat it does
POST /githome/importsStart an import: source (one repository) or source_owner (a GitHub organization or user, with optional repositories, exclude_forks, exclude_archived and concurrency), owner, and optional name, visibility (keep, private or public), github_token, github_api_url and options. A source of another forge takes forge, forge_url and forge_token (Import from GitLab, Forgejo or Gitea). Returns 201 with the import.
GET /githome/imports, GET /githome/imports/{id}Your imports, and one import with its status, per-step progress (steps), the Git transfer (git), summary (counts, secret names to set by hand, push protection findings), warnings, error and, for an organization import, children.
POST /githome/imports/{id}/cancelStop an import that hasn't finished.
POST /githome/imports/{id}/retryRun a failed or cancelled import again (202), optionally with a new github_token. Only whoever started it.
POST /githome/imports/{id}/selectionStart an organization import that was drafted in the web form, with the chosen repositories.

Things to know when you script imports:

  • Token: starting one needs a write or admin token (or a browser session). Old history with credentials in it is imported with an audited push protection bypass only when a person who administers the target started the import with an admin token or the web UI; otherwise the import fails and names the findings. See Push protection.
  • Agents can import only into an organization that owns them or that their owner owns or administers. Their imports never bypass push protection and skip Actions variables and environments.
  • Secret values are never read or copied. Only secret names are listed in summary, for a repository admin to set by hand.
  • Limits: one organization import and three repository imports at a time per account and per target (429), and more. See Limits.
  • The GitHub token is encrypted, used only by that import and deleted when it ends. It is never returned.

Every endpoint, with its fields and errors, is in the API reference.

Using the API

The examples below list open pull requests in acme/website and open a new issue. Replace ghome_... with your token.

curl

export GITHOME_TOKEN=ghome_...

# Who am I?
curl -sS https://git.pkgforge.dev/api/v3/user -H "Authorization: Bearer $GITHOME_TOKEN"

# Open pull requests
curl -sS "https://git.pkgforge.dev/api/v3/repos/acme/website/pulls?state=open" \
  -H "Authorization: Bearer $GITHOME_TOKEN"

# Open an issue
curl -sS -X POST https://git.pkgforge.dev/api/v3/repos/acme/website/issues \
  -H "Authorization: Bearer $GITHOME_TOKEN" \
  -H "content-type: application/json" \
  --data '{"title": "Broken link on the pricing page", "body": "The FAQ link returns 404.", "labels": ["bug"]}'

# Report an external CI result on a commit
curl -sS -X POST https://git.pkgforge.dev/api/v3/repos/acme/website/statuses/9be41d07 \
  -H "Authorization: Bearer $GITHOME_TOKEN" -H "content-type: application/json" \
  --data '{"state": "success", "context": "ci/external", "target_url": "https://ci.example.com/builds/812"}'

Add -f (--fail) in scripts so curl exits non-zero on 4xx and 5xx, or --fail-with-body to still print Githome's message.

To follow the Link header in a shell script:

url="https://git.pkgforge.dev/api/v3/repos/acme/website/issues?state=all&per_page=100"
while [ -n "$url" ]; do
  curl -fsS -D headers.txt "$url" -H "Authorization: Bearer $GITHOME_TOKEN" | jq -r '.[] | "#\(.number) \(.title)"'
  url=$(tr -d '\r' < headers.txt | sed -n 's/^link: .*<\([^>]*\)>; rel="next".*/\1/Ip')
done

fetch

From Node.js 26 or later, Deno, Bun or a Cloudflare Worker:

const base = "https://git.pkgforge.dev/api/v3";
const headers = { authorization: `Bearer ${process.env.GITHOME_TOKEN}`, "content-type": "application/json" };

async function api(method, path, body) {
  const res = await fetch(`${base}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined });
  const data = res.status === 204 ? null : await res.json().catch(() => null);
  if (!res.ok) throw new Error(`${method} ${path}: ${res.status} ${data?.message ?? res.statusText}`);
  return data;
}

const pulls = await api("GET", "/repos/acme/website/pulls?state=open");
for (const pr of pulls) console.log(`#${pr.number} ${pr.title} (${pr.head.ref})`);

const issue = await api("POST", "/repos/acme/website/issues", { title: "Broken link on the pricing page" });
console.log(issue.html_url);

Browsers on other websites can't call the API: Githome sends no CORS headers.

Octokit

Point Octokit at Githome with baseUrl. Octokit sends the token as Authorization: token ghome_..., which Githome accepts, and its pagination, errors and release uploads work as they do against GitHub:

import { readFile } from "node:fs/promises";
import { Octokit } from "@octokit/rest";

const octokit = new Octokit({ baseUrl: "https://git.pkgforge.dev/api/v3", auth: process.env.GITHOME_TOKEN });
const R = { owner: "acme", repo: "website" };

const { data: repo } = await octokit.rest.repos.get(R);
console.log(repo.default_branch, repo.html_url);

// Every open issue and pull request, following the Link header.
const all = await octokit.paginate(octokit.rest.issues.listForRepo, { ...R, state: "open", per_page: 100 });
console.log(`${all.length} open issues and pull requests`);

await octokit.rest.issues.createComment({ ...R, issue_number: 42, body: "Looks good to me." });
await octokit.rest.pulls.merge({ ...R, pull_number: 42, merge_method: "squash" });

// A release with an asset.
const { data: release } = await octokit.rest.repos.createRelease({ ...R, tag_name: "v1.4.0", generate_release_notes: true });
await octokit.rest.repos.uploadReleaseAsset({
  ...R,
  release_id: release.id,
  url: release.upload_url,
  name: "website-1.4.0.tar.gz",
  data: await readFile("website-1.4.0.tar.gz"),
  headers: { "content-type": "application/gzip" },
});

// Errors carry status, message and documentation_url.
try {
  await octokit.rest.repos.getContent({ ...R, path: "missing.txt" });
} catch (e) {
  console.log(e.status, e.response.data.message); // 404 Not Found
}

Things to know when you reuse GitHub code:

  • Use Personal access tokens or agent tokens from Githome, not GitHub tokens or GitHub App credentials.
  • Methods for features Githome doesn't have (see Not supported yet) fail, usually with 404 or 422.
  • octokit.paginate() doesn't work on listWorkflowRunsForWorkflow or on package versions, which send no Link header; page those with page.
  • Workflow run and job IDs are strings. TypeScript types say number; pass the value you got back unchanged.

The Githome CLI

The githome CLI wraps the API with commands for common tasks (githome pr list, githome issue create, githome run watch) and has a raw api command for anything else:

curl -fsSL https://git.pkgforge.dev/install | bash
githome login                    # in a browser; scripts use --token ghome_... or GITHOME_TOKEN

githome api METHOD PATH sends an authenticated request and prints the JSON response. PATH is relative to /api/v3. -f key=value adds a string field to the JSON body (or to the query string for GET), and --input FILE (or - for stdin) sends a JSON body from a file. It exits with status 1 on any 4xx or 5xx response.

githome api GET /repos/acme/website/pulls -f state=open
githome api POST /repos/acme/website/issues -f title="Broken link on the pricing page"
echo '{"event":"APPROVE","body":"Ship it"}' | githome api POST /repos/acme/website/pulls/42/reviews --input -

The CLI reads GITHOME_URL and GITHOME_TOKEN from the environment before its config file, which is handy in CI. See the CLI reference for every command.

Versioning

There is one version of the API, v3, named after the GitHub REST API version it follows. It is part of the path (/api/v3). Githome does not read GitHub's X-GitHub-Api-Version request header, so sending it is harmless; responses report X-GitHub-Api-Version-Selected: 2022-11-28. New endpoints and new response fields are added within v3; clients should ignore fields they don't know. The API reference and /docs/api/openapi.json are generated from the running code, so they always describe the version of Githome that serves them.

Troubleshooting

SymptomCause and fix
401 unrecognised token; create one under Settings → Access tokensThe client sent a GitHub token or another credential. Use a ghome_ token.
404 repository not found for a repository you expectThe token is restricted to other repositories, your role doesn't reach it, or (for an agent) its owner can't see it.
404 from an Octokit method that works on GitHubThe endpoint isn't implemented (the body is {"message": "Not Found", ...}). Check the API reference.
401 authentication required when opening an issue or commenting on a public repositoryAnonymous callers can only read. Send a token.
403 this token needs the write scopeThe token has only the read scope. Use a write token.
403 this token needs the admin scope or 403 this token is limited to repositories and can't change account settingsAccount-level endpoints (organizations, invitations, agents, organization secrets, revoking tokens) need an admin token without a repository list. See Token scopes.
405 pull request is not mergeable: ... (as an admin you can bypass with bypass_rules)Meet the requirements, or, as an admin, send "bypass_rules": true. bypass_merge_queue alone doesn't skip rules. The bypass is audited.
403 Resource not accessible by integration (...) from a workflowThe job's permissions: don't grant what the endpoint needs. Add the permission the message names, for example issues: write. See Workflow job tokens.
403 GitHub Actions is not permitted to create pull requests (or approve)An admin allows it with PUT /repos/{owner}/{repo}/actions/permissions/workflow and {"can_approve_pull_request_reviews": true}.
refusing to allow a GitHub App to create or update workflow ...A workflow's token can't change workflow files. Push that change with a personal or agent token.
410 Artifact has expiredThe artifact passed its retention. Upload it again with a longer retention-days (at most 90).
403 repository is archived (read-only)Unarchive the repository first (admin): PATCH /repos/{owner}/{repo} with {"archived": false}.
301 or 307 with {"message": "Moved Permanently", "url": ...}The repository was transferred or renamed. Follow Location (curl -L, Octokit follows it), and update the owner and name your client uses. See Transferred and renamed repositories.
409 <path> does not match <sha> from the contents APIThe file changed since you read it. Read it again and send the new sha.
422 push protection: possible secret detected (...)The content looks like a credential. Remove it; see Push protection.
413 or 429 from zipball or tarballThe repository is too big for an archive, or too many new archives were built this hour. Clone it with git.
413 release assets are limited to 100 MBSplit the file, or host it elsewhere and link to it from the release notes.
search returns incomplete_results: trueMore than 50 repositories matched. Add repo: or org: to the query.
A list stops after one pageThe client doesn't follow Link. Use octokit.paginate() or loop on rel="next".
429 too many repository imports are already running (at most 3 at a time); wait for one to finishYou, or the account you import into, already run three repository imports (or one organization import: an organization import is already in progress). Wait, or cancel one with POST /githome/imports/{id}/cancel.
422 github_api_url is required when a github_token is given and the source names a GitHub Enterprise hostSend github_api_url (such as https://ghe.example.com/api/v3) on the same host as the source. Githome never sends a token to a host taken only from the source URL.