# Test the Cramdeck Integrations API locally with Claude

> **Audience:** a developer running Cramdeck on their own machine (or local
> network) who wants to verify the integrations API end-to-end with Claude
> as the deck author.
>
> **Spec version:** 1.3.0 — see [`CHANGELOG.md`](./CHANGELOG.md).

This is the fastest path that works on **any** Claude surface (claude.ai
chat, Claude Desktop, the Anthropic API) without writing a tool-use
adapter. Claude generates a `.deck.md` file, you `curl` it to the import
endpoint, the deck shows up in your library.

If you want autonomous tool-use (Claude calls the API directly), see
[`end-user-setup.md` Step 4](./end-user-setup.md#step-4--tool-use-pattern-claude-projects)
and [`ai-agent-tool-use-prompt.md`](./ai-agent-tool-use-prompt.md). This
guide covers the **author-only** flow because it requires no plumbing
and is the right way to confirm the round-trip works.

**Want a real Claude Desktop "connector"?** This repo ships a local
stdio MCP server in
[`packages/cramdeck-mcp/`](../../packages/cramdeck-mcp/README.md) that
exposes the v1 API as five tools (`cramdeck_whoami`,
`cramdeck_list_decks`, `cramdeck_get_deck`, `cramdeck_import_deck`,
`cramdeck_update_deck`). Build it once, register it in
`claude_desktop_config.json`, and Claude Desktop calls the API directly
— no `curl` needed. See that package's README for the full setup.

---

## 0. Prerequisites

- Cramdeck dev server running locally — typically
  `npm run dev` and reachable at one of:
  - `https://localhost:3000` (loopback, browser-only)
  - `https://<your-LAN-ip>:3000` (e.g. `https://192.168.100.4:3000`,
    reachable from other devices and from `curl`)
- A signed-in user account on that instance.
- `curl` available in your shell (any recent version).
- Access to Claude — claude.ai, Claude Desktop, or any client that lets
  you set a system prompt.

> **About the self-signed cert.** The local dev server uses HTTPS with a
> self-signed certificate. Browsers prompt once and remember the
> exception; `curl` rejects it by default. Add `-k` (`--insecure`) to
> every `curl` command in this guide to skip verification — that's safe
> against your own machine, never against production.

Throughout this guide we use `https://192.168.100.4:3000` as the example
host. Replace it with the URL printed by `npm run dev`.

---

## 1. Create a Personal Access Token (PAT)

1. Open the dev app in your browser:
   `https://192.168.100.4:3000/profile`
2. Switch to the **Integrations** tab.
3. Click **Create token**, give it a name like `local-claude-test`,
   pick the **read & write** preset, click **Generate**.
4. Copy the token immediately. It starts with `cdk_pat_…` and is shown
   exactly once.

Verify the token works:

```bash
curl -k https://192.168.100.4:3000/api/v1/integrations/whoami \
  -H "Authorization: Bearer cdk_pat_…your_token…"
```

You should get back a JSON object with your user id and the token's
scopes. If you see `401 UNAUTHORIZED`, the token was copied with
whitespace or has been revoked — re-issue it and try again.

---

## 2. Prime Claude with the author system prompt

The author system prompt teaches Claude the `.deck.md` format, the nine
canonical card types, the per-field length caps, the HTML allow-list,
and the KaTeX rules. Without it Claude will guess at the format and you
will spend most of your testing time fixing import errors.

1. Open the live system prompt in your browser and copy everything:
   `https://192.168.100.4:3000/integrations/ai-agent-author-prompt.md`
2. Paste it into Claude as the system prompt:
   - **claude.ai chat** — start a new chat, paste it as the *first
     message* and add: *"Acknowledge by replying OK and then wait for
     my deck request."*
   - **claude.ai Projects** — open the project, edit **Project
     instructions**, paste it there.
   - **Claude Desktop** — same as a regular chat: first message of a new
     conversation.
   - **Anthropic API** — pass it as the `system` parameter of your
     `messages.create` call.

The prompt is ~360 lines but it's all instructions and a few worked
examples — Claude has plenty of context budget for it.

---

## 3. Ask Claude for a deck

In the same conversation, request a deck. Anything works, but a good
first test exercises multiple card types and content modes:

> Generate a 12-card deck on the Krebs cycle in English. Mix
> definition, explanation, comparison, and pitfall cards. Use HTML for
> any chemical formulas (subscripts) and LaTeX for any equations.

Claude will reply with **exactly one fenced code block** containing a
complete `.deck.md` document. The file has YAML-ish frontmatter, a deck
title, then one `### Card N` section per card with `Front:` / `Back:` /
optional `Extras:` sub-sections.

Save the contents to a file:

```bash
# In a scratch directory:
pbpaste > krebs.deck.md      # macOS
# or just paste into your editor and save it.
```

Sanity-check that the file starts with `---` and contains your card
count:

```bash
head -20 krebs.deck.md
grep -c '^### Card ' krebs.deck.md
```

---

## 4. Import the deck

```bash
curl -k -X POST https://192.168.100.4:3000/api/v1/integrations/decks/import \
  -H "Authorization: Bearer cdk_pat_…your_token…" \
  -H "Content-Type: text/markdown" \
  --data-binary @krebs.deck.md
```

A successful response looks like:

```json
{
  "deck": {
    "id": "ck_…",
    "title": "Krebs cycle",
    "cardCount": 12,
    "url": "https://192.168.100.4:3000/decks/ck_…"
  }
}
```

Open the URL — the deck is in your library, ready to study.

> **Tip — JSON instead of markdown.** If you'd rather have Claude emit
> JSON, paste [`ai-agent-tool-use-prompt.md`](./ai-agent-tool-use-prompt.md)
> as the system prompt instead. Then pipe Claude's reply through `jq`
> and `curl` it with `-H "Content-Type: application/json"`. The
> markdown route is friendlier to copy-paste because it round-trips
> through the editor too.

---

## 5. Iterate

The strength of this loop is that you can iterate quickly:

- *"Add 5 more pitfall cards focused on common student mistakes."* →
  Claude regenerates the whole deck → save → re-import with
  `?mode=update&targetDeckId=ck_…` to replace the existing one in
  place:

  ```bash
  curl -k -X POST \
    "https://192.168.100.4:3000/api/v1/integrations/decks/import?mode=update&targetDeckId=ck_…" \
    -H "Authorization: Bearer cdk_pat_…" \
    -H "Content-Type: text/markdown" \
    --data-binary @krebs.deck.md
  ```

- *"Convert all the formula cards to use the `formula` cardType with
  proper LaTeX."* → repeat.

- *"Trim the back of every card to ≤ 280 characters."* → the importer
  enforces 4,000 chars per side; this is just for reading-quality.

---

## 6. Common errors and fixes

| Status | Code | What it means | Fix |
|---|---|---|---|
| `401` | `UNAUTHORIZED` | Token missing or malformed. | Re-copy the PAT (no whitespace). |
| `401` | `TOKEN_REVOKED` | The PAT was revoked or expired. | Issue a new one, update your `curl` command. |
| `403` | `FORBIDDEN` | Token lacks `decks:write`. | Re-issue with the *read & write* preset. |
| `400` | `PARSE_FAILED` | `.deck.md` frontmatter is invalid (often missing `lang:`). | Tell Claude: *"`lang` is required in the frontmatter; use BCP-47 like en, fr, pl."* |
| `400` | `CARD_FIELD_TOO_LONG` | A field exceeds its cap (4,000 / 1,000 chars etc.). | Response lists offenders; ask Claude to shorten. |
| `400` | `INVALID_IMAGE_URL` | An image URL uses `data:`, `javascript:`, `http:`, or is over 2,048 chars. | Tell Claude: *"image URLs must be `https://` and ≤ 2,048 chars."* |
| `400` | `TOO_MANY_CARDS` | Deck has > 250 cards. | Split into multiple decks. |
| `429` | `RATE_LIMITED` | Hit the integrations bucket (60 req/min, 2,000 req/day per token). | Wait the seconds shown in `Retry-After`. |
| `429` | `CARD_WRITE_RATE_LIMITED` | Hit the session-API bucket (only relevant if you're also using the editor). | Same — back off using `Retry-After`. |
| Any | TLS error like `SSL certificate problem` | You forgot `-k` for the self-signed cert. | Add `-k` (only on local dev, never production). |

For the full error envelope and code list, hit
`https://192.168.100.4:3000/docs/integrations` (rendered docs) or
`https://192.168.100.4:3000/api/v1/integrations/openapi.json` (machine
spec).

---

## 7. Optional — one-shot script

If you'll be doing this often, save the following as `import-deck.sh`:

```bash
#!/usr/bin/env bash
set -euo pipefail

HOST="${CRAMDECK_HOST:-https://192.168.100.4:3000}"
TOKEN="${CRAMDECK_TOKEN:?set CRAMDECK_TOKEN to your cdk_pat_… secret}"
FILE="${1:?usage: ./import-deck.sh <path-to-deck.md> [targetDeckId]}"
TARGET="${2:-}"

URL="$HOST/api/v1/integrations/decks/import"
if [ -n "$TARGET" ]; then
  URL="$URL?mode=update&targetDeckId=$TARGET"
fi

curl -k -fsS -X POST "$URL" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: text/markdown" \
  --data-binary "@$FILE" | jq .
```

Then:

```bash
export CRAMDECK_HOST=https://192.168.100.4:3000
export CRAMDECK_TOKEN=cdk_pat_…
./import-deck.sh krebs.deck.md            # create
./import-deck.sh krebs.deck.md ck_abc123  # update existing
```

---

## What to test next

Once the basic loop works, exercise each card feature using the canonical
sample decks as Claude's reference:

- [`samples/french-a1-essentials.deck.md`](./samples/french-a1-essentials.deck.md) — plain text round-trip.
- [`samples/js-array-methods.deck.md`](./samples/js-array-methods.deck.md) — markdown with code blocks.
- [`samples/chemistry-common-compounds.deck.md`](./samples/chemistry-common-compounds.deck.md) — HTML allow-list (`sub`/`sup`).
- [`samples/physics-mechanics-formulas.deck.md`](./samples/physics-mechanics-formulas.deck.md) — LaTeX with `cardType: formula` and `name-formula`.
- [`samples/world-flags-europe.deck.md`](./samples/world-flags-europe.deck.md) — `frontMedia.imageUrl` + the auto image-dominant layout.

A quick way to validate every feature: ask Claude to *"adapt
`<sample>` into a similar deck for `<your topic>`, keeping every
content type and `cardType` distribution intact"*, then re-import and
visually scan the result.
