# Adpilot Gateway — UI API Integration Guide

**Audience:** Frontend / mobile developers integrating with Adpilot  
**Service:** `adpilot-gateway` (`api.lucos.com`)  
**Machine-readable contract:** [openapi.yaml](./openapi.yaml) (import into Swagger UI, Postman, or Insomnia)  
**Backend integration contract:** [GATEWAY-REPO-SYNC-CONTRACT.md](./GATEWAY-REPO-SYNC-CONTRACT.md)  
**Lucos IDE Google OAuth:** [GOOGLE-IDE-OAUTH.md](./GOOGLE-IDE-OAUTH.md)

---

## Base URLs

| Environment | Base URL |
|---|---|
| Local | `http://localhost:3007` |
| Production | Set by deployment (e.g. `https://api.lucos.com`) |

All UI routes are under **`/api/v1`**, except liveness probe **`GET /healthz`** (no auth).

**Do not call** internal routes from the browser:

- `/internal/v1/*` — service-to-service only
- Repo-sync `:6001` directly — use gateway proxies instead

---

## Authentication

### Obtaining a session

`POST /api/v1/auth/authenticate` returns a JWT and sets an **httpOnly** cookie named `token`.

Supported methods:

| `authType` | Use case |
|---|---|
| `email` | Sign-in / sign-up with password |
| `google` | Google OAuth (`idToken` + `google_id` + `email`) |

### Sending credentials on subsequent requests

Use **either** (both work):

1. **Bearer header** (recommended for SPA API clients):
   ```
   Authorization: Bearer <jwt>
   ```
2. **Cookie** (automatic when using `credentials: 'include'` on fetch):
   ```
   Cookie: token=<jwt>
   ```

Empty `Authorization: Bearer ` (whitespace only) is rejected with **401**.

### CORS

Gateway reads `CORS_ORIGINS` (comma-separated).

**Lucos IDE / Electron:** the workbench often sends `Origin: vscode-file://vscode-app`. The gateway allows that (and `vscode-webview://…`) when `CORS_ALLOW_ELECTRON_IDE` is not `false` (default: allowed). You can also add the origin explicitly to `CORS_ORIGINS`.

Browser clients must:

```javascript
fetch(`${API_BASE}/api/v1/auth/me`, {
  credentials: 'include',
  headers: { Authorization: `Bearer ${token}` } // optional if cookie is set
});
```

### JWT context (resolved per request)

After auth, the gateway attaches org context server-side. The UI does **not** send `orgId` on most routes — it is derived from membership.

| Field (server-side) | Meaning |
|---|---|
| `orgId` | Active organization |
| `orgRole` | `owner` \| `developer` \| … |
| `isOrgMember` | `true` → access all org repos; `false` → team-scoped `repoIds` only |
| `planCode` | e.g. `free`, `pro` |
| `subscriptionStatus` | `active`, `canceled`, `past_due`, … |

---

## Roles & gates

| Gate | Applies to | HTTP when blocked |
|---|---|---|
| `requireAuth` | Most `/api/v1/*` | 401 |
| `requireOwner` | Connect/disconnect providers, sync catalog, import | 403 |
| `requireActiveSubscription` | RAG, indexed repos, repo status | 403 `SUBSCRIPTION_INACTIVE` |
| `requireRepoLimit` | Import, admin re-sync | 403 `REPO_LIMIT_EXCEEDED` |
| `requireModelAccess` | `POST /search`, `POST /query` | 403 `MODEL_NOT_ALLOWED` |
| `requireRepoAccess` | `GET /repos/:id/status` | 403 `REPO_ACCESS_DENIED` |

---

## Response envelopes

### Success (most routes)

```json
{
  "success": true,
  "...": "route-specific fields"
}
```

Auth routes also return `user` and `token`. Integration list routes may omit `success` and return domain objects directly (see OpenAPI per path).

### Error (all routes)

```json
{
  "success": false,
  "message": "Human-readable summary",
  "details": {
    "code": "ERROR_CODE",
    "subscriptionStatus": "canceled"
  }
}
```

### Common error codes

| Code | HTTP | When |
|---|---|---|
| `SUBSCRIPTION_INACTIVE` | 403 | Org subscription not active |
| `ORG_REQUIRED` | 403 | No org context |
| `REPO_ACCESS_DENIED` | 403 | Team member without repo assignment |
| `REPO_LIMIT_EXCEEDED` | 403 | Plan indexed-repo cap |
| `MODEL_NOT_ALLOWED` | 403 | Model not on plan |
| `INVALID_PAYLOAD` | 400 | Bad request body |
| `PROVIDER_REPO_NOT_FOUND` | 404 | Import ID unknown for org |
| `NOT_IMPORTABLE` | 400 | Repo flagged not importable |
| `CONNECTION_INACTIVE` | 400 | Provider disconnected |
| `BITBUCKET_SERVER_NOT_ENABLED` | 403 | Org lacks BB Server feature |
| `BITBUCKET_SERVER_NOT_CONFIGURED` | 503 | Server env not set |
| `TOKEN_MINT_FAILED` | 503 | Clone credential mint failed |

---

## UI journeys

### 1. Sign in

```
POST /api/v1/auth/authenticate  →  store token (optional) + cookie set
GET  /api/v1/auth/me            →  profile
GET  /api/v1/plan-usage         →  plan + daily question limits
```

### 2. Connect providers & browse catalog

```mermaid
sequenceDiagram
    participant UI
    participant GW as Gateway

    UI->>GW: GET /integrations/status
    alt GitHub / Bitbucket not connected
        UI->>GW: POST /integrations/{provider}/connect
        GW-->>UI: redirectUrl
        Note over UI: Browser OAuth / App install
        Note over GW: Callback saves connection + auto-syncs catalog
    end
    alt Bitbucket Server (admedia orgs)
        Note over UI: Auto-connected on signup; catalog synced in background
    end
    UI->>GW: GET /integrations/repos?limit=50&offset=0
    GW-->>UI: unified catalog (all providers)
```

**Provider URL slugs** (path segment): `github`, `bitbucket`, `bitbucket-server`  
**Provider values in JSON bodies/fields:** `github`, `bitbucket`, `bitbucket_server`

### 3. Import & monitor indexing

```
POST /api/v1/repos/import              →  202, jobs with repoId
GET  /api/v1/repos/{repoId}/status     →  poll sync (URL-encode repoId)
GET  /api/v1/indexed-repos             →  org's imported repos + index status
```

**repoId format** after import: `github:org/repo`, `bitbucket:workspace/slug`, `bitbucket_server:PROJECT/slug`  
URL-encode for path: `github%3Aorg%2Frepo`

**providerRepoIds** for import come from catalog `_id` or `providerRepoId` — not the repo-sync `repoId`.

### 4. Ask questions (RAG)

```
POST /api/v1/search   →  rag-service POST /api/v1/search/
POST /api/v1/query    →  rag-service POST /api/v1/query/  (Accept: text/event-stream for SSE)
GET  /api/v1/models   →  rag-service GET  /api/v1/models/
GET  /api/v1/repos    →  rag-service GET  /api/v1/repos/  (RAG repos — not provider catalog)
```

Gateway injects default `model` from plan if omitted. See [openapi.yaml](./openapi.yaml) for request fields.

---

## Endpoint index

### Health

| Method | Path | Auth |
|---|---|---|
| GET | `/healthz` | No |
| GET | `/api/v1/health` | No |

### Auth — `/api/v1/auth`

| Method | Path | Auth | Description |
|---|---|---|---|
| POST | `/authenticate` | No | Sign in / sign up (email or Google) |
| GET | `/me` | Yes | Current user profile |
| POST | `/logout` | Yes | Clear session cookie |
| POST | `/forgot-password` | No | Send reset OTP |
| POST | `/reset-password` | No | Reset password with OTP |
| DELETE | `/delete-account` | Yes | Delete account |

### Integrations — `/api/v1/integrations`

| Method | Path | Role | Description |
|---|---|---|---|
| GET | `/status` | member | Connection status per provider |
| POST | `/github/connect` | owner | GitHub App install URL |
| GET | `/github/callback` | — | Browser callback (gateway redirects) |
| POST | `/bitbucket/connect` | owner | Bitbucket OAuth URL |
| GET | `/bitbucket/callback` | — | Browser callback |
| GET | `/repos` | member | **Unified catalog** across providers (paginated) |
| GET | `/repos/search` | member | Keyword search on catalog |
| GET | `/{provider}/repos` | member | Per-provider catalog |
| POST | `/{provider}/repos/sync` | owner | Refresh catalog from provider API |
| DELETE | `/{provider}` | owner | Disconnect provider |

#### `GET /integrations/repos` (unified catalog)

```http
GET /api/v1/integrations/repos?limit=50&offset=0&provider=github
Authorization: Bearer <token>
```

| Query | Default | Description |
|---|---|---|
| `limit` | 50 | 1–100 |
| `offset` | 0 | Pagination offset |
| `provider` | — | Optional filter: `github`, `bitbucket`, `bitbucket-server` |

Includes **Bitbucket Server** when org `features.bitbucketServer` is true and catalog was synced.

Response:

```json
{
  "repos": [ { "_id": "...", "provider": "github", "fullName": "org/repo", "isImportable": true } ],
  "providers": ["github", "bitbucket_server"],
  "count": 50,
  "total": 142,
  "limit": 50,
  "offset": 0,
  "hasMore": true
}
```

#### `ProviderRepo` shape (catalog item)

| Field | Type | Notes |
|---|---|---|
| `_id` | string | Use in `providerRepoIds` for import |
| `provider` | string | `github` \| `bitbucket` \| `bitbucket_server` |
| `providerRepoId` | string | Provider-native ID (also valid for import) |
| `fullName` | string | `owner/repo` or `PROJECT/slug` |
| `defaultBranch` | string | e.g. `main` |
| `visibility` | string | `public` \| `private` \| `internal` |
| `isImportable` | boolean | `false` → import returns 400 |
| `lastSeenAt` | string | ISO date from last sync |

### Repositories & indexing — `/api/v1`

| Method | Path | Role | Description |
|---|---|---|---|
| POST | `/repos/import` | owner | Import selected catalog repos |
| GET | `/indexed-repos` | member | Imported repos + indexing status |
| GET | `/repos/:id/status` | member | Sync status for one repo |
| POST | `/admin/repos/:id/sync` | owner | Trigger re-sync (legacy/admin) |
| POST | `/search` | member | RAG semantic search → `POST /api/v1/search/` |
| POST | `/query` | member | RAG Q&A (JSON or SSE) → `POST /api/v1/query/` |
| GET | `/models` | member | Available LLM models → `GET /api/v1/models/` |
| GET | `/repos` | member | RAG indexed repo list → `GET /api/v1/repos/` |

> **Naming note:** `GET /api/v1/integrations/repos` = provider **catalog**.  
> `GET /api/v1/repos` = **RAG** indexed repo metadata. Different resources.

#### `POST /repos/import`

```json
{ "providerRepoIds": ["665a1b2c3d4e5f6789012345"] }
```

Response **202** includes `jobs[].repoId` (repo-sync ID) for polling.

#### `GET /indexed-repos`

```json
{
  "repos": [
    {
      "repo_id": "github:my-org/my-repo",
      "repo_name": "my-repo",
      "git_url": "https://github.com/my-org/my-repo.git",
      "status": "ready",
      "last_indexed_at": "2026-06-18T14:00:00.000Z"
    }
  ]
}
```

| `status` | UI hint |
|---|---|
| `indexing` | Show spinner |
| `ready` | Enable Q&A |
| `failed` | Show retry / support |

### Billing — `/api/v1/billing`

| Method | Path | Description |
|---|---|---|
| GET | `/summary` | Plan, renewal, payment method |
| GET | `/invoices` | Invoice list |
| GET | `/invoices/:id/download` | Download invoice (PDF bytes) |
| POST | `/payment-method` | Update card on file |
| POST | `/subscribe` | Subscribe to plan |
| POST | `/subscription/cancel` | Cancel paid plan |
| POST | `/subscription/reactivate-free` | Downgrade to free |

### Payments — `/api/v1/payments`

| Method | Path | Description |
|---|---|---|
| POST | `/charge` | One-off charge |
| GET | `/` | List payments |
| GET | `/:id` | Payment detail |

### Plan usage — `/api/v1/plan-usage`

| Method | Path | Description |
|---|---|---|
| GET | `/` | Current plan, catalog, daily question usage |

### Chat — `/api/v1/chat`

| Method | Path | Description |
|---|---|---|
| POST | `/conversations` | Create conversation |
| GET | `/conversations` | List conversations |
| GET | `/conversations/:id` | Get conversation |
| GET | `/conversations/:id/messages` | List messages |
| POST | `/conversations/:id/messages` | Send message (sync bot reply) |

### Appearance — `/api/v1/appearance`

| Method | Path | Description |
|---|---|---|
| GET | `/` | User UI preferences |
| PATCH | `/` | Update preferences (`theme`, `language`, `density`, …) |

---

## Bitbucket Server (on-prem)

| Topic | Behavior |
|---|---|
| Eligibility | Org `features.bitbucketServer === true` (auto for `@admedia.com` signups) |
| Connect | No OAuth — auto-bootstrapped + catalog synced on `@admedia.com` signup when env configured |
| Catalog | Appears in `GET /integrations/repos` when eligible + synced |
| `fullName` | `PROJECT/slug` (e.g. `AD/my-repo`) |
| Import `repoId` | `bitbucket_server:AD/my-repo` |

---

## Streaming query (SSE)

```javascript
const res = await fetch(`${API_BASE}/api/v1/query`, {
  method: 'POST',
  credentials: 'include',
  headers: {
    'Content-Type': 'application/json',
    Accept: 'text/event-stream',
    Authorization: `Bearer ${token}`
  },
  body: JSON.stringify({ question: 'How is auth implemented?', repoIds: ['github:org/repo'] })
});

const reader = res.body.getReader();
// parse SSE frames from reader
```

Without `Accept: text/event-stream`, response is JSON (proxied from RAG service).

---

## OpenAPI / Swagger

Import **`docs/openapi.yaml`**:

- **Swagger UI:** https://editor.swagger.io → File → Import
- **Postman:** Import → OpenAPI 3.0
- **Local:** `npx @redocly/cli preview-docs docs/openapi.yaml`

Regenerate client SDKs from the same file if needed.

---

## Related docs

| Document | Purpose |
|---|---|
| [openapi.yaml](./openapi.yaml) | Full request/response schemas |
| [API-REFERENCE.md](./API-REFERENCE.md) | Operator / curl-focused reference |
| [GATEWAY-REPO-SYNC-CONTRACT.md](./GATEWAY-REPO-SYNC-CONTRACT.md) | Import payload contract |
| [SETUP.md](../SETUP.md) | Local dev setup |
