
Your free AI travel planner — describe your dream trip, and Pepper plans it with you.
Free forever · no credit card
Loading your board…
Every approved video lands on its channel's next free slot automatically — cadence set per channel. Rejected videos never appear here.
Issue a key pair from your browser session, trade it for a 1-hour token, then drive the board from any script. Every command below runs as printed — set the placeholders once and paste.
export PEPPERBOARD="https://pepperboard-production.up.railway.app"
Creates a client_key (pk_live_…, public) + client_secret (sk_live_…, shown ONCE) for your workspace.
curl -X POST "$PEPPERBOARD/api/agent/keys/v2" \
-b "pb_session=$PB_SESSION" \
-H "Content-Type: application/json" \
-d '{"name":"My agent","scopes":["board:read","instructions:write","videos:write","ideas:write","webhooks:write","media:read"]}'
Runs with your dashboard session: -b sends the pb_session cookie — copy its value from your browser's DevTools (Application → Cookies) while logged in. Save $CLIENT_KEY and $CLIENT_SECRET from the response now; the secret is never shown again (rotate the key if you lose it).
Trades the key pair for a Bearer access token — save it as $TOKEN.
curl -X POST "$PEPPERBOARD/api/v1/auth/token" \
-H "X-Client-Key: $CLIENT_KEY" \
-H "Content-Type: application/json" \
-d "{\"client_secret\":\"$CLIENT_SECRET\"}"
The client_key goes in the X-Client-Key header; the secret travels only here, in the JSON body — never as a Bearer token, never in a URL. Response: {"access_token":"…","token_type":"Bearer","expires_in":3600,"scopes":[…]}.
Submits a new channel idea — a card on the idea-first board — to your workspace.
curl -X POST "$PEPPERBOARD/api/v1/ideas" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"text":"Rainbow Giggles: a 2-minute lullaby episode about a sleepy moon"}'
Any /api/v1 write works the same way: Authorization: Bearer $TOKEN plus a JSON body. Send an Idempotency-Key header on every mutation; replays within 24h return the original response.
Drains unclaimed Muse-inbox events, oldest first — take the first event's id for step 5.
curl -s "$PEPPERBOARD/api/v1/inbox?limit=50" \ -H "Authorization: Bearer $TOKEN"
Each event row carries id, event, payload, and a pre-computed signature (same HMAC scheme as step 6, over the JSON payload).
Claims the event so no other worker grabs it, then marks it handled when the work is done.
export EVENT_ID="paste-an-id-from-step-4"
curl -X POST "$PEPPERBOARD/api/v1/inbox/claim" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"ids\":[\"$EVENT_ID\"]}"
# …do the work the event asks for…
curl -X POST "$PEPPERBOARD/api/v1/inbox/$EVENT_ID/handled" \
-H "Authorization: Bearer $TOKEN"
Claiming sets claimed_at; handling sets handled_at. If your worker crashes between the two, the event is safe to re-claim.
Checks the X-Pepperboard-Signature header on incoming webhooks.
const crypto = require('node:crypto'); // inside your webhook handler
const expected = 'sha256=' +
crypto.createHmac('sha256', WEBHOOK_SECRET) // your endpoint's webhook secret
.update(rawBody) // the EXACT request bytes, before JSON.parse
.digest('hex'); // lowercase hex, prefixed with "sha256="
const ok = sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)); // constant-time compare
The scheme is exactly what the server signs with: sha256= + lowercase hex HMAC-SHA256 of the raw request body, keyed with your endpoint's webhook secret. Webhooks also carry X-Pepperboard-Event and X-Pepperboard-Delivery headers. Reject and report on mismatch.
Know the budgets before you script a loop — and re-exchange before the token dies.
| Surface | Limit | On breach |
|---|---|---|
| /api/v1 calls | 300 / 5 min per key | 429 rate_limited + Retry-After |
| POST /api/v1/auth/token | 30 / min per client_key | 429 rate_limited + Retry-After |
Tokens live 1 hour (expires_in: 3600) — re-run step 2 before expiry. Rotation or revocation kills outstanding tokens on their very next request: the key row is re-read on every call, so there is nothing to flush. Defaults come from V1_RATE_LIMIT_PER_5MIN / TOKEN_RATE_LIMIT_PER_MIN on the server.
Full OpenAPI spec: GET /api/openapi.json — every /api/v1 endpoint plus the key/workspace lifecycle above. Key lifecycle from the dashboard: issue (POST /api/agent/keys/v2), rotate (POST /api/agent/keys/{id}/rotate), revoke (POST /api/agent/keys/{id}/revoke), revoke a whole workspace (POST /api/agent/workspaces/{workspaceId}/revoke-keys).
Connect your Muse: scoped API keys, signed webhooks, the setup kit — and a full audit trail of everything the agent does.
Loading…
Loading…
| Need | Endpoint | How often |
|---|---|---|
| New instructions | GET /api/v1/instructions?status=new | every 2–5 min |
| Board state | GET /api/v1/board | every 5 min |
| Inbox events | GET /api/v1/inbox | every 2–5 min |
| Media URLs | GET /api/v1/videos/{id}/signed-urls | on demand (24h expiry) |
1. GET /api/v1/inbox
→ unclaimed events, oldest first
2. Verify each signature:
expected = "sha256=" + HMAC_SHA256(webhook_secret,
raw_json(payload))
3. POST /api/v1/inbox/claim {"ids":[…]}
→ marks claimed_at
4. …do the work…
5. POST /api/v1/inbox/{id}/handled
→ marks handled_at
Claimed-but-unhandled rows are safe to
re-claim after a crash.