# API

**Full API contract (request/response schemas):** [API-REFERENCE.md](./API-REFERENCE.md)

## Current Endpoints

| Method | Path | Description |
| --- | --- | --- |
| GET | `/` | Service status |
| GET | `/healthz` | Liveness probe |
| GET | `/readyz` | Readiness probe |
| GET | `/version` | Service version |
| POST | `/admin/repos/{repo_id}/sync` | Ingest or re-sync a repository (202 Accepted, async) |
| POST | `/admin/repos/{repo_id}/force-sync` | Purge pipeline state, delete clone, fresh re-index (202 Accepted, async) |
| POST | `/repos/import` | Gateway-driven batch import with per-request clone credentials (202 Accepted, async) |
| POST | `/webhooks/bitbucket` | Bitbucket Server push webhook (`repo:refs_changed`) |
| GET | `/repos/{repo_id}` | Repository metadata |
| GET | `/repos/{repo_id}/status` | Repository sync status (gateway polling) |
| GET | `/repos/{repo_id}/snapshots/{snapshot_id}` | Snapshot metadata |
| GET | `/indexed-repos` | All tracked repositories with minimal indexing status for UI |

### Indexed repositories (UI list)

Return all tracked repositories with minimal metadata for UI listing. Status is derived from the latest indexing run:

| Status | Meaning |
| --- | --- |
| `indexing` | Pipeline still in progress or no indexing run yet |
| `ready` | All required stages complete, including embedding |
| `failed` | Run or any critical stage failed |

```http
GET /indexed-repos
```

**Response (200 OK):**

```json
{
  "repos": [
    {
      "repo_id": "ad/adpilot-indexing-repo-sync.com",
      "repo_name": "adpilot-indexing-repo-sync.com",
      "git_url": "https://bit.admedia.com/scm/ad/adpilot-indexing-repo-sync.com.git",
      "status": "ready",
      "last_indexed_at": "2026-06-17T14:00:00Z"
    },
    {
      "repo_id": "ad/example-repo.com",
      "repo_name": "example-repo.com",
      "git_url": "https://bit.admedia.com/scm/ad/example-repo.com.git",
      "status": "failed",
      "last_indexed_at": "2026-06-17T10:00:00Z",
      "latest_snapshot_id": "snap_failed123",
      "failed_stage": "commit_intel",
      "error": "save delta: upsert graph delta: an inserted document is too large"
    },
    {
      "repo_id": "ad/indexing-repo.com",
      "repo_name": "indexing-repo.com",
      "git_url": "https://bit.admedia.com/scm/ad/indexing-repo.com.git",
      "status": "indexing",
      "last_indexed_at": "2026-06-17T10:00:00Z"
    }
  ]
}
```

Results are sorted newest-first by `last_indexed_at` (falls back to repository `updated_at` when completion time is unavailable). Repositories with no sync history return `status: "indexing"` and `last_indexed_at: null`.

Detailed stage progress remains available via `GET /repos/{repo_id}/indexing-runs/latest`.


Trigger first-time clone or incremental sync for a Bitbucket repository.

```http
POST /admin/repos/ad%2Fadpilot-indexing-repo-sync.com/sync
```

URL-encode `/` in `repo_id` as `%2F` (path must be a single segment for the router).

Omit `ref` to sync **`master`** (default). All repository ingestion uses the `master` branch.

| Query | Default | Description |
| --- | --- | --- |
| `ref` | `master` | Branch name or commit SHA to sync |

**Response (202 Accepted):**

```json
{
  "repo_id": "ad/adpilot-indexing-repo-sync.com",
  "ref": "master",
  "job_id": "job_a1b2c3d4e5f67890",
  "status": "accepted"
}
```

Sync runs in the background: git clone/fetch → MongoDB metadata → Redis events.

`repo_id` format: `{project_key}/{repo_slug}` (must match Bitbucket project and slug).

### Force sync (stuck or failed repositories)

Purges per-repo pipeline Mongo data across all indexing databases, deletes Qdrant vectors via embedding-engine, removes the on-disk git clone, then starts a **fresh** sync (`mode: fresh`).

Use when a repository is stuck in `indexing` or `failed`. For healthy repositories, use `/sync` (re-index) instead.

```http
POST /admin/repos/ad%2Fadpilot-indexing-repo-sync.com/force-sync
```

| Query | Default | Description |
| --- | --- | --- |
| `ref` | `master` | Branch name or commit SHA to sync |
| `force` | `false` | Admin override to allow force sync when latest run is `ready` |

**Response (202 Accepted):**

```json
{
  "repo_id": "ad/adpilot-indexing-repo-sync.com",
  "ref": "master",
  "job_id": "job_a1b2c3d4e5f67890",
  "status": "accepted",
  "mode": "fresh",
  "purged": {
    "adpilot_repo_sync.repositories": 1,
    "adpilot_repo_sync.indexing_runs": 2,
    "adpilot_code_parser.code_chunks": 120,
    "adpilot_indexing.embedding_records": 95,
    "repo_docs.doc_chunks": 8
  }
}
```

Returns **409 Conflict** when another sync is in progress or when the latest run completed successfully (unless `force=true`).

Redis streams are **not** purged (shared infrastructure).

### Gateway import — batch repository import

Accepts a gateway-orchestrated import payload with short-lived clone credentials (contract v1.1). Does not persist credentials. Each repo is cloned with the provided `cloneAuth`, then follows the same metadata + Redis pipeline as admin sync.

```http
POST /repos/import
Content-Type: application/json
X-Org-Id: org_abc123
```

**Request body:**

```json
{
  "importJobId": "550e8400-e29b-41d4-a716-446655440000",
  "orgId": "org_abc123",
  "connectionId": "conn_xyz",
  "provider": "github",
  "cloneAuth": {
    "username": "x-access-token",
    "password": "ghp_...",
    "expiresAt": "2026-06-18T12:00:00Z"
  },
  "repos": [
    {
      "providerRepoId": "123456",
      "fullName": "acme/widget",
      "defaultBranch": "main",
      "cloneUrl": "https://github.com/acme/widget.git"
    }
  ]
}
```

Gateway `repo_id` format: `{provider}:{owner}/{repo}` (e.g. `github:acme/widget`). Legacy Bitbucket `repo_id` (`project/slug`) is unchanged for admin sync and webhooks.

**Response (202 Accepted):**

```json
{
  "importJobId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "accepted",
  "jobs": [
    {
      "jobId": "job_a1b2c3d4e5f67890",
      "providerRepoId": "123456",
      "fullName": "acme/widget",
      "repoId": "github:acme/widget",
      "status": "pending"
    }
  ]
}
```

**Duplicate import (409 Conflict):** same `importJobId` already processed; body returns the existing job snapshot.

**Errors:** `400` invalid payload; `500` internal failure.

### Bitbucket webhook — push to master

Receives Bitbucket Server `repo:refs_changed` events. Only **master** branch pushes trigger sync; other branches return 202 ignored.

```http
POST /webhooks/bitbucket
X-Event-Key: repo:refs_changed
X-Hub-Signature: sha256=<hmac-hex>
Content-Type: application/json
```

Configure the same secret in Bitbucket and `BITBUCKET_WEBHOOK_SECRET`. Signature is HMAC-SHA256 of the **raw request body**.

**Accepted (202):**

```json
{
  "status": "accepted",
  "repo_id": "ad/adpilot-indexing-repo-sync.com",
  "ref": "master",
  "job_id": "job_a1b2c3d4e5f67890"
}
```

**Ignored (202)** — no sync triggered:

```json
{ "status": "ignored", "reason": "not master branch", "repo_id": "ad/example-repo" }
```

Other ignored reasons: `unsupported event`, `duplicate commit`.

**Errors:** `401` invalid/missing signature (required when secret is set or `ENVIRONMENT` is not `local`); `400` malformed payload.

Local dev with `ENVIRONMENT=local` and empty `BITBUCKET_WEBHOOK_SECRET`: signature verification is skipped.

### Repository metadata

Return persisted repository metadata from MongoDB.

```http
GET /repos/ad%2Fadpilot-indexing-repo-sync.com
```

**Response (200 OK):**

```json
{
  "repo_id": "ad/adpilot-indexing-repo-sync.com",
  "clone_url": "https://bit.admedia.com/scm/ad/adpilot-indexing-repo-sync.com.git",
  "default_ref": "master",
  "last_commit_sha": "178864a7d521b6f5e720b386b2c2b0ef8563e0dc",
  "last_sync_at": "2026-05-26T12:00:00Z",
  "status": "active",
  "updated_at": "2026-05-26T12:00:00Z"
}
```

**Errors:** `404` repository not found; `400` invalid `repo_id`.

### Repository sync status

Lightweight status for gateway polling after import.

```http
GET /repos/github%3Aacme%2Fwidget/status
```

URL-encode `:` as `%3A` and `/` as `%2F` in gateway `repo_id`.

**Response (200 OK):**

```json
{
  "repo_id": "github:acme/widget",
  "status": "active",
  "last_commit_sha": "178864a7d521b6f5e720b386b2c2b0ef8563e0dc",
  "last_sync_at": "2026-06-18T12:00:00Z",
  "default_ref": "main"
}
```

**Errors:** `404` repository not found; `400` invalid `repo_id`.

### Snapshot metadata

Return persisted snapshot metadata for a repository.

```http
GET /repos/ad%2Fadpilot-indexing-repo-sync.com/snapshots/snap_a1b2c3d4e5f67890
```

**Response (200 OK):**

```json
{
  "snapshot_id": "snap_a1b2c3d4e5f67890",
  "repo_id": "ad/adpilot-indexing-repo-sync.com",
  "commit_sha": "178864a7d521b6f5e720b386b2c2b0ef8563e0dc",
  "ref": "master",
  "file_count": 128,
  "status": "ready",
  "created_at": "2026-05-26T12:00:00Z"
}
```

**Errors:** `404` snapshot not found; `400` invalid `repo_id` or `snapshot_id`.

URL-encode `/` in `repo_id` as `%2F` for all repo paths.

## Environment Variables

| Variable | Default | Description |
| --- | --- | --- |
| `HOST` | `0.0.0.0` | Bind host |
| `PORT` | `6001` | Bind port |
| `LOG_LEVEL` | `info` | Log level |
| `ENVIRONMENT` | `local` | Runtime environment (`local` skips required-field validation) |
| `REDIS_URL` | `redis://localhost:6379` | Redis connection URL (local default only) |
| `REDIS_STREAM_PREFIX` | *(empty)* | Optional stream name prefix for multi-env isolation |
| `GIT_WORKSPACE_PATH` | `./data/repos` | Local directory for cloned repositories (local default only) |
| `BITBUCKET_BASE_URL` | `https://bit.admedia.com` | Bitbucket Server base URL (local default only) |
| `BITBUCKET_USERNAME` | — | Bitbucket HTTP username for clone/fetch (required when `ENVIRONMENT` is not `local`) |
| `BITBUCKET_TOKEN` | — | HTTP access token for clone/fetch (required when `ENVIRONMENT` is not `local`) |
| `BITBUCKET_WEBHOOK_SECRET` | — | Webhook signature secret (required when `ENVIRONMENT` is not `local`) |
| `MONGO_URI` | `mongodb://localhost:27018/adpilot_repo_sync` | MongoDB connection string (local Docker default) |
| `MONGODB_DATABASE` | `adpilot_repo_sync` | MongoDB database name |

When `ENVIRONMENT` is not `local`, `REDIS_URL`, `GIT_WORKSPACE_PATH`, `BITBUCKET_BASE_URL`, `BITBUCKET_USERNAME`, `BITBUCKET_TOKEN`, `BITBUCKET_WEBHOOK_SECRET`, `MONGO_URI`, and `MONGODB_DATABASE` must all be set explicitly.

MongoDB setup: [mongodb.md](mongodb.md).
