> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ezforge.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Hub API Reference

> Manage Hub API keys and query listing analytics via the Hub REST API

## Overview

The Hub API lets you manage consumer-facing Hub API keys and query per-listing analytics. All endpoints require a **session token** — pass the authenticated user's Bearer token in the `Authorization` header. Hub API keys themselves (for agent-developer access to Hub discovery endpoints) are a separate credential type; see [Create API key](#create-api-key) below.

**Base URL:** `https://ezforge.ai`

**Authentication:** All endpoints require `Authorization: Bearer <user-token>`.

<Tip>
  Dedicated pages provide deeper coverage of each area: [API Keys](/hub/api-keys) (security model, header usage, deprecated endpoint migration) and [Listing Analytics](/hub/analytics-listings) (tier comparison table, metric definitions).
</Tip>

***

## API keys

Hub API keys allow agent developers to call Hub discovery and data endpoints. Keys are tier-gated with daily request limits:

| Tier       | Daily limit          |
| ---------- | -------------------- |
| `explorer` | 100 requests/day     |
| `builder`  | 10,000 requests/day  |
| `partner`  | 100,000 requests/day |

### Create API key

```
POST /api/v1/hub/api-keys
```

Creates a new Hub API key for the authenticated user. The raw key is returned **exactly once** — store it immediately; it cannot be retrieved again.

**Request body:**

```json theme={null}
{
  "tier": "explorer"
}
```

| Field  | Type   | Required | Description                                                            |
| ------ | ------ | -------- | ---------------------------------------------------------------------- |
| `tier` | string | —        | Key tier: `explorer`, `builder`, or `partner`. Defaults to `explorer`. |

**Response `201`:**

```json theme={null}
{
  "data": {
    "key": "hub_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "meta": {
      "id": "01JQXYZ0000000000000000000",
      "userId": "01JQABC0000000000000000000",
      "keyPrefix": "hub_live_xxxxxxx",
      "tier": "explorer",
      "dailyRequestCount": 0,
      "lastRequestAt": null,
      "createdAt": "2026-05-15T10:00:00.000Z",
      "revokedAt": null
    }
  }
}
```

<Warning>
  The `key` field is returned only once, at creation time. Store it securely — it cannot be retrieved again.
</Warning>

**Response fields (`meta`):**

| Field               | Type               | Description                                         |
| ------------------- | ------------------ | --------------------------------------------------- |
| `id`                | string             | ULID key identifier                                 |
| `userId`            | string             | ULID of the owning user                             |
| `keyPrefix`         | string             | First 16 characters of the key (safe to display)    |
| `tier`              | string             | Key tier: `explorer`, `builder`, or `partner`       |
| `dailyRequestCount` | integer            | Requests made today (resets UTC midnight)           |
| `lastRequestAt`     | ISO 8601 or `null` | Timestamp of the most recent request                |
| `createdAt`         | ISO 8601           | Key creation timestamp                              |
| `revokedAt`         | ISO 8601 or `null` | Set when the key is revoked; `null` for active keys |

**Error responses:**

| Status | Code                  | Condition                                                                        |
| ------ | --------------------- | -------------------------------------------------------------------------------- |
| 401    | `unauthorized`        | Missing or invalid session token                                                 |
| 422    | `validation_error`    | `tier` is not one of `explorer`, `builder`, `partner`                            |
| 429    | `rate_limited`        | IP rate limit exceeded (`Retry-After` reflects seconds until window reset, ≤ 60) |
| 503    | `service_unavailable` | Rate-limit service temporarily unavailable                                       |

***

### List API keys

```
GET /api/v1/hub/api-keys
```

Returns all active (non-revoked) Hub API keys for the authenticated user, ordered oldest-first. Returns up to 100 keys. **Raw key values are never returned.**

**Response `200`:**

```json theme={null}
{
  "data": [
    {
      "id": "01JQXYZ0000000000000000000",
      "userId": "01JQABC0000000000000000000",
      "keyPrefix": "hub_live_xxxxxxx",
      "tier": "builder",
      "dailyRequestCount": 42,
      "lastRequestAt": "2026-05-15T09:14:00.000Z",
      "createdAt": "2026-03-01T08:00:00.000Z",
      "revokedAt": null
    }
  ]
}
```

**Error responses:**

| Status | Code                  | Condition                                                                        |
| ------ | --------------------- | -------------------------------------------------------------------------------- |
| 401    | `unauthorized`        | Missing or invalid session token                                                 |
| 429    | `rate_limited`        | IP rate limit exceeded (`Retry-After` reflects seconds until window reset, ≤ 60) |
| 503    | `service_unavailable` | Rate-limit service temporarily unavailable                                       |

***

### Revoke API key

```
DELETE /api/v1/hub/api-keys/:id
```

Immediately revokes a Hub API key. Any subsequent requests using this key will receive a `401`. Revocation cannot be undone.

**Path parameters:**

| Parameter | Type   | Description               |
| --------- | ------ | ------------------------- |
| `id`      | string | ULID of the key to revoke |

**Response `204`:** No content.

**Error responses:**

| Status | Code                  | Condition                                                                        |
| ------ | --------------------- | -------------------------------------------------------------------------------- |
| 401    | `unauthorized`        | Missing or invalid session token                                                 |
| 404    | `not_found`           | Key not found or not owned by the authenticated user                             |
| 409    | `conflict`            | Key is already revoked                                                           |
| 429    | `rate_limited`        | IP rate limit exceeded (`Retry-After` reflects seconds until window reset, ≤ 60) |
| 503    | `service_unavailable` | Rate-limit service temporarily unavailable                                       |

***

## Analytics

### Get listing analytics

```
GET /api/v1/hub/analytics/listings/:id
```

Returns analytics for a Hub listing. The response shape is gated by the listing's tier — callers receive only the fields their tier makes available. Only the listing owner can access this endpoint.

**Path parameters:**

| Parameter | Type   | Description                      |
| --------- | ------ | -------------------------------- |
| `id`      | string | 26-character ULID of the listing |

**Query parameters:**

| Parameter | Type   | Default | Description                             |
| --------- | ------ | ------- | --------------------------------------- |
| `period`  | string | `30d`   | Reporting window: `7d`, `30d`, or `90d` |

**Response `200` — Basic tier:**

```json theme={null}
{
  "data": {
    "tier": "basic",
    "period": "30d",
    "view_count": 312
  }
}
```

**Response `200` — Featured tier:**

```json theme={null}
{
  "data": {
    "tier": "featured",
    "period": "30d",
    "view_count": 312,
    "click_count": 87,
    "click_through_rate": 27.88
  }
}
```

**Response `200` — Premium tier:**

```json theme={null}
{
  "data": {
    "tier": "premium",
    "period": "30d",
    "view_count": 312,
    "click_count": 87,
    "click_through_rate": 27.88,
    "search_impressions": 1540,
    "referral_clicks": 34,
    "conversion_rate": 3.21
  }
}
```

**Response fields:**

| Field                | Type    | Tiers             | Description                                                                |
| -------------------- | ------- | ----------------- | -------------------------------------------------------------------------- |
| `tier`               | string  | all               | Listing tier used to gate this response: `basic`, `featured`, or `premium` |
| `period`             | string  | all               | Reporting window reflected back: `7d`, `30d`, or `90d`                     |
| `view_count`         | integer | all               | Total listing views in the period                                          |
| `click_count`        | integer | Featured, Premium | Total click-throughs from search results                                   |
| `click_through_rate` | number  | Featured, Premium | `click_count / view_count × 100`, rounded to 2 decimal places              |
| `search_impressions` | integer | Premium           | Times the listing appeared in search results                               |
| `referral_clicks`    | integer | Premium           | Referral-link interactions originating from Hub discovery                  |
| `conversion_rate`    | number  | Premium           | `transactions / view_count × 100`, rounded to 2 decimal places             |

**Error responses:**

| Status | Code                  | Condition                                                                        |
| ------ | --------------------- | -------------------------------------------------------------------------------- |
| 400    | `invalid_input`       | `id` is not a valid 26-character ULID, or `period` is not `7d`, `30d`, or `90d`  |
| 401    | `unauthorized`        | Missing or invalid session token                                                 |
| 403    | `forbidden`           | Authenticated user does not own this listing                                     |
| 404    | `not_found`           | Listing not found                                                                |
| 429    | `rate_limited`        | IP rate limit exceeded (`Retry-After` reflects seconds until window reset, ≤ 60) |
| 503    | `service_unavailable` | Rate-limit service temporarily unavailable                                       |

***

## Capability documents

Capability documents let listing owners describe their MCP server's tools and skills in structured form (SKILL.md or JSON). Once uploaded, the document is publicly retrievable and indexed by the Hub-level skills discovery registry at `GET /.well-known/mcp-skills`.

### Upload capability document

```
POST /api/v1/hub/listings/:id/capability-doc
```

Uploads a SKILL.md or JSON capability document for the specified listing. The listing must be active and owned by the authenticated user. Only one capability document is stored per listing — uploading replaces any previously stored document.

**Auth:** Session token (listing owner required).

**Path parameters:**

| Parameter | Type   | Description                      |
| --------- | ------ | -------------------------------- |
| `id`      | string | 26-character ULID of the listing |

**Request body:** `multipart/form-data`

| Field  | Type | Required | Description                                                                                                                                                                                                                 |
| ------ | ---- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `file` | file | Yes      | SKILL.md or JSON capability document (max 512 KB). Accepted MIME types: `text/markdown`, `text/plain`, `text/x-markdown` (parsed as SKILL.md, `format: "skill_md"`), `application/json` (parsed as JSON, `format: "json"`). |

**Example request:**

```bash theme={null}
curl -X POST https://ezforge.ai/api/v1/hub/listings/01JQXYZ.../capability-doc \
  -H "Authorization: Bearer <user-token>" \
  -F "file=@SKILL.md;type=text/markdown"
```

**Response `201`:**

```json theme={null}
{
  "capabilityDocUrl": "https://storage.ezforge.ai/hub-bucket/hub/capability-docs/01JQXYZ.../01JQABC....md",
  "format": "skill_md",
  "parsed": {
    "name": "Detroit Eats MCP",
    "description": "Restaurant discovery and reservation tools for Detroit metro.",
    "tools": [
      { "name": "search_restaurants", "description": "Search restaurants by cuisine and location." },
      { "name": "book_reservation", "description": "Reserve a table at a restaurant." }
    ]
  }
}
```

**Response fields:**

| Field              | Type   | Description                                                    |
| ------------------ | ------ | -------------------------------------------------------------- |
| `capabilityDocUrl` | string | Public URL of the stored capability document in object storage |
| `format`           | string | Detected format: `skill_md` or `json`                          |
| `parsed`           | object | Structured representation of the capability document           |

**Error responses:**

| Status | Code                  | Condition                                                                                                                                 |
| ------ | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `validation_error`    | Invalid `id` path parameter (not a valid 26-character ULID), missing or empty `file` field, unsupported MIME type, or file exceeds 512 KB |
| 401    | `unauthorized`        | Missing or invalid session token                                                                                                          |
| 403    | `forbidden`           | Authenticated user does not own this listing                                                                                              |
| 404    | `not_found`           | Listing not found                                                                                                                         |
| 422    | `validation_error`    | Listing is delisted — capability documents cannot be uploaded to delisted listings                                                        |
| 429    | `rate_limited`        | IP or user rate limit exceeded (`Retry-After` header set)                                                                                 |
| 503    | `service_unavailable` | Storage or rate-limit service temporarily unavailable                                                                                     |

***

### Get capability document

```
GET /api/v1/hub/listings/:id/capability-doc
```

Returns the parsed capability document for a listing. Public — no authentication required.

**Path parameters:**

| Parameter | Type   | Description                      |
| --------- | ------ | -------------------------------- |
| `id`      | string | 26-character ULID of the listing |

**Example request:**

```bash theme={null}
curl https://ezforge.ai/api/v1/hub/listings/01JQXYZ.../capability-doc
```

**Response `200`:**

```json theme={null}
{
  "data": {
    "capabilityDocUrl": "https://storage.ezforge.ai/hub-bucket/hub/capability-docs/01JQXYZ.../01JQABC....md",
    "format": "skill_md",
    "parsed": {
      "name": "Detroit Eats MCP",
      "description": "Restaurant discovery and reservation tools for Detroit metro.",
      "tools": [
        { "name": "search_restaurants", "description": "Search restaurants by cuisine and location." },
        { "name": "book_reservation", "description": "Reserve a table at a restaurant." }
      ]
    }
  }
}
```

Responses are cached for 120 seconds (`Cache-Control: public, max-age=120, stale-while-revalidate=600`).

**Error responses:**

| Status | Code                  | Condition                                                                        |
| ------ | --------------------- | -------------------------------------------------------------------------------- |
| 400    | `validation_error`    | `id` is not a valid 26-character ULID                                            |
| 404    | `not_found`           | Listing not found, not active, or has no capability document                     |
| 429    | `rate_limited`        | IP rate limit exceeded (`Retry-After` reflects seconds until window reset, ≤ 60) |
| 503    | `service_unavailable` | Rate-limit service temporarily unavailable                                       |

***

### Get listing SKILL.md

```
GET /api/v1/hub/listings/:id/skill.md
```

Returns the Hub-generated [SKILL.md](/hub/skill-md) for a listing — the portable capability manifest that describes the MCP server's tools, authentication, vertical metadata, and trust signals. This is the endpoint referenced as `skill_md_endpoint` in the [Hub MCP registry document](/hub/discovery). Public — no authentication required.

Fetching this endpoint records a `click` analytics event for the listing (fire-and-forget).

**Path parameters:**

| Parameter | Type   | Description                      |
| --------- | ------ | -------------------------------- |
| `id`      | string | 26-character ULID of the listing |

**Example request:**

```bash theme={null}
curl https://ezforge.ai/api/v1/hub/listings/01JQXYZ.../skill.md
```

**Response `200`:**

Returns the SKILL.md content as `text/markdown; charset=utf-8`. The body is a Markdown document with YAML frontmatter containing `hub:`-prefixed flat keys (e.g. `hub:listing_id`, `hub:listing_tier`) as defined by the Hub SKILL.md spec.

```markdown theme={null}
---
hub:listing_id: "01JQXYZ0000000000000000000"
hub:listing_tier: "featured"
hub:verification_tier: 2
hub:health_score: 92
hub:hub_url: "https://hub.ezforge.ai/listings/detroiteats"
hub:referral_endpoint: "https://hub.ezforge.ai/api/v1/hub/listings/01JQXYZ.../referral"
---

# Detroit Eats MCP

Family-owned restaurant discovery and reservation tools for Detroit metro.

...
```

**Response headers (`200`):**

| Header                    | Description                                      |
| ------------------------- | ------------------------------------------------ |
| `Content-Type`            | `text/markdown; charset=utf-8`                   |
| `X-Hub-Listing-Id`        | ULID of the listing                              |
| `X-Hub-Listing-Tier`      | Listing tier: `basic`, `featured`, or `premium`  |
| `X-Hub-Verification-Tier` | Numeric verification tier (e.g., `1`, `2`)       |
| `X-Hub-Health-Score`      | Numeric health score (0–100)                     |
| `X-Hub-Url`               | Canonical Hub listing URL                        |
| `Cache-Control`           | `public, max-age=60, stale-while-revalidate=300` |

The `X-Hub-*` headers expose the structured listing metadata so callers can access it programmatically without parsing the Markdown body.

**Error responses:**

| Status | Code                  | Condition                                                                        |
| ------ | --------------------- | -------------------------------------------------------------------------------- |
| 400    | `validation_error`    | `id` is not a valid 26-character ULID                                            |
| 404    | `not_found`           | Listing not found or not active                                                  |
| 429    | `rate_limited`        | IP rate limit exceeded (`Retry-After` reflects seconds until window reset, ≤ 60) |
| 503    | `service_unavailable` | Rate-limit service temporarily unavailable                                       |

***

### Skills discovery registry

```
GET /.well-known/mcp-skills
```

Returns all active Hub listings that have a capability document. Agent platforms use this endpoint to enumerate available MCP server skill sets without crawling individual listings. Public — no authentication required.

**Query parameters:**

| Parameter    | Type    | Default | Description                                                             |
| ------------ | ------- | ------- | ----------------------------------------------------------------------- |
| `listing_id` | string  | —       | Filter to a single listing by ULID                                      |
| `format`     | string  | —       | Filter by document format: `skill_md` or `json`                         |
| `limit`      | integer | `50`    | Number of results to return (1–200)                                     |
| `cursor`     | string  | —       | ULID cursor for keyset pagination (from a previous `next_cursor` value) |

**Example request:**

```bash theme={null}
curl "https://ezforge.ai/.well-known/mcp-skills?format=skill_md&limit=10"
```

**Response `200`:**

```json theme={null}
{
  "listings": [
    {
      "id": "01JQXYZ0000000000000000000",
      "name": "Detroit Eats MCP",
      "capability_doc_url": "https://storage.ezforge.ai/hub-bucket/hub/capability-docs/01JQXYZ.../01JQABC....md",
      "capability_doc_format": "skill_md"
    },
    {
      "id": "01JQXYZ0000000000000000001",
      "name": "Metro Parking MCP",
      "capability_doc_url": "https://storage.ezforge.ai/hub-bucket/hub/capability-docs/01JQXYZ.../01JQABC....json",
      "capability_doc_format": "json"
    }
  ],
  "next_cursor": "01JQXYZ0000000000000000001"
}
```

`next_cursor` is only present when more results exist. Pass its value as `cursor` in the next request to retrieve the following page.

**Response fields (per listing):**

| Field                   | Type   | Description                                  |
| ----------------------- | ------ | -------------------------------------------- |
| `id`                    | string | ULID of the listing                          |
| `name`                  | string | Listing display name                         |
| `capability_doc_url`    | string | Public URL of the stored capability document |
| `capability_doc_format` | string | Document format: `skill_md` or `json`        |

Responses are cached for 60 seconds (`Cache-Control: public, max-age=60, stale-while-revalidate=300`).

**Error responses:**

| Status | Code                  | Condition                                                                                                                     |
| ------ | --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| 400    | `validation_error`    | Invalid `listing_id` (not a valid ULID), invalid `format` value, `limit` out of range, or invalid `cursor` (not a valid ULID) |
| 429    | `rate_limited`        | IP rate limit exceeded (`Retry-After` reflects seconds until window reset, ≤ 60)                                              |
| 503    | `service_unavailable` | Rate-limit service temporarily unavailable                                                                                    |

***

## Deprecated endpoints

<Warning>
  **`/api/v1/hub/keys` is deprecated.** Use [`/api/v1/hub/api-keys`](#api-keys) for all new integrations. The `/api/v1/hub/keys` path will be sunset on **2028-01-01** and removed in a future release.
</Warning>

Responses from the deprecated path include the following headers to signal migration:

```
Deprecation: true
Link: </api/v1/hub/api-keys>; rel="successor-version"
Sunset: Sat, 01 Jan 2028 00:00:00 GMT
```

| Deprecated path               | Replacement                       |
| ----------------------------- | --------------------------------- |
| `POST /api/v1/hub/keys`       | `POST /api/v1/hub/api-keys`       |
| `GET /api/v1/hub/keys`        | `GET /api/v1/hub/api-keys`        |
| `DELETE /api/v1/hub/keys/:id` | `DELETE /api/v1/hub/api-keys/:id` |
