# API Routes

Next.js API routes providing server-side functionality.



Next.js API routes providing server-side functionality.

## /api/notes/\[slug]/views [#apinotesslugviews]

Per-article view tracking.

* `GET` - Retrieve view count for a slug
* `POST` - Increment view count (rate-limited to 1/min per user/IP/slug)

**Database**: Vercel Postgres.

## /api/notes/views [#apinotesviews]

Batch view count retrieval for multiple articles in a single request.

* `GET` - Retrieve view counts for multiple slugs

**Query params**: `slugs` (comma-separated list of slugs)

**Example**: `/api/notes/views?slugs=my-first-post,my-second-post`

Used by the homepage to fetch all article view counts in one request instead of N individual calls.

## /api/vsco [#apivsco]

VSCO gallery integration with pagination and local manifest fallback.

**Query params**: `limit`, `offset`

## /api/gumroad/products [#apigumroadproducts]

Gumroad product listing. Responses are cached in-memory for 1 hour with `Cache-Control` headers.

## /api/chatbot [#apichatbot]

AI chatbot powered by OpenAI Assistant API. Logs all queries to `chatbot_logs` table in Postgres.

* `POST` - Send a message to the assistant

**Body params**: `message` (required), `threadId` (optional), `noteSlug` (optional)

When `noteSlug` is provided and maps to a valid article, the note's content is injected as `additional_instructions` into the OpenAI run so the assistant can answer questions specific to that note. See [Chatbot — Note-Aware Chat](/docs/features/chatbot#note-aware-chat) for details.

## /api/dashboard/chatbot-logs [#apidashboardchatbot-logs]

Paginated chatbot query logs for the private dashboard.

* `GET` - Returns chatbot logs (newest first, 50 per page)

**Auth**: Requires `Authorization: Bearer <DASHBOARD_SECRET>` header.

**Query params**: `page` (default: 1)

**Env vars**: `DASHBOARD_SECRET`, `POSTGRES_URL`

## /api/dashboard/chatbot-stats [#apidashboardchatbot-stats]

Aggregate analytics for the private dashboard, computed directly in the database.

* `GET` - Returns summary stats, 30-day daily counts, and 24-hour distribution

**Auth**: Requires `Authorization: Bearer <DASHBOARD_SECRET>` header.

**Env vars**: `DASHBOARD_SECRET`, `POSTGRES_URL`

## /api/emails [#apiemails]

Project intake delivery through Formcarry (`POST`). The server validates the same
schema as the form in `src/lib/content/contact.ts`.

Required: `name`, `email`, `projectType`, `timeline`, and `message` (20–5,000
characters). Optional: `company`, HTTP(S) `website`, `budget`, and `source`
(`clients` or `contact`). Invalid JSON or fields return 400; an unavailable
configuration returns 503; delivery failure or timeout returns 502. Successful
requests return 200. Errors are JSON with a recoverable `message`.

The UI retains details after a failed request and replaces the form with a
confirmation after success. Tests mock delivery; they never send actual messages.

## /api/github/repos [#apigithubrepos]

GitHub repository data for the open source section. Responses include `Cache-Control: public, s-maxage=3600, stale-while-revalidate=7200`.

## /api/spotify/now-playing [#apispotifynow-playing]

Spotify integration showing the current or recently played tracks.

* `GET` - Returns current track (if playing) and up to 3 recently played tracks

Refreshes the access token automatically using the stored refresh token. The player state and recently-played list are fetched in parallel. Context names (playlist/artist) are cached in-memory for 5 minutes to avoid a serial upstream call on every poll.

**Cache-Control**: `public, s-maxage=15, stale-while-revalidate=30`

**Response**:

```json
{
  "isPlaying": true,
  "current": {
    "title": "...",
    "artist": "...",
    "album": "...",
    "albumArt": "https://i.scdn.co/...",
    "spotifyUrl": "https://open.spotify.com/track/..."
  },
  "recentlyPlayed": [],
  "progressMs": 42000,
  "durationMs": 213000,
  "device": "MacBook Pro",
  "context": {
    "name": "My Playlist",
    "type": "private playlist",
    "url": "https://open.spotify.com/playlist/..."
  },
  "shuffle": false,
  "repeat": "off"
}
```

**Env vars**: `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET`, `SPOTIFY_REFRESH_TOKEN`

## /api/search [#apisearch]

Full-text content search across blog posts.

## /api/og [#apiog]

Dynamic OG image generation. See [SEO documentation](/docs/features/seo).

**Query params**: `title`, `description`, `type`, `theme`, `image`

## Error Handling [#error-handling]

All routes return consistent error responses:

```json
{
  "error": "Error message",
  "status": 400
}
```
