# StoreFrame Agent API reference

Base URL: `https://api.slidemint.shop/api/v1/storeframe/`

All agent routes require:

```http
Authorization: Bearer sf_xxxxxxxx
Content-Type: application/json
```

Pro or Max plan only. Errors use `{ "detail": "..." }` with HTTP status.

---

## Primary workflow (YAML ↔ cloud project)

This is the intended agent loop — **not** the same as clicking around the web editor.

1. Author / edit a local pack file (YAML or JSON) with `storeframe-template-from-brief`.
2. Convert YAML → JSON if needed.
3. **First import:** `POST agent/template-packs` → save returned `public_id`.
4. User asks for changes → edit the **same local pack file**.
5. **Re-import:** `POST agent/template-packs` with `project_id` = that `public_id`.
6. **Fill screens:** `POST agent/projects/:id/screenshots` with base64/data_url images by page index.
7. **Render for Codex:** `POST agent/projects/:id/exports` → paste response `markdown` (or `pages[].url`) into chat.
8. Optional: open `editor_url` for browser PNG ZIP if you need pixel-identical Custom Studio export.

Keep the local YAML as the source of truth for copy/layout. Screenshots live as hosted asset URLs.

---

## Catalog search workflow (find → show → use)

When the user wants to **start from an existing template** (not author YAML from scratch):

1. Read their brief. Generate **3–8 English keywords** (app category, vibe, features: e.g. `habit`, `streak`, `fitness`).
2. `POST agent/templates/search` with `{ "prompt": "<full brief>", "keywords": [...], "platform": "iphone" }`.
3. Present top picks: `name`, `reason`, `catalog_url` / `preview_url`, and `preview_frames` (titles). There is **no static PNG thumbnail URL** yet — `preview_url` opens the live editor preview.
4. Confirm with the user, then `POST agent/templates/:slug/projects` to clone into their cloud project.
5. Give them `editor_url`. Optionally continue with YAML edit → re-import, or edit in the browser.

---

## GET `agent/me`

Verify the API key and membership.

**Request:** no body

**Response `200`**

```json
{
  "user": {
    "id": 12,
    "email": "maker@example.com",
    "name": "Maker",
    "avatar_url": null,
    "plan": "pro",
    "plan_expires_at": "2026-08-22T00:00:00.000Z",
    "exports_used": 2,
    "export_period": "2026-07",
    "export_bonus": 0
  },
  "skill_pack_url": "https://aistoreframe.shop/skills/storeframe-agent.zip",
  "skill_docs_url": "https://aistoreframe.shop/skills/storeframe-agent/SKILL.md",
  "editor_base_url": "https://aistoreframe.shop"
}
```

**Errors:** `401` invalid key · `402` not Pro/Max · `403` disabled user

`agent/me` is a quick check. For **remaining quota + export history**, use `GET agent/account/usage`.

---

## GET `agent/account/usage`

Membership status, export balance (remaining quota), and paginated consumption history.

**Query**

| Param | Type | Default |
|-------|------|---------|
| `page` | int ≥ 1 | `1` |
| `per_page` | 1–50 | `20` |

**Response `200`**

```json
{
  "membership": {
    "plan": "pro",
    "status": "active",
    "plan_expires_at": "2026-08-22T00:00:00.000Z",
    "plan_name": "Pro"
  },
  "quota": {
    "period": "2026-07",
    "plan_limit": 50,
    "unlimited": false,
    "exports_used": 2,
    "export_bonus": 5,
    "plan_remaining": 48,
    "bonus_remaining": 5,
    "total_remaining": 53,
    "watermark": false
  },
  "exports": {
    "items": [
      {
        "request_id": "…",
        "page_count": 5,
        "width": 1290,
        "height": 2796,
        "status": "completed",
        "created_at": "2026-07-22T05:00:00.000Z",
        "project_name": "Habit screens",
        "template_name": null,
        "template_slug": null
      }
    ],
    "total": 12,
    "page": 1,
    "per_page": 20,
    "pages": 1
  }
}
```

Notes:

- `membership.status`: `free` | `active` | `expired`
- `quota.unlimited: true` means Max-style unlimited plan exports (`plan_limit` / `total_remaining` are `null`)
- Each `exports.items` row is one consumed export credit
- Paginate with `page` when `pages > 1`

**Errors:** `401` · `402` not Pro/Max

---

## GET `agent/projects`

List the member’s active cloud projects.

**Response `200`**

```json
{
  "items": [
    {
      "public_id": "11111111-1111-1111-1111-111111111111",
      "name": "Agent Orbit project",
      "template_id": null,
      "project": { "editorMode": "custom", "platform": "iphone", "pages": [] },
      "updated_at": "2026-07-22T05:00:00.000Z",
      "editor_url": "https://aistoreframe.shop/editor?mode=custom&platform=iphone&project=11111111-1111-1111-1111-111111111111"
    }
  ],
  "total": 1
}
```

---

## GET `agent/projects/:id`

`:id` = project `public_id` (UUID).

**Response `200`:** same object shape as one item above.

**Errors:** `404` not found

---

## POST `agent/projects`

Create a project from a full StoreFrame `project` JSON object (advanced). Prefer `agent/template-packs` for the YAML workflow.

**Request body**

| Field | Type | Required | Notes |
|-------|------|----------|--------|
| `name` | string | yes | 1–120 chars |
| `project` | object | yes | Full editor project JSON |
| `template_id` | number \| null | no | Default `null` |

**Response `201`:** project object + `editor_url`

**Errors:** `402` project limit · `413` payload too large

---

## PUT `agent/projects/:id`

Replace project JSON (low-level). For pack iteration prefer re-import with `project_id`.

**Request body**

| Field | Type | Required |
|-------|------|----------|
| `name` | string | yes |
| `project` | object | yes |
| `status` | `"active"` \| `"archived"` | no (default `active`) |

**Response `200`:** updated project object

---

## POST `agent/template-packs`

Import a portable `storeframe-template` pack (JSON). This is the main create / re-import endpoint.

**Request body**

| Field | Type | Required | Notes |
|-------|------|----------|--------|
| `pack` | object | yes | Must match pack schema (`format: storeframe-template`) |
| `name` | string | no | Overrides project display name |
| `project_id` | string (UUID) | no | If set → **update** that project; if omitted → **create** |

**Create example**

```json
{
  "pack": {
    "format": "storeframe-template",
    "version": 1,
    "slug": "agent-orbit-iphone",
    "name": "Agent Orbit",
    "platform": "iphone",
    "template": {
      "palette": ["#071018", "#14B8A6", "#F8FAFC"],
      "linked": true,
      "trustLabel": "You stay in control",
      "pages": [
        { "title": "Your agent\nat the helm", "subtitle": "Goals in, clear next moves out" }
      ]
    }
  }
}
```

**Re-import example**

```json
{
  "project_id": "11111111-1111-1111-1111-111111111111",
  "pack": { "...": "updated pack JSON" }
}
```

**Response `201` (create) / `200` (update)**

```json
{
  "public_id": "11111111-1111-1111-1111-111111111111",
  "name": "Agent Orbit project",
  "template_id": null,
  "project": {},
  "updated_at": "2026-07-22T05:00:00.000Z",
  "editor_url": "https://aistoreframe.shop/editor?mode=custom&platform=iphone&project=11111111-1111-1111-1111-111111111111",
  "action": "created",
  "workflow": "yaml_edit_reimport",
  "note": "..."
}
```

`action` is `"created"` or `"updated"`.

**Errors:** `404` unknown `project_id` · `402` project limit (create only) · `422` invalid pack

Pack field details: `../storeframe-template-from-brief/references/schema.md`

---

## POST `agent/projects/:id/screenshots`

Upload phone screenshots and attach them to project frames by index.

**Request body**

```json
{
  "pages": [
    { "index": 0, "content_type": "image/jpeg", "image_base64": "<base64 without data: prefix>" },
    { "index": 1, "data_url": "data:image/png;base64,...." }
  ]
}
```

Limits: 1–10 pages per call · jpeg/png/webp · ≤4.5MB each.

**Response** includes updated `project`, `editor_url`, and `attached: [{ index, url, bytes }]`.

---

## POST `agent/projects/:id/exports`

Consume one export credit and **render store frames** for Codex (default).

**Request body (optional)**

```json
{ "render": true }
```

Set `"render": false` to only reserve the credit without generating images.

**Response `201`**

```json
{
  "request_id": "22222222-2222-2222-2222-222222222222",
  "watermark": false,
  "plan": "pro",
  "page_count": 5,
  "editor_url": "https://aistoreframe.shop/editor?mode=custom&platform=iphone&project=…",
  "png_bytes_available": true,
  "format": "svg",
  "download_url": "https://api.slidemint.shop/api/v1/storeframe/assets/…",
  "pages": [
    {
      "index": 0,
      "url": "https://api.slidemint.shop/api/v1/storeframe/assets/…",
      "content_type": "image/svg+xml",
      "width": 1290,
      "height": 2796,
      "markdown": "![Frame 1](https://api.slidemint.shop/api/v1/storeframe/assets/…)"
    }
  ],
  "markdown": "![Frame 1](...)\n\n![Frame 2](...)",
  "instructions": "Paste markdown into Codex to display the frames."
}
```

**Errors:** `404` project · `402` export limit reached

Agent should paste `markdown` (or each `pages[].url`) into the chat so the user sees the rendered store frames.

---

## POST `agent/templates/search`

Smart search: multi-keyword retrieval → AI rank against the user brief.

**Request body**

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `prompt` | string | yes | Full user need (2–1000 chars) |
| `keywords` | string[] | no | 1–12 keywords; if omitted, server derives from `prompt` |
| `platform` | `iphone` \| `ipad` | no | Filter device |
| `locale` | string | no | Affects AI reason language (`zh` / `en`) |
| `limit` | int | no | Final picks, 1–18, default `8` |
| `candidate_limit` | int | no | Keyword pool size, 8–60, default `40` |

**Example**

```http
POST /api/v1/storeframe/agent/templates/search
Authorization: Bearer sf_xxx
Content-Type: application/json

{
  "prompt": "Habit tracker with daily streaks for a wellness iPhone app",
  "keywords": ["habit", "streak", "wellness", "daily", "rewards"],
  "platform": "iphone",
  "limit": 6
}
```

**Response `200`**

```json
{
  "summary": "These templates lean into streak boards and daily habit stories.",
  "keywords_used": ["habit", "streak", "wellness", "daily", "rewards"],
  "mode": "keyword_then_ai",
  "total": 3,
  "items": [
    {
      "id": 12,
      "slug": "streak-board-daily-rewards-iphone",
      "name": "Streak Board",
      "description": "...",
      "platform": "iphone",
      "category": "Health & Fitness",
      "tier": "free",
      "page_count": 5,
      "preview_gradient": "linear-gradient(...)",
      "hit_score": 3,
      "matched_keywords": ["habit", "streak", "daily"],
      "reason": "Closest match for streak-led habit marketing.",
      "catalog_url": "https://aistoreframe.shop/?template=streak-board-daily-rewards-iphone#templates",
      "editor_url": "https://aistoreframe.shop/editor?template=streak-board-daily-rewards-iphone&platform=iphone",
      "preview_url": "https://aistoreframe.shop/editor?template=streak-board-daily-rewards-iphone&platform=iphone",
      "preview_frames": [
        { "index": 0, "title": "Keep the streak", "subtitle": "...", "background": "#0f172a", "accent": "#22c55e" }
      ],
      "use_template": "POST agent/templates/streak-board-daily-rewards-iphone/projects"
    }
  ],
  "note": "Show catalog_url or preview_url. No static PNG thumbnail yet."
}
```

**Errors:** `402` not Pro/Max · `422` invalid body

`mode` is `keyword_then_ai` when OpenRouter ranking is available, otherwise `keyword_only`.

---

## GET `agent/templates`

Simple paginated keyword search (`q` is a single string). Prefer `POST agent/templates/search` for agent UX.

**Query**

| Param | Type | Default |
|-------|------|---------|
| `platform` | `iphone` \| `ipad` | — |
| `q` | string | — |
| `page` | int ≥ 1 | `1` |
| `per_page` | 1–60 | `24` |

**Response `200`** — each item includes `preview_gradient`, `catalog_url`, `editor_url`, `preview_url`.

---

## GET `agent/templates/:slug`

Fetch one public catalog template including `template` definition JSON, preview links, and `preview_frames`.

**Response `200`**

```json
{
  "id": 3,
  "slug": "fintech-pulse",
  "name": "Fintech Pulse",
  "description": "...",
  "platform": "iphone",
  "category": "Finance",
  "tier": "free",
  "page_count": 5,
  "canvas_width": 1290,
  "canvas_height": 2796,
  "preview_gradient": "linear-gradient(...)",
  "catalog_url": "https://aistoreframe.shop/?template=fintech-pulse#templates",
  "editor_url": "https://aistoreframe.shop/editor?template=fintech-pulse&platform=iphone",
  "preview_url": "https://aistoreframe.shop/editor?template=fintech-pulse&platform=iphone",
  "preview_frames": [{ "index": 0, "title": "...", "subtitle": "...", "background": null, "accent": null }],
  "template": {
    "palette": ["#111827", "#2563eb", "#F8FAFC"],
    "pages": [{ "title": "...", "subtitle": "..." }]
  }
}
```

---

## POST `agent/templates/:slug/projects`

Clone a catalog template into a new cloud project (the “use this template” step).

**Request body** (optional)

```json
{ "name": "My Habit Screenshots" }
```

**Response `201`**

```json
{
  "public_id": "11111111-1111-1111-1111-111111111111",
  "name": "My Habit Screenshots",
  "template_id": 12,
  "template_slug": "streak-board-daily-rewards-iphone",
  "project": { "editorMode": "template", "platform": "iphone", "pages": [] },
  "updated_at": "2026-07-22T05:00:00.000Z",
  "editor_url": "https://aistoreframe.shop/editor?template=streak-board-daily-rewards-iphone&project=11111111-1111-1111-1111-111111111111",
  "action": "created_from_catalog",
  "catalog_url": "https://aistoreframe.shop/?template=streak-board-daily-rewards-iphone#templates",
  "note": "Open editor_url to customize."
}
```

**Errors:** `404` unknown slug · `402` project limit

---

## Session-only key management (browser, not agent)

These use the Google session cookie on `aistoreframe.shop`, not the Bearer key:

| Method | Path | Purpose |
|--------|------|---------|
| GET | `api-keys` | List active keys (`name`, `key_prefix`, `last_used_at`, `created_at`) |
| POST | `api-keys` | Create key → returns `{ token }` once; body `{ "name" }` |
| PUT | `api-keys/:id` | Rename key → body `{ "name" }` |
| DELETE | `api-keys/:id` | Revoke |

`last_used_at` updates automatically whenever the Bearer key calls any agent route.

Agents should never call these; the human creates the key in **Your work → Agent API keys**.

---

## Common status codes

| Code | Meaning |
|------|---------|
| 200 / 201 | Success |
| 401 | Missing/invalid Bearer key |
| 402 | Plan / project / export limit |
| 403 | User disabled |
| 404 | Not found |
| 413 | Body too large |
| 422 | Validation error |
| 429 | Too many API keys (create key) |
