# Brandlit curl cookbook

**Base:** `https://brandlit.blacklabelbots.com`  
**Auth:** cookie jar only (`bl_uid` guest · `bl_session` after signup/Connect). No `Authorization: Bearer` yet.  
**Always:** `-c /tmp/brandlit.jar -b /tmp/brandlit.jar` so identity sticks.

> **Prefer a CLI?** Every recipe below has a one-liner in the [CLI README](/docs/CLI-README.md) (`brandlit connect login`, `projects save/publish`, `domain attach/verify`). The CLI's jar (`~/.config/brandlit/cookies.txt`) is curl-compatible.

```bash
BASE=https://brandlit.blacklabelbots.com
JAR=/tmp/brandlit.jar
rm -f "$JAR"
```

---

## 1) Guest session (mint `bl_uid`)

```bash
curl -sS -c "$JAR" -b "$JAR" "$BASE/api/auth/me" | jq .
# First call Set-Cookie: bl_uid=… (HttpOnly). Reuse the jar.
```

Optional explicit guest uid header (when cookies unavailable):

```bash
curl -sS -H "X-Brandlit-Uid: $(uuidgen | tr -d '-')" "$BASE/api/auth/me" | jq .
```

---

## 2) Discovery

```bash
curl -sS "$BASE/health" | jq '{ok,platformBrain,stripe,payg,money,hosting,identity}'
curl -sS "$BASE/api/mode" | jq .
curl -sS "$BASE/api/billing/products" | jq .
curl -sS "$BASE/api/templates" | jq '.templates[] | {id,name,category}'
curl -sS "$BASE/api/domain/instructions?projectId=YOUR_PROJECT_ID" | jq .
```

> **Prices:** read the current catalog from `/api/billing/products`. Do not hard-code amounts.

---

## 3) Connect with Grok Bot (device-code poll loop)

```bash
# Start
START=$(curl -sS -c "$JAR" -b "$JAR" "$BASE/api/auth/grokbot/start")
echo "$START" | jq .
DEVICE=$(echo "$START" | jq -r .device_code)
USER=$(echo "$START" | jq -r .user_code)
echo "Approve user_code=$USER at $(echo "$START" | jq -r .verification_uri_complete)"
echo "Or open deep link: $(echo "$START" | jq -r .grokbot_deep_link)"

# Poll until approved (interval ≈ 2s). Approved response Sets bl_session.
while true; do
  POLL=$(curl -sS -c "$JAR" -b "$JAR" "$BASE/api/auth/grokbot/poll?device_code=$DEVICE")
  STATUS=$(echo "$POLL" | jq -r .status)
  echo "$POLL" | jq '{ok,status,message,brainMode,hasGrokBrain}'
  case "$STATUS" in
    approved) break ;;
    pending) sleep 2 ;;
    *) echo "stop: $STATUS"; break ;;
  esac
done

curl -sS -c "$JAR" -b "$JAR" "$BASE/api/auth/grokbot/status" | jq .
```

Same-browser approve (after start):

```bash
curl -sS -c "$JAR" -b "$JAR" -X POST "$BASE/api/auth/grokbot/approve" \
  -H 'content-type: application/json' \
  -d "{\"device_code\":\"$DEVICE\"}" | jq .
# Then poll again to mint bl_session
```

The OAuth callback route returns **501**; use the device-code flow.

---

## 4) Email signup / signin (also mints `bl_session`)

```bash
curl -sS -c "$JAR" -b "$JAR" -X POST "$BASE/api/auth/signup" \
  -H 'content-type: application/json' \
  -d '{"email":"you@example.com","password":"at-least-8-chars"}' | jq .

curl -sS -c "$JAR" -b "$JAR" -X POST "$BASE/api/auth/signin" \
  -H 'content-type: application/json' \
  -d '{"email":"you@example.com","password":"at-least-8-chars"}' | jq .

curl -sS -c "$JAR" -b "$JAR" "$BASE/api/auth/me" | jq '{authed,guest,uid,authMethod,usage}'
```

---

## 5) Create project → save → publish

```bash
# Blank create (reliable). templateId can 400 unknown_template if catalog ids drift.
CREATE=$(curl -sS -c "$JAR" -b "$JAR" -X POST "$BASE/api/projects" \
  -H 'content-type: application/json' \
  -d '{"name":"My site","mode":"blank"}')
echo "$CREATE" | jq .
PID=$(echo "$CREATE" | jq -r .project.id)

curl -sS -c "$JAR" -b "$JAR" "$BASE/api/projects" | jq .
curl -sS -c "$JAR" -b "$JAR" "$BASE/api/projects/$PID" | jq '{ok,project:{id,name,published,previewUrl,siteUrl}}'

curl -sS -c "$JAR" -b "$JAR" -X POST "$BASE/api/projects/$PID/save" \
  -H 'content-type: application/json' \
  -d '{"name":"My site","html":"<!doctype html><html><body><h1>Hello Brandlit</h1></body></html>"}' | jq .

curl -sS -c "$JAR" -b "$JAR" -X POST "$BASE/api/projects/$PID/publish" \
  -H 'content-type: application/json' \
  -d '{}' | jq '{ok,publicUrl,pathUrl,siteUrl,byoCnameTarget}'

# Public fetch (no cookie needed)
curl -sS -o /dev/null -w '%{http_code}\n' "$BASE/p/$PID"
curl -sS -o /dev/null -w '%{http_code}\n' "https://$PID.brandlit.blacklabelbots.com/"
```

---

## 6) Grok / DomOps call

Requires Connect (and/or vaulted key depending on live gate). Expect `connect_grokbot` or `needs_key` until linked.

```bash
curl -sS -c "$JAR" -b "$JAR" -X POST "$BASE/api/grok" \
  -H 'content-type: application/json' \
  -d '{"utterance":"Make the headline say Hello from curl","snapshot":{}}' | jq .

# Optional key ping
curl -sS -c "$JAR" -b "$JAR" "$BASE/api/grok/test" | jq .
```

Advanced BYOK vault (optional):

```bash
curl -sS -c "$JAR" -b "$JAR" -X POST "$BASE/api/key" \
  -H 'content-type: application/json' \
  -d "{\"key\":\"$XAI_API_KEY\"}" | jq .
curl -sS -c "$JAR" -b "$JAR" "$BASE/api/key/status" | jq .
```

---

## 7) Domain instructions / BYO attach / verify

```bash
curl -sS "$BASE/api/domain/instructions?projectId=$PID" | jq .

curl -sS -c "$JAR" -b "$JAR" -X POST "$BASE/api/domain/attach" \
  -H 'content-type: application/json' \
  -d "{\"domain\":\"www.example.com\",\"projectId\":\"$PID\"}" | jq .

# After DNS CNAME www → {projectId}.brandlit.blacklabelbots.com
curl -sS -c "$JAR" -b "$JAR" -X POST "$BASE/api/domain/verify" \
  -H 'content-type: application/json' \
  -d "{\"domain\":\"www.example.com\",\"projectId\":\"$PID\"}" | jq '{ok,verified,cnameHit,message,cnameTarget}'

curl -sS "$BASE/api/domain/search?q=mybrand&limit=5" | jq .
```

`POST /api/domain/buy` is partial — prefer BYO attach when register is blocked.

---

## 8) Billing products (read-only discovery)

```bash
curl -sS "$BASE/api/billing/products" | jq .
```

Checkout and confirm need a signed-in `bl_session` and are meant for people in a browser, not automation.

---

## 9) Usage meters

```bash
curl -sS -c "$JAR" -b "$JAR" "$BASE/api/usage" | jq .
curl -sS -c "$JAR" -b "$JAR" -X POST "$BASE/api/usage/heartbeat" \
  -H 'content-type: application/json' \
  -d '{"focused":true,"deltaSec":15}' | jq .
```

---

## Error codes (branch on `code`)

| code | meaning |
|------|---------|
| `connect_grokbot` | Need device-code Connect |
| `needs_key` | Live gate asking for BYOK / paired path |
| `free_exhausted` | Meter gated (402 on some writes) |
| `auth_required` | Need `bl_session` (billing confirm/checkout) |
| `unknown_template` | `templateId` not in live catalog |
| `oauth_unconfigured` / `oauth_pending` | OAuth callback not enabled (501) — use device-code |
| `expired` | Device code gone |
| `platform_brain_unconfigured` | Grok is temporarily unavailable on the service |

Stable envelope: `{ ok, error?, code?, … }`.


## Project ops, sidecar, and assets (added 2026-10-05)

`GET /api/projects/{id}` now also returns `ops`, `sidecar`, and `assets[]`. Cookie auth is the same as the other project calls.

```bash
JAR=jar.txt; B=https://brandlit.blacklabelbots.com
# save html + ops + sidecar (sidecar: object replaces, null deletes, omitted = unchanged; max 64 KB)
curl -s -b $JAR -c $JAR -X POST $B/api/projects/$ID/save -H 'content-type: application/json' \
  -d '{"html":"<!doctype html><h1 data-bl=title>Hi</h1>","ops":[{"op":"setText","sel":"[data-bl=title]","text":"Hi"}],"sidecar":{"city":"Austin","logoPath":"logos/brand.png"}}'

# upload many files at once (filename may include folders; optional prefix)
curl -s -b $JAR -X POST $B/api/projects/$ID/assets -F prefix=logos -F file=@brand.png -F file=@mark.svg

# or one raw file
curl -s -b $JAR -X PUT $B/api/projects/$ID/assets/fonts/brand.woff2 -H 'content-type: font/woff2' --data-binary @brand.woff2

curl -s -b $JAR $B/api/projects/$ID/assets          # list
curl -s -b $JAR $B/api/projects/$ID | jq '{ops, sidecar, assets: [.assets[].path]}'
curl -s -b $JAR -X DELETE $B/api/projects/$ID/assets/logos/mark.svg
```

Each asset has a stable `url`: `https://brandlit.blacklabelbots.com/project-assets/{id}/{path}`. You can keep relative refs in draft HTML (`src="logos/brand.png"`); on publish, `GET /p/{id}` rewrites matching relative `src`/`href`/`srcset`/CSS `url()` to that absolute URL (draft storage stays relative). Absolute http(s) and `data:` URLs are left alone. Assets are public after publish; before publish, only the owner can read them.
Limits: 10 MB per file, 20 files per request, 25 MB per request, 200 files and 50 MB per project. HTML uploads are rejected (415).
