# Admin API

> Panonun kendi JSON REST API'si — kimlik doğrulama, SSO, projeler, arama, kaynaklar, üyeler, MCP tokenları, webhook'lar, metrikler, audit log ve paylaşılan hata biçimleri.

- Güncelleme: 2026-09-25
- Kaynak: https://contextator.com/tr/docs/admin-api/
- Dil: tr-TR
- Yazar: Muhammet Şafak

---
Panonun yapabildiği her şey bir JSON REST çağrısıdır — pano yalnızca bu API'nin bir istemcisidir. Onu
betiklenmiş kurulum, zamanlanmış yeniden indeksleme ya da izleme için kullanın.

**Temel URL** `http://localhost:3444/api`

## Kimlik doğrulama

Her istek şu üç kimlik bilgisinden birini taşır:

| | Nedir | Kim kullanır |
|---|---|---|
| **Bir oturum çerezi** | `POST /api/auth/login` tarafından ayarlanan `contextator_session` | Pano, ve bir tarayıcıyı süren her şey |
| **`Authorization: Bearer ctxk_…`** | Bir hesabın kendi **API token'ı** — adlandırılmış, rota alt kümesine ve opsiyonel olarak bir projeye kapsanmış, kendi başına iptal edilebilir | Bir hesabın yetkisinin yalnızca *bir kısmını* taşıması gereken betikler ve CI job'ları — aşağıda [API tokenları](#api-tokenları)'na bakın |
| **`Authorization: Bearer $ADMIN_TOKEN`** | `root` izinleriyle makine erişimi | Örneğin tamamına ihtiyaç duyan betikler, CI, cron |

```bash
export API=http://localhost:3444/api
export TOKEN=your-admin-token
curl -s $API/projects -H "Authorization: Bearer $TOKEN" | jq
```

Bu ayrımdan beş şey çıkar:

- **Ne yapabileceğiniz kim olduğunuza bağlıdır.** Bir oturum bir hesaba bağlıdır, bu yüzden onun örnek
  rolü ve proje başına üyelikleri her çağrıya karar verir — bkz.
  [Hesaplar ve İzinler](/tr/docs/accounts-and-permissions/). `ADMIN_TOKEN` her zaman `root` gibi davranır.
- **Bir API token'ı, onu üreten hesaptan asla daha fazlasını yapamaz.** Kapsamı, o hesabın kendi rolü ve
  üyeliğinden *sonra* denetlenir, bu yüzden onu daraltmak — bir projeye, ya da bir avuç rotaya —
  yalnızca yetkiyi azaltabilir, asla artıramaz.
- **Bir şeyi değiştiren bir çerez isteği bu siteden gelmelidir.** Sunucu `Sec-Fetch-Site`'ı denetler,
  yoksa `Origin` ve `Referer`'a düşer, aksi halde `403 csrf_blocked` yanıtlar. Bearer istekleri
  muaftır: hiçbir çevresel kimlik bilgisi taşımazlar, bu yüzden ikisini de göndermeyen `curl` ve CI
  değişmeden çalışır.
- **İki bearer token'dan hiçbiri, bu rotalar söz konusu olduğunda, oturum açmış bir hesap sayılmaz.**
  *Sizin* hesabınızla ilgili endpoint'ler (`/api/auth/password`, `/api/auth/sessions`) bir oturum
  gerektirir — bu denetim, tam olarak o rotaya izin verecek şekilde kapsanmış bir token dahil,
  `ADMIN_TOKEN`'ı ve kapsamlı bir API token'ını aynı şekilde `403 token_has_no_account` ile reddeder.
  Yalnızca bir oturum çerezi taşıyan bir tarayıcı onlara ulaşabilir.
- **Ulaşamadığınız bir proje `403` değil `404` yanıtlar**, var olmayan bir projeyle aynı şekilde, bu
  yüzden id'ler yoklanamaz. Ulaşabildiğiniz ama bu şekilde kullanamayacağınız bir rota `403` yanıtlar.

`GET /api/health`, `GET /api/setup/status`, `POST /api/setup`, `POST /api/auth/login`,
`GET /api/auth/oidc/login` ve `GET /api/auth/oidc/callback`, hiçbir kimlik bilgisi olmadan ulaşılabilen
tek endpoint'lerdir — son ikisi, tarayıcı yönlendirmeyi istediğinde henüz bir oturumu olmadığı için, ve
sağlayıcı **tarayıcıyı** kodla birlikte, bir yönlendirmeyle callback'e geri gönderdiği için — sağlayıcının
kendisinden sunucudan sunucuya bir çağrı değil. Zaten oturum açmış bir hesabı aynı sağlayıcıya
bağlamak (`POST /api/auth/oidc/link`, `DELETE /api/auth/oidc/link` — bkz. [Tek Oturum
Açma](/tr/docs/sso/#kendi-hesabınızı-bağlamak-ya-da-bağlantısını-kesmek)) farklıdır: çağıranın kendi
oturumuna ihtiyaç duyar, tıpkı başka her self-servis hesap endpoint'i gibi.

---

## Sağlık

```bash
curl -s $API/health | jq
```

```json
{
  "ok": true,
  "version": "0.2.0",
  "uptimeSec": 3610,
  "db": "up",
  "embeddings": {
    "id": "local:Xenova/multilingual-e5-small:fp32:\"query: \"+\"passage: \"",
    "provider": "local", "model": "Xenova/multilingual-e5-small",
    "dimensions": 384, "dtype": "fp32", "ready": true
  },
  "sessions": { "total": 2, "streamable": 2, "sse": 0 },
  "allowedDocRoots": ["/docs"],
  "dataDir": "/data",
  "secretKeyConfigured": true,
  "uploads": { "maxFileBytes": 52428800, "maxFilesPerRequest": 500, "maxArchiveBytes": 268435456 }
}
```

İyi bir canlılık probu: `ok === true && db === "up"`. İyi bir hazırlık probu: ayrıca
`embeddings.ready`. Yanıt, veritabanına ulaşılamadığında aynı gövdeyle `503` yanıtlar, aksi halde
`200` — bir izleyicinin bunun için ikinci bir denetime ihtiyacı yoktur.

`embeddings` ayrıca modelin girdi penceresini ve `CHUNK_MAX_TOKENS`'ın onun içine sığıp sığmadığını
taşır, bu yüzden parçaları sessizce kesecek bir yanlış yapılandırma arama kalitesinde ortaya çıkmadan
önce burada görünür — bkz. [Yapılandırma](/tr/docs/configuration/#değişiklikleri-uygulamak).

Anonim bir çağıran daha kısa bir cevap alır — `ok`, `version`, `db`, `authRequired` ve `needsSetup` —
çünkü geri kalanı makineyi anlatır. Bir izleyicinin izlediği alanlar her iki biçimde de vardır.

## Kurulum ve oturum açma

| Yöntem ve yol | Açıklama |
|---------------|-------------|
| `GET /api/setup/status` | `{ needsSetup }` — bu örneğin hiç hesabı yokken `true`. Herkese açık |
| `POST /api/setup` | `{ code, username, displayName?, email?, password }` → `201 { user }`, ve oturum çerezi. İlk `root` hesabını oluşturur. Yanlış kodda `403 invalid_setup_code`; bir hesap var olduktan sonra `409`. Herkese açık |
| `POST /api/auth/login` | `{ username, password }` → `{ user, mustChangePassword }` ve çerez. Hem yanlış bir parola hem de bilinmeyen bir kullanıcı adı için `401 invalid_credentials`, `403 account_disabled`, hız sınırlandığında `Retry-After` ile `429`. Herkese açık |
| `POST /api/auth/logout` | Bu oturumu iptal eder ve çerezi temizler → `204`. İdempotenttir |
| `GET /api/auth/me` | Oturum açmış hesap, rolü, `authKind` (`session`\|`token`\|`apiToken` — bir pano oturumu, `ADMIN_TOKEN` ya da bir API token'ı), ve bir `member` için `projects` içindeki proje başına rolleri. Ayrıca `oidc` (`{ enabled, buttonLabel }`), `canLinkOidc` (root için `false`) ve `federatedProviders` — aşağıda [Tek oturum açma](#tek-oturum-açma)'ya bakın |
| `POST /api/auth/password` | `{ currentPassword, newPassword }` → `204`. `mustChangePassword`'ü temizler ve bu hesabın **diğer** oturumlarını iptal eder |
| `GET /api/auth/sessions` | Kendi canlı oturumlarınız: her biri ne zaman başladı, en son ne zaman görüldü, user agent'ı ve IP'si, ve hangisi `current` |
| `DELETE /api/auth/sessions?scope=others\|all` | Onları sonlandırır → `204`. Varsayılan `others`'tır; `all` sizi de çıkarır |

```bash
# sign in and keep the cookie in a jar, then use it like a browser would
curl -s -c jar -X POST $API/auth/login -H 'content-type: application/json' \
  -d '{"username":"alice","password":"…"}' | jq
curl -s -b jar $API/auth/me | jq
```

## Tek oturum açma

Yalnızca bu örnekte bir OIDC sağlayıcısı yapılandırılmışsa bulunur — kimin bağlayabileceği ve root'un
neden bağlayamadığı için bkz. [Tek Oturum Açma](/tr/docs/sso/).

| Yöntem ve yol | Açıklama |
|---------------|-------------|
| `GET /api/auth/oidc/login` | Oturum açmak için kimlik sağlayıcıya yönlendirir. Herkese açık — tarayıcının henüz bir oturumu yoktur |
| `GET /api/auth/oidc/callback` | Kimlik sağlayıcı **tarayıcıyı** kodla birlikte, bir yönlendirmeyle buraya geri gönderir — sağlayıcıdan sunucudan sunucuya bir çağrı değil. Herkese açıktır çünkü bir oturum açma henüz bir oturum olmadan gelir; bir bağlama ise onu başlatan oturumla, yönlendirmenin kendi çerezinin taşıdığı şekilde gelir. Bir `root` hesabını oturum açtırmayı reddeder (`root_local_only`), bağlantısı bir `root`'a yükseltilmeden önce yapılmış olsa bile — rol, bağlantının yapıldığı andan varsayılmaz, callback anında hesaptan yeniden okunur |
| `POST /api/auth/oidc/link` | Zaten oturum açmış bir hesap için self-servis: aynı yönlendirmeyi başlatır, ama oturum açmak yerine sağlayıcıyı bu hesaba *bağlamak* için. Bir `POST` olduğu için same-site denetimi geçerlidir. → gidilecek `200 { url }`. Root için reddedilir (`403 root_local_only`) — root asla SSO üzerinden oturum açmaz |
| `DELETE /api/auth/oidc/link` | Çağıranın kendi bağlı kimliğini kaldırır → `204`. Kendi hesabınıza kapsanmıştır; başka bir hesabın bağlantısı üzerinde bir admin görünümü yoktur. Root için reddedilmez — root'a yükseltilmeden önce yapılmış bir bağlantı kaldırılana kadar yerinde kalır, ve bunu yapan yol budur. Ayrıca hesabın tuttuğu her oturumu ve API token'ını, bu çağrıyı yapanı da dahil, ve — aynı transaction içinde — hesaba verilmiş her MCP OAuth erişim ve yenileme token'ını iptal eder, bu yüzden onunla oturum açmış bir MCP istemcisi yeniden yetkilendirene kadar `401` alır; diğer hesaplar dokunulmadan kalır. Bağlantı kesmenin audit olayı sayıyı `detail.revokedMcpCredentials` olarak kaydeder. **`409 last_sign_in_method`** — hiçbir şey kaldırılmaz, hiçbir şey iptal edilmez — hesabın yerel bir parolası yoksa ve bu bağlantı oturum açmasının tek yoluysa; bir admin önce `POST /api/users/:id/password` ile bir parola ayarlar, ve bağlantı kesme ardından başarılı olur |

## Hesaplar

`/api/users/*`, `admin` ya da `root` gerektirir. Yalnızca bir `root` hesabı başka bir `root` hesabı
oluşturabilir, değiştirebilir ya da silebilir. `PATCH /api/users/:id`, hâlâ bağlı bir SSO kimliği olan
bir hesapta `role: "root"` ayarlamak istendiğinde `409 root_requires_unlink` ile reddeder — önce onu
yukarıdaki `DELETE /api/auth/oidc/link` ile bağlantısını kesin.

| Yöntem ve yol | Açıklama |
|---------------|-------------|
| `GET /api/users` | Her hesap: rol, active anahtarı, `mustChangePassword`, proje sayısı, son oturum açma, ve `activeSessionCount` |
| `POST /api/users` | `{ username, displayName?, email?, role?, password?, mustChangePassword? }` → `201 { user, temporaryPassword }`. `role` varsayılan olarak `member`, `mustChangePassword` varsayılan olarak `true`; `password`'ü atlayın, biri üretilir ve **bir kez** döndürülür |
| `GET /api/users/:id` | `{ user, activeSessionCount }` |
| `PATCH /api/users/:id` | `{ displayName?, email?, role?, isActive? }` → güncellenmiş hesap. Bir düşürme ya da devre dışı bırakma o hesabın oturumlarını da sona erdirir |
| `POST /api/users/:id/password` | `{ password? }` → `{ temporaryPassword }`. Sonraki oturum açmada bir değişikliği zorlar ve o hesabın oturumlarını sona erdirir |
| `DELETE /api/users/:id` | Hesabı, oturumlarını ve üyeliklerini siler → `204` |
| `DELETE /api/users/:id/sessions` | O hesabı her yerde oturumdan çıkarır → `204` |

`409`, son aktif root hesabını silinmeye, düşürülmeye ve devre dışı bırakılmaya karşı korur; `403`, bir
admin'in bir root hesabına uzanmasına ve herkesin kendi rolüne ya da active anahtarına uzanmasına karşı
korur.

```bash
# hand someone a new temporary password without touching the dashboard
ID=$(curl -s $API/users -H "Authorization: Bearer $TOKEN" | jq -r '.[] | select(.username=="alice") | .id')
curl -s -X POST $API/users/$ID/password -H "Authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' -d '{}' | jq -r .temporaryPassword
```

## API tokenları

`/api/tokens*` yalnızca bir oturum gerektirir — her hesap **kendi** tokenlarını yönetir, ve başkasının
tokenları üzerinde bir admin görünümü yoktur. Bir betiğin ya da CI job'unun `ADMIN_TOKEN`'dan daha dar
bir kimlik bilgisi alma yolu budur: adlandırın, opsiyonel olarak bir projeye ve bir son kullanma
tarihine sabitleyin, ve tam olarak hangi rotaları çağırabileceğini listeleyin. Panonun kendi **Account
menu → API tokens** sayfası tam olarak bu endpoint'lerin bir istemcisidir — orada `curl`'e hiç
dokunmadan üretin, listeleyin ve iptal edin.

| Yöntem ve yol | Açıklama |
|---------------|-------------|
| `GET /api/tokens` | Kendi tokenlarınız: `id`, `name`, `prefix`, `scope`, `projectId`, `createdAt`, `lastUsedAt`, `revokedAt`, `expiresAt`. Asla secret değil |
| `POST /api/tokens` | `{ name?, scope, projectId?, expiresAt? }` → `201 { token, secret }`. `scope`, `"POST /api/projects/:id/reindex"` gibi bir ya da daha fazla `"<METHOD> <route template>"` dizesidir. Üretme yalnızca bu biçimi ve, verildiyse, `expiresAt`'in gelecekte olduğunu denetler — bir girdiyi kendi rolünüze karşı, ya da `projectId`'yi kendi projelerinize karşı denetlemez. Token'ı sınırlı tutan şey, her isteğin *canlı* rolünüzü ve proje erişiminizi çağrı anında yeniden denetlemesidir. `secret` yalnız bu yanıtta döner, bir daha asla döndürülmez. Üretim çalışabilmeden önce oturumunuz iptal edildiyse `401 session_revoked` |
| `DELETE /api/tokens/:tokenId` | Kendi tokenlarınızdan birini iptal eder → `204`. Bir sonraki istekte hemen etkili olur |

```bash
# mint a token that can only reindex one project — /api/tokens needs a session, not
# ADMIN_TOKEN, so sign in first (see "Setup and sign-in" above) and reuse the cookie jar.
# a cookie-authenticated request that changes something needs Sec-Fetch-Site too — see the
# CSRF rule above — curl never sends it on its own, unlike a real page's fetch would
curl -s -X POST $API/tokens -b jar \
  -H 'content-type: application/json' -H 'Sec-Fetch-Site: same-origin' \
  -d '{"name":"ci-reindex","scope":["POST /api/projects/:id/reindex"],"projectId":"'"$P"'"}' | jq
```

`projectId` içinde adlandırılan projeyi silmek, erişimini genişletmek yerine token'ı da siler.

## Proje üyeleri

Bir projeye kim ulaşır. `root`, `admin` ve `ADMIN_TOKEN` her projeye bir üyelik olmadan ulaşır, bu
yüzden burada yalnızca `member` hesapları görünür.

| Yöntem ve yol | Açıklama |
|---------------|-------------|
| `GET /api/projects/:id/members` | Projenin üyeleri ve rolleri. Projenin herhangi bir üyesi bunu okuyabilir |
| `PUT /api/projects/:id/members/:userId` | `{ "role": "viewer" \| "editor" }` → üye. Üyeliği oluşturur ya da değiştirir (root/admin) |
| `DELETE /api/projects/:id/members/:userId` | Erişimi iptal eder → `204` (root/admin) |

Bir `member` hesabının sonra görebileceği bir projeye üye eklemek:

```bash
curl -s -X PUT $API/projects/$P/members/$ID -H "Authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' -d '{"role":"editor"}' | jq
```

Hedef bir `admin` ya da `root` hesabıysa `400` — zaten erişimleri vardır, bu yüzden bir üyelik satırı
doğru olmayan bir şey söylerdi.

## Projeler

| Yöntem ve yol | Açıklama |
|---------------|-------------|
| `GET /api/projects` | Sayılarla, `mcpUrl` ile ve canlı indeksleme `job`'uyla birlikte tüm projeler |
| `POST /api/projects` | `{ "name": "demo", "rootPath": "/docs/demo", "index": true }` → `201`. `rootPath` opsiyoneldir ve ilk yerel kaynağı oluşturur |
| `GET /api/projects/:id/status` | Bir proje, artı job'u |
| `POST /api/projects/:id/reindex?force=true` | Bir çalışmayı kuyruğa alır → `202 { job }` |
| `GET /api/projects/:id/runs` | En yeniden en eskiye son indeksleme çalışmaları |
| `GET /api/projects/:id/search?q=…&limit=…&source=…&path_prefix=…&version=…` | Projenin `search_docs` aracının çalıştırdığı aynı arama, JSON olarak: `{ query, limit, source, pathPrefix, version, belowFloor, scoreFloor, hits: [{ score, fusedScore, denseRank, lexicalRank, path, title, headingPath, chunkIndex, content, contextBefore, contextAfter }] }`. `score` cosine similarity'dir; yalnız gösterilir, sıralamada kullanılmaz; listeyi sıralayan `fusedScore`'dur, ve iki rank alıntıyı aramanın hangi yarısının bulduğunu söyler (bulmayan yarı için `null`). `belowFloor`, bir ajana *iyi eşleşme yok* denip denmeyeceğidir — hit'ler her iki durumda da geri gelir, böylece pano nelerin gizlendiğini gösterebilir. `limit` 1–20'dir (varsayılan 5); `source`, `path_prefix` ve `version` isteğe bağlıdır. Bu projenin sahip olmadığı bir kaynak ya da sürüm için `400 invalid_request` (mesaj sahip olduklarını adlandırır), projenin hiç parçası yoksa `409 not_indexed`, başka bir modelle gömüldülerse `409 model_mismatch` |
| `DELETE /api/projects/:id` | Projeyi siler → `204` (indekslerken `409`) |

```bash
# create and index
curl -s -X POST $API/projects -H "Authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' \
  -d '{"name":"handbook","rootPath":"/docs/handbook","index":true}' | jq

# re-index everything, every night
for id in $(curl -s $API/projects -H "Authorization: Bearer $TOKEN" | jq -r '.[].id'); do
  curl -s -X POST "$API/projects/$id/reindex" -H "Authorization: Bearer $TOKEN" > /dev/null
done
```

### Bir job'u izlemek

`GET /api/projects`, canlı bir çalışması olan her proje için bir `job` içerir:

```json
{ "phase": "embedding", "filesTotal": 84, "filesDone": 31, "filesSkipped": 22,
  "filesRemoved": 0, "chunksDone": 96,
  "sources": [{ "name": "handbook", "type": "local", "status": "synced" }],
  "queue": null }
```

`phase`, `queued`, `syncing`, `scanning`, `embedding`, `finalizing`, `done`, `error`'dan biridir.
Kuyruktaki bir job `queue.aheadProjectName` taşır.

## MCP erişimi

| Yöntem ve yol | Açıklama |
|---------------|-------------|
| `GET /api/projects/:id/mcp-tokens` | Projenin canlı tokenları: `id`, `name`, `prefix`, `createdAt`, `lastUsedAt`. Secret'ın kendisi asla döndürülmez |
| `POST /api/projects/:id/mcp-tokens` | `{ "name": "claude-code" }` → `201 { token, secret }`. **`secret` token'ın okunabilir olduğu tek andır** |
| `DELETE /api/projects/:id/mcp-tokens/:tokenId` | İptal eder → `204`, projenin açık MCP oturumlarını kapatır |
| `PATCH /api/projects/:id/mcp-auth` | `{ "mode": "open" \| "token" \| "account" }` → `{ "mcpAuth" }`. `open`'dan uzaklaşmak da projenin açık oturumlarını kapatır. `account`, çağıranın `/mcp/*` üzerinde OAuth 2.1 üzerinden projenin bir üyesi olmasını gerektirir; bu modda statik bir `ctxm_…` token reddedilir |

`GET /api/projects`, geçerli modu `mcpAuth` olarak bildirir. Bir endpoint'i kapatmak ve bir token
üretmek:

```bash
curl -s -X PATCH $API/projects/$P/mcp-auth -H "Authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' -d '{"mode":"token"}' | jq

curl -s -X POST $API/projects/$P/mcp-tokens -H "Authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' -d '{"name":"claude-code"}' | jq -r .secret
```

İstemci sonra bu secret'ı `/mcp/<project-name>` üzerinde `Authorization: Bearer ctxm_…` olarak gönderir
— bkz. [Yapay Zekâ İstemcilerini Bağlamak](/tr/docs/connecting-ai-clients/).

## Kaynaklar

| Yöntem ve yol | Açıklama |
|---------------|-------------|
| `GET /api/projects/:id/sources` | Projenin kaynakları. Secret'lar asla döndürülmez — yalnızca `hasSecret` |
| `POST /api/projects/:id/sources` `{ type, name, label?, flavor?, config?, secret?, syncIntervalMinutes?, index? }` | Bir kaynak ekler. `type`, `local`, `git`, `upload`, `notion`, `confluence` ya da `web`'dir; `config` türe özeldir — aşağıdaki tabloya bakın. `secret`, yalnızca bir kimlik bilgisi kullanan türlerce alınır (`git`, `notion`, `confluence`); `local`, `upload` ya da `web` üzerinde türü adlandıran bir `400 invalid_request` yanıtlar, ve `secret: null` ("secret yok" anlamında) her türde kabul edilir. `syncIntervalMinutes`, 5–43200 ya da `null`'dır; atlanırsa örnek varsayılanı alınır |
| `PATCH /api/projects/:id/sources/:sid` | Etiketi, içerik türünü, config'i ya da token'ı değiştirir (`"secret": null` onu kaldırır). Tür ve ad değiştirilemez |
| `DELETE /api/projects/:id/sources/:sid` | Onu, dokümanlarını ve dosyalarını kaldırır (indekslerken `409`) |
| `POST /api/projects/:id/sources/:sid/sync` | Bir çalışmayı kuyruğa alır → `202 { job }` |
| `POST /api/projects/:id/sources/:sid/test` | Bağlantı denetimi → `{ ok, message }` |
| `POST /api/projects/:id/sources/:sid/webhook-secret` | Yeni bir webhook secret'ı üretir: bir git kaynağında onu rotate eder, ve bir Confluence kaynağında webhook'u açar (ya da yeniden üretir) — bkz. [Push Webhook'ları](/tr/docs/push-webhooks/#confluence-data-center) |
| `DELETE /api/projects/:id/sources/:sid/webhook-secret` | Yalnızca Confluence: webhook'u kapatır. Teslimatlar ardından yeniden `not_enabled` ile reddedilir |

Bir git kaynağı eklemek:

```bash
curl -s -X POST $API/projects/$PROJECT_ID/sources \
  -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{
        "type": "git",
        "name": "api-docs",
        "label": "API reference",
        "config": { "url": "https://github.com/org/repo.git", "branch": "main", "subdir": "docs" },
        "secret": "ghp_…",
        "index": true
      }' | jq
```

Türe göre `config`:

| Tür | Anahtarlar |
|------|------|
| `local` | `path`, `extensions` |
| `git` | `url`, `branch`, `subdir`, `username`, `provider`, `extensions` |
| `upload` | `extensions` |
| `notion` | `rootIds`, `extensions` |
| `confluence` | `deployment` (`cloud` \| `datacenter`), `siteUrl`, `accountEmail` (yalnızca Cloud), `spaces` — bkz. [Confluence](/tr/docs/confluence/) |
| `web` | `entryUrl`, `entryKind` (`auto` \| `sitemap` \| `llms` \| `crawl`, varsayılan `auto`), `extensions` — yalnızca herkese açık sayfalar, kimlik bilgisi yok |

## Yüklemeler

| Yöntem ve yol | Açıklama |
|---------------|-------------|
| `POST /api/projects/:id/sources/:sid/uploads` | Bir oturum açar → `{ session }` |
| `POST …/uploads/:session/files` | `multipart/form-data`; her parçanın `filename`'i kaynak içindeki yoldur. Arşivler sunucu tarafında açılır |
| `POST …/uploads/:session/commit?mode=add\|replace` | Bekletilen dosyaları içeri taşır ve bir çalışmayı kuyruğa alır → `202` |
| `DELETE …/uploads/:session` | Bekletilen yüklemeyi atar |
| `GET /api/projects/:id/sources/:sid/files` | Bir yükleme kaynağının dosyalarını listeler |
| `DELETE /api/projects/:id/sources/:sid/files?path=…` | Birini siler ve yeniden indeksler |

```bash
SESSION=$(curl -s -X POST $API/projects/$P/sources/$S/uploads -H "Authorization: Bearer $TOKEN" | jq -r .session)
curl -s -X POST "$API/projects/$P/sources/$S/uploads/$SESSION/files" \
     -H "Authorization: Bearer $TOKEN" -F 'file=@guide.md;filename=guides/guide.md'
curl -s -X POST "$API/projects/$P/sources/$S/uploads/$SESSION/commit?mode=add" \
     -H "Authorization: Bearer $TOKEN"
```

## Webhook'lar

| Yöntem ve yol | Açıklama |
|---------------|-------------|
| `POST /api/webhooks/git/:sourceId` | Bir git kaynağı için push webhook'u, bir hesap ya da `ADMIN_TOKEN` **değil**, kaynak başına secret ile kimliklendirilir |
| `POST /api/webhooks/confluence/:sourceId` | Confluence Data Center webhook'u, kaynak başına secret üzerinden `X-Hub-Signature` ile imzalanır. Kaynağın webhook'u açılana kadar `401 not_enabled` |

Bkz. [Push Webhook'ları](/tr/docs/push-webhooks/).

## Metrikler

```bash
curl -s http://localhost:3444/metrics -H "Authorization: Bearer $METRICS_TOKEN"
```

`GET /metrics`, kendi kimlik bilgisi kurallarına sahip ayrı, Prometheus biçimli bir endpoint'tir
(`text/plain; version=0.0.4`) — `/api/*`'ın **bir parçası değildir** ve varsayılan olarak **herkese
açık değildir**. Şeride göre indeksleme kuyruğunu, birinin çalışıp çalışmadığını, son indeksleme
çalışmasını ve işe yarayıp yaramadığını, aktöre göre aramaları, veritabanı bağlantı havuzunu ve
veritabanının kendisinin yanıt verip vermediğini bildirir.

Açık bir oturumu, `ADMIN_TOKEN`'ı, bir `METRICS_TOKEN` bearer'ını, ya da
`METRICS_PUBLIC=1` iken hiçbir kimlik bilgisini kabul eder — bkz.
[Yapılandırma](/tr/docs/configuration/#gözlemlenebilirlik). Veritabanı çökükken bile `200` yanıtlar,
`contextator_db_up 0` ile ve veritabanı gerektiren satırlar dışarıda bırakılarak, bu yüzden bir scrape
boşluğu bir kesintinin bildirilme yolu asla olmaz. **Veritabanı çökükken yalnızca veritabanı okuması
gerektirmeyen üç kimlik bilgisi çalışmaya devam eder** — `ADMIN_TOKEN`, `METRICS_TOKEN`,
`METRICS_PUBLIC` — bir oturum çerezi bir veritabanı satırıdır ve doğrulanamaz, bu yüzden bir tane
sunan bir tarayıcı bir kesinti sırasında `500` yerine `401` alır. `METRICS_TOKEN`'ı bir kesinti
sırasında değil öncesinde yapılandırmanın nedeni budur.

## Audit log

`/api/audit`, `root` ya da `admin` gerektirir — log örnek geneli olduğu için, bir proje üyeliği kime
root rolü verildiğini okumak için bir dayanak değildir.

| Yöntem ve yol | Açıklama |
|---------------|-------------|
| `GET /api/audit?actor&actorUser&action&project&from&to&limit&cursor` | Audit log, en yeniden en eskiye. Her filtre SQL'de uygulanır: `actor`, o anda kaydedildiği haliyle etikettir; `actorUser`, işlemi yapan hesaba karşı **tam olarak** eşleştirilen bir hesap id'sidir — o hesabın kendi oturumlarını **ve** sahip olduğu her API token'ını döndürür, tokenlar nasıl adlandırılmış olursa olsun; bir UUID olmayan bir değer `400 validation_failed` yanıtlar. `action`, `<METHOD> <rota şablonu>`'dur; `project`, bir proje id'si ya da `none`'dır. `from`/`to` **UTC günleridir**, ve `to` adlandırılan günü dahil eder. `limit` 1–200'dür (varsayılan 50). `cursor`, önceki sayfanın `nextCursor`'ıdır — kodlanmış bir an değil her zaman bir **satır id'si**, bu yüzden aynı milisaniye içindeki iki olay bir sayfa sınırında birbirini asla kaybedemez; hiçbir satırı adlandırmayan bir cursor `400` yanıtlar. `{ events, nextCursor, filters, retentionDays }` döner — son sayfada `nextCursor` `null`'dır, ve `filters` (farklı aktörler, eylemler, projeler, ve olayları olan hesaplar, seçiciler için) yalnızca ilk sayfada geri gelir |

```bash
curl -s "$API/audit?action=DELETE%20/api/projects/:id&limit=50" \
  -H "Authorization: Bearer $TOKEN" | jq
```

### Nasıl yazılır

Yukarıdaki durum değiştiren her istek, `audit_events`'te isteği yapan hesabı adlandıran bir satır
bırakır — ne yapıldığı, hangi projeye, kaynağa, token'a, üyeliğe ya da hesaba, ve ne zaman. Bir oturum
açma ve ilk hesabın oluşturulması da kaydedilir, ve bir kişinin bir konnektöre bir projeye kalıcı okuma
erişimi verdiği `POST /oauth/authorize` de öyle.

Her handler tarafından değil **politika katmanı** tarafından yazılır, bu yüzden kapsanmadan
eklenebilecek bir rota yoktur — ve katılmama seçeneği olan da yoktur. İstisnalar **sekiz** tanedir — bir
kaynağın test ve upload-session rotaları, üç webhook teslimatı (git, Notion, Confluence) ve üç OAuth
client-registration rotası (`register`, `token`, `revoke`) — her biri `src/auth/policy.ts` içinde kendi
gerekçesiyle adlandırılmıştır.

Bir şey *oluşturan* bir rota kendi yolunda hiçbir şey adlandırmaz, bu yüzden yeni nesnenin id'si yanıttan
geri okunur, aynı dosyadaki sabit yollardan oluşan bir tablo üzerinden, ve yalnızca orada bulunan değer
bir UUID olduğunda tutulur — "bunu kim üretti" bu yüzden log'un yanıtladığı bir sorudur, ve aynı id'yi
adlandıran iptalle hizalanır.

### Neyi tutmaz

Satırlar hiçbir kullanıcı içeriği taşımaz: bir soru, bir belge ve bir alıntı bu tablonun bir sütununa
asla ulaşmaz. Herhangi bir eylemin kaydedebileceği tek gövde alanları aynı politika dosyasında
adlandırılır, her biri kapalı bir değer kümesiyle sınırlıdır (`mode`, `open`/`token`/`account`'tan
biridir, ve benzeri). Bu, onu sorgu günlüğünden farklı bir kayıt tutar — o, ajanların ne sorduğunu
tutar, kendi saklama süresiyle yönetilir ve kendi proje başına anahtarına sahiptir — ikisi bilerek tek
bir tablo değildir.

Bunun ötesinde iki şey bilerek kaydedilmez. **Reddedilen istekler** — bir reddediş, izin matrisinin
çalışmasıdır, ve her yoklamayı loglamak tabloyu bir tarama günlüğüne dönüştürürdü; tek istisna bir
kişinin `/oauth/authorize`'da bir konnektörü reddetmesidir, ki bu matrisin karar vermesi değil birinin
karar vermesidir. Ve **bir şeyi değiştiren ve ardından `5xx` yanıtlayan bir eylem**: satır yalnızca
400'ün altındaki bir yanıt için yazılır, bu yüzden commit edip sonra başarısız olan bir handler hiçbir
şey bırakmaz. Bu API'de şu anda bu şekilde biçimlenmiş hiçbir handler yok, ve `SECURITY.md` bunu olduğu
sınır olarak adlandırır.

### Okumak

`GET /api/audit` ve panonun **Audit log** paneli (hesap menüsü, Users'ın yanında) ikisi de yalnızca
root/admin'dir. Her satır, saklandığı sütunlar yerine bir cümle olarak render edilir ("dana deleted a source from
handbook" — "dana handbook'tan bir kaynak sildi"). "Kim" iki seçicidir: **Actor**, kaydedildiği haliyle etikettir, ve **Account**, hesap
id'sidir — bu, o hesabın kendi eylemlerini sahip olduğu her API token'ınınkilerle birlikte bulur, çünkü
bir token'ın etiketi yalnızca adı ve sahibidir ve onları kendi başına toplayamaz. Bir proje o zamandan
beri silinmişse satırları hâlâ vardır: `project_id` bir foreign key taşımaz, bu yüzden panel *adını*
arayamaz, ve bunu boş bırakmak yerine satırda söyler.

Panelin filtrelediği iki sütun — `actor_label`, çünkü hesaptan daha uzun ömürlüdür, ve `action` —
`0011` migration'ı ile indekslenir. 200.004 satırda, seçici bir actor filtresi indekssiz 9,7 ms'ye karşı
0,07 ms'de çalışır, bir sayfa çevirme 3 ms'dir, ve ilk sayfa 19–52 ms'dir çünkü hiçbir indeksin
yardım edemeyeceği üç `DISTINCT` taramasıyla filtre açılır menülerini de doldurur — filtre değişimi
başına bir kez ödenir, hiçbir zaman sayfa çevirme başına değil.

Log **tamper-evident değildir** — veritabanı erişimi olan herkes bir satırı kaldırabilir ve burada
hiçbir şey bunu göstermez (`SECURITY.md`) — ve `AUDIT_LOG_RETENTION_DAYS`'ten daha eski satırlar,
süresi dolmuş oturumlarla aynı çeyrek saatlik zamanlayıcıda süpürülür.

## Hatalar

| Durum | Anlamı |
|--------|---------|
| `400` | Doğrulama başarısız oldu, ya da geçersiz bir istek (bozuk yol, eksik `SECRET_KEY`, politikanın reddettiği bir parola) — `message` açıklar |
| `401` | Kimlik bilgisi yok, ya da yanlış. `setup_required`, bu örneğin henüz hiç hesabı olmadığı anlamına gelir |
| `403` | Oturum açık, ama izin yok: `forbidden`, `csrf_blocked`, `password_change_required`, `account_disabled`, `token_has_no_account` |
| `404` | Bilinmeyen proje, kaynak ya da oturum — **ve** bu hesabın erişimi olmadığı her proje |
| `409` | Yinelenen ad, proje indeksleniyor, kurulum zaten tamamlandı, ya da son root hesabı. `DELETE /api/auth/oidc/link`, hesabın yerel bir parolası olmadığında bunun yerine `409 last_sign_in_method` yanıtlar — bağlantıyı kesmek onu oturum açacak bir yol bırakmazdı, bu yüzden bir admin bir tane ayarlayana kadar hiçbir şey kaldırılmaz |
| `429` | Çok fazla oturum açma denemesi. `Retry-After` ve `retryAfterSec` ne kadar süreceğini söyler |
| `500` | İç hata — cevap yalnızca bunu söyler; ayrıntılar loglardadır |

Tüm hatalar JSON'dur: `{ "error": "conflict", "message": "…" }`.

## Notlar

- Secret'lar hiçbir endpoint tarafından asla döndürülmez; bir kaynak yalnızca `hasSecret`'ı bildirir, ve
  bir parola, bir geçici parolanın üretildiği o tek sefer dışında hiç döndürülmez.
- `POST /api/projects/:id/reindex`'i sık çağırmak güvenlidir: indeksleme artımlıdır.
- Yetkilendirme, rotaları yöntemlerine ve biçimlerine göre kapsayan tek bir politika tablosundan
  sunucu tarafında kararlaştırılır, bu yüzden yeni bir endpoint var olduğu anda korunur. Panonun
  gizlediği şey kozmetiktir.
- API, panoyla ve MCP endpoint'leriyle aynı porta bağlıdır, bu yüzden birine ulaşabilen ötekilere de
  ulaşabilir ([Güvenlik](/tr/docs/security/)).
