---
title: Authentication
description: How to authenticate with the PostGrad Knowledge API using API keys.
---

## API Keys

PostGrad uses API keys to authenticate requests to the Knowledge API. Keys use the `pg_live_` prefix for production environments and `pg_test_` for test environments.

To get an API key:

1. Sign up at [postgrad.io](https://postgrad.io) and subscribe to a plan
2. Go to **Dashboard > API Keys**
3. Click **Create New Key** and give it a descriptive name
4. Copy the key immediately -- it is shown only once

Store your API key securely. If you lose it, you can revoke the old key and create a new one from the dashboard.

## Using Your API Key

Every knowledge request needs **two** headers:

```
Authorization: Bearer pg_live_xxxxxxxxxxxxx   ← your API key
X-PostGrad-Feed: <feed-uuid>                   ← which feed to query
```

The `X-PostGrad-Feed` header tells PostGrad which of your subscribed feeds the request should run against. Call `GET /api/v1/feeds` (no feed header required) to list feed ids for your account.

Accepted values:

- A feed UUID — scopes the request to that one feed.
- `all` — search every feed you're actively subscribed to and get merged, relevance-ranked results. Each hit includes `feed_id` + `feed_name`. Fan-out counts as one request against your monthly quota. Currently supported on `/knowledge/search` only.
- Omitted — on search-style endpoints, PostGrad auto-picks your most-populated feed and returns which one it used in the `X-PostGrad-Feed-Auto-Selected` response header. On list/fetch-by-id endpoints, the header is required.

Legacy compatibility: the feed id is also accepted as a `?feed_id=<uuid>` query parameter or `{ "feed_id": "<uuid>" }` JSON body field. Both paths return a `Warning: 299 postgrad` response header asking you to migrate to the `X-PostGrad-Feed` header form. Prefer the header for all new integrations.

### curl

```bash
curl -H "Authorization: Bearer pg_live_xxxxxxxxxxxxx" \
     -H "X-PostGrad-Feed: b1a2c3d4-e5f6-7890-abcd-ef0123456789" \
  https://postgrad.io/api/v1/knowledge
```

### JavaScript (fetch)

```typescript
const response = await fetch('https://postgrad.io/api/v1/knowledge', {
  headers: {
    'Authorization': 'Bearer pg_live_xxxxxxxxxxxxx',
    'X-PostGrad-Feed': 'b1a2c3d4-e5f6-7890-abcd-ef0123456789',
  },
});
const data = await response.json();
```

### JavaScript (axios)

```typescript
import axios from 'axios';

const client = axios.create({
  baseURL: 'https://postgrad.io/api/v1',
  headers: {
    'Authorization': 'Bearer pg_live_xxxxxxxxxxxxx',
    'X-PostGrad-Feed': 'b1a2c3d4-e5f6-7890-abcd-ef0123456789',
  },
});

const { data } = await client.get('/knowledge');
```

### Python (requests)

```python
import requests

response = requests.get(
    "https://postgrad.io/api/v1/knowledge",
    headers={
        "Authorization": "Bearer pg_live_xxxxxxxxxxxxx",
        "X-PostGrad-Feed": "b1a2c3d4-e5f6-7890-abcd-ef0123456789",
    },
)
data = response.json()
```

## Tier Access

Your subscription tier determines rate limits and monthly quotas. **All tiers have access to every feed and category they're subscribed to, and to every search mode** — there are no category or search-mode restrictions by tier.

| Feature | Starter | Pro | Scale |
|---|---|---|---|
| **Rate limit** | 20 req/min | 60 req/min | 200 req/min |
| **Monthly quota** | 1,000 requests | 10,000 requests | 50,000 requests |
| **Search modes** | Keyword + Semantic + Hybrid | Keyword + Semantic + Hybrid | Keyword + Semantic + Hybrid |
| **Feed access** | All subscribed | All subscribed | All subscribed |

### Search Modes (available on every tier)

As of 2026-06-20, all three search modes work on every tier. Mode is a
per-query tuning choice, not a paid upgrade. Tiers differ on **volume**
(monthly queries), **rate limit** (per-minute burst), and **ops features**
(webhooks, priority/dedicated support) — not on retrieval capability.

| Mode | Availability | Description |
|---|---|---|
| `keyword` | All tiers | Postgres full-text search (`tsvector` + `ts_rank`). Best for exact-term / acronym / SKU queries. |
| `semantic` | All tiers | Gemini 768-dim embeddings + pgvector cosine distance. Best for paraphrase / conceptual queries. Query embedding is Upstash-cached for 24h. |
| `hybrid` | All tiers | Reciprocal Rank Fusion of keyword + semantic (k=60). Best for general-purpose agents with heterogeneous query distributions. |

For a full explanation of each mode and how to pick, see [MCP Integration → Search modes explained](/docs/mcp-integration#search-modes-explained).

No tier returns `TIER_INSUFFICIENT` for a search mode anymore — request
`keyword`, `semantic`, or `hybrid` freely on any plan.

## Error Responses

All errors follow the standard response envelope with `data: null` and an `error` object.

### 401 Unauthorized

Returned when the API key is missing or invalid.

```json
{
  "data": null,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid or missing API key"
  }
}
```

### Complete Error Reference

Every error returns the envelope `{ "data": null, "error": { "code", "message", "details?" } }`. Some include a `details` object with actionable fields (retry time, upgrade URL, feed ID).

| HTTP | Code | When | What to do |
|---|---|---|---|
| 400 | `VALIDATION_ERROR` | Request body / query params missing or malformed | Fix the request and retry |
| 400 | `INVALID_FEED` | `X-PostGrad-Feed` header missing or malformed UUID on a multi-feed account | Call `GET /api/v1/feeds` to discover valid feed IDs |
| 401 | `UNAUTHORIZED` | Missing, invalid, or revoked API key | Check your `Authorization: Bearer` header. If the key is revoked, create a new one in the dashboard |
| 403 | `SUBSCRIPTION_INACTIVE` | Your PostGrad platform subscription is past_due or canceled | Reactivate at `/dashboard/settings?tab=billing` |
| 403 | `FEED_NOT_SUBSCRIBED` | You're not subscribed to the feed you tried to query | Subscribe via the dashboard or the `subscribe_feed` MCP tool |
| 403 | `TIER_INSUFFICIENT` | Reserved for tier-gated features. **Search modes are no longer gated** — all modes work on all tiers — so you will not hit this for `mode=`. | Upgrade your tier only if a future tier-gated feature returns this |
| 404 | `NOT_FOUND` | Requested entry/resource doesn't exist | Double-check the ID |
| 404 | `FEED_UNAVAILABLE` | Feed exists but has been archived or suspended | Try a different feed — check `/api/v1/feeds` |
| 412 | `LEGAL_REVIEW_REQUIRED` | You must accept updated terms before continuing | Visit the `accept_url` in `details` (usually `/signup/legal`) |
| 412 | `STRIPE_CONNECT_REQUIRED` | Licensor action needs Stripe Connect setup (e.g. creating a paid feed) | Visit `/dashboard/connect` to finish Connect onboarding |
| 429 | `RATE_LIMIT_EXCEEDED` | Per-minute or monthly quota exceeded | Respect the `Retry-After` response header |
| 500 | `INTERNAL_ERROR` | Unexpected server error | Retry with exponential backoff. If it persists, check status page |
| 503 | `SERVICE_UNAVAILABLE` | Upstream dependency (Gemini, Supabase) is degraded | Retry with backoff |

### Handling 429 Rate Limits

Rate-limit responses include a `Retry-After` header specifying how many seconds to wait:

```
HTTP/2 429
Retry-After: 45
```

**Handling rate limits:**
- Always respect `Retry-After` — exponential backoff on top is a bonus but the header value is the floor
- Check the `meta.queries_remaining` field in successful responses to monitor usage
- Use the [GET /usage](/docs/api/getUsage) endpoint to check your current period consumption
- Consider upgrading your tier if you consistently hit limits

### MCP Errors

MCP tools return errors wrapped in the tool result's `content[0].text` as a JSON string, with `isError: true` on the response. The error `code` values match REST API codes above, so you can write shared retry logic:

```json
{
  "content": [{ "type": "text", "text": "{\"error\":\"FEED_NOT_SUBSCRIBED\",\"message\":\"...\"}" }],
  "isError": true
}
```

One MCP-only code exists: `CATEGORY_RESTRICTED` (403-equivalent) — your API key is limited to a subset of categories via an admin-set `allowed_categories` override.

## Key Rotation

To rotate an API key with zero downtime:

1. **Create a new key** from Dashboard > API Keys
2. **Update your integration** to use the new key
3. **Verify** the new key works by making a test request
4. **Revoke the old key** from the dashboard

By creating the new key before revoking the old one, your integration experiences no interruption. Both keys remain active during the transition window.

## Security Best Practices

- **Never commit keys to version control.** Use `.env` files or a secrets manager.
- **Use environment variables** to store keys in your application:
  ```bash
  export POSTGRAD_API_KEY=pg_live_xxxxxxxxxxxxx
  ```
- **Use different keys for different environments.** Create separate keys for development, staging, and production.
- **Use different keys for different services.** If multiple services consume the API, give each its own key for independent revocation.
- **Revoke unused keys promptly.** Remove keys for decommissioned services or departed team members.
- **Monitor usage regularly.** Check the dashboard or the `/usage` endpoint for unexpected spikes that could indicate a leaked key.


## Browser usage and CORS

**The REST API is server-side only.** PostGrad intentionally does not emit CORS headers on its `/api/v1/*` endpoints — a browser request will fail at preflight with no `Access-Control-Allow-Origin`. This is by design: a `pg_live_*` API key embedded in browser code can be lifted from the page source or DevTools network tab, and a leaked key can burn through your monthly quota or be used to query feeds you've paid for.

You have three options for browser-facing applications:

1. **Proxy through your server.** Your frontend calls your backend; your backend holds the API key and calls PostGrad. Standard pattern, no key exposure.
2. **Use the MCP custom connector.** [https://postgrad.io/docs/mcp-integration](/docs/mcp-integration) — agents authenticate via OAuth in the browser, no API key in client code.
3. **Run a Cloudflare/Vercel edge worker** that holds the key in secrets and proxies requests, with your own rate-limit + origin check in front.

If you have a legitimate use case for direct browser-side access (e.g. an internal dashboard on a private network), [contact support](mailto:info@postgrad.io) and we can issue a key with a constrained origin allow-list.
