---
name: forgejo-api
description: |
  Exhaustive Forgejo REST API skill covering all 473 endpoints across 25 categories.
  Use this skill whenever you need to interact with the Forgejo API via curl or need to
  understand Forgejo API behavior, parameters, response shapes, or error handling.

  Covers: authentication (tokens, basic auth, OAuth2, web login/CSRF), repositories
  (CRUD, settings, forks, deploy keys, topics, avatars, flags, issue config, transfers,
  mirrors, collaborators, stars/watchers), issues (CRUD, comments, reactions, attachments,
  dependencies, time tracking, stopwatches, pinning, subscriptions), pull requests
  (CRUD, reviews with inline comments, merging all 6 styles, automerge scheduling,
  server-side rebase without local clone, changed files, diff/patch), branches and tags
  (CRUD, rename, branch protections, tag protections), labels (repo-level, org-level,
  exclusive labels, label templates), milestones, organizations (CRUD, teams, members,
  quota, blocks), users (profile, SSH/GPG keys, API tokens with scopes, emails, quota,
  followers, settings), files and content (CRUD with SHA locking, raw, media, archives,
  diffpatch, multi-file commits), CI/CD actions (workflow runs, dispatch, secrets,
  variables, runner registration), commit statuses (quality gates, combined status,
  individual checks), webhooks (repo, org, user, system, git hooks, all event types),
  notifications (threads, filtering, pinning), releases (CRUD, asset upload), wiki,
  packages (20 package types), git objects (blobs, commits, trees, refs, notes),
  ActivityPub federation, miscellaneous (gitignore/license templates, markdown rendering,
  nodeinfo), activity feeds, admin operations (users, orgs, cron, quota, unadopted repos),
  web interface (CI log access via CSRF session), and 10 complex multi-step workflow
  recipes (PR review cycle, issue lifecycle, CI status check, release, branch protection
  setup, org setup, fork contribution, server-side rebase, automerge, tips & patterns).
references:
  - authentication
  - repositories
  - issues
  - pull-requests
  - branches-tags
  - labels
  - milestones
  - organizations
  - users
  - files-content
  - ci-actions
  - commit-statuses
  - notifications
  - releases
  - search
  - webhooks
  - settings-admin
  - wiki
  - packages
  - git-objects
  - activitypub
  - miscellaneous
  - activity-feeds
  - web-interface
  - complex-workflows
---

# Forgejo REST API Skill

Complete reference for the Forgejo REST API. **473 endpoints** across **25 categories**,
**77 reference files**, **149 distinct path parameter types**, all curl commands fully
parameterized and tested against a live Forgejo 14.0.4 server.

---

## 🌐 Environment Variables

This skill assumes the following shell variables are set:

| Variable | Purpose | Example |
|----------|---------|---------|
| `${FORGEJO_URL}` | Base server URL (no trailing slash) | `https://git.cleverthis.com` |
| `${FORGEJO_PAT}` | Personal Access Token (primary bot) | `abc123...` |
| `${FORGEJO_USERNAME}` | Username of the token owner | `HAL9000` |
| `${FORGEJO_PASSWORD}` | Password (basic auth, token mgmt only) | `secret` |
| `${FORGEJO_REVIEWER_PAT}` | PAT for reviewer bot (second identity) | `def456...` |
| `${FORGEJO_REVIEWER_USERNAME}` | Reviewer bot username | `reviewer-bot` |

**Validate your token is working:**
```bash
curl -s "${FORGEJO_URL}/api/v1/user" -H "Authorization: token ${FORGEJO_PAT}" | jq '.login'
```

---

## ⚡ Quick Answers (No Reference Load Needed)

### Issues & PRs — Most Common Operations

**Get a single issue or PR (with labels, milestone, assignees):**
```bash
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

**List open issues:**
```bash
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues?state=open&type=issues&limit=50" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

**List open PRs:**
```bash
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls?state=open&limit=50" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

**Get a PR's full state (mergeable, CI, labels, milestone, base/head SHAs):**
```bash
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

**Add a comment to an issue or PR:**
```bash
curl -s -X POST "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/comments" \
  -H "Authorization: token ${FORGEJO_PAT}" -H "Content-Type: application/json" \
  -d '{"body": "{comment_text}"}'
```

**List comments on an issue or PR:**
```bash
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/comments" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

**Close an issue or PR:**
```bash
curl -s -X PATCH "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}" \
  -H "Authorization: token ${FORGEJO_PAT}" -H "Content-Type: application/json" \
  -d '{"state": "closed"}'
```

**Edit issue or PR title/body/assignees/milestone:**
```bash
curl -s -X PATCH "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}" \
  -H "Authorization: token ${FORGEJO_PAT}" -H "Content-Type: application/json" \
  -d '{"title": "{new_title}", "milestone": {milestone_id}}'
```

**Edit a PR (update title, body, base branch, assignees, allow maintainer edit):**
```bash
curl -s -X PATCH "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}" \
  -H "Authorization: token ${FORGEJO_PAT}" -H "Content-Type: application/json" \
  -d '{"title": "{new_title}", "body": "{new_body}"}'
```

**Create an issue:**
```bash
curl -s -X POST "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues" \
  -H "Authorization: token ${FORGEJO_PAT}" -H "Content-Type: application/json" \
  -d '{"title": "{title}", "body": "{body}", "labels": [{label_id}], "milestone": {milestone_id}}'
```

**Create a pull request:**
```bash
curl -s -X POST "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls" \
  -H "Authorization: token ${FORGEJO_PAT}" -H "Content-Type: application/json" \
  -d '{"title": "{title}", "body": "{body}", "head": "{head_branch}", "base": "{base_branch}"}'
```

**List PR reviews:**
```bash
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

**Approve a PR:**
```bash
curl -s -X POST "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews" \
  -H "Authorization: token ${FORGEJO_PAT}" -H "Content-Type: application/json" \
  -d '{"event": "APPROVED", "body": "{review_comment}"}'
```

**Merge a PR:**
```bash
curl -s -X POST "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}/merge" \
  -H "Authorization: token ${FORGEJO_PAT}" -H "Content-Type: application/json" \
  -d '{"Do": "squash", "delete_branch_after_merge": true}'
```

**List files changed in a PR:**
```bash
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}/files" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

**Get PR diff:**
```bash
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}.diff" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

**Add issue dependency (this issue depends on another):**
```bash
curl -s -X POST "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/dependencies" \
  -H "Authorization: token ${FORGEJO_PAT}" -H "Content-Type: application/json" \
  -d '{"index": {dependency_index}}'
```

**List issue dependencies:**
```bash
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/dependencies" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

### Labels

**Get all repo labels (with IDs):**
```bash
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/labels?limit=50" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

**Get all org labels (with IDs):**
```bash
curl -s "${FORGEJO_URL}/api/v1/orgs/{org}/labels?limit=50" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

**Get labels currently on an issue or PR:**
```bash
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

**Apply labels to an issue or PR (replace all):**
```bash
curl -s -X PUT "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels" \
  -H "Authorization: token ${FORGEJO_PAT}" -H "Content-Type: application/json" \
  -d '{"labels": [{label_id_1}, {label_id_2}]}'
```

**Remove a single label by ID:**
```bash
curl -s -X DELETE "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels/{label_id}" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

### Milestones

**List all milestones:**
```bash
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/milestones?state=all&limit=50" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

**Get milestone completion percentage:**
```bash
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/milestones/{milestone_id}" \
  -H "Authorization: token ${FORGEJO_PAT}" \
  | jq '{title, state, open: .open_issues, closed: .closed_issues,
          pct: (if (.open_issues + .closed_issues) > 0
                then (.closed_issues * 100 / (.open_issues + .closed_issues) | floor)
                else 0 end)}'
```

### CI & Commits

**Get combined CI status for a branch or SHA:**
```bash
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/commits/{branch_or_sha}/status" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

**List workflow runs:**
```bash
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/actions/runs?limit=20" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

**Get a specific workflow run:**
```bash
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/actions/runs/{run_id}" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

**List recent commits on a branch:**
```bash
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/commits?sha={branch}&limit=20" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

### Files & Repos

**Get file content (returns base64, also gives current SHA for updates):**
```bash
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/contents/{filepath}?ref={branch}" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

**List branches:**
```bash
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/branches?limit=50" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

**List repo notifications:**
```bash
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/notifications?status-types=unread" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

---

## 🔑 Authentication Quick Reference

| Method | Header / Flag | When to use |
|--------|--------------|-------------|
| **Token (PAT)** | `-H "Authorization: token ${FORGEJO_PAT}"` | All REST API calls — primary method |
| **Basic Auth** | `-u "${FORGEJO_USERNAME}:${FORGEJO_PASSWORD}"` | **Only** for token CRUD (`/users/{username}/tokens`) |
| **OAuth2 Bearer** | `-H "Authorization: Bearer {access_token}"` | After OAuth2 authorization code flow |
| **Reviewer PAT** | `-H "Authorization: token ${FORGEJO_REVIEWER_PAT}"` | Approving PRs as a second identity |
| **Web Session** | Cookie-based CSRF flow | CI action logs only — not in REST API |

> **Multi-identity auth**: `${FORGEJO_REVIEWER_PAT}` / `${FORGEJO_REVIEWER_USERNAME}`
> are the second bot account. Use them when branch protection requires approval from
> someone other than the PR author. See `references/authentication/README.md`.

---

## 🚨 Critical Concepts (Always Keep in Mind)

### PRs Are Issues
Every pull request is also an issue. PR index = issue index — the same number.
All issue endpoints work on PRs: comments, labels, reactions, subscriptions, pins,
timeline, dependencies, time tracking.

```bash
# These are equivalent for a PR numbered {index}:
GET /repos/{owner}/{repo}/issues/{index}/comments   # ✓ works for PRs
GET /repos/{owner}/{repo}/issues/{index}/labels     # ✓ works for PRs
```

### Exclusive Labels
Labels with `exclusive: true` and a name containing `/` (e.g. `State/Open`,
`State/Closed`) are mutually exclusive within their prefix group. Applying one
automatically removes the others in the same group.

**Always use `PUT` (replace-all) not `POST` (add) when managing exclusive label
groups**, to avoid leaving conflicting labels behind.

### Mergeability Is Lazy-Computed
`PR.mergeable` is `null` immediately after a push — Forgejo hasn't computed it yet.
Poll until it is `true` or `false`. `true` = no conflicts, `false` = conflicts exist.

### Label Lookup: IDs and Names Both Work
`PUT/POST /issues/{index}/labels` accepts **either** integer IDs or string names:
```json
{"labels": [42, 7]}         // by ID (faster)
{"labels": ["bug", "State/Open"]}  // by name
```
Labels also come embedded in every issue/PR response — no separate fetch needed when
you've already loaded the issue.

### SHA Locking for File Updates
`PUT /repos/{owner}/{repo}/contents/{filepath}` requires `"sha": "{current_sha}"` in
the body — the SHA of the current version of the file. Fetch it first:
```bash
SHA=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/contents/{filepath}" \
  -H "Authorization: token ${FORGEJO_PAT}" | jq -r '.sha')
```
Then update with `"sha": "${SHA}"`. Returns 409 if the SHA is stale.

### Auto-Close Keywords in PR Bodies
If a PR body contains `Closes #N`, `Fixes #N`, or `Resolves #N` (one per line),
Forgejo automatically closes those issues when the PR is merged. Multiple issues
can be referenced. Example:
```
Closes #42
Closes #43
```

### Token Scopes
Tokens need explicit scopes at creation. The `"all"` scope grants full access.
Common scopes: `read:repository`, `write:repository`, `read:issue`, `write:issue`,
`read:user`, `write:user`, `read:organization`, `write:organization`.
The full token value (`sha1`) is only returned **once** at creation — save it.

### CI Logs Require Web Login
Workflow step logs are not available via REST API. Commit statuses (via
`GET /commits/{sha}/status`) contain `target_url` fields with links to run/job pages.
Access logs through those URLs using a web session.
→ `references/web-interface/ci-logs.md`

### Search Response Envelopes Differ
Different search endpoints return different shapes — always check:
```
GET /repos/search          → {"ok": true, "data": [...]}        use .data[]
GET /repos/issues/search   → [...]                               direct array
GET /users/search          → {"ok": true, "data": [...]}        use .data[]
GET /topics/search         → {"topics": [...]}                   use .topics[]
GET /orgs/teams/search     → [...]                               direct array
All other list endpoints   → [...]                               direct array
```

### Error Response Format
All API errors return:
```json
{"message": "error description", "url": "https://.../api/swagger", "errors": [...]}
```
Use `jq -r '.message'` to extract the error message. The `errors` array has details.

### Notification Parameters Are Query Strings
`PUT /notifications` uses **query parameters** (`?to-status=read&status-types=unread`),
not a JSON body. Both forms work in practice, but query params are canonical per spec.

### 412 Precondition Failed — Stale Edit Protection
When editing issues or PRs via PATCH, you can include `"updated_at": "{timestamp}"`
in the body. If the server's record is newer, it returns 412 to prevent overwriting
a concurrent edit. Omit `updated_at` to skip this check.

---

## 🔧 jq Cheat Sheet for Chaining API Calls

These patterns extract the values most commonly needed to chain one API call into
the next:

```bash
# From a PR response:
PR=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}" \
  -H "Authorization: token ${FORGEJO_PAT}")

HEAD_SHA=$(echo "$PR"      | jq -r '.head.sha')          # head commit SHA
HEAD_BRANCH=$(echo "$PR"   | jq -r '.head.ref')          # head branch name
BASE_BRANCH=$(echo "$PR"   | jq -r '.base.ref')          # base branch name
MERGE_BASE=$(echo "$PR"    | jq -r '.merge_base')        # common ancestor SHA
MERGEABLE=$(echo "$PR"     | jq -r '.mergeable')         # true/false/null
MERGED=$(echo "$PR"        | jq -r '.merged')            # true/false
STATE=$(echo "$PR"         | jq -r '.state')             # open/closed
PR_NUM=$(echo "$PR"        | jq -r '.number')            # PR index number
LABELS=$(echo "$PR"        | jq '[.labels[].id]')        # label IDs as array

# From a combined commit status response:
STATUS=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/commits/${HEAD_SHA}/status" \
  -H "Authorization: token ${FORGEJO_PAT}")

COMBINED_STATE=$(echo "$STATUS" | jq -r '.state')                    # success/failure/pending/error
TOTAL=$(echo "$STATUS"          | jq -r '.total_count')              # number of checks
FAILING=$(echo "$STATUS"        | jq -r '.statuses[] | select(.state=="failure") | .context')
TARGET_URLS=$(echo "$STATUS"    | jq -r '.statuses[].target_url')    # links to CI jobs

# From an issue response:
ISSUE=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}" \
  -H "Authorization: token ${FORGEJO_PAT}")

MILESTONE_ID=$(echo "$ISSUE"  | jq -r '.milestone.id // empty')
LABEL_IDS=$(echo "$ISSUE"     | jq '[.labels[].id]')
LABEL_NAMES=$(echo "$ISSUE"   | jq -r '[.labels[].name] | join(",")')
ASSIGNEES=$(echo "$ISSUE"     | jq -r '[.assignees[].login] | join(",")')

# Find label ID by name from a label list:
LABELS=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/labels?limit=50" \
  -H "Authorization: token ${FORGEJO_PAT}")
LABEL_ID=$(echo "$LABELS" | jq -r '.[] | select(.name == "{label_name}") | .id')

# Find milestone ID by title:
MILESTONES=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/milestones?state=all&limit=50" \
  -H "Authorization: token ${FORGEJO_PAT}")
MS_ID=$(echo "$MILESTONES" | jq -r '.[] | select(.title == "{milestone_title}") | .id')

# From a file content response:
FILE=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/contents/{filepath}" \
  -H "Authorization: token ${FORGEJO_PAT}")
FILE_SHA=$(echo "$FILE"     | jq -r '.sha')              # required for updates
CONTENT=$(echo "$FILE"      | jq -r '.content' | base64 -d)  # decoded content

# From a workflow run list (repos/search has .data wrapper):
RUNS=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/actions/runs?limit=10" \
  -H "Authorization: token ${FORGEJO_PAT}")
RUN_ID=$(echo "$RUNS"       | jq -r '.workflow_runs[0].id')
RUN_STATUS=$(echo "$RUNS"   | jq -r '.workflow_runs[0].status')  # waiting/running/success/failure

# From repos/search (note: wrapped in .data):
REPOS=$(curl -s "${FORGEJO_URL}/api/v1/repos/search?q={query}&limit=20" \
  -H "Authorization: token ${FORGEJO_PAT}")
REPO_NAMES=$(echo "$REPOS" | jq -r '.data[].full_name')

# Check HTTP status code:
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}/merge" \
  -H "Authorization: token ${FORGEJO_PAT}")
# 204 = PR is merged, 404 = PR not merged

# Handle errors gracefully:
RESPONSE=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}" \
  -H "Authorization: token ${FORGEJO_PAT}")
if echo "$RESPONSE" | jq -e '.message' > /dev/null 2>&1; then
  echo "Error: $(echo "$RESPONSE" | jq -r '.message')"
else
  # process successful response
  echo "$RESPONSE" | jq -r '.title'
fi
```

---

## 🗂️ Navigation Decision Trees

### "I need to work with pull requests"

```
Pull request operations?
├─ List/get PRs → GET /repos/{owner}/{repo}/pulls
│   ├─ Filter by author → ?poster={username}
│   ├─ Filter by label → ?labels={label_name}
│   ├─ Check if mergeable → PR.mergeable (null=computing, true=ok, false=conflicts)
│   ├─ Check if PR is stale → compare PR.merge_base vs current base branch HEAD SHA
│   └─ Full reference → references/pull-requests/README.md
├─ Review a PR
│   ├─ List all reviews → GET /pulls/{index}/reviews
│   ├─ Approve → POST /pulls/{index}/reviews {"event": "APPROVED"}
│   ├─ Request changes → POST /pulls/{index}/reviews {"event": "REQUEST_CHANGES"}
│   ├─ Add inline comments → POST /pulls/{index}/reviews with comments[{path,body,new_position}]
│   ├─ Request specific reviewers → POST /pulls/{index}/requested_reviewers
│   └─ Full guide → references/pull-requests/reviews.md
├─ Merge a PR
│   ├─ Styles: merge | rebase | rebase-merge | squash | fast-forward-only
│   ├─ Auto-merge when CI passes → body: {"merge_when_checks_succeed": true, "Do": "squash"}
│   ├─ Cancel auto-merge → DELETE /pulls/{index}/merge
│   ├─ Check if auto-merge is set → DELETE and check: 204=was set, 404=not set
│   └─ Full guide → references/pull-requests/merging.md
├─ Update stale PR branch (no local clone)
│   ├─ POST /pulls/{index}/update?style=rebase  (rebase head onto base)
│   ├─ POST /pulls/{index}/update?style=merge   (merge base into head)
│   └─ Full guide → references/complex-workflows/server-side-rebase.md
└─ Get changed files / diff / patch → references/pull-requests/files.md
```

### "I need to work with issues"

```
Issue operations?
├─ List/get/create/update/close → references/issues/README.md
│   ├─ Filter by label → ?labels={label_name}
│   ├─ Filter by milestone → ?milestones={milestone_name}
│   ├─ Filter by assignee → ?assigned_by={username}
│   ├─ Filter by author → ?created_by={username}
│   └─ Search text → ?q={search_term}
├─ Comments → references/issues/comments.md
│   └─ NOTE: Same endpoint for PRs — POST /issues/{index}/comments
├─ Labels → references/labels/issue-pr-labels.md
│   ├─ Read labels → GET /issues/{index}/labels  (also embedded in issue response)
│   ├─ Add labels → POST /issues/{index}/labels  {"labels": [{id},...]}
│   ├─ Replace all → PUT /issues/{index}/labels  {"labels": [{id},...]}
│   └─ Remove one → DELETE /issues/{index}/labels/{identifier}
├─ Dependencies (blocks/is-blocked-by) → references/issues/dependencies.md
├─ Reactions (emoji) → references/issues/reactions.md
├─ Attachments → references/issues/attachments.md
├─ Time tracking & stopwatches → references/issues/time-tracking.md
├─ Pinning → POST /issues/{index}/pin
└─ Full lifecycle cookbook → references/complex-workflows/issue-lifecycle.md
```

### "I need CI/CD information"

```
CI/CD operations?
├─ Check if a PR passes all quality gates
│   ├─ HEAD_SHA=$(PR | jq -r '.head.sha')
│   ├─ Combined state → GET /commits/{HEAD_SHA}/status → .state
│   │   (success=all pass, failure/error=blocked, pending=running, warning=caution)
│   ├─ Per-check details → GET /commits/{HEAD_SHA}/statuses → .state per .context
│   ├─ Find run → .target_url format: /{owner}/{repo}/actions/runs/{run_id}/jobs/{n}
│   └─ Full guide → references/complex-workflows/ci-status-check.md
├─ Get CI action run logs (requires web login!)
│   └─ references/web-interface/ci-logs.md
├─ List workflow runs → GET /repos/{owner}/{repo}/actions/runs
├─ Get specific run → GET /repos/{owner}/{repo}/actions/runs/{run_id}
├─ Trigger workflow manually → POST /actions/workflows/{workflow_file}/dispatches
├─ Manage secrets (repo/org/user) → references/ci-actions/secrets.md
├─ Manage variables (repo/org/user) → references/ci-actions/variables.md
└─ Register a runner → references/ci-actions/workflows.md
```

### "I need to manage labels"

```
Label operations?
├─ Repo labels (scoped to one repo)
│   ├─ List with IDs → GET /repos/{owner}/{repo}/labels?limit=50
│   └─ CRUD → references/labels/repo-labels.md
├─ Org labels (inherited by all repos in the org)
│   ├─ List with IDs → GET /orgs/{org}/labels?limit=50
│   └─ CRUD → references/labels/org-labels.md
├─ Apply/remove on issues or PRs → references/labels/issue-pr-labels.md
│   ├─ Labels embedded in issue/PR response — no extra call needed to READ
│   ├─ Dedicated GET → GET /issues/{index}/labels
│   └─ Apply by name or ID — both work
├─ Exclusive labels (mutually exclusive within prefix group)
│   ├─ E.g. "State/Open", "State/Closed" — only one active at a time
│   ├─ Use PUT (replace-all) to safely switch
│   └─ references/labels/README.md
└─ Built-in template sets → references/labels/label-templates.md
```

### "I need to manage milestones"

```
Milestone operations?
├─ List milestones (open/closed/all) → GET /repos/{owner}/{repo}/milestones?state=all
├─ Get single milestone → GET /repos/{owner}/{repo}/milestones/{milestone_id}
│   └─ Completion: closed_issues / (open_issues + closed_issues) * 100
├─ Create → POST /repos/{owner}/{repo}/milestones {"title", "due_on", "description"}
├─ Close (mark done) → PATCH /repos/{owner}/{repo}/milestones/{id} {"state": "closed"}
├─ Delete → DELETE /repos/{owner}/{repo}/milestones/{id}
├─ Search by name → GET /milestones?q={title_fragment}  (query param: name)
└─ Full reference → references/milestones/README.md
```

### "I need to manage branches"

```
Branch operations?
├─ List/create/get/delete → references/branches-tags/branches.md
│   └─ Create from SHA → body: {"new_branch_name": "{name}", "old_ref_name": "{sha}"}
├─ Rename a branch → PATCH /repos/{owner}/{repo}/branches/{branch} {"name": "{new}"}
├─ Branch protection rules
│   ├─ Require N approvals, specific CI checks, signed commits, push restrictions
│   ├─ CRUD → GET/POST/PATCH/DELETE /repos/{owner}/{repo}/branch_protections
│   └─ references/branches-tags/protections.md
├─ Tag CRUD → references/branches-tags/tags.md
├─ Tag protection rules → references/branches-tags/protections.md
└─ Server-side PR rebase → references/complex-workflows/server-side-rebase.md
```

### "I need to manage releases"

```
Release operations?
├─ List releases → GET /repos/{owner}/{repo}/releases?limit=20
├─ Get latest → GET /repos/{owner}/{repo}/releases/latest
├─ Get by tag → GET /repos/{owner}/{repo}/releases/tags/{tag}
├─ Create release
│   └─ POST /repos/{owner}/{repo}/releases
│      {"tag_name": "{tag}", "target_commitish": "{branch_or_sha}",
│       "name": "{title}", "body": "{changelog}", "draft": false, "prerelease": false}
├─ Upload asset to release
│   └─ POST /repos/{owner}/{repo}/releases/{release_id}/assets?name={filename}
│      -F "attachment=@{/path/to/file}"
├─ Edit/delete release → PATCH/DELETE /repos/{owner}/{repo}/releases/{id}
└─ Full reference + release workflow → references/releases/README.md
                                       references/complex-workflows/release-workflow.md
```

### "I need to search or discover things"

```
Search operations?
├─ Search repos → GET /repos/search?q={term}&sort=updated&order=desc
│   Response: {"ok": true, "data": [...]}  ← note: wrapped in .data
├─ Search issues/PRs across all repos → GET /repos/issues/search?q={term}&state=open
│   Response: [...] (direct array)
│   Extra filters: ?labels=, ?milestones=, ?type=pulls, ?review_requested=true
├─ Search users → GET /users/search?q={term}
│   Response: {"ok": true, "data": [...]}  ← note: wrapped in .data
├─ Search topics → GET /topics/search?q={term}
│   Response: {"topics": [...]}  ← note: wrapped in .topics
├─ Search team → GET /orgs/{org}/teams/search?q={term}
│   Response: [...] (direct array)
└─ Full reference → references/search/README.md
```

### "I need to work with organizations"

```
Organization operations?
├─ Org CRUD, rename, avatar, block users → references/organizations/README.md
├─ Team management (units, permissions, repos, members) → references/organizations/teams.md
├─ Member management → references/organizations/members.md
├─ Org secrets/variables → references/ci-actions/secrets.md / variables.md
├─ Org webhooks → references/webhooks/README.md
├─ Org labels → references/labels/org-labels.md
├─ Org quota → references/users/quota.md
└─ Full org setup cookbook → references/complex-workflows/org-setup.md
```

### "I need to manage users"

```
User operations?
├─ Current user / search / heatmap → references/users/profile.md
├─ SSH keys / GPG keys → references/users/keys.md
├─ API tokens → references/users/tokens.md
│   └─ REQUIRES BASIC AUTH: -u "${FORGEJO_USERNAME}:${FORGEJO_PASSWORD}"
├─ Email management → references/users/emails.md
├─ Quota → references/users/quota.md
├─ Followers / following / teams → references/users/social.md
├─ User settings → references/users/settings.md
├─ OAuth2 apps → references/authentication/oauth2.md
└─ Admin: create/edit/delete users → references/settings-admin/README.md
```

### "I need to work with repositories"

```
Repository operations?
├─ CRUD, migration, create from template → references/repositories/README.md
│   └─ Migrate from GitHub/GitLab/Gitea/plain git (preserving issues, labels, etc.)
├─ Settings (merge styles, features, default branch) → references/repositories/settings.md
├─ Collaborators + team access → references/repositories/collaborators.md
├─ Deploy keys (SSH for CI/CD read/write) → references/repositories/deploy-keys.md
├─ Fork / sync with upstream → references/repositories/forks.md
├─ Push mirrors / transfer / convert fork → references/repositories/transfers.md
├─ Topics → references/repositories/topics.md
├─ Stars and watchers → references/repositories/stars-watchers.md
├─ Avatar → references/repositories/avatars.md
├─ Issue config and templates → references/repositories/issue-config.md
└─ Repository flags (admin) → references/repositories/flags.md
```

### "I need to read or write files"

```
File content operations?
├─ List directory → GET /repos/{owner}/{repo}/contents/{dirpath}?ref={branch}
├─ Get file (base64 encoded + SHA) → GET /repos/{owner}/{repo}/contents/{filepath}?ref={branch}
├─ Create file → POST /repos/{owner}/{repo}/contents/{filepath}
│   body: {"message": "{msg}", "content": "$(echo -n '{text}' | base64)", "branch": "{branch}"}
├─ Update file (SHA required!) → PUT /repos/{owner}/{repo}/contents/{filepath}
│   body: {"message": "{msg}", "content": "$(echo -n '{text}' | base64)",
│           "sha": "{current_sha}", "branch": "{branch}"}
├─ Delete file (SHA required) → DELETE /repos/{owner}/{repo}/contents/{filepath}
│   body: {"message": "{msg}", "sha": "{current_sha}"}
├─ Create multiple files in one commit → POST /repos/{owner}/{repo}/contents
│   body: {"files": [{"path": "{filepath}", "content": "{base64}", "operation": "create"},...]}
├─ Get raw file → GET /repos/{owner}/{repo}/raw/{filepath}?ref={branch}
├─ Download archive → GET /repos/{owner}/{repo}/archive/{branch}.tar.gz
└─ Full reference → references/files-content/
```

### "I need admin access"

```
Admin operations?
├─ List/create/edit/delete users → GET/POST/PATCH/DELETE /admin/users
├─ List all organizations → GET /admin/orgs
├─ Rename user → POST /admin/users/{username}/rename {"new_username": "{name}"}
├─ Create repo for user → POST /admin/users/{username}/repos
├─ Run cron task → POST /admin/cron/{task_name}
│   Common tasks: update_mirrors, repo_health_check
├─ Quota management (groups, rules, user assignments) → references/users/quota.md
├─ Unadopted repos → GET /admin/unadopted → adopt with POST, purge with DELETE
├─ System webhooks → GET/POST/PATCH/DELETE /admin/hooks
└─ Full reference → references/settings-admin/README.md
```

### "I need to manage notifications or webhooks"

```
Notifications?
├─ List unread → GET /notifications?status-types=unread
├─ List for a repo → GET /repos/{owner}/{repo}/notifications
├─ Mark all read → PUT /notifications?to-status=read        ← query param, not body!
├─ Mark thread read → PATCH /notifications/threads/{id}?to-status=read
├─ Pin thread → PATCH /notifications/threads/{id}?to-status=pinned
└─ Full reference → references/notifications/README.md

Webhooks?
├─ Repo hooks → GET/POST/PATCH/DELETE /repos/{owner}/{repo}/hooks
├─ Org hooks → GET/POST/PATCH/DELETE /orgs/{org}/hooks
├─ Test a hook → POST /repos/{owner}/{repo}/hooks/{hook_id}/tests
├─ Events: push, pull_request, issues, issue_comment, release, and 15+ more
└─ Full reference → references/webhooks/README.md
```

---

## 📏 API Conventions

### Pagination
All list endpoints support `?page=1&limit=50` (max limit is **50**):

```bash
PAGE=1
while true; do
  BATCH=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues?page=${PAGE}&limit=50&state=open" \
    -H "Authorization: token ${FORGEJO_PAT}")
  echo "$BATCH" | jq -r '.[].number'  # process results
  [ "$(echo "$BATCH" | jq length)" -lt 50 ] && break
  PAGE=$((PAGE + 1))
done
```

> **Note:** Forgejo does **not** return an `X-Total-Count` header. The only way to
> know if more pages exist is whether the current page returned `limit` items.

### HTTP Status Codes

| Code | Meaning | Common trigger |
|------|---------|----------------|
| 200 | OK | Successful GET, PATCH |
| 201 | Created | Successful POST |
| 204 | No Content | DELETE succeeded; or "resource exists" checks |
| 304 | Not Modified | Already in target state (e.g., subscription) |
| 400 | Bad Request | Invalid params / body |
| 401 | Unauthorized | Missing or invalid auth |
| 403 | Forbidden | Authenticated but insufficient permission |
| 404 | Not Found | Resource missing; also: "PR not merged" on merge check |
| 405 | Method Not Allowed | Blocked by protection (protected tag/branch, archived) |
| 409 | Conflict | Merge conflict; duplicate resource; optimistic lock |
| 412 | Precondition Failed | `updated_at` in body is stale |
| 413 | Entity Too Large | Quota exceeded or file too large |
| 422 | Unprocessable Entity | Validation error |
| 423 | Locked | Repo archived, mid-transfer, or otherwise locked |
| 429 | Too Many Requests | Rate limited — back off and retry |

### Common Curl Patterns

```bash
# Standard GET
curl -s "${FORGEJO_URL}/api/v1/..." \
  -H "Authorization: token ${FORGEJO_PAT}"

# POST/PUT/PATCH with JSON
curl -s -X POST "${FORGEJO_URL}/api/v1/..." \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -H "Content-Type: application/json" \
  -d '{"key": "{value}"}'

# File upload (multipart)
curl -s -X POST "${FORGEJO_URL}/api/v1/.../assets?name={filename}" \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -F "attachment=@{/path/to/file}"

# Check HTTP status code only
curl -s -o /dev/null -w "%{http_code}" \
  "${FORGEJO_URL}/api/v1/..." \
  -H "Authorization: token ${FORGEJO_PAT}"

# Basic auth (token management only)
curl -s "${FORGEJO_URL}/api/v1/users/${FORGEJO_USERNAME}/tokens" \
  -u "${FORGEJO_USERNAME}:${FORGEJO_PASSWORD}"

# Get raw diff
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}.diff" \
  -H "Authorization: token ${FORGEJO_PAT}"
```

---

## 📂 Complete Reference Index

### Core Operations

| Reference | Files | Covers |
|-----------|-------|--------|
| `references/pull-requests/` | README, reviews, merging, files | PR CRUD, 6 merge methods, automerge, server-side rebase, inline review comments, reviewer requests, diff/patch/compare |
| `references/issues/` | README, comments, reactions, attachments, dependencies, time-tracking | Issue CRUD, comments, emoji reactions, file attachments, dependency/blocking chains, time logging, stopwatches, pinning |
| `references/branches-tags/` | branches, protections, tags | Branch CRUD/rename, tag CRUD, branch protection (approvals, CI checks, push restrictions), tag protections |
| `references/files-content/` | README, crud, raw-media | File CRUD with SHA locking, multi-file commits, diffpatch, raw, media, archives, compare |
| `references/labels/` | README, repo-labels, org-labels, issue-pr-labels, label-templates | Repo/org label CRUD, exclusive labels, apply/replace/remove on issues+PRs, template sets |
| `references/milestones/` | README | Milestone CRUD, progress %, due dates |
| `references/commit-statuses/` | README | Combined status, per-check states, create custom checks, quality gate verification |

### People & Organizations

| Reference | Files | Covers |
|-----------|-------|--------|
| `references/organizations/` | README, teams, members | Org CRUD/rename/avatar/blocks, team CRUD with units/permissions, member management |
| `references/users/` | profile, keys, tokens, emails, quota, settings, social | Profile/search/heatmap, SSH/GPG keys, API token CRUD (scopes), emails, quota, settings, followers/teams |
| `references/authentication/` | README, tokens, oauth2, web-login | Token, basic auth, OAuth2 code flow, CSRF web session |

### DevOps & Automation

| Reference | Files | Covers |
|-----------|-------|--------|
| `references/ci-actions/` | README, runs, secrets, variables, workflows | Workflow runs, manual dispatch, repo/org/user secrets+variables, runner registration |
| `references/webhooks/` | README | Repo/org/user/system/git hooks, 20+ event types, testing, git hooks |
| `references/repositories/` | README + 10 more | Repo lifecycle: CRUD, migration, templates, settings, collaborators, deploy keys, forks, mirrors, transfer, topics, stars, avatar, issue config, flags |

### Content & Discovery

| Reference | Files | Covers |
|-----------|-------|--------|
| `references/git-objects/` | README | Blobs (batch), commits, trees, refs, annotated tags, notes, language stats |
| `references/releases/` | README | Release CRUD, asset upload, latest/by-tag lookup |
| `references/wiki/` | README | Wiki page CRUD, revision history |
| `references/packages/` | README | 20 package types (npm, pypi, container, helm, maven…), repo linking |
| `references/search/` | README | Repo/issue/user/topic search — response envelope differences documented |
| `references/notifications/` | README | List/filter, mark-read, thread management, pinning |
| `references/activity-feeds/` | README | Repo/org/team/user activity, all 28 op_type values |

### System & Meta

| Reference | Files | Covers |
|-----------|-------|--------|
| `references/settings-admin/` | README | Public settings; admin: user/org CRUD, cron tasks, quota groups/rules, unadopted repos, email search |
| `references/miscellaneous/` | README | Gitignore/license templates, markdown/markup render, NodeInfo, version, signing keys |
| `references/activitypub/` | README | Instance/repo/user actors, inbox/outbox for ForgeFed |
| `references/web-interface/` | README, ci-logs | What requires web session vs REST; CI log access step-by-step |

### Cookbook

| Recipe | Solves |
|--------|--------|
| `complex-workflows/pr-review-cycle.md` | Create branch → commit → PR → request reviewers → review → address → merge |
| `complex-workflows/issue-lifecycle.md` | Create issue → assign → label → comment → link PR → close |
| `complex-workflows/ci-status-check.md` | Get PR head SHA → combined status → failing jobs → run IDs → log access |
| `complex-workflows/server-side-rebase.md` | Detect staleness → `POST /pulls/{index}/update?style=rebase` |
| `complex-workflows/automerge-workflow.md` | Set automerge → poll CI → confirm merge or cancel |
| `complex-workflows/release-workflow.md` | Verify PRs → tag → release → assets → close milestone |
| `complex-workflows/branch-protection-setup.md` | Approvals + CI checks + signed commits + push restrictions |
| `complex-workflows/org-setup.md` | Org → teams → members → repos → labels → webhooks → secrets |
| `complex-workflows/fork-contribute.md` | Fork → branch → commit → cross-repo PR → upstream sync |
| `complex-workflows/tips-and-patterns.md` | Multi-identity auth, subtask checkboxes, auto-close, pagination, optimistic locking, base64, large diffs, 16-code HTTP reference |

---

## 🔎 Server Info

- **Base URL**: `${FORGEJO_URL}/api/v1`
- **Swagger UI**: `${FORGEJO_URL}/api/swagger`
- **Swagger JSON**: `${FORGEJO_URL}/swagger.v1.json`
- **Tested against**: Forgejo 14.0.4 (Gitea 1.22.0 compatible)
- **Max page size**: 50 items per request
- **Default page size**: 30 items
- **No X-Total-Count header**: Use `len(results) == limit` to detect more pages
