# Marketplace API

The Marketplace API lets you programmatically create, list, sell, and manage digital assets - including prompt packs, templates, lead lists, workflow configurations, and skill packs. It is designed for both human developers and LLM-based agents.

> **Catalog access:** Listing endpoints (`GET /listings`, `GET /listings/:slug`) require an API key with the `listings:read` scope. Public without a key: `GET /health`, `GET /manifest`, and `GET /openapi.json`. Mint keys in Creator Studio → API Console. See also `/agent-readme.md` and `/mcp` on auglet.com.

---

## Base URL and Versioning

**Base URL:**

```
https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1
```

All endpoints are prefixed with this base URL. For example, the health endpoint is:

```
GET https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/health
```

**Versioning:**

- Current version: **v1**
- Version is embedded in the URL path - there is no version header.
- Deprecated versions will be announced in the manifest's `versioning.deprecated` array before removal.
- No breaking changes will be introduced within a version.

**Content type:** All requests and responses use `application/json`.

---

## Overview Table

| Method | Endpoint | Description | Auth | Scope | Rate Limit |
|--------|----------|-------------|------|-------|------------|
| `GET` | `/health` | Health check | None | - | - |
| `GET` | `/manifest` | Discovery document for apps and agents | None | - | - |
| `GET` | `/openapi.json` | OpenAPI 3.1.0 spec | None | - | - |
| `GET` | `/assets` | List assets (paginated, filterable) | API key | `assets:read` | 100/min |
| `GET` | `/assets/:id` | Get single asset by UUID | API key | `assets:read` | 100/min |
| `POST` | `/assets` | Create a new asset (draft) | API key | `assets:write` | 100/min |
| `PATCH` | `/assets/:id` | Update an asset you own | API key | `assets:write` | 100/min |
| `GET` | `/listings` | List published marketplace listings | API key | `listings:read` | 100/min |
| `GET` | `/listings/:slug` | Get listing by slug with asset | API key | `listings:read` | 100/min |
| `GET` | `/orders` | List your orders (PII redacted) | API key | `orders:read` | 100/min |
| `GET` | `/orders/:id` | Get single order by UUID | API key | `orders:read` | 100/min |
| `GET` | `/purchases` | List your purchased assets | API key | `purchases:read` | 100/min |
| `GET` | `/purchases/:id` | Get purchase details with asset & order | API key | `purchases:read` | 100/min |
| `GET` | `/purchases/:id/download` | Get download URL or payload | API key | `purchases:read` | 100/min |
| `POST` | `/checkout` | Create Stripe checkout session | API key | `checkout:create` | **10/min** |
| `POST` | `/feedback` | Submit structured feedback | API key | `feedback:write` | 100/min |

---

## Authentication

The API uses **API keys** for authentication. Every authenticated request must include a key via one of two headers:

```
X-Api-Key: mk_live_<64-character-hex>
```

or

```
Authorization: Bearer mk_live_<64-character-hex>
```

Both are equivalent. `X-Api-Key` is preferred for clarity.

### Obtaining a Key

API keys are created through the key management endpoint (authenticated with your user session JWT):

```bash
curl -X POST \
  https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-keys/ \
  -H "Authorization: Bearer <your_session_jwt>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Integration",
    "scopes": ["assets:read", "listings:read", "feedback:write"]
  }'
```

The response includes the raw key **exactly once**. It is never stored or returned again.

### Available Scopes

Request only the scopes your integration needs (least-privilege).

| Scope | Grants |
|-------|--------|
| `assets:read` | List and retrieve assets |
| `assets:write` | Create and update assets |
| `listings:read` | List and retrieve listings |
| `orders:read` | Retrieve your orders |
| `purchases:read` | List purchases and download purchased assets |
| `checkout:create` | Create Stripe checkout sessions |
| `feedback:write` | Submit feedback reports |
| `analytics:read` | Read analytics data |

### Key Safety

- **Store keys securely.** Treat them like passwords. Do not commit them to source control.
- **Rotate keys regularly.** Create a new key, update your integration, then revoke the old key.
- **Use least-privilege scopes.** Only request the scopes your integration actually uses.
- **Revoke compromised keys immediately** via `DELETE /api-keys/:id`.

### Revoking a Key

```bash
curl -X DELETE \
  https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-keys/<key-id> \
  -H "Authorization: Bearer <your_session_jwt>"
```

Revocation is immediate. All subsequent requests with the key return `401`.

---

## Rate Limits

Rate limits are enforced **per API key** on a per-minute sliding window.

| Endpoint Group | Limit |
|----------------|-------|
| All general endpoints | **100 requests/minute** |
| `POST /checkout` | **10 requests/minute** |

### Response Headers

Every authenticated response includes:

| Header | Description |
|--------|-------------|
| `x-ratelimit-limit` | Max requests allowed in the current window |
| `x-ratelimit-remaining` | Requests remaining in the current window |
| `x-ratelimit-reset` | ISO 8601 timestamp when the window resets |

### When Rate Limited (429)

```json
{
  "ok": false,
  "apiVersion": "v1",
  "app": { "id": "marketplace", "name": "Marketplace", "version": "0.1.0" },
  "timestamp": "2026-03-16T22:05:00.000Z",
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit 100/min exceeded",
    "details": null
  }
}
```

The response also includes a `retry-after: 60` header. Wait for the `x-ratelimit-reset` timestamp before retrying.

---

## Conventions

### Response Envelope

**Every** response uses the same envelope shape:

**Success:**

```json
{
  "ok": true,
  "apiVersion": "v1",
  "app": {
    "id": "marketplace",
    "name": "Marketplace",
    "version": "0.1.0"
  },
  "timestamp": "2026-03-16T22:00:00.000Z",
  "data": { }
}
```

**Error:**

```json
{
  "ok": false,
  "apiVersion": "v1",
  "app": {
    "id": "marketplace",
    "name": "Marketplace",
    "version": "0.1.0"
  },
  "timestamp": "2026-03-16T22:00:00.000Z",
  "error": {
    "code": "not_found",
    "message": "Asset not found",
    "details": null
  }
}
```

Check `ok` first. If `false`, read `error.code` for machine-readable handling and `error.message` for display. `error.details` is an array of field-level errors when present (e.g. validation failures), otherwise `null`.

### Error Codes

| Code | HTTP Status | Meaning |
|------|-------------|---------|
| `missing_api_key` | 401 | No API key provided |
| `invalid_api_key` | 401 | Key not recognized or revoked |
| `forbidden` | 403 | Key lacks required scope or access |
| `not_found` | 404 | Resource or route does not exist |
| `validation_error` | 400 | Input failed validation |
| `invalid_request` | 400 / 413 / 415 | Malformed request, too large, or wrong Content-Type |
| `invalid_feedback_payload` | 400 | Feedback-specific validation failure |
| `rate_limit_exceeded` | 429 | Rate limit exceeded |
| `internal_error` | 500 | Unexpected server error |

### Pagination

List endpoints use **cursor-based** pagination.

**Query parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `limit` | integer | `25` | Items per page (1–100) |
| `cursor` | string | - | Cursor from `next_cursor` of previous response |

**Response shape (inside `data`):**

```json
{
  "items": [ ... ],
  "next_cursor": "uuid-or-timestamp-or-null",
  "count": 25
}
```

Pass `next_cursor` as the `cursor` parameter to get the next page. When `next_cursor` is `null`, there are no more items.

### Filtering & Sorting (Assets)

| Parameter | Values | Default |
|-----------|--------|---------|
| `category` | `prompt_pack`, `skill_pack`, `harness_pack` | - |
| `status` | `draft`, `published`, `archived` | `published` |
| `tag` | Any string | - |
| `sort` | `created_at`, `updated_at`, `title`, `price` | `created_at` |
| `order` | `asc`, `desc` | `desc` |

### Filtering (Listings)

| Parameter | Values | Default |
|-----------|--------|---------|
| `featured` | `true`, `false` | - |

### Request IDs

Every response includes an `x-request-id` header containing a UUID. Include this ID when contacting support or reporting issues.

### Content-Type Requirement

All `POST` and `PATCH` requests **must** include `Content-Type: application/json`. Requests without it receive a `415` error.

### Payload Size Limits

| Endpoint | Max Body Size |
|----------|---------------|
| `POST /assets`, `PATCH /assets/:id` | 256 KB |
| `POST /feedback` | 64 KB |

---

## Endpoints

### GET /health

Health check. Confirms the API is alive and returns discovery URLs. **No authentication required.**

```bash
curl https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/health
```

**Response (200):**

```json
{
  "ok": true,
  "apiVersion": "v1",
  "app": { "id": "marketplace", "name": "Marketplace", "version": "0.1.0" },
  "timestamp": "2026-03-16T22:00:00.000Z",
  "data": {
    "status": "healthy",
    "environment": "production",
    "baseUrl": "https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1",
    "manifestUrl": "https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/manifest",
    "openApiUrl": "https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/openapi.json"
  }
}
```

---

### GET /manifest

Machine-readable discovery document for apps and AI agents. **No authentication required.**

```bash
curl https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/manifest
```

**Response (200):**

```json
{
  "ok": true,
  "apiVersion": "v1",
  "app": { "id": "marketplace", "name": "Marketplace", "version": "0.1.0" },
  "timestamp": "2026-03-16T22:00:00.000Z",
  "data": {
    "title": "Marketplace API",
    "description": "Digital asset marketplace API for creating, listing, selling, and analyzing digital assets.",
    "baseUrl": "https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1",
    "namespace": "marketplace",
    "docsUrl": null,
    "openApiUrl": "https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/openapi.json",
    "healthUrl": "https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/health",
    "auth": {
      "type": "api_key",
      "header": "X-Api-Key",
      "bearerSupported": true,
      "keyPrefix": "mk_live_"
    },
    "capabilities": [
      {
        "id": "health_check",
        "name": "Health check",
        "description": "Quick health check confirming the API is alive.",
        "method": "GET",
        "path": "/api/v1/health"
      },
      {
        "id": "list_assets",
        "name": "List assets",
        "description": "Returns paginated published assets with filtering and sorting.",
        "method": "GET",
        "path": "/api/v1/assets"
      },
      {
        "id": "submit_feedback",
        "name": "Submit feedback",
        "description": "Submit a structured bug report, feature request, improvement request, support request, or app-to-app integration request.",
        "method": "POST",
        "path": "/api/v1/feedback"
      }
    ],
    "defaultHeaders": { "Content-Type": "application/json" },
    "versioning": { "current": "v1", "supported": ["v1"], "deprecated": [] }
  }
}
```

> **Note:** `capabilities` array is truncated above for readability. The full response includes all 13 capabilities.

---

### GET /openapi.json

Returns a valid OpenAPI 3.1.0 JSON specification for all public routes. **No authentication required.**

```bash
curl https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/openapi.json
```

The response is a standard OpenAPI document wrapped in the success envelope under `data`. It includes security schemes, request/response schemas, and path definitions for every public endpoint.

---

### GET /assets

List published assets with pagination, filtering, and sorting. **Requires `assets:read` scope.**

```bash
curl "https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/assets?limit=2&category=prompt_pack" \
  -H "X-Api-Key: mk_live_your_key_here"
```

**Query parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `limit` | integer | 25 | Items per page (1–100) |
| `cursor` | string | - | Cursor from previous response |
| `category` | string | - | Filter by asset category |
| `status` | string | `published` | Filter by status |
| `tag` | string | - | Filter by tag |
| `sort` | string | `created_at` | Sort field |
| `order` | string | `desc` | Sort direction (`asc` or `desc`) |

**Response (200):**

```json
{
  "ok": true,
  "apiVersion": "v1",
  "app": { "id": "marketplace", "name": "Marketplace", "version": "0.1.0" },
  "timestamp": "2026-03-16T22:00:00.000Z",
  "data": {
    "items": [
      {
        "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "title": "SaaS Cold Outreach Prompts",
        "slug": "saas-cold-outreach-prompts",
        "short_description": "12 battle-tested GPT prompts for SaaS cold email sequences.",
        "category": "prompt_pack",
        "tags": ["sales", "email", "saas", "outreach"],
        "audience": "B2B SaaS Sales Teams",
        "preview_image_url": null,
        "asset_type": "prompt_pack",
        "fulfillment_type": "downloadable_json",
        "price": 29,
        "currency": "USD",
        "status": "published",
        "payload": {},
        "metadata": {},
        "created_at": "2026-03-01T00:00:00Z",
        "updated_at": "2026-03-10T00:00:00Z"
      }
    ],
    "next_cursor": null,
    "count": 1
  }
}
```

**Common errors:**

| Code | Cause |
|------|-------|
| `missing_api_key` | No API key provided |
| `forbidden` | Key lacks `assets:read` scope |
| `validation_error` | Invalid `status` or `category` value |

---

### GET /assets/:id

Get a single asset by UUID. **Requires `assets:read` scope.**

```bash
curl https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/assets/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
  -H "X-Api-Key: mk_live_your_key_here"
```

**Response (200):** Full asset object in `data`.

**Errors:** `not_found` (404) if UUID doesn't match a visible asset.

---

### POST /assets

Create a new digital asset. Assets are created in `draft` status. **Requires `assets:write` scope.**

```bash
curl -X POST https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/assets \
  -H "X-Api-Key: mk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Email Sequence Templates",
    "slug": "email-sequence-templates",
    "price": 39,
    "category": "prompt_pack",
    "tags": ["email", "templates"],
    "audience": "Email marketers",
    "short_description": "5 proven email sequence templates.",
    "full_description": "Detailed description with usage instructions.",
    "fulfillment_type": "downloadable_file",
    "payload": { "file_ref": "templates/email-sequences.zip" },
    "metadata": { "version": "1.0" }
  }'
```

**Required fields:**

| Field | Type | Constraints |
|-------|------|-------------|
| `title` | string | 1–200 characters |
| `slug` | string | 1–200 characters, auto-normalized to lowercase alphanumeric + hyphens |
| `price` | number | 0–1,000,000 (in whole currency units, e.g. 29 = $29) |

**Optional fields:**

| Field | Type | Default |
|-------|------|---------|
| `category` | string | `prompt_pack` |
| `tags` | string[] | `[]` |
| `audience` | string | `""` |
| `short_description` | string | `""` |
| `full_description` | string | `""` |
| `fulfillment_type` | string | `downloadable_file` |
| `currency` | string | `USD` |
| `payload` | object | `{}` |
| `metadata` | object | `{}` |

**Response (201):** Created asset object in `data`.

**Common errors:**

| Code | HTTP | Cause |
|------|------|-------|
| `validation_error` | 400 | Missing or invalid required fields |
| `invalid_request` | 409 | Slug already exists |
| `invalid_request` | 413 | Body exceeds 256 KB |

---

### PATCH /assets/:id

Partial update of an asset you own. **Requires `assets:write` scope.**

```bash
curl -X PATCH https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/assets/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
  -H "X-Api-Key: mk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"price": 49, "status": "published"}'
```

Send only the fields you want to update. All fields from `POST /assets` are supported except `slug`.

**Response (200):** Updated asset object in `data`.

**Errors:** `forbidden` (403) if you don't own the asset. `not_found` (404) if it doesn't exist.

---

### GET /listings

List published marketplace listings. **Requires `listings:read` scope.**

```bash
curl "https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/listings?featured=true&limit=5" \
  -H "X-Api-Key: mk_live_your_key_here"
```

**Query parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `limit` | integer | 25 | Items per page (1–100) |
| `cursor` | string | - | Cursor from previous response |
| `featured` | string | - | Filter to featured only (`true`) |

**Response (200):**

```json
{
  "ok": true,
  "apiVersion": "v1",
  "app": { "id": "marketplace", "name": "Marketplace", "version": "0.1.0" },
  "timestamp": "2026-03-16T22:00:00.000Z",
  "data": {
    "items": [
      {
        "id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
        "asset_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "slug": "saas-cold-outreach-prompts",
        "title": "SaaS Cold Outreach Prompts",
        "subtitle": "12 battle-tested GPT prompts for SaaS cold email sequences.",
        "featured": true,
        "sort_order": 0,
        "published_at": "2026-03-10T00:00:00Z",
        "created_at": "2026-03-01T00:00:00Z",
        "updated_at": "2026-03-10T00:00:00Z"
      }
    ],
    "next_cursor": null,
    "count": 1
  }
}
```

---

### GET /listings/:slug

Get a single listing by slug, including its full asset data. **Requires `listings:read` scope.**

```bash
curl https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/listings/saas-cold-outreach-prompts \
  -H "X-Api-Key: mk_live_your_key_here"
```

**Response (200):** Listing object with nested `assets` object in `data`.

---

### GET /orders

List your orders with pagination. Buyer emails are partially redacted. **Requires `orders:read` scope.**

```bash
curl "https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/orders?limit=10" \
  -H "X-Api-Key: mk_live_your_key_here"
```

**Query parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `limit` | integer | Items per page (1–100, default 25) |
| `cursor` | string | Cursor from previous `next_cursor` (ISO timestamp) |
| `status` | string | Filter by order status (`pending`, `completed`, `refunded`) |

**Response (200):** Paginated order list. `buyer_email` is redacted to `joh***@example.com` format.

---

### GET /orders/:id

Get a single order by UUID. Only returns orders belonging to the authenticated key's user. **Requires `orders:read` scope.**

```bash
curl https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/orders/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
  -H "X-Api-Key: mk_live_your_key_here"
```

---

### GET /purchases

List your purchased assets with asset metadata. **Requires `purchases:read` scope.**

```bash
curl "https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/purchases?limit=10" \
  -H "X-Api-Key: mk_live_your_key_here"
```

**Query parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `limit` | integer | 25 | Items per page (1–100) |
| `cursor` | string | - | Cursor from previous response (UUID) |

**Response (200):**

```json
{
  "ok": true,
  "apiVersion": "v1",
  "app": { "id": "marketplace", "name": "Marketplace", "version": "0.1.0" },
  "timestamp": "2026-03-17T12:00:00.000Z",
  "data": {
    "items": [
      {
        "id": "p1a2b3c4-d5e6-7890-abcd-ef1234567890",
        "asset_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "order_id": "o1a2b3c4-d5e6-7890-abcd-ef1234567890",
        "created_at": "2026-03-15T10:00:00Z",
        "assets": {
          "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
          "title": "SaaS Cold Outreach Prompts",
          "slug": "saas-cold-outreach-prompts",
          "category": "prompt_pack",
          "fulfillment_type": "downloadable_json",
          "price": 29,
          "currency": "USD",
          "preview_image_url": null
        }
      }
    ],
    "next_cursor": null,
    "count": 1
  }
}
```

---

### GET /purchases/:id

Get a single purchase by UUID with full asset and order details. **Requires `purchases:read` scope.**

```bash
curl https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/purchases/p1a2b3c4-d5e6-7890-abcd-ef1234567890 \
  -H "X-Api-Key: mk_live_your_key_here"
```

**Response (200):** Purchase object with nested `assets` and `orders` objects in `data`, including asset metadata (title, slug, category, price, tags, etc.) and order details (amount, status, etc.).

**Errors:** `not_found` (404) if UUID doesn't exist. `forbidden` (403) if you don't own it.

---

### GET /purchases/:id/download

Get a download URL or inline payload for a purchased asset. **Requires `purchases:read` scope.**

```bash
curl https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/purchases/p1a2b3c4-d5e6-7890-abcd-ef1234567890/download \
  -H "X-Api-Key: mk_live_your_key_here"
```

**Response (200) - File download:**

```json
{
  "ok": true,
  "apiVersion": "v1",
  "app": { "id": "marketplace", "name": "Marketplace", "version": "0.1.0" },
  "timestamp": "2026-03-17T12:00:00.000Z",
  "data": {
    "type": "file",
    "title": "Email Templates Pack",
    "download_url": "https://...",
    "file_name": "templates.zip",
    "expires_in": 3600
  }
}
```

**Response (200) - JSON payload:**

```json
{
  "ok": true,
  "apiVersion": "v1",
  "app": { "id": "marketplace", "name": "Marketplace", "version": "0.1.0" },
  "timestamp": "2026-03-17T12:00:00.000Z",
  "data": {
    "type": "json",
    "title": "SaaS Cold Outreach Prompts",
    "data": { "prompts": ["..."] }
  }
}
```

**Errors:** `not_found` (404) if purchase doesn't exist. `forbidden` (403) if you don't own it.

---

### POST /checkout

Create a Stripe checkout session for a published asset. **Requires `checkout:create` scope.** Rate limited to **10 requests/minute**.

```bash
curl -X POST https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/checkout \
  -H "X-Api-Key: mk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"asset_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}'
```

**Request body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `asset_id` | string (UUID) | Yes | The ID of the published asset to purchase |

The price is always validated from the database - it cannot be manipulated via the API.

**Response (201):**

```json
{
  "ok": true,
  "apiVersion": "v1",
  "app": { "id": "marketplace", "name": "Marketplace", "version": "0.1.0" },
  "timestamp": "2026-03-16T22:00:00.000Z",
  "data": {
    "checkout_url": "https://checkout.stripe.com/c/pay/cs_live_...",
    "session_id": "cs_live_..."
  }
}
```

Redirect the user to `checkout_url` to complete payment.

**Common errors:**

| Code | Cause |
|------|-------|
| `not_found` | Asset UUID doesn't exist |
| `validation_error` | Asset is not in `published` status |
| `rate_limit_exceeded` | More than 10 checkout requests/minute |

---

### POST /feedback

Submit structured feedback - bug reports, feature requests, improvement suggestions, support requests, or app-to-app integration requests. **Requires `feedback:write` scope.**

```bash
curl -X POST https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/feedback \
  -H "X-Api-Key: mk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Add bulk asset import",
    "message": "We need a bulk import endpoint that accepts CSV files with multiple assets.",
    "category": "feature_request",
    "severity": "medium",
    "source": {
      "type": "app",
      "appId": "my-crm-integration"
    },
    "target": {
      "appId": "marketplace",
      "capabilityId": "create_asset",
      "path": "/api/v1/assets"
    },
    "context": {
      "requestId": "req-abc-123",
      "environment": "production",
      "url": "https://my-app.com/import",
      "metadata": { "csv_row_count": 500 }
    }
  }'
```

**Request body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `title` | string | Yes | Short title (1–200 characters) |
| `message` | string | Yes | Detailed description (1–5,000 characters) |
| `category` | string | Yes | One of: `bug`, `feature_request`, `improvement`, `integration_request`, `support`, `other` |
| `severity` | string | Yes | One of: `low`, `medium`, `high`, `critical` |
| `source` | object | Yes | Who is sending this feedback |
| `source.type` | string | Yes | One of: `user`, `app`, `agent`, `external` |
| `source.appId` | string | No | Source application ID (used when `type` is `app` or `agent`) |
| `target` | object | No | What this feedback is about |
| `target.appId` | string | No | Target application ID |
| `target.capabilityId` | string | No | Specific capability ID from manifest |
| `target.path` | string | No | Specific API path |
| `context` | object | No | Additional debugging context |
| `context.requestId` | string | No | Related request ID |
| `context.environment` | string | No | Environment name |
| `context.url` | string | No | Related URL |
| `context.metadata` | object | No | Arbitrary key-value metadata |

> **Security note:** `source.userId` is always resolved from the authenticated API key's user - it cannot be spoofed via the request body.

**Response (201):**

```json
{
  "ok": true,
  "apiVersion": "v1",
  "app": { "id": "marketplace", "name": "Marketplace", "version": "0.1.0" },
  "timestamp": "2026-03-16T22:00:00.000Z",
  "data": {
    "feedback": {
      "id": "f9e8d7c6-b5a4-3210-fedc-ba9876543210",
      "status": "open",
      "createdAt": "2026-03-16T22:00:00.000Z"
    }
  }
}
```

**Common errors:**

| Code | Cause |
|------|-------|
| `invalid_feedback_payload` | Missing required fields or invalid enum values |
| `forbidden` | Key lacks `feedback:write` scope |

---

## Webhooks

The Marketplace uses Stripe webhooks to process payment completions. This is an internal integration - external developers do not register webhook endpoints.

**How it works:**

1. When a checkout session completes, Stripe sends a `checkout.session.completed` event to the webhook endpoint.
2. The webhook verifies the signature using `stripe.webhooks.constructEventAsync()` with `STRIPE_WEBHOOK_SECRET`.
3. On success, the corresponding order is updated to `completed` status.

If you need to be notified of order status changes, poll `GET /orders` or `GET /orders/:id`.

---

## Examples

### Quickstart - curl

```bash
# 1. Check the API is alive
curl https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/health

# 2. List published assets (using X-Api-Key)
curl https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/assets \
  -H "X-Api-Key: mk_live_your_key_here"

# 3. List published assets (using Authorization: Bearer)
curl https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/assets \
  -H "Authorization: Bearer mk_live_your_key_here"

# 4. Get a specific asset
curl https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/assets/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
  -H "X-Api-Key: mk_live_your_key_here"

# 5. Submit feedback (X-Api-Key)
curl -X POST https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/feedback \
  -H "X-Api-Key: mk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Great API",
    "message": "The assets endpoint works perfectly for our use case.",
    "category": "other",
    "severity": "low",
    "source": {"type": "user"}
  }'

# 6. Submit feedback (Authorization: Bearer)
curl -X POST https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/feedback \
  -H "Authorization: Bearer mk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Bug report",
    "message": "Asset creation returns 500 when tags array contains numbers.",
    "category": "bug",
    "severity": "high",
    "source": {"type": "app", "appId": "my-integration"}
  }'
```

### Quickstart - JavaScript (fetch)

```javascript
const BASE_URL = "https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1";
const API_KEY = "mk_live_your_key_here";

// Helper: make an authenticated request
async function api(path, options = {}) {
  const res = await fetch(`${BASE_URL}${path}`, {
    ...options,
    headers: {
      "X-Api-Key": API_KEY,
      "Content-Type": "application/json",
      ...options.headers,
    },
  });

  const json = await res.json();
  if (!json.ok) {
    throw new Error(`[${json.error.code}] ${json.error.message}`);
  }
  return json.data;
}

// List published assets
const assets = await api("/assets?limit=5&category=prompt_pack");
console.log(`Found ${assets.count} assets`);

for (const asset of assets.items) {
  console.log(`- ${asset.title} ($${asset.price})`);
}

// Create an asset
const newAsset = await api("/assets", {
  method: "POST",
  body: JSON.stringify({
    title: "My New Asset",
    slug: "my-new-asset",
    price: 29,
    category: "prompt_pack",
    tags: ["sales", "email"],
    short_description: "A great set of prompts.",
  }),
});
console.log(`Created asset: ${newAsset.id}`);

// Paginate through all assets
let cursor = null;
do {
  const page = await api(`/assets?limit=25${cursor ? `&cursor=${cursor}` : ""}`);
  for (const asset of page.items) {
    console.log(asset.title);
  }
  cursor = page.next_cursor;
} while (cursor);
```

---

## Troubleshooting

### "missing_api_key" (401)

You did not include an API key. Add one of:
- `X-Api-Key: mk_live_...` header
- `Authorization: Bearer mk_live_...` header

### "invalid_api_key" (401)

The key is not recognized, has been revoked, or has an invalid format. API keys must start with `mk_live_` or `mk_test_` followed by 64 hex characters.

### "forbidden" (403)

Your API key does not have the required scope. Create a new key with the necessary scopes.

### "validation_error" (400)

Check `error.details` for an array of specific field errors:

```json
{
  "error": {
    "code": "validation_error",
    "message": "Invalid input",
    "details": [
      "title must be a string",
      "price must be a number between 0 and 1000000"
    ]
  }
}
```

### "invalid_request" (415)

You forgot `Content-Type: application/json` on a POST or PATCH request.

### "rate_limit_exceeded" (429)

Wait until the time indicated by the `x-ratelimit-reset` header. The `retry-after` header is also set to `60` seconds.

### Debugging with Request IDs

Every response includes an `x-request-id` header. Save this value - it correlates to server-side audit logs and is essential for support requests.

```bash
# Capture the request ID
curl -v https://fjozfegsxseblmpplcrb.supabase.co/functions/v1/api-v1/assets \
  -H "X-Api-Key: mk_live_your_key_here" 2>&1 | grep x-request-id
```

---

## Security

### Acceptable Use

- **Do not** share API keys between applications or users.
- **Do not** use the API for automated scraping or bulk data extraction beyond normal integration patterns.
- **Do not** attempt to bypass rate limits by rotating keys.
- **Do not** submit fabricated or misleading feedback.

### PII Handling

- Buyer email addresses are partially redacted in API responses (shown as `joh***@example.com`).
- API keys are never logged in full - only the first 12 characters (prefix) appear in audit logs.
- The feedback endpoint resolves `source.userId` from the authenticated key's user - it cannot be spoofed.

### Key Security

- Raw API keys are returned **exactly once** at creation time. They are stored as SHA-256 hashes - we cannot recover lost keys.
- Revoke compromised keys immediately. Revocation takes effect instantly.
- Use separate keys for separate environments (development vs. production).

### Reporting Security Issues

If you discover a security vulnerability, please report it by submitting feedback with `category: "bug"` and `severity: "critical"`, or contact the team directly through the application.
