# Gitea Wiki V2 API Developer Guide **For .NET Plugin Developers** This guide explains how to integrate with the Gitea Wiki V2 API from your .NET application. The V2 API provides structured, AI-friendly endpoints designed for external tool integration. --- ## Table of Contents 1. [Authentication](#authentication) 2. [Base URL](#base-url) 3. [API Endpoints](#api-endpoints) - [List Wiki Pages](#list-wiki-pages) - [Get Wiki Page](#get-wiki-page) - [Create Wiki Page](#create-wiki-page) - [Update Wiki Page](#update-wiki-page) - [Delete Wiki Page](#delete-wiki-page) - [Search Wiki](#search-wiki) - [Get Link Graph](#get-link-graph) - [Get Wiki Statistics](#get-wiki-statistics) - [Get Page Revisions](#get-page-revisions) 4. [Data Models](#data-models) 5. [C# Example Code](#c-example-code) 6. [Error Handling](#error-handling) 7. [Best Practices](#best-practices) --- ## Authentication All write operations (create, update, delete) require authentication. Read operations on public repositories work without authentication. ### Token Authentication Use a personal access token in the `Authorization` header: ```http Authorization: token YOUR_ACCESS_TOKEN ``` ### Creating a Token 1. Go to Gitea → Settings → Applications → Generate New Token 2. Select scopes: `write:repository` (for wiki access) 3. Copy the token and store securely ### C# HttpClient Setup ```csharp var client = new HttpClient(); client.BaseAddress = new Uri("https://your-gitea-instance.com/api/v2/"); client.DefaultRequestHeaders.Add("Authorization", $"token {accessToken}"); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); ``` --- ## Base URL ``` https://{gitea-instance}/api/v2/repos/{owner}/{repo}/wiki ``` Replace: - `{gitea-instance}` - Your Gitea server domain - `{owner}` - Repository owner (user or organization) - `{repo}` - Repository name --- ## API Endpoints ### List Wiki Pages Returns all wiki pages with metadata. ```http GET /api/v2/repos/{owner}/{repo}/wiki/pages ``` **Query Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `page` | int | 1 | Page number (1-based) | | `limit` | int | 30 | Items per page (max 100) | **Response:** `WikiPageListV2` ```json { "pages": [ { "name": "Home", "title": "Home", "path": "Home.md", "url": "https://gitea.example.com/api/v2/repos/owner/repo/wiki/pages/Home", "html_url": "https://gitea.example.com/owner/repo/wiki/Home", "word_count": 250, "last_commit": { "sha": "abc123...", "message": "Update Home page", "date": "2026-01-09T10:30:00Z", "author": { "name": "John Doe", "email": "john@example.com" } } } ], "total_count": 15, "has_more": false } ``` --- ### Get Wiki Page Returns a single page with full content, HTML rendering, and link information. ```http GET /api/v2/repos/{owner}/{repo}/wiki/pages/{pageName} ``` **Response:** `WikiPageV2` ```json { "name": "Getting-Started", "title": "Getting Started", "path": "Getting-Started.md", "url": "https://gitea.example.com/api/v2/repos/owner/repo/wiki/pages/Getting-Started", "html_url": "https://gitea.example.com/owner/repo/wiki/Getting-Started", "content": "# Getting Started\n\nWelcome to the project...", "content_html": "

Getting Started

\n

Welcome to the project...

", "word_count": 450, "links_out": ["Installation", "Configuration", "FAQ"], "links_in": ["Home", "README"], "sidebar": "## Navigation\n- [[Home]]\n- [[Getting-Started]]", "footer": "Copyright 2026", "history_url": "https://gitea.example.com/api/v2/repos/owner/repo/wiki/pages/Getting-Started/revisions", "last_commit": { "sha": "def456...", "message": "Add quick start section", "date": "2026-01-08T14:22:00Z", "author": { "name": "Jane Smith", "email": "jane@example.com" }, "committer": { "name": "Jane Smith", "email": "jane@example.com" } } } ``` **Key Fields for AI Integration:** | Field | Description | |-------|-------------| | `content` | Raw markdown content (use for AI processing) | | `content_html` | Pre-rendered HTML (use for display) | | `links_out` | Pages this page links to | | `links_in` | Pages that link to this page | | `word_count` | Word count for content analysis | --- ### Create Wiki Page Creates a new wiki page. ```http POST /api/v2/repos/{owner}/{repo}/wiki/pages ``` **Request Body:** `CreateWikiPageV2Option` ```json { "name": "New-Feature", "title": "New Feature Documentation", "content": "# New Feature\n\nThis page documents the new feature...", "message": "Add documentation for new feature" } ``` | Field | Required | Description | |-------|----------|-------------| | `name` | Yes | Page name (used in URL, spaces become dashes) | | `title` | No | Display title (defaults to name) | | `content` | Yes | Markdown content | | `message` | No | Commit message (auto-generated if empty) | **Response:** Redirects to the created page (HTTP 302) --- ### Update Wiki Page Updates an existing wiki page. Can also rename the page. ```http PUT /api/v2/repos/{owner}/{repo}/wiki/pages/{pageName} ``` **Request Body:** `UpdateWikiPageV2Option` ```json { "title": "Updated Title", "content": "# Updated Content\n\nNew content here...", "message": "Update page content", "rename_to": "New-Page-Name" } ``` | Field | Required | Description | |-------|----------|-------------| | `title` | No | New display title | | `content` | No | New markdown content | | `message` | No | Commit message | | `rename_to` | No | New page name (renames the page) | **Response:** Redirects to the updated page (HTTP 302) --- ### Delete Wiki Page Deletes a wiki page. ```http DELETE /api/v2/repos/{owner}/{repo}/wiki/pages/{pageName} ``` **Response:** `WikiDeleteResponseV2` ```json { "success": true } ``` --- ### Search Wiki Full-text search across all wiki pages. ```http GET /api/v2/repos/{owner}/{repo}/wiki/search?q={query} ``` **Query Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `q` | string | required | Search query | | `limit` | int | 20 | Max results (max 100) | | `offset` | int | 0 | Skip N results | **Response:** `WikiSearchResponseV2` ```json { "query": "installation", "results": [ { "name": "Getting-Started", "title": "Getting Started", "snippet": "...Follow these steps for **installation**. First, download the package...", "score": 8.5, "word_count": 450, "last_updated": "2026-01-08T14:22:00Z" }, { "name": "Installation", "title": "Installation Guide", "snippet": "# **Installation** Guide\n\nThis page covers all **installation** methods...", "score": 12.3, "word_count": 890, "last_updated": "2026-01-05T09:15:00Z" } ], "total_count": 2 } ``` **Score Calculation:** - Title match: +10 points - Page name match: +8 points - Content matches: +0.5 per occurrence - Normalized by word count --- ### Get Link Graph Returns the wiki's link relationship graph (for visualization or analysis). ```http GET /api/v2/repos/{owner}/{repo}/wiki/graph ``` **Response:** `WikiGraphV2` ```json { "nodes": [ { "name": "Home", "title": "Home", "word_count": 250 }, { "name": "Getting-Started", "title": "Getting Started", "word_count": 450 }, { "name": "Installation", "title": "Installation Guide", "word_count": 890 }, { "name": "Configuration", "title": "Configuration", "word_count": 320 } ], "edges": [ { "source": "Home", "target": "Getting-Started" }, { "source": "Home", "target": "Installation" }, { "source": "Getting-Started", "target": "Installation" }, { "source": "Getting-Started", "target": "Configuration" }, { "source": "Installation", "target": "Configuration" } ] } ``` **Use Cases:** - Build a visual wiki map - Find navigation paths between pages - Identify central/hub pages - Detect isolated page clusters --- ### Get Wiki Statistics Returns comprehensive wiki statistics and health metrics. ```http GET /api/v2/repos/{owner}/{repo}/wiki/stats ``` **Response:** `WikiStatsV2` ```json { "total_pages": 15, "total_words": 12500, "total_commits": 87, "last_updated": "2026-01-09T10:30:00Z", "contributors": 5, "health": { "orphaned_pages": [ { "name": "Old-Feature", "word_count": 120 } ], "dead_links": [ { "page": "Getting-Started", "broken_link": "Deprecated-Page" } ], "outdated_pages": [ { "name": "Legacy-Setup", "last_edit": "2025-03-15T00:00:00Z", "days_old": 300 } ], "short_pages": [ { "name": "TODO", "word_count": 25 } ] }, "top_linked": [ { "name": "Home", "incoming_links": 12 }, { "name": "Installation", "incoming_links": 8 }, { "name": "Configuration", "incoming_links": 6 } ] } ``` **Health Metrics:** | Metric | Description | |--------|-------------| | `orphaned_pages` | Pages with no incoming links (except Home) | | `dead_links` | Links pointing to non-existent pages | | `outdated_pages` | Pages not edited in 180+ days | | `short_pages` | Pages with < 100 words | --- ### Get Page Revisions Returns the revision history for a specific page. ```http GET /api/v2/repos/{owner}/{repo}/wiki/pages/{pageName}/revisions ``` **Query Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `page` | int | 1 | Page number | **Response:** `WikiRevisionsV2` ```json { "page_name": "Getting-Started", "revisions": [ { "sha": "abc123...", "message": "Add troubleshooting section", "date": "2026-01-09T10:30:00Z", "author": { "name": "John Doe", "email": "john@example.com" } }, { "sha": "def456...", "message": "Fix typo in installation steps", "date": "2026-01-08T14:22:00Z", "author": { "name": "Jane Smith", "email": "jane@example.com" } } ], "total_count": 15 } ``` --- ## Data Models ### C# Model Definitions ```csharp // Wiki Page (full details) public class WikiPageV2 { public string Name { get; set; } public string Title { get; set; } public string Path { get; set; } public string Url { get; set; } public string HtmlUrl { get; set; } public string Content { get; set; } public string ContentHtml { get; set; } public int WordCount { get; set; } public List LinksOut { get; set; } public List LinksIn { get; set; } public string Sidebar { get; set; } public string Footer { get; set; } public string HistoryUrl { get; set; } public WikiCommitV2 LastCommit { get; set; } } // Wiki Commit public class WikiCommitV2 { public string Sha { get; set; } public string Message { get; set; } public DateTime Date { get; set; } public WikiAuthorV2 Author { get; set; } public WikiAuthorV2 Committer { get; set; } } // Wiki Author public class WikiAuthorV2 { public string Username { get; set; } public string Name { get; set; } public string Email { get; set; } public string AvatarUrl { get; set; } } // Page List Response public class WikiPageListV2 { public List Pages { get; set; } public long TotalCount { get; set; } public bool HasMore { get; set; } } // Search Result public class WikiSearchResultV2 { public string Name { get; set; } public string Title { get; set; } public string Snippet { get; set; } public float Score { get; set; } public int WordCount { get; set; } public DateTime LastUpdated { get; set; } } // Search Response public class WikiSearchResponseV2 { public string Query { get; set; } public List Results { get; set; } public long TotalCount { get; set; } } // Graph Node public class WikiGraphNodeV2 { public string Name { get; set; } public string Title { get; set; } public int WordCount { get; set; } } // Graph Edge public class WikiGraphEdgeV2 { public string Source { get; set; } public string Target { get; set; } } // Graph Response public class WikiGraphV2 { public List Nodes { get; set; } public List Edges { get; set; } } // Health Metrics public class WikiHealthV2 { public List OrphanedPages { get; set; } public List DeadLinks { get; set; } public List OutdatedPages { get; set; } public List ShortPages { get; set; } } public class WikiOrphanedPageV2 { public string Name { get; set; } public int WordCount { get; set; } } public class WikiDeadLinkV2 { public string Page { get; set; } public string BrokenLink { get; set; } } public class WikiOutdatedPageV2 { public string Name { get; set; } public DateTime LastEdit { get; set; } public int DaysOld { get; set; } } public class WikiShortPageV2 { public string Name { get; set; } public int WordCount { get; set; } } // Statistics Response public class WikiStatsV2 { public long TotalPages { get; set; } public long TotalWords { get; set; } public long TotalCommits { get; set; } public DateTime LastUpdated { get; set; } public long Contributors { get; set; } public WikiHealthV2 Health { get; set; } public List TopLinked { get; set; } } public class WikiTopLinkedPageV2 { public string Name { get; set; } public int IncomingLinks { get; set; } } // Create/Update Options public class CreateWikiPageV2Option { public string Name { get; set; } public string Title { get; set; } public string Content { get; set; } public string Message { get; set; } } public class UpdateWikiPageV2Option { public string Title { get; set; } public string Content { get; set; } public string Message { get; set; } public string RenameTo { get; set; } } ``` --- ## C# Example Code ### Complete Wiki Client ```csharp using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; public class GiteaWikiClient : IDisposable { private readonly HttpClient _client; private readonly string _owner; private readonly string _repo; private readonly JsonSerializerOptions _jsonOptions; public GiteaWikiClient(string baseUrl, string owner, string repo, string accessToken) { _owner = owner; _repo = repo; _client = new HttpClient { BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/") }; _client.DefaultRequestHeaders.Add("Authorization", $"token {accessToken}"); _jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; } private string WikiPath => $"api/v2/repos/{_owner}/{_repo}/wiki"; // List all pages public async Task ListPagesAsync(int page = 1, int limit = 30) { var response = await _client.GetAsync($"{WikiPath}/pages?page={page}&limit={limit}"); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(_jsonOptions); } // Get a single page public async Task GetPageAsync(string pageName) { var response = await _client.GetAsync($"{WikiPath}/pages/{Uri.EscapeDataString(pageName)}"); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(_jsonOptions); } // Create a new page public async Task CreatePageAsync(string name, string content, string title = null, string message = null) { var options = new CreateWikiPageV2Option { Name = name, Title = title ?? name, Content = content, Message = message }; var response = await _client.PostAsJsonAsync($"{WikiPath}/pages", options, _jsonOptions); response.EnsureSuccessStatusCode(); // Follow redirect to get the created page if (response.Headers.Location != null) { return await GetPageAsync(name); } return null; } // Update a page public async Task UpdatePageAsync(string pageName, string content = null, string title = null, string renameTo = null, string message = null) { var options = new UpdateWikiPageV2Option { Title = title, Content = content, Message = message, RenameTo = renameTo }; var response = await _client.PutAsJsonAsync($"{WikiPath}/pages/{Uri.EscapeDataString(pageName)}", options, _jsonOptions); response.EnsureSuccessStatusCode(); var finalName = renameTo ?? pageName; return await GetPageAsync(finalName); } // Delete a page public async Task DeletePageAsync(string pageName) { var response = await _client.DeleteAsync($"{WikiPath}/pages/{Uri.EscapeDataString(pageName)}"); response.EnsureSuccessStatusCode(); return true; } // Search wiki public async Task SearchAsync(string query, int limit = 20, int offset = 0) { var url = $"{WikiPath}/search?q={Uri.EscapeDataString(query)}&limit={limit}&offset={offset}"; var response = await _client.GetAsync(url); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(_jsonOptions); } // Get link graph public async Task GetGraphAsync() { var response = await _client.GetAsync($"{WikiPath}/graph"); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(_jsonOptions); } // Get statistics public async Task GetStatsAsync() { var response = await _client.GetAsync($"{WikiPath}/stats"); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(_jsonOptions); } // Get page revisions public async Task GetRevisionsAsync(string pageName, int page = 1) { var url = $"{WikiPath}/pages/{Uri.EscapeDataString(pageName)}/revisions?page={page}"; var response = await _client.GetAsync(url); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(_jsonOptions); } public void Dispose() { _client?.Dispose(); } } ``` ### Usage Examples ```csharp // Initialize client using var wiki = new GiteaWikiClient( baseUrl: "https://gitea.example.com", owner: "myorg", repo: "myproject", accessToken: "your_token_here" ); // List all pages var pageList = await wiki.ListPagesAsync(); Console.WriteLine($"Found {pageList.TotalCount} wiki pages"); foreach (var page in pageList.Pages) { Console.WriteLine($"- {page.Title} ({page.WordCount} words)"); } // Get a specific page var page = await wiki.GetPageAsync("Getting-Started"); Console.WriteLine($"Content: {page.Content}"); Console.WriteLine($"Links to: {string.Join(", ", page.LinksOut)}"); Console.WriteLine($"Linked from: {string.Join(", ", page.LinksIn)}"); // Create a new page var newPage = await wiki.CreatePageAsync( name: "API-Reference", content: "# API Reference\n\nThis page documents the API...", title: "API Reference", message: "Add API documentation" ); // Update a page var updated = await wiki.UpdatePageAsync( pageName: "API-Reference", content: "# API Reference\n\nUpdated content...", message: "Update API docs" ); // Rename a page var renamed = await wiki.UpdatePageAsync( pageName: "API-Reference", renameTo: "API-Documentation", message: "Rename API page" ); // Search var results = await wiki.SearchAsync("installation"); foreach (var result in results.Results) { Console.WriteLine($"Found: {result.Title} (score: {result.Score})"); Console.WriteLine($" {result.Snippet}"); } // Get wiki health var stats = await wiki.GetStatsAsync(); Console.WriteLine($"Wiki has {stats.TotalPages} pages with {stats.TotalWords} total words"); if (stats.Health.DeadLinks.Any()) { Console.WriteLine("Broken links found:"); foreach (var deadLink in stats.Health.DeadLinks) { Console.WriteLine($" {deadLink.Page} -> {deadLink.BrokenLink}"); } } // Delete a page await wiki.DeletePageAsync("Old-Page"); ``` ### AI Function Calling Integration If you're using the wiki with AI function calling (e.g., OpenAI, Azure OpenAI): ```csharp // Define functions for AI var functions = new[] { new { name = "search_wiki", description = "Search the project wiki for information", parameters = new { type = "object", properties = new { query = new { type = "string", description = "Search query" } }, required = new[] { "query" } } }, new { name = "get_wiki_page", description = "Get the full content of a wiki page", parameters = new { type = "object", properties = new { page_name = new { type = "string", description = "Name of the wiki page" } }, required = new[] { "page_name" } } }, new { name = "create_wiki_page", description = "Create a new wiki page", parameters = new { type = "object", properties = new { name = new { type = "string", description = "Page name" }, content = new { type = "string", description = "Markdown content" }, message = new { type = "string", description = "Commit message" } }, required = new[] { "name", "content" } } }, new { name = "get_wiki_stats", description = "Get wiki statistics including health metrics", parameters = new { type = "object", properties = new { } } } }; // Handle function calls async Task HandleFunctionCall(string functionName, JsonElement arguments) { switch (functionName) { case "search_wiki": var query = arguments.GetProperty("query").GetString(); var results = await wiki.SearchAsync(query); return JsonSerializer.Serialize(results); case "get_wiki_page": var pageName = arguments.GetProperty("page_name").GetString(); var page = await wiki.GetPageAsync(pageName); return JsonSerializer.Serialize(page); case "create_wiki_page": var name = arguments.GetProperty("name").GetString(); var content = arguments.GetProperty("content").GetString(); var message = arguments.TryGetProperty("message", out var msg) ? msg.GetString() : null; var newPage = await wiki.CreatePageAsync(name, content, message: message); return JsonSerializer.Serialize(newPage); case "get_wiki_stats": var stats = await wiki.GetStatsAsync(); return JsonSerializer.Serialize(stats); default: throw new ArgumentException($"Unknown function: {functionName}"); } } ``` --- ## Error Handling ### Error Response Format All errors return a consistent JSON structure: ```json { "error": { "code": "WIKI_PAGE_NOT_FOUND", "message": "Wiki page not found", "details": {} } } ``` ### Error Codes | Code | HTTP Status | Description | |------|-------------|-------------| | `WIKI_PAGE_NOT_FOUND` | 404 | The requested page doesn't exist | | `WIKI_PAGE_ALREADY_EXISTS` | 409 | A page with that name already exists | | `WIKI_RESERVED_NAME` | 400 | The page name is reserved (e.g., `_Sidebar`) | | `WIKI_DISABLED` | 403 | Wiki is disabled for this repository | | `AUTH_TOKEN_MISSING` | 401 | No authentication token provided | | `REPO_NOT_FOUND` | 404 | Repository doesn't exist | | `REPO_ARCHIVED` | 403 | Repository is archived (read-only) | | `ACCESS_DENIED` | 403 | Insufficient permissions | ### C# Error Handling ```csharp public class GiteaApiException : Exception { public string ErrorCode { get; } public int StatusCode { get; } public GiteaApiException(string code, string message, int statusCode) : base(message) { ErrorCode = code; StatusCode = statusCode; } } // In your client methods: public async Task GetPageAsync(string pageName) { var response = await _client.GetAsync($"{WikiPath}/pages/{Uri.EscapeDataString(pageName)}"); if (!response.IsSuccessStatusCode) { var error = await response.Content.ReadFromJsonAsync(_jsonOptions); throw new GiteaApiException( error?.Error?.Code ?? "UNKNOWN", error?.Error?.Message ?? "Unknown error", (int)response.StatusCode ); } return await response.Content.ReadFromJsonAsync(_jsonOptions); } // Usage: try { var page = await wiki.GetPageAsync("NonExistent"); } catch (GiteaApiException ex) when (ex.ErrorCode == "WIKI_PAGE_NOT_FOUND") { Console.WriteLine("Page doesn't exist, creating it..."); await wiki.CreatePageAsync("NonExistent", "# New Page"); } ``` --- ## Best Practices ### 1. Page Naming - Use URL-friendly names: `Getting-Started`, not `Getting Started` - Avoid special characters: `/`, `\`, `?`, `#`, `%` - Keep names concise but descriptive - The API automatically converts spaces to dashes ### 2. Content Format - Use standard Markdown - Wiki links: `[[Page-Name]]` or `[[Page-Name|Display Text]]` - Relative links: `[text](./Other-Page)` or `[text](Other-Page)` - The API extracts both formats for the `links_out` field ### 3. Commit Messages - Always provide meaningful commit messages - If omitted, auto-generated: "Add {page}" or "Update {page}" - Good for audit trail and history ### 4. Rate Limiting - Be mindful of API rate limits - Cache responses when appropriate - Use pagination for large wikis ### 5. Indexing - The wiki is automatically indexed for search - First access may trigger background indexing - Stats and graph endpoints trigger re-indexing if needed ### 6. Error Recovery ```csharp // Retry logic for transient failures public async Task WithRetryAsync(Func> action, int maxRetries = 3) { for (int i = 0; i < maxRetries; i++) { try { return await action(); } catch (HttpRequestException) when (i < maxRetries - 1) { await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i))); } } throw new Exception("Max retries exceeded"); } // Usage: var page = await WithRetryAsync(() => wiki.GetPageAsync("Home")); ``` --- ## Support For issues with the Wiki V2 API: - Check the [Gitea documentation](https://docs.gitea.com) - Report bugs at the Gitea repository - For .NET client issues, check your authentication and base URL --- *Document Version: 1.0 | API Version: v2 | Last Updated: January 2026*