{"id":"8900d12c-807b-4581-8323-8f8c5dd7a00a","entityType":"agent","slug":"clawhub-chrischall-zola-api","name":"zola-api","canonicalUrl":"https://www.xpersona.co/agent/clawhub-chrischall-zola-api","canonicalPath":"/agent/clawhub-chrischall-zola-api","generatedAt":"2026-10-10T21:48:02.286Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T18:15:17.713Z","emptyReason":null},"description":"Query or update Zola wedding-planning data (vendors, budget, guests, seating, events/RSVPs, registry, gift tracker, inquiries, wedding website) straight from a shell with curl against mobile-api.zola.com, instead of running the zola-mcp server. Use when you want Zola data without the MCP, in a script, or on a machine where the MCP isn't installed. Triggers on \"check Zola\", \"Zola vendors/budget/guests/RSVP/seating/registry\", or any Zola wedding data request that should hit the API directly.","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.3K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:zola-api","sourceUrl":"https://clawhub.ai/chrischall/zola-api","homepage":"https://clawhub.ai/chrischall/skills/zola-api","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/chrischall/zola-api","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/chrischall/skills/zola-api","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":62,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"zola-api technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T18:15:17.713Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T18:15:17.713Z","emptyReason":null},"stars":null,"forks":null,"downloads":1300,"packageName":null,"latestVersion":"2.1.10","tractionLabel":"1.3K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T18:15:17.712Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T18:15:17.713Z","lastCrawledAt":"2026-10-10T18:15:17.712Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T18:15:17.712Z","lastVerifiedAt":null,"highlights":[{"version":"2.1.10","createdAt":"2026-10-09T23:26:49.449Z","changelog":"- Removed the file skill-card.md. - No feature, functionality, or documentation changes in this release.","fileCount":4,"zipByteSize":10396},{"version":"2.1.9","createdAt":"2026-10-07T13:41:45.993Z","changelog":"- Removed the file skill-card.md. - No user-facing functionality or documentation changes; maintenance release only.","fileCount":4,"zipByteSize":10407},{"version":"2.1.8","createdAt":"2026-10-05T02:51:38.665Z","changelog":"- Removed the sample skill card file (skill-card.md). - No user-facing changes to functionality or documentation. - Internal cleanup of unused files.","fileCount":4,"zipByteSize":10402},{"version":"2.1.7","createdAt":"2026-10-03T01:47:00.018Z","changelog":"- Removed the sample file skill-card.md. - No user-facing functionality changes; documentation and API usage remain unchanged.","fileCount":4,"zipByteSize":10472},{"version":"2.1.6","createdAt":"2026-10-02T15:49:19.691Z","changelog":"- Removed the skill-card.md file. - No feature or documentation changes in this version.","fileCount":4,"zipByteSize":10454},{"version":"2.1.5","createdAt":"2026-09-28T13:57:10.952Z","changelog":"- Removed the sample file skill-card.md. - No other user-facing changes in this version.","fileCount":4,"zipByteSize":10417},{"version":"2.1.4","createdAt":"2026-09-25T15:53:03.524Z","changelog":"- Removed the file skill-card.md. - No user-facing features or documentation were changed. - Maintenance release with minor cleanup (file removal only).","fileCount":4,"zipByteSize":10380},{"version":"2.1.3","createdAt":"2026-09-23T21:47:16.567Z","changelog":"- Removed unnecessary skill-card.md file to streamline repository. - No functional or documentation changes to the zola-api skill itself.","fileCount":4,"zipByteSize":10592}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:zola-api","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:zola-api` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/chrischall/zola-api before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-zola-api/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-zola-api/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-zola-api/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-zola-api/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-zola-api/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-zola-api/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T21:48:02.283Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-zola-api/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-zola-api/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-zola-api/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-zola-api/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T18:15:17.713Z","emptyReason":null},"readme":"Skill: zola-api\n\nOwner: chrischall\n\nSummary: Query or update Zola wedding-planning data (vendors, budget, guests, seating, events/RSVPs, registry, gift tracker, inquiries, wedding website) straight from a shell with curl against mobile-api.zola.com, instead of running the zola-mcp server. Use when you want Zola data without the MCP, in a script, or on a machine where the MCP isn't installed. Triggers on \"check Zola\", \"Zola vendors/budget/guests/RSVP/seating/registry\", or any Zola wedding data request that should hit the API directly.\n\nTags: latest:2.1.10\n\nVersion history:\n\nv2.1.10 | 2026-10-09T23:26:49.449Z | auto\n\n- Removed the file skill-card.md.\n- No feature, functionality, or documentation changes in this release.\n\nv2.1.9 | 2026-10-07T13:41:45.993Z | auto\n\n- Removed the file skill-card.md.\n- No user-facing functionality or documentation changes; maintenance release only.\n\nv2.1.8 | 2026-10-05T02:51:38.665Z | auto\n\n- Removed the sample skill card file (skill-card.md).\n- No user-facing changes to functionality or documentation.\n- Internal cleanup of unused files.\n\nv2.1.7 | 2026-10-03T01:47:00.018Z | auto\n\n- Removed the sample file skill-card.md.\n- No user-facing functionality changes; documentation and API usage remain unchanged.\n\nv2.1.6 | 2026-10-02T15:49:19.691Z | auto\n\n- Removed the skill-card.md file.\n- No feature or documentation changes in this version.\n\nv2.1.5 | 2026-09-28T13:57:10.952Z | auto\n\n- Removed the sample file skill-card.md.  \n- No other user-facing changes in this version.\n\nv2.1.4 | 2026-09-25T15:53:03.524Z | auto\n\n- Removed the file skill-card.md.\n- No user-facing features or documentation were changed.\n- Maintenance release with minor cleanup (file removal only).\n\nv2.1.3 | 2026-09-23T21:47:16.567Z | auto\n\n- Removed unnecessary skill-card.md file to streamline repository.\n- No functional or documentation changes to the zola-api skill itself.\n\nv2.1.2 | 2026-09-23T15:41:41.783Z | auto\n\n- Removed the sample skill card file (skill-card.md).\n- No user-facing functionality changes.\n\nv2.1.1 | 2026-09-21T04:13:40.864Z | auto\n\n- Removed the file skill-card.md.\n- No changes to core logic or user-facing features.\n\nv2.1.0 | 2026-09-20T02:52:15.800Z | auto\n\n- Removed the file skill-card.md.\n- No changes were made to the core functionality or documentation in SKILL.md.\n\nv2.0.0 | 2026-09-19T11:18:55.585Z | auto\n\nVersion 2.0.0\n\n- Removed the sample file: skill-card.md.\n- No other functionality or documentation was changed.\n\nv1.12.4 | 2026-09-15T19:24:38.405Z | auto\n\n- Removed the unnecessary skill-card.md file.\n- No functional changes to code or skill behavior; this is a documentation cleanup.\n\nv1.12.3 | 2026-09-14T14:12:26.095Z | auto\n\n- Removed the sample file skill-card.md.\n- No functional or user-facing changes; documentation and behavior remain unchanged.\n\nv1.12.2 | 2026-09-10T17:51:55.444Z | auto\n\n- Removed sample file skill-card.md.\n- No changes to core functionality or interfaces.\n- Documentation and usage instructions remain unchanged.\n\nv1.12.1 | 2026-09-09T21:16:52.333Z | auto\n\n- Removed redundant file: skill-card.md, streamlining the repository.\n- No functional or user-facing changes; all API usage and documentation remain unchanged.\n\nv1.12.0 | 2026-09-04T22:23:27.363Z | auto\n\n- Removed the file skill-card.md.\n- No changes to API or usage; documentation and functionality remain the same.\n\nv1.11.0 | 2026-08-31T00:22:51.334Z | auto\n\n- Removed the file skill-card.md.\n- No functional or user-facing changes; documentation and usage remain unchanged.\n\nv1.10.0 | 2026-08-29T21:37:47.918Z | auto\n\n- Removed the file skill-card.md.\n- No changes to code or core functionality.\n\nv1.9.0 | 2026-08-29T13:54:47.500Z | auto\n\nzola-api 1.9.0\n\n- Removed the file sample: skill-card.md\n- No changes to core functionality or usage.\n\nv1.8.1 | 2026-08-28T21:07:46.252Z | auto\n\n- Removed the skill-card.md file.\n- No changes to core functionality or usage; documentation and API guidance remain unchanged.\n\nv1.8.0 | 2026-08-28T11:35:20.632Z | auto\n\n- Add comprehensive setup and usage documentation in SKILL.md for authenticating and querying Zola's mobile API via curl (no MCP server required).\n- Document one-time authentication using the Zola refresh token and common session workflow for API calls.\n- Provide examples for resolving necessary account, registry, and wedding IDs.\n- Clarify best practices for safe read-modify-write operations and error handling when working with Zola endpoints.\n\nArchive index:\n\nArchive v2.1.10: 4 files, 10396 bytes\n\nFiles: references/mobile-api-endpoints.md (18315b), skill-card.md (1865b), SKILL.md (5663b), _meta.json (128b)\n\nFile v2.1.10:SKILL.md\n\n---\nname: zola-api\ndescription: >-\n  Query or update Zola wedding-planning data (vendors, budget, guests, seating,\n  events/RSVPs, registry, gift tracker, inquiries, wedding website) straight\n  from a shell with curl against mobile-api.zola.com, instead of running the\n  zola-mcp server. Use when you want Zola data without the MCP, in a script,\n  or on a machine where the MCP isn't installed. Triggers on \"check Zola\",\n  \"Zola vendors/budget/guests/RSVP/seating/registry\", or any Zola wedding\n  data request that should hit the API directly.\n---\n\n# Zola mobile API via curl (no MCP)\n\nZola's mobile API (`mobile-api.zola.com` — the same surface the iOS/iPad app\nand `zola-mcp` use) is a plain Bearer-JWT REST API reachable directly from a\nserver or shell — no browser bridge needed. This skill shells out to `curl`\nwith the JWT in an `Authorization: Bearer` header, exactly as\n`zola-mcp`'s `src/client.ts` does.\n\n## One-time setup: get the refresh token\n\nYou need Zola's long-lived (~1 year) refresh JWT — the `usr` cookie from a\nsigned-in `zola.com` session. Same credential `zola-mcp` uses:\n\n```sh\n# Prefer the env var zola-mcp itself reads (check its .env first):\ngrep -h ZOLA_REFRESH_TOKEN ~/git/zola-mcp/.env 2>/dev/null\nexport ZOLA_REFRESH_TOKEN='eyJhbGciOi...'   # or export directly if you have it\n```\n\nIf you don't have it yet: sign into zola.com, open DevTools → Application →\nCookies → `https://www.zola.com`, copy the `usr` value. (`zola-mcp` also has a\nfetchproxy fallback that reads this cookie from a signed-in browser tab — see\nits README — but that's the MCP's path, not this skill's; this skill assumes\nyou already have the token in hand.)\n\n## Core call pattern\n\nEvery mobile-api call needs a short-lived (30 min) **session token**, minted\nfrom the refresh token, plus a fixed set of headers (CloudFront WAF requires\n`x-zola-session-id` on every request — omit it and you get a 403).\n\n```sh\nBASE=https://mobile-api.zola.com\nDEVICE_SESSION_ID=$(uuidgen | tr 'a-z' 'A-Z')   # one per \"session\"; reuse across calls\nUA='Zola/42.5.0 (iPad; iOS 26.4; Scale/2.0)'\n\n# 1. Mint a 30-min session token from the refresh token\nSESSION_TOKEN=$(curl -sS -X POST \"$BASE/v3/sessions/refresh\" \\\n  -H 'content-type: application/json' -H 'accept: application/json' \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" \\\n  -H \"user-agent: $UA\" \\\n  -d \"{\\\"token\\\":\\\"$ZOLA_REFRESH_TOKEN\\\"}\" | jq -r '.data.session_token')\n\n# 2. Every subsequent call reuses $SESSION_TOKEN until it expires (~30 min),\n#    then re-run step 1. All calls carry the same 4 headers:\ncurl -sS -X GET \"$BASE/v3/users/me/context\" \\\n  -H \"authorization: Bearer $SESSION_TOKEN\" \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" \\\n  -H \"user-agent: $UA\" | jq '.data'\n```\n\nWrap this in a shell function or export the 4 headers once — every recipe in\n`references/mobile-api-endpoints.md` reuses `$BASE`, `$SESSION_TOKEN`,\n`$DEVICE_SESSION_ID`, `$UA`. POST/PUT bodies additionally need\n`-H 'content-type: application/json'`.\n\n(The app also sends a fifth header, `x-zola-user-session-id`, derived from a\nclaim inside the session JWT — `zola-mcp` only attaches it when the decode\nsucceeds, so it's not load-bearing; every endpoint below works without it.)\n\n## The one rule: resolve context first\n\nMost write/list endpoints are scoped by numeric IDs, never inferred — call\n`GET /v3/users/me/context` once per session and keep the three IDs around:\n\n```sh\nCTX=$(curl -sS \"$BASE/v3/users/me/context\" -H \"authorization: Bearer $SESSION_TOKEN\" \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" -H \"user-agent: $UA\")\nACCT=$(jq -r '.data.wedding_account.wedding_account_id' <<<\"$CTX\")\nREG=$(jq -r '.data.registry.id' <<<\"$CTX\")\nWEDDING_ID=$(jq -r '.data.wedding.wedding_id' <<<\"$CTX\")\n```\n\n`ACCT` (`wedding_account_id`) feeds guest/event/website-content paths; `REG`\n(`registry_id`) feeds registry/gift-tracker paths.\n\n## Ready-to-run endpoints\n\n`references/mobile-api-endpoints.md` has real, ready-to-run `curl` + `jq`\nrecipes for every one of the 30 `zola-mcp` tools' underlying calls\n(vendors, budget, guests, seating, inquiries, events/RSVPs/gifts/registry,\ndiscover/storefronts, registry items, invitations/card-projects, website\npages, website theme/customization, website content). Transcribed straight\nfrom `src/tools/*.ts` — same paths, same request bodies.\n\n## Mutation gotcha: most writes are read-modify-write\n\nZola's write endpoints replace whole objects, not deltas — the same tools\nthat build a request first re-GET the current record and only patch the\nrequested fields, or a full-state write can wipe unrelated fields.\n`docs/zola-api-quirks.md` in this repo documents the worst offenders (a\n`header_font` write nulling every other website color; guest writes needing\nto preserve `event_invitations` verbatim or lose them). Follow the\nread-modify-write shape shown per-endpoint in the references file — don't\nsend a bare partial body to `PUT`/`POST` write endpoints.\n\n## Output / error contract\n\n- A 2xx body is `{\"data\": ...}` for nearly every endpoint — pipe to\n  `jq '.data'`.\n- `401` — session token expired; re-mint via `POST /v3/sessions/refresh`\n  (step 1 above) and retry once.\n- `429` — rate limited; back off ~2s and retry once.\n- Any other non-2xx — the body is a JSON error envelope; read it directly,\n  it is not redacted here (unlike the MCP, which redacts secrets from error\n  text — don't paste raw error bodies containing the session token anywhere\n  public).\n- This project (`zola-mcp`) is developed and maintained by AI (Claude).\n\nFile v2.1.10:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"zola-api\",\n  \"version\": \"2.1.10\",\n  \"publishedAt\": 1791588409449\n}\n\nFile v2.1.10:references/mobile-api-endpoints.md\n\n# Zola mobile-api endpoints (curl + jq)\n\nAll paths are relative to `$BASE=https://mobile-api.zola.com`. Every call\ncarries `authorization: Bearer $SESSION_TOKEN`, `x-zola-platform-type:\niphone_app`, `x-zola-session-id: $DEVICE_SESSION_ID`, `user-agent: $UA` (see\n`SKILL.md`); POST/PUT/DELETE-with-body also need `-H 'content-type:\napplication/json'`. `$ACCT` = `wedding_account_id`, `$REG` = `registry_id`,\n`$WEDDING_ID` = `wedding_id`, all three from `GET /v3/users/me/context` (see\n`SKILL.md`'s \"resolve context first\" section). Response envelope is `{\"data\": ...}`\nunless noted. Paths/bodies below are transcribed from `src/tools/*.ts` —\neach section names its source file.\n\nShorthand used below:\n\n```sh\nH=(-H \"authorization: Bearer $SESSION_TOKEN\" -H \"x-zola-platform-type: iphone_app\" \\\n   -H \"x-zola-session-id: $DEVICE_SESSION_ID\" -H \"user-agent: $UA\")\nHJ=(\"${H[@]}\" -H 'content-type: application/json')\n```\n\n---\n\n## Context (`src/client.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/users/me/context\" | jq '.data'\n# .data.user.id, .data.wedding_account.wedding_account_id,\n# .data.wedding.{wedding_id,wedding_date,slug}, .data.registry.id\n```\n\n---\n\n## Vendors (`src/tools/vendors.ts`)\n\n**List booked vendors** — `POST /v3/account-vendors/booked-list` body `{}`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/account-vendors/booked-list\" -d '{}' \\\n  | jq '.data.booked_vendors'\n```\n\n**Search vendors (typeahead)** — `POST /v3/reference-vendors/typeahead-taxonomy`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/reference-vendors/typeahead-taxonomy\" \\\n  -d '{\"query\":\"Acme Photography\",\"taxonomy_key\":\"wedding-photographers\"}' | jq '.data'\n# taxonomy_key default: wedding-venues. Other keys: wedding-planners, wedding-bands-djs, ...\n```\n\n**Book a vendor** — find an unbooked slot from `booked-list` for the\n`vendor_type`, then `PUT /v5/account-vendors/vendor` (note: **v5**, not v3):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v5/account-vendors/vendor\" -d '{\n  \"uuid\": \"<slot-uuid-from-booked-list>\", \"id\": 0, \"vendor_type\": \"PHOTOGRAPHER\",\n  \"booked\": true, \"booking_source\": \"BOOKED_VENDORS\",\n  \"price_cents\": 350000, \"event_date\": null,\n  \"sync_with_budget_tool_enabled\": true, \"facet_keys\": [],\n  \"reference_vendor_request\": {\n    \"id\": null, \"name\": \"Acme Photography\", \"email\": null, \"phone\": null,\n    \"address\": {\"city\": \"Charlotte\", \"state_province_region\": \"NC\"}\n  }\n}' | jq '.data'\n```\n\n**Update a booked vendor** — same `PUT /v5/account-vendors/vendor`, but\nread-modify-write: GET `booked-list`, find by `uuid`, keep `id`/`vendor_type`,\nonly patch the fields you're changing (name/city/state/email/price/date\ndefault to the current value).\n\n**Unbook a vendor** — `POST /v3/account-vendors/vendor/unbook`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/account-vendors/vendor/unbook\" \\\n  -d '{\"uuid\":\"<vendor-uuid>\"}'\n```\n\n---\n\n## Budget (`src/tools/budget.ts`)\n\n**Get budget** — `GET /v3/budgets`:\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/budgets\" | jq '{\n  budgeted_cents: .data.budgeted_cents, cost_cents: .data.cost_cents,\n  paid_cents: .data.paid_cents, balance_due_cents: .data.balance_due_cents,\n  items: [.data.taxonomy_nodes[].items[] | {uuid, title, cost_cents, paid_cents}]\n}'\n```\n\n**Update a budget item** — read-modify-write: `GET /v3/budgets`, find the item\nby `uuid`, then `PUT /v3/budgets/items` with every required field (server\nrejects a bare partial):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/budgets/items\" -d '{\n  \"item_uuid\": \"<uuid>\", \"taxonomy_node_uuid\": \"<from-get>\",\n  \"estimated_cost_cents\": <from-get>, \"actual_cost_cents\": 250000,\n  \"note\": \"Deposit paid\", \"item_type\": \"<from-get, e.g. VENUE>\",\n  \"title\": \"<from-get>\"\n}' | jq '.data'\n```\n\n---\n\n## Guests (`src/tools/guests.ts`)\n\n**List guests** — `POST /v3/guestlists/directory/wedding-accounts/$ACCT` body\n`{\"sort_by_name_asc\": true}`. Guest shape is **flat** (fields directly on the\nguest object, no `{guest:{...}}` wrapper):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/guestlists/directory/wedding-accounts/$ACCT\" \\\n  -d '{\"sort_by_name_asc\": true}' \\\n  | jq '.data.guest_groups[] | {guest_group_id, guests: [.guests[] | {guest_id, first_name, family_name, rsvp}]}'\n```\n\n**Add a guest group** — `POST /v3/guestlists/groups`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/guestlists/groups\" -d '{\n  \"wedding_account_id\": '\"$ACCT\"',\n  \"guests\": [{\n    \"first_name\": \"Mike\", \"family_name\": \"Smith\", \"relationship_type\": \"PRIMARY\",\n    \"source\": \"IOS\", \"email_address\": \"\", \"mobile_phone\": \"\", \"affiliation\": \"PRIMARY_FRIEND\",\n    \"tier\": \"A\", \"country_code\": \"US\", \"prefix\": \"\", \"middle_name\": \"\", \"suffix\": \"\",\n    \"home_phone\": \"\", \"address1\": \"\", \"address2\": \"\", \"city\": \"\", \"state_province\": \"\",\n    \"postal_code\": \"\", \"event_invitations\": [], \"tags\": []\n  }],\n  \"guest_group_affiliation\": \"PRIMARY_FRIEND\", \"guest_group_tier\": \"A\",\n  \"guest_group_uuid\": \"\", \"envelope_recipient\": \"\", \"invited\": true,\n  \"invitation_sent\": false, \"save_the_date_sent\": false,\n  \"rsvp_question_answers\": [], \"gift_count\": 0\n}' | jq '.data'\n```\n\n**Update a guest group's address** — read-modify-write via\n`PUT /v3/guestlists/groups/wedding-accounts/$ACCT/bulk/directory`. **Must**\npreserve each guest's existing `event_invitations` verbatim (a group that\nsends `event_invitations: []` wipes them, per `docs/zola-api-quirks.md` §5):\n\n```sh\n# 1. GET the directory (above), find the target group by guest_group_id,\n#    keep its `guests[]` array as-is except the address fields you're changing.\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/guestlists/groups/wedding-accounts/$ACCT/bulk/directory\" \\\n  -d '{\"updated_guest_groups\": [ <full-group-object-with-patched-guest-addresses> ]}' \\\n  | jq '.data'\n```\n\n**Remove a guest group** — `PUT /v3/guestlists/groups/wedding-accounts/$ACCT/delete`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/guestlists/groups/wedding-accounts/$ACCT/delete\" \\\n  -d '{\"wedding_account_id\": '\"$ACCT\"', \"guest_group_ids\": [<id>]}'\n```\n\n---\n\n## Seating (`src/tools/seating.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/seating-charts/summaries\" | jq '.'   # NOT wrapped in `data`\ncurl -sS \"${H[@]}\" \"$BASE/v3/seating-charts/<chart-uuid>\" | jq '.'\n```\n\n**Unseated guests** — `POST /v3/guestlists/directory/wedding-accounts/$ACCT`\n(same call as guests list), filter client-side for\n`.guests[].seating_chart_seat == null`.\n\n**Assign a seat** — `PUT /v3/seating-charts/seats`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/seating-charts/seats\" -d '{\n  \"guest_uuid\": \"<guest-uuid>\", \"seat_uuid\": \"<seat-uuid>\",\n  \"table_uuid\": \"<table-uuid>\", \"seating_chart_uuid\": \"<chart-uuid>\"\n}' | jq '.'\n```\n\n---\n\n## Inquiries (`src/tools/inquiries.ts`)\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/inquiries/unified-inquiries\" -d '{}' \\\n  | jq '.data[].inquiry_summaries[] | {inquiry_uuid, vendor_name: .vendor_card.vendor_name, unread, status_text}'\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/inquiries/<inquiry-uuid>/conversation\" | jq '.data.messages'\n\ncurl -sS \"${H[@]}\" -X PUT \"$BASE/v3/inquiries/<inquiry-uuid>/conversation/read\"\n```\n\n---\n\n## Events, RSVPs, gifts, registry (`src/tools/events.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/events/wedding-accounts/$ACCT/groups\" \\\n  | jq '.data[].events[]'   # event_entity_id, name, start_at, num_guests_*\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/events/track-rsvps\" | jq '.data.modules'\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/gift_tracker/$REG\" | jq '.data | del(.info_modules)'\n\ncurl -sS \"${H[@]}\" \"$BASE/v4/shop/registry?registry_id=$REG&updated_modules=true\" | jq '.data'\n```\n\n**Update an event** — read-modify-write: GET the groups above, find by\n`event_entity_id`, then `PUT /v3/websites/events/{event_id}` with the full\nevent object (patch only the fields you're changing; everything else —\n`type`, `uuid`, `wedding_account_id`, counts, `meal_options` — round-trips\nfrom the GET):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/websites/events/<event_id>\" -d '{\n  \"event_entity_id\": <event_id>, \"uuid\": \"<from-get>\", \"wedding_account_id\": '\"$ACCT\"',\n  \"type\": \"<from-get>\", \"name\": \"Reception\", \"start_at\": \"<from-get>\", \"end_at\": \"<from-get>\",\n  \"timezone\": \"<from-get>\", \"venue_name\": \"The Grand Hall\", \"address1\": \"\", \"address2\": \"\",\n  \"city\": \"Charlotte\", \"state_province\": \"NC\", \"postal_code\": \"\", \"country_code\": \"US\",\n  \"note\": \"\", \"attire\": \"Black tie\", \"collect_rsvps\": true, \"public\": <from-get>,\n  \"display_order\": 0, \"num_guests_attending\": <from-get>, \"num_guests_declined\": <from-get>,\n  \"num_guests_not_responded\": <from-get>, \"meal_options\": <from-get>,\n  \"rsvp_questions\": [], \"add_booked_vendor\": false\n}' | jq '.data'\n```\n\n**Event invitations (per-guest, read-modify-write)** — from\n`src/tools/event-invitations.ts`. Each guest carries an `event_invitations`\narray; invite = append `{\"event_id\":<id>,\"id\":null,\"rsvp_type\":\"NO_RESPONSE\"}`,\nuninvite = drop that element, **preserve every other element verbatim**\n(same wipe risk as the guest-address write above). Write through the same\n`PUT /v3/guestlists/groups/wedding-accounts/$ACCT/bulk/directory` used for\nguest writes, sending `{\"updated_guest_groups\": [...]}` with full group\nobjects (each guest keeps its full shape, only `event_invitations` patched).\n\n---\n\n## Discover / storefronts (`src/tools/discover.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v4/your-wedding\" | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/storefronts/search\" -d '{\n  \"taxonomy_node_id\": 2, \"city\": \"Charlotte\", \"state\": \"NC\",\n  \"limit\": 24, \"offset\": 0, \"facets\": {},\n  \"metro_types\": [\"HOME\",\"HOME_SERVICE\",\"AWAY\"], \"metros\": [],\n  \"exclude_inquired_storefronts\": false, \"exclude_booked_storefronts\": false,\n  \"boost_featured_storefronts\": false, \"suggested_vendors_for_inquiry_limit\": 12\n}' | jq '.data'\n# taxonomy_node_id: 1=Venues 2=Photographers 3=Florists 7=Planners 9=Bands/DJs\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/storefronts/<storefront-uuid>\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/favorites/\" | jq '.data'\n```\n\n---\n\n## Registry items (`src/tools/registry-items.ts`)\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/categories/<category_id>/entities\" \\\n  -d '{\"offset\":0,\"limit\":50,\"registry_id\":\"'\"$REG\"'\"}' | jq '.data'\n\n# Default collection id: `.data.default_collection_id` from the registry GET\n# above, or the first `groups[].modules[]` entry with `type == \"COLLECTION\"`.\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/registries/$REG/collections/<collection_id>\" \\\n  -d '{\"sku_id\":\"<sku>\",\"quantity\":1,\"most_wanted\":false,\"enable_group_gifting\":false}' \\\n  | jq '.data'\n\n# Full replace — all fields required:\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/registries/$REG/items/<collection_item_id>\" -d '{\n  \"quantity\": 2, \"group_gift\": false, \"marked_fulfilled\": false,\n  \"personal_note\": \"\", \"most_wanted\": true, \"collection_id\": \"<collection_id>\"\n}' | jq '.data'\n\ncurl -sS \"${H[@]}\" -X DELETE \"$BASE/v3/registries/$REG/items/<collection_item_id>\"\n```\n\n---\n\n## Website pages & settings (`src/tools/website.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/pages/wedding-accounts/full\" | jq '.data'\n\ncurl -sS \"${H[@]}\" -X PUT \"$BASE/v3/websites/pages/<page_id>/hidden/true\"  # or /false\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/websites/pages/wedding-accounts/$ACCT/reorder\" \\\n  -d '{\"ids\": [<page_id_1>, <page_id_2>, ...]}' | jq '.data'\n\n# Partial patch is fine here (unlike most write endpoints):\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/websites/pages-v2/<page_id>\" \\\n  -d '{\"page_id\": <page_id>, \"title\": \"Our Story\"}' | jq '.data'\n```\n\n**Wedding settings** — read via `GET /v3/users/me/context` → `.data.wedding`;\nwrite is read-modify-write (all fields required) to\n`PUT /v3/weddings/{wedding_id}`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/weddings/$WEDDING_ID\" -d '{\n  \"wedding_id\": '\"$WEDDING_ID\"', \"account_id\": '\"$ACCT\"',\n  \"slug\": \"<from-get>\", \"owner_first_name\": \"<from-get>\", \"owner_last_name\": \"<from-get>\",\n  \"partner_first_name\": \"<from-get>\", \"partner_last_name\": \"<from-get>\",\n  \"title\": \"Alex & Jordan\", \"wedding_date\": \"<from-get>\", \"hashtag\": \"#alexandjordan2026\",\n  \"enable_search_engine\": true, \"enable_search_zola\": true,\n  \"city\": \"Charlotte\", \"state_province\": \"NC\", \"guest_count\": 150\n}' | jq '.data'\n```\n\n---\n\n## Website theme & customization (`src/tools/website-theme.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/themes/current\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/website-customizations/context\" | jq '.data.current_style_customizations'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/themes/search\" \\\n  -d '{\"limit\":50,\"offset\":0,\"theme_layout_types\":[\"MULTI_PAGE\"]}' | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/themes/current\" \\\n  -d '{\"theme_key\":\"galata\",\"theme_layout_type\":\"MULTI_PAGE\"}' | jq '.data'\n```\n\n**Update colors/fonts** — `POST /v3/websites/website-customizations/context`.\n**Gotcha (see `docs/zola-api-quirks.md` §1–4):** changing `header_font`\n(i.e. `header_font_family_id`) wipes every *other* active color unless you\nre-send them in the same POST — GET the current state first and bundle its\nnon-null `accent_color` / `background_color` / `body.color` /\n`navigation_customization.background_color` into the body. `body_font`'s\n`font_family_id` only accepts `68` (Libre Baskerville) or `198` (Circular) —\nany other value 500s. `header_color` / `nav_font_color` are **not** writable\non this API at all (web-api-only, cookie+CSRF auth — out of scope here).\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/websites/website-customizations/context\" -d '{\n  \"accent_color\": \"C9A66B\",\n  \"background_color\": \"FFFFFF\",\n  \"body_font\": {\"color\": \"222222\"},\n  \"navigation_customization\": {\"background_color\": \"FFFFFF\"}\n}' | jq '.data'\n```\n\n---\n\n## Website content: FAQs / home sections / POIs / travel (`src/tools/website-content.ts`)\n\nAll four follow the same list/add/update/remove shape;\n`{wedding_account_id: $ACCT}` and an `_entity_id: 0` (create) or the real id\n(update) prefix every write body. Deletes share one path shape:\n`DELETE /v3/websites/{pages}/{entities}/{entity_id}/wedding-accounts/$ACCT`\nwhere `{pages}` is looked up per-type from\n`GET /v3/websites/pages/wedding-accounts/full` (`.data.{home,faq,poi,travel}_page.page_id`).\n\n```sh\n# FAQs\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/faqs/wedding-accounts/$ACCT\" | jq '.data'\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/websites/faqs\" -d '{\n  \"wedding_account_id\": '\"$ACCT\"', \"faq_entity_id\": 0,\n  \"question\": \"What is the dress code?\", \"answer\": \"Cocktail attire\", \"display_order\": 0\n}' | jq '.data'\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/websites/faqs/<faq_entity_id>\" -d '{\n  \"wedding_account_id\": '\"$ACCT\"', \"faq_entity_id\": <faq_entity_id>,\n  \"question\": \"...\", \"answer\": \"...\", \"display_order\": 0\n}' | jq '.data'\ncurl -sS \"${H[@]}\" -X DELETE \"$BASE/v3/websites/pages/<faq_page_id>/entities/<faq_entity_id>/wedding-accounts/$ACCT\"\n\n# Home page story sections — same shape, path `/v3/websites/home-sections[...]`,\n# id field `homepage_entity_id`, extra `hidden` boolean, `title`+`subtitle`+`description`.\n\n# Points of interest — `/v3/websites/points-of-interest[...]`, id field\n# `poi_entity_id`; fields: title, description, address1/2, city, state_province,\n# postal_code, country_code, latitude, longitude, google_place_id, contact_phone, url.\n\n# Travel items — `/v3/websites/travel[...]`, id field `travel_entity_id`;\n# fields: type (HOTEL|FLIGHT|TRAIN|BUS|CAR|OTHER), name, note, code,\n# address1/2, city, state_province, postal_code, country_code, latitude,\n# longitude, google_place_id, contact_number, email_address, url, source\n# (GOOGLE_PLACES|MANUAL), timezone, display_order.\n```\n\nHome-section/POI/travel `list`/`add`/`update`/`remove` all mirror the FAQ\ncalls above 1:1 (same verbs, same `wedding_account_id` prefix, same delete\nshape) — see `src/tools/website-content.ts` for the exact field names per\ntype if you need the full body.\n\n---\n\n## Invitations / card projects (`src/tools/invitations.ts`)\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/card-projects/search_request\" -d '{\n  \"completed\": false, \"limit\": 30, \"card_suite_uuids\": [], \"card_suite_ids\": [],\n  \"offset\": 0, \"fetch_customizations\": true, \"include_deleted\": false,\n  \"medium\": [\"PAPER\",\"MAGNET\",\"DIGITAL\"], \"single_sample\": false\n}' | jq '.data'\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/card-projects/<project_uuid>\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/card-projects/<project_uuid>/validate\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/card-projects/<project_uuid>/project-guest-groups\" | jq '.data'\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v4/card-catalog/suites/details/<suite_uuid>\" | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/card-catalog/search/faceted\" -d '{\n  \"lead_card_types\": [], \"include_updated_proof_module\": true, \"limit\": 50, \"offset\": 0,\n  \"digital_suite\": false, \"include_lead_card_type_metadata\": true,\n  \"lead_card_type\": \"INVITATION\", \"include_module\": true\n}' | jq '.data'\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/favorites/card-suites/\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/rsvps/wedding-accounts/$ACCT\" | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/card-projects\" -d '{\n  \"quantity\": 150, \"lead_variation_uuid\": \"<variation-uuid>\",\n  \"extra_customizable\": false, \"account_id\": '\"$ACCT\"', \"suite_uuid\": \"<suite-uuid>\"\n}' | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/card-projects/<project_uuid>\" -d '{\n  \"customizations\": {\"<customization_uuid>\": {\"variation_uuid\": \"<new-variation-uuid>\"}}\n}' | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/card-projects/<project_uuid>/project-guest-groups\" -d '{\n  \"guest_group_requests\": [{\"guest_group_id\": <id>, \"enabled\": true}]\n}' | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/card-templates/preview\" -d '{\n  \"variation_uuids\": [\"<variation-uuid>\"], \"customizable\": true,\n  \"substitutions\": {\"first_name\": \"Alex\", \"wedding_date\": \"2026-10-17\"}\n}' | jq '.data'\n```\n\n**QR code** — preview returns raw image bytes (Content-Type lies as\n`image/jpeg` even for a PNG — sniff magic bytes if scripting):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/card-projects/qrcode/preview\" \\\n  -H 'accept: */*' -d '{\"dimension\":\"MEDIUM\",\"url_type\":\"CUSTOM\",\"enabled\":true,\"url\":\"https://example.com\"}' \\\n  -o qrcode.png\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/card-projects/<project_uuid>/customization/page/<page_uuid>/qrcode\" -d '{\n  \"dimension\": \"MEDIUM\", \"url_type\": \"CUSTOM\", \"color\": \"000000\",\n  \"enabled\": true, \"url\": \"https://example.com\"\n}' | jq '.data'\n```\n\nFile v2.1.10:skill-card.md\n\n## Description:\n\nGuides agents in querying and updating Zola wedding-planning data directly from a shell using authenticated API requests, without the MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nZola account holders and developers use this skill to inspect or manage wedding-planning records, including guests, budgets, events, RSVPs, registries, and websites, from a shell without an MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The long-lived Zola refresh token effectively grants account access and can leak through chats, logs, or shared terminals.\n\nMitigation: Keep tokens out of chats, logs, and committed files; handle them temporarily and rotate any exposed token.\n\nRisk: Write and delete examples can change live wedding data, and incomplete update bodies can erase unrelated fields.\n\nMitigation: Back up or export data, confirm each production change, and read the current record before sending a complete update body.\n\n## Reference(s):\n\n- [Zola API endpoint recipes](references/mobile-api-endpoints.md)\n- [ClawHub skill page](https://clawhub.ai/chrischall/skills/zola-api)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell command examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [API responses may contain private wedding-planning information.]\n\n## Skill Version(s):\n\n2.1.10 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v2.1.9: 4 files, 10407 bytes\n\nFiles: references/mobile-api-endpoints.md (18315b), skill-card.md (1906b), SKILL.md (5663b), _meta.json (127b)\n\nFile v2.1.9:SKILL.md\n\n---\nname: zola-api\ndescription: >-\n  Query or update Zola wedding-planning data (vendors, budget, guests, seating,\n  events/RSVPs, registry, gift tracker, inquiries, wedding website) straight\n  from a shell with curl against mobile-api.zola.com, instead of running the\n  zola-mcp server. Use when you want Zola data without the MCP, in a script,\n  or on a machine where the MCP isn't installed. Triggers on \"check Zola\",\n  \"Zola vendors/budget/guests/RSVP/seating/registry\", or any Zola wedding\n  data request that should hit the API directly.\n---\n\n# Zola mobile API via curl (no MCP)\n\nZola's mobile API (`mobile-api.zola.com` — the same surface the iOS/iPad app\nand `zola-mcp` use) is a plain Bearer-JWT REST API reachable directly from a\nserver or shell — no browser bridge needed. This skill shells out to `curl`\nwith the JWT in an `Authorization: Bearer` header, exactly as\n`zola-mcp`'s `src/client.ts` does.\n\n## One-time setup: get the refresh token\n\nYou need Zola's long-lived (~1 year) refresh JWT — the `usr` cookie from a\nsigned-in `zola.com` session. Same credential `zola-mcp` uses:\n\n```sh\n# Prefer the env var zola-mcp itself reads (check its .env first):\ngrep -h ZOLA_REFRESH_TOKEN ~/git/zola-mcp/.env 2>/dev/null\nexport ZOLA_REFRESH_TOKEN='eyJhbGciOi...'   # or export directly if you have it\n```\n\nIf you don't have it yet: sign into zola.com, open DevTools → Application →\nCookies → `https://www.zola.com`, copy the `usr` value. (`zola-mcp` also has a\nfetchproxy fallback that reads this cookie from a signed-in browser tab — see\nits README — but that's the MCP's path, not this skill's; this skill assumes\nyou already have the token in hand.)\n\n## Core call pattern\n\nEvery mobile-api call needs a short-lived (30 min) **session token**, minted\nfrom the refresh token, plus a fixed set of headers (CloudFront WAF requires\n`x-zola-session-id` on every request — omit it and you get a 403).\n\n```sh\nBASE=https://mobile-api.zola.com\nDEVICE_SESSION_ID=$(uuidgen | tr 'a-z' 'A-Z')   # one per \"session\"; reuse across calls\nUA='Zola/42.5.0 (iPad; iOS 26.4; Scale/2.0)'\n\n# 1. Mint a 30-min session token from the refresh token\nSESSION_TOKEN=$(curl -sS -X POST \"$BASE/v3/sessions/refresh\" \\\n  -H 'content-type: application/json' -H 'accept: application/json' \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" \\\n  -H \"user-agent: $UA\" \\\n  -d \"{\\\"token\\\":\\\"$ZOLA_REFRESH_TOKEN\\\"}\" | jq -r '.data.session_token')\n\n# 2. Every subsequent call reuses $SESSION_TOKEN until it expires (~30 min),\n#    then re-run step 1. All calls carry the same 4 headers:\ncurl -sS -X GET \"$BASE/v3/users/me/context\" \\\n  -H \"authorization: Bearer $SESSION_TOKEN\" \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" \\\n  -H \"user-agent: $UA\" | jq '.data'\n```\n\nWrap this in a shell function or export the 4 headers once — every recipe in\n`references/mobile-api-endpoints.md` reuses `$BASE`, `$SESSION_TOKEN`,\n`$DEVICE_SESSION_ID`, `$UA`. POST/PUT bodies additionally need\n`-H 'content-type: application/json'`.\n\n(The app also sends a fifth header, `x-zola-user-session-id`, derived from a\nclaim inside the session JWT — `zola-mcp` only attaches it when the decode\nsucceeds, so it's not load-bearing; every endpoint below works without it.)\n\n## The one rule: resolve context first\n\nMost write/list endpoints are scoped by numeric IDs, never inferred — call\n`GET /v3/users/me/context` once per session and keep the three IDs around:\n\n```sh\nCTX=$(curl -sS \"$BASE/v3/users/me/context\" -H \"authorization: Bearer $SESSION_TOKEN\" \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" -H \"user-agent: $UA\")\nACCT=$(jq -r '.data.wedding_account.wedding_account_id' <<<\"$CTX\")\nREG=$(jq -r '.data.registry.id' <<<\"$CTX\")\nWEDDING_ID=$(jq -r '.data.wedding.wedding_id' <<<\"$CTX\")\n```\n\n`ACCT` (`wedding_account_id`) feeds guest/event/website-content paths; `REG`\n(`registry_id`) feeds registry/gift-tracker paths.\n\n## Ready-to-run endpoints\n\n`references/mobile-api-endpoints.md` has real, ready-to-run `curl` + `jq`\nrecipes for every one of the 30 `zola-mcp` tools' underlying calls\n(vendors, budget, guests, seating, inquiries, events/RSVPs/gifts/registry,\ndiscover/storefronts, registry items, invitations/card-projects, website\npages, website theme/customization, website content). Transcribed straight\nfrom `src/tools/*.ts` — same paths, same request bodies.\n\n## Mutation gotcha: most writes are read-modify-write\n\nZola's write endpoints replace whole objects, not deltas — the same tools\nthat build a request first re-GET the current record and only patch the\nrequested fields, or a full-state write can wipe unrelated fields.\n`docs/zola-api-quirks.md` in this repo documents the worst offenders (a\n`header_font` write nulling every other website color; guest writes needing\nto preserve `event_invitations` verbatim or lose them). Follow the\nread-modify-write shape shown per-endpoint in the references file — don't\nsend a bare partial body to `PUT`/`POST` write endpoints.\n\n## Output / error contract\n\n- A 2xx body is `{\"data\": ...}` for nearly every endpoint — pipe to\n  `jq '.data'`.\n- `401` — session token expired; re-mint via `POST /v3/sessions/refresh`\n  (step 1 above) and retry once.\n- `429` — rate limited; back off ~2s and retry once.\n- Any other non-2xx — the body is a JSON error envelope; read it directly,\n  it is not redacted here (unlike the MCP, which redacts secrets from error\n  text — don't paste raw error bodies containing the session token anywhere\n  public).\n- This project (`zola-mcp`) is developed and maintained by AI (Claude).\n\nFile v2.1.9:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"zola-api\",\n  \"version\": \"2.1.9\",\n  \"publishedAt\": 1791380505993\n}\n\nFile v2.1.9:references/mobile-api-endpoints.md\n\n# Zola mobile-api endpoints (curl + jq)\n\nAll paths are relative to `$BASE=https://mobile-api.zola.com`. Every call\ncarries `authorization: Bearer $SESSION_TOKEN`, `x-zola-platform-type:\niphone_app`, `x-zola-session-id: $DEVICE_SESSION_ID`, `user-agent: $UA` (see\n`SKILL.md`); POST/PUT/DELETE-with-body also need `-H 'content-type:\napplication/json'`. `$ACCT` = `wedding_account_id`, `$REG` = `registry_id`,\n`$WEDDING_ID` = `wedding_id`, all three from `GET /v3/users/me/context` (see\n`SKILL.md`'s \"resolve context first\" section). Response envelope is `{\"data\": ...}`\nunless noted. Paths/bodies below are transcribed from `src/tools/*.ts` —\neach section names its source file.\n\nShorthand used below:\n\n```sh\nH=(-H \"authorization: Bearer $SESSION_TOKEN\" -H \"x-zola-platform-type: iphone_app\" \\\n   -H \"x-zola-session-id: $DEVICE_SESSION_ID\" -H \"user-agent: $UA\")\nHJ=(\"${H[@]}\" -H 'content-type: application/json')\n```\n\n---\n\n## Context (`src/client.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/users/me/context\" | jq '.data'\n# .data.user.id, .data.wedding_account.wedding_account_id,\n# .data.wedding.{wedding_id,wedding_date,slug}, .data.registry.id\n```\n\n---\n\n## Vendors (`src/tools/vendors.ts`)\n\n**List booked vendors** — `POST /v3/account-vendors/booked-list` body `{}`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/account-vendors/booked-list\" -d '{}' \\\n  | jq '.data.booked_vendors'\n```\n\n**Search vendors (typeahead)** — `POST /v3/reference-vendors/typeahead-taxonomy`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/reference-vendors/typeahead-taxonomy\" \\\n  -d '{\"query\":\"Acme Photography\",\"taxonomy_key\":\"wedding-photographers\"}' | jq '.data'\n# taxonomy_key default: wedding-venues. Other keys: wedding-planners, wedding-bands-djs, ...\n```\n\n**Book a vendor** — find an unbooked slot from `booked-list` for the\n`vendor_type`, then `PUT /v5/account-vendors/vendor` (note: **v5**, not v3):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v5/account-vendors/vendor\" -d '{\n  \"uuid\": \"<slot-uuid-from-booked-list>\", \"id\": 0, \"vendor_type\": \"PHOTOGRAPHER\",\n  \"booked\": true, \"booking_source\": \"BOOKED_VENDORS\",\n  \"price_cents\": 350000, \"event_date\": null,\n  \"sync_with_budget_tool_enabled\": true, \"facet_keys\": [],\n  \"reference_vendor_request\": {\n    \"id\": null, \"name\": \"Acme Photography\", \"email\": null, \"phone\": null,\n    \"address\": {\"city\": \"Charlotte\", \"state_province_region\": \"NC\"}\n  }\n}' | jq '.data'\n```\n\n**Update a booked vendor** — same `PUT /v5/account-vendors/vendor`, but\nread-modify-write: GET `booked-list`, find by `uuid`, keep `id`/`vendor_type`,\nonly patch the fields you're changing (name/city/state/email/price/date\ndefault to the current value).\n\n**Unbook a vendor** — `POST /v3/account-vendors/vendor/unbook`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/account-vendors/vendor/unbook\" \\\n  -d '{\"uuid\":\"<vendor-uuid>\"}'\n```\n\n---\n\n## Budget (`src/tools/budget.ts`)\n\n**Get budget** — `GET /v3/budgets`:\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/budgets\" | jq '{\n  budgeted_cents: .data.budgeted_cents, cost_cents: .data.cost_cents,\n  paid_cents: .data.paid_cents, balance_due_cents: .data.balance_due_cents,\n  items: [.data.taxonomy_nodes[].items[] | {uuid, title, cost_cents, paid_cents}]\n}'\n```\n\n**Update a budget item** — read-modify-write: `GET /v3/budgets`, find the item\nby `uuid`, then `PUT /v3/budgets/items` with every required field (server\nrejects a bare partial):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/budgets/items\" -d '{\n  \"item_uuid\": \"<uuid>\", \"taxonomy_node_uuid\": \"<from-get>\",\n  \"estimated_cost_cents\": <from-get>, \"actual_cost_cents\": 250000,\n  \"note\": \"Deposit paid\", \"item_type\": \"<from-get, e.g. VENUE>\",\n  \"title\": \"<from-get>\"\n}' | jq '.data'\n```\n\n---\n\n## Guests (`src/tools/guests.ts`)\n\n**List guests** — `POST /v3/guestlists/directory/wedding-accounts/$ACCT` body\n`{\"sort_by_name_asc\": true}`. Guest shape is **flat** (fields directly on the\nguest object, no `{guest:{...}}` wrapper):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/guestlists/directory/wedding-accounts/$ACCT\" \\\n  -d '{\"sort_by_name_asc\": true}' \\\n  | jq '.data.guest_groups[] | {guest_group_id, guests: [.guests[] | {guest_id, first_name, family_name, rsvp}]}'\n```\n\n**Add a guest group** — `POST /v3/guestlists/groups`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/guestlists/groups\" -d '{\n  \"wedding_account_id\": '\"$ACCT\"',\n  \"guests\": [{\n    \"first_name\": \"Mike\", \"family_name\": \"Smith\", \"relationship_type\": \"PRIMARY\",\n    \"source\": \"IOS\", \"email_address\": \"\", \"mobile_phone\": \"\", \"affiliation\": \"PRIMARY_FRIEND\",\n    \"tier\": \"A\", \"country_code\": \"US\", \"prefix\": \"\", \"middle_name\": \"\", \"suffix\": \"\",\n    \"home_phone\": \"\", \"address1\": \"\", \"address2\": \"\", \"city\": \"\", \"state_province\": \"\",\n    \"postal_code\": \"\", \"event_invitations\": [], \"tags\": []\n  }],\n  \"guest_group_affiliation\": \"PRIMARY_FRIEND\", \"guest_group_tier\": \"A\",\n  \"guest_group_uuid\": \"\", \"envelope_recipient\": \"\", \"invited\": true,\n  \"invitation_sent\": false, \"save_the_date_sent\": false,\n  \"rsvp_question_answers\": [], \"gift_count\": 0\n}' | jq '.data'\n```\n\n**Update a guest group's address** — read-modify-write via\n`PUT /v3/guestlists/groups/wedding-accounts/$ACCT/bulk/directory`. **Must**\npreserve each guest's existing `event_invitations` verbatim (a group that\nsends `event_invitations: []` wipes them, per `docs/zola-api-quirks.md` §5):\n\n```sh\n# 1. GET the directory (above), find the target group by guest_group_id,\n#    keep its `guests[]` array as-is except the address fields you're changing.\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/guestlists/groups/wedding-accounts/$ACCT/bulk/directory\" \\\n  -d '{\"updated_guest_groups\": [ <full-group-object-with-patched-guest-addresses> ]}' \\\n  | jq '.data'\n```\n\n**Remove a guest group** — `PUT /v3/guestlists/groups/wedding-accounts/$ACCT/delete`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/guestlists/groups/wedding-accounts/$ACCT/delete\" \\\n  -d '{\"wedding_account_id\": '\"$ACCT\"', \"guest_group_ids\": [<id>]}'\n```\n\n---\n\n## Seating (`src/tools/seating.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/seating-charts/summaries\" | jq '.'   # NOT wrapped in `data`\ncurl -sS \"${H[@]}\" \"$BASE/v3/seating-charts/<chart-uuid>\" | jq '.'\n```\n\n**Unseated guests** — `POST /v3/guestlists/directory/wedding-accounts/$ACCT`\n(same call as guests list), filter client-side for\n`.guests[].seating_chart_seat == null`.\n\n**Assign a seat** — `PUT /v3/seating-charts/seats`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/seating-charts/seats\" -d '{\n  \"guest_uuid\": \"<guest-uuid>\", \"seat_uuid\": \"<seat-uuid>\",\n  \"table_uuid\": \"<table-uuid>\", \"seating_chart_uuid\": \"<chart-uuid>\"\n}' | jq '.'\n```\n\n---\n\n## Inquiries (`src/tools/inquiries.ts`)\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/inquiries/unified-inquiries\" -d '{}' \\\n  | jq '.data[].inquiry_summaries[] | {inquiry_uuid, vendor_name: .vendor_card.vendor_name, unread, status_text}'\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/inquiries/<inquiry-uuid>/conversation\" | jq '.data.messages'\n\ncurl -sS \"${H[@]}\" -X PUT \"$BASE/v3/inquiries/<inquiry-uuid>/conversation/read\"\n```\n\n---\n\n## Events, RSVPs, gifts, registry (`src/tools/events.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/events/wedding-accounts/$ACCT/groups\" \\\n  | jq '.data[].events[]'   # event_entity_id, name, start_at, num_guests_*\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/events/track-rsvps\" | jq '.data.modules'\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/gift_tracker/$REG\" | jq '.data | del(.info_modules)'\n\ncurl -sS \"${H[@]}\" \"$BASE/v4/shop/registry?registry_id=$REG&updated_modules=true\" | jq '.data'\n```\n\n**Update an event** — read-modify-write: GET the groups above, find by\n`event_entity_id`, then `PUT /v3/websites/events/{event_id}` with the full\nevent object (patch only the fields you're changing; everything else —\n`type`, `uuid`, `wedding_account_id`, counts, `meal_options` — round-trips\nfrom the GET):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/websites/events/<event_id>\" -d '{\n  \"event_entity_id\": <event_id>, \"uuid\": \"<from-get>\", \"wedding_account_id\": '\"$ACCT\"',\n  \"type\": \"<from-get>\", \"name\": \"Reception\", \"start_at\": \"<from-get>\", \"end_at\": \"<from-get>\",\n  \"timezone\": \"<from-get>\", \"venue_name\": \"The Grand Hall\", \"address1\": \"\", \"address2\": \"\",\n  \"city\": \"Charlotte\", \"state_province\": \"NC\", \"postal_code\": \"\", \"country_code\": \"US\",\n  \"note\": \"\", \"attire\": \"Black tie\", \"collect_rsvps\": true, \"public\": <from-get>,\n  \"display_order\": 0, \"num_guests_attending\": <from-get>, \"num_guests_declined\": <from-get>,\n  \"num_guests_not_responded\": <from-get>, \"meal_options\": <from-get>,\n  \"rsvp_questions\": [], \"add_booked_vendor\": false\n}' | jq '.data'\n```\n\n**Event invitations (per-guest, read-modify-write)** — from\n`src/tools/event-invitations.ts`. Each guest carries an `event_invitations`\narray; invite = append `{\"event_id\":<id>,\"id\":null,\"rsvp_type\":\"NO_RESPONSE\"}`,\nuninvite = drop that element, **preserve every other element verbatim**\n(same wipe risk as the guest-address write above). Write through the same\n`PUT /v3/guestlists/groups/wedding-accounts/$ACCT/bulk/directory` used for\nguest writes, sending `{\"updated_guest_groups\": [...]}` with full group\nobjects (each guest keeps its full shape, only `event_invitations` patched).\n\n---\n\n## Discover / storefronts (`src/tools/discover.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v4/your-wedding\" | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/storefronts/search\" -d '{\n  \"taxonomy_node_id\": 2, \"city\": \"Charlotte\", \"state\": \"NC\",\n  \"limit\": 24, \"offset\": 0, \"facets\": {},\n  \"metro_types\": [\"HOME\",\"HOME_SERVICE\",\"AWAY\"], \"metros\": [],\n  \"exclude_inquired_storefronts\": false, \"exclude_booked_storefronts\": false,\n  \"boost_featured_storefronts\": false, \"suggested_vendors_for_inquiry_limit\": 12\n}' | jq '.data'\n# taxonomy_node_id: 1=Venues 2=Photographers 3=Florists 7=Planners 9=Bands/DJs\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/storefronts/<storefront-uuid>\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/favorites/\" | jq '.data'\n```\n\n---\n\n## Registry items (`src/tools/registry-items.ts`)\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/categories/<category_id>/entities\" \\\n  -d '{\"offset\":0,\"limit\":50,\"registry_id\":\"'\"$REG\"'\"}' | jq '.data'\n\n# Default collection id: `.data.default_collection_id` from the registry GET\n# above, or the first `groups[].modules[]` entry with `type == \"COLLECTION\"`.\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/registries/$REG/collections/<collection_id>\" \\\n  -d '{\"sku_id\":\"<sku>\",\"quantity\":1,\"most_wanted\":false,\"enable_group_gifting\":false}' \\\n  | jq '.data'\n\n# Full replace — all fields required:\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/registries/$REG/items/<collection_item_id>\" -d '{\n  \"quantity\": 2, \"group_gift\": false, \"marked_fulfilled\": false,\n  \"personal_note\": \"\", \"most_wanted\": true, \"collection_id\": \"<collection_id>\"\n}' | jq '.data'\n\ncurl -sS \"${H[@]}\" -X DELETE \"$BASE/v3/registries/$REG/items/<collection_item_id>\"\n```\n\n---\n\n## Website pages & settings (`src/tools/website.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/pages/wedding-accounts/full\" | jq '.data'\n\ncurl -sS \"${H[@]}\" -X PUT \"$BASE/v3/websites/pages/<page_id>/hidden/true\"  # or /false\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/websites/pages/wedding-accounts/$ACCT/reorder\" \\\n  -d '{\"ids\": [<page_id_1>, <page_id_2>, ...]}' | jq '.data'\n\n# Partial patch is fine here (unlike most write endpoints):\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/websites/pages-v2/<page_id>\" \\\n  -d '{\"page_id\": <page_id>, \"title\": \"Our Story\"}' | jq '.data'\n```\n\n**Wedding settings** — read via `GET /v3/users/me/context` → `.data.wedding`;\nwrite is read-modify-write (all fields required) to\n`PUT /v3/weddings/{wedding_id}`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/weddings/$WEDDING_ID\" -d '{\n  \"wedding_id\": '\"$WEDDING_ID\"', \"account_id\": '\"$ACCT\"',\n  \"slug\": \"<from-get>\", \"owner_first_name\": \"<from-get>\", \"owner_last_name\": \"<from-get>\",\n  \"partner_first_name\": \"<from-get>\", \"partner_last_name\": \"<from-get>\",\n  \"title\": \"Alex & Jordan\", \"wedding_date\": \"<from-get>\", \"hashtag\": \"#alexandjordan2026\",\n  \"enable_search_engine\": true, \"enable_search_zola\": true,\n  \"city\": \"Charlotte\", \"state_province\": \"NC\", \"guest_count\": 150\n}' | jq '.data'\n```\n\n---\n\n## Website theme & customization (`src/tools/website-theme.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/themes/current\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/website-customizations/context\" | jq '.data.current_style_customizations'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/themes/search\" \\\n  -d '{\"limit\":50,\"offset\":0,\"theme_layout_types\":[\"MULTI_PAGE\"]}' | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/themes/current\" \\\n  -d '{\"theme_key\":\"galata\",\"theme_layout_type\":\"MULTI_PAGE\"}' | jq '.data'\n```\n\n**Update colors/fonts** — `POST /v3/websites/website-customizations/context`.\n**Gotcha (see `docs/zola-api-quirks.md` §1–4):** changing `header_font`\n(i.e. `header_font_family_id`) wipes every *other* active color unless you\nre-send them in the same POST — GET the current state first and bundle its\nnon-null `accent_color` / `background_color` / `body.color` /\n`navigation_customization.background_color` into the body. `body_font`'s\n`font_family_id` only accepts `68` (Libre Baskerville) or `198` (Circular) —\nany other value 500s. `header_color` / `nav_font_color` are **not** writable\non this API at all (web-api-only, cookie+CSRF auth — out of scope here).\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/websites/website-customizations/context\" -d '{\n  \"accent_color\": \"C9A66B\",\n  \"background_color\": \"FFFFFF\",\n  \"body_font\": {\"color\": \"222222\"},\n  \"navigation_customization\": {\"background_color\": \"FFFFFF\"}\n}' | jq '.data'\n```\n\n---\n\n## Website content: FAQs / home sections / POIs / travel (`src/tools/website-content.ts`)\n\nAll four follow the same list/add/update/remove shape;\n`{wedding_account_id: $ACCT}` and an `_entity_id: 0` (create) or the real id\n(update) prefix every write body. Deletes share one path shape:\n`DELETE /v3/websites/{pages}/{entities}/{entity_id}/wedding-accounts/$ACCT`\nwhere `{pages}` is looked up per-type from\n`GET /v3/websites/pages/wedding-accounts/full` (`.data.{home,faq,poi,travel}_page.page_id`).\n\n```sh\n# FAQs\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/faqs/wedding-accounts/$ACCT\" | jq '.data'\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/websites/faqs\" -d '{\n  \"wedding_account_id\": '\"$ACCT\"', \"faq_entity_id\": 0,\n  \"question\": \"What is the dress code?\", \"answer\": \"Cocktail attire\", \"display_order\": 0\n}' | jq '.data'\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/websites/faqs/<faq_entity_id>\" -d '{\n  \"wedding_account_id\": '\"$ACCT\"', \"faq_entity_id\": <faq_entity_id>,\n  \"question\": \"...\", \"answer\": \"...\", \"display_order\": 0\n}' | jq '.data'\ncurl -sS \"${H[@]}\" -X DELETE \"$BASE/v3/websites/pages/<faq_page_id>/entities/<faq_entity_id>/wedding-accounts/$ACCT\"\n\n# Home page story sections — same shape, path `/v3/websites/home-sections[...]`,\n# id field `homepage_entity_id`, extra `hidden` boolean, `title`+`subtitle`+`description`.\n\n# Points of interest — `/v3/websites/points-of-interest[...]`, id field\n# `poi_entity_id`; fields: title, description, address1/2, city, state_province,\n# postal_code, country_code, latitude, longitude, google_place_id, contact_phone, url.\n\n# Travel items — `/v3/websites/travel[...]`, id field `travel_entity_id`;\n# fields: type (HOTEL|FLIGHT|TRAIN|BUS|CAR|OTHER), name, note, code,\n# address1/2, city, state_province, postal_code, country_code, latitude,\n# longitude, google_place_id, contact_number, email_address, url, source\n# (GOOGLE_PLACES|MANUAL), timezone, display_order.\n```\n\nHome-section/POI/travel `list`/`add`/`update`/`remove` all mirror the FAQ\ncalls above 1:1 (same verbs, same `wedding_account_id` prefix, same delete\nshape) — see `src/tools/website-content.ts` for the exact field names per\ntype if you need the full body.\n\n---\n\n## Invitations / card projects (`src/tools/invitations.ts`)\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/card-projects/search_request\" -d '{\n  \"completed\": false, \"limit\": 30, \"card_suite_uuids\": [], \"card_suite_ids\": [],\n  \"offset\": 0, \"fetch_customizations\": true, \"include_deleted\": false,\n  \"medium\": [\"PAPER\",\"MAGNET\",\"DIGITAL\"], \"single_sample\": false\n}' | jq '.data'\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/card-projects/<project_uuid>\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/card-projects/<project_uuid>/validate\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/card-projects/<project_uuid>/project-guest-groups\" | jq '.data'\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v4/card-catalog/suites/details/<suite_uuid>\" | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/card-catalog/search/faceted\" -d '{\n  \"lead_card_types\": [], \"include_updated_proof_module\": true, \"limit\": 50, \"offset\": 0,\n  \"digital_suite\": false, \"include_lead_card_type_metadata\": true,\n  \"lead_card_type\": \"INVITATION\", \"include_module\": true\n}' | jq '.data'\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/favorites/card-suites/\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/rsvps/wedding-accounts/$ACCT\" | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/card-projects\" -d '{\n  \"quantity\": 150, \"lead_variation_uuid\": \"<variation-uuid>\",\n  \"extra_customizable\": false, \"account_id\": '\"$ACCT\"', \"suite_uuid\": \"<suite-uuid>\"\n}' | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/card-projects/<project_uuid>\" -d '{\n  \"customizations\": {\"<customization_uuid>\": {\"variation_uuid\": \"<new-variation-uuid>\"}}\n}' | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/card-projects/<project_uuid>/project-guest-groups\" -d '{\n  \"guest_group_requests\": [{\"guest_group_id\": <id>, \"enabled\": true}]\n}' | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/card-templates/preview\" -d '{\n  \"variation_uuids\": [\"<variation-uuid>\"], \"customizable\": true,\n  \"substitutions\": {\"first_name\": \"Alex\", \"wedding_date\": \"2026-10-17\"}\n}' | jq '.data'\n```\n\n**QR code** — preview returns raw image bytes (Content-Type lies as\n`image/jpeg` even for a PNG — sniff magic bytes if scripting):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/card-projects/qrcode/preview\" \\\n  -H 'accept: */*' -d '{\"dimension\":\"MEDIUM\",\"url_type\":\"CUSTOM\",\"enabled\":true,\"url\":\"https://example.com\"}' \\\n  -o qrcode.png\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/card-projects/<project_uuid>/customization/page/<page_uuid>/qrcode\" -d '{\n  \"dimension\": \"MEDIUM\", \"url_type\": \"CUSTOM\", \"color\": \"000000\",\n  \"enabled\": true, \"url\": \"https://example.com\"\n}' | jq '.data'\n```\n\nFile v2.1.9:skill-card.md\n\n## Description:\n\nGuides agents to query and update Zola wedding-planning data through shell commands using the Zola mobile API without an MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nAgents and developers working with an authorized Zola account can retrieve wedding-planning information and prepare updates to vendors, budgets, guest lists, events, registry items, and website settings without an MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: A long-lived Zola login token can expose private wedding account data if leaked.\n\nMitigation: Keep the refresh token in a secure secret store and redact tokens, guest data, and raw error bodies from logs and shared chats.\n\nRisk: Commands can change or delete live wedding account data, including unrelated fields on full-record updates.\n\nMitigation: Start with read-only operations, confirm writes and deletes explicitly, verify record IDs, and preserve existing fields when updating records.\n\n## Reference(s):\n\n- [Zola API skill release](https://clawhub.ai/chrischall/skills/zola-api)\n- [Mobile API endpoint recipes](references/mobile-api-endpoints.md)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell commands and JSON response examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Commands act on a live Zola account and require account credentials.]\n\n## Skill Version(s):\n\n2.1.9 (source: server-resolved ClawHub release)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v2.1.8: 4 files, 10402 bytes\n\nFiles: references/mobile-api-endpoints.md (18315b), skill-card.md (1853b), SKILL.md (5663b), _meta.json (127b)\n\nFile v2.1.8:SKILL.md\n\n---\nname: zola-api\ndescription: >-\n  Query or update Zola wedding-planning data (vendors, budget, guests, seating,\n  events/RSVPs, registry, gift tracker, inquiries, wedding website) straight\n  from a shell with curl against mobile-api.zola.com, instead of running the\n  zola-mcp server. Use when you want Zola data without the MCP, in a script,\n  or on a machine where the MCP isn't installed. Triggers on \"check Zola\",\n  \"Zola vendors/budget/guests/RSVP/seating/registry\", or any Zola wedding\n  data request that should hit the API directly.\n---\n\n# Zola mobile API via curl (no MCP)\n\nZola's mobile API (`mobile-api.zola.com` — the same surface the iOS/iPad app\nand `zola-mcp` use) is a plain Bearer-JWT REST API reachable directly from a\nserver or shell — no browser bridge needed. This skill shells out to `curl`\nwith the JWT in an `Authorization: Bearer` header, exactly as\n`zola-mcp`'s `src/client.ts` does.\n\n## One-time setup: get the refresh token\n\nYou need Zola's long-lived (~1 year) refresh JWT — the `usr` cookie from a\nsigned-in `zola.com` session. Same credential `zola-mcp` uses:\n\n```sh\n# Prefer the env var zola-mcp itself reads (check its .env first):\ngrep -h ZOLA_REFRESH_TOKEN ~/git/zola-mcp/.env 2>/dev/null\nexport ZOLA_REFRESH_TOKEN='eyJhbGciOi...'   # or export directly if you have it\n```\n\nIf you don't have it yet: sign into zola.com, open DevTools → Application →\nCookies → `https://www.zola.com`, copy the `usr` value. (`zola-mcp` also has a\nfetchproxy fallback that reads this cookie from a signed-in browser tab — see\nits README — but that's the MCP's path, not this skill's; this skill assumes\nyou already have the token in hand.)\n\n## Core call pattern\n\nEvery mobile-api call needs a short-lived (30 min) **session token**, minted\nfrom the refresh token, plus a fixed set of headers (CloudFront WAF requires\n`x-zola-session-id` on every request — omit it and you get a 403).\n\n```sh\nBASE=https://mobile-api.zola.com\nDEVICE_SESSION_ID=$(uuidgen | tr 'a-z' 'A-Z')   # one per \"session\"; reuse across calls\nUA='Zola/42.5.0 (iPad; iOS 26.4; Scale/2.0)'\n\n# 1. Mint a 30-min session token from the refresh token\nSESSION_TOKEN=$(curl -sS -X POST \"$BASE/v3/sessions/refresh\" \\\n  -H 'content-type: application/json' -H 'accept: application/json' \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" \\\n  -H \"user-agent: $UA\" \\\n  -d \"{\\\"token\\\":\\\"$ZOLA_REFRESH_TOKEN\\\"}\" | jq -r '.data.session_token')\n\n# 2. Every subsequent call reuses $SESSION_TOKEN until it expires (~30 min),\n#    then re-run step 1. All calls carry the same 4 headers:\ncurl -sS -X GET \"$BASE/v3/users/me/context\" \\\n  -H \"authorization: Bearer $SESSION_TOKEN\" \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" \\\n  -H \"user-agent: $UA\" | jq '.data'\n```\n\nWrap this in a shell function or export the 4 headers once — every recipe in\n`references/mobile-api-endpoints.md` reuses `$BASE`, `$SESSION_TOKEN`,\n`$DEVICE_SESSION_ID`, `$UA`. POST/PUT bodies additionally need\n`-H 'content-type: application/json'`.\n\n(The app also sends a fifth header, `x-zola-user-session-id`, derived from a\nclaim inside the session JWT — `zola-mcp` only attaches it when the decode\nsucceeds, so it's not load-bearing; every endpoint below works without it.)\n\n## The one rule: resolve context first\n\nMost write/list endpoints are scoped by numeric IDs, never inferred — call\n`GET /v3/users/me/context` once per session and keep the three IDs around:\n\n```sh\nCTX=$(curl -sS \"$BASE/v3/users/me/context\" -H \"authorization: Bearer $SESSION_TOKEN\" \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" -H \"user-agent: $UA\")\nACCT=$(jq -r '.data.wedding_account.wedding_account_id' <<<\"$CTX\")\nREG=$(jq -r '.data.registry.id' <<<\"$CTX\")\nWEDDING_ID=$(jq -r '.data.wedding.wedding_id' <<<\"$CTX\")\n```\n\n`ACCT` (`wedding_account_id`) feeds guest/event/website-content paths; `REG`\n(`registry_id`) feeds registry/gift-tracker paths.\n\n## Ready-to-run endpoints\n\n`references/mobile-api-endpoints.md` has real, ready-to-run `curl` + `jq`\nrecipes for every one of the 30 `zola-mcp` tools' underlying calls\n(vendors, budget, guests, seating, inquiries, events/RSVPs/gifts/registry,\ndiscover/storefronts, registry items, invitations/card-projects, website\npages, website theme/customization, website content). Transcribed straight\nfrom `src/tools/*.ts` — same paths, same request bodies.\n\n## Mutation gotcha: most writes are read-modify-write\n\nZola's write endpoints replace whole objects, not deltas — the same tools\nthat build a request first re-GET the current record and only patch the\nrequested fields, or a full-state write can wipe unrelated fields.\n`docs/zola-api-quirks.md` in this repo documents the worst offenders (a\n`header_font` write nulling every other website color; guest writes needing\nto preserve `event_invitations` verbatim or lose them). Follow the\nread-modify-write shape shown per-endpoint in the references file — don't\nsend a bare partial body to `PUT`/`POST` write endpoints.\n\n## Output / error contract\n\n- A 2xx body is `{\"data\": ...}` for nearly every endpoint — pipe to\n  `jq '.data'`.\n- `401` — session token expired; re-mint via `POST /v3/sessions/refresh`\n  (step 1 above) and retry once.\n- `429` — rate limited; back off ~2s and retry once.\n- Any other non-2xx — the body is a JSON error envelope; read it directly,\n  it is not redacted here (unlike the MCP, which redacts secrets from error\n  text — don't paste raw error bodies containing the session token anywhere\n  public).\n- This project (`zola-mcp`) is developed and maintained by AI (Claude).\n\nFile v2.1.8:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"zola-api\",\n  \"version\": \"2.1.8\",\n  \"publishedAt\": 1791168698665\n}\n\nFile v2.1.8:references/mobile-api-endpoints.md\n\n# Zola mobile-api endpoints (curl + jq)\n\nAll paths are relative to `$BASE=https://mobile-api.zola.com`. Every call\ncarries `authorization: Bearer $SESSION_TOKEN`, `x-zola-platform-type:\niphone_app`, `x-zola-session-id: $DEVICE_SESSION_ID`, `user-agent: $UA` (see\n`SKILL.md`); POST/PUT/DELETE-with-body also need `-H 'content-type:\napplication/json'`. `$ACCT` = `wedding_account_id`, `$REG` = `registry_id`,\n`$WEDDING_ID` = `wedding_id`, all three from `GET /v3/users/me/context` (see\n`SKILL.md`'s \"resolve context first\" section). Response envelope is `{\"data\": ...}`\nunless noted. Paths/bodies below are transcribed from `src/tools/*.ts` —\neach section names its source file.\n\nShorthand used below:\n\n```sh\nH=(-H \"authorization: Bearer $SESSION_TOKEN\" -H \"x-zola-platform-type: iphone_app\" \\\n   -H \"x-zola-session-id: $DEVICE_SESSION_ID\" -H \"user-agent: $UA\")\nHJ=(\"${H[@]}\" -H 'content-type: application/json')\n```\n\n---\n\n## Context (`src/client.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/users/me/context\" | jq '.data'\n# .data.user.id, .data.wedding_account.wedding_account_id,\n# .data.wedding.{wedding_id,wedding_date,slug}, .data.registry.id\n```\n\n---\n\n## Vendors (`src/tools/vendors.ts`)\n\n**List booked vendors** — `POST /v3/account-vendors/booked-list` body `{}`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/account-vendors/booked-list\" -d '{}' \\\n  | jq '.data.booked_vendors'\n```\n\n**Search vendors (typeahead)** — `POST /v3/reference-vendors/typeahead-taxonomy`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/reference-vendors/typeahead-taxonomy\" \\\n  -d '{\"query\":\"Acme Photography\",\"taxonomy_key\":\"wedding-photographers\"}' | jq '.data'\n# taxonomy_key default: wedding-venues. Other keys: wedding-planners, wedding-bands-djs, ...\n```\n\n**Book a vendor** — find an unbooked slot from `booked-list` for the\n`vendor_type`, then `PUT /v5/account-vendors/vendor` (note: **v5**, not v3):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v5/account-vendors/vendor\" -d '{\n  \"uuid\": \"<slot-uuid-from-booked-list>\", \"id\": 0, \"vendor_type\": \"PHOTOGRAPHER\",\n  \"booked\": true, \"booking_source\": \"BOOKED_VENDORS\",\n  \"price_cents\": 350000, \"event_date\": null,\n  \"sync_with_budget_tool_enabled\": true, \"facet_keys\": [],\n  \"reference_vendor_request\": {\n    \"id\": null, \"name\": \"Acme Photography\", \"email\": null, \"phone\": null,\n    \"address\": {\"city\": \"Charlotte\", \"state_province_region\": \"NC\"}\n  }\n}' | jq '.data'\n```\n\n**Update a booked vendor** — same `PUT /v5/account-vendors/vendor`, but\nread-modify-write: GET `booked-list`, find by `uuid`, keep `id`/`vendor_type`,\nonly patch the fields you're changing (name/city/state/email/price/date\ndefault to the current value).\n\n**Unbook a vendor** — `POST /v3/account-vendors/vendor/unbook`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/account-vendors/vendor/unbook\" \\\n  -d '{\"uuid\":\"<vendor-uuid>\"}'\n```\n\n---\n\n## Budget (`src/tools/budget.ts`)\n\n**Get budget** — `GET /v3/budgets`:\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/budgets\" | jq '{\n  budgeted_cents: .data.budgeted_cents, cost_cents: .data.cost_cents,\n  paid_cents: .data.paid_cents, balance_due_cents: .data.balance_due_cents,\n  items: [.data.taxonomy_nodes[].items[] | {uuid, title, cost_cents, paid_cents}]\n}'\n```\n\n**Update a budget item** — read-modify-write: `GET /v3/budgets`, find the item\nby `uuid`, then `PUT /v3/budgets/items` with every required field (server\nrejects a bare partial):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/budgets/items\" -d '{\n  \"item_uuid\": \"<uuid>\", \"taxonomy_node_uuid\": \"<from-get>\",\n  \"estimated_cost_cents\": <from-get>, \"actual_cost_cents\": 250000,\n  \"note\": \"Deposit paid\", \"item_type\": \"<from-get, e.g. VENUE>\",\n  \"title\": \"<from-get>\"\n}' | jq '.data'\n```\n\n---\n\n## Guests (`src/tools/guests.ts`)\n\n**List guests** — `POST /v3/guestlists/directory/wedding-accounts/$ACCT` body\n`{\"sort_by_name_asc\": true}`. Guest shape is **flat** (fields directly on the\nguest object, no `{guest:{...}}` wrapper):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/guestlists/directory/wedding-accounts/$ACCT\" \\\n  -d '{\"sort_by_name_asc\": true}' \\\n  | jq '.data.guest_groups[] | {guest_group_id, guests: [.guests[] | {guest_id, first_name, family_name, rsvp}]}'\n```\n\n**Add a guest group** — `POST /v3/guestlists/groups`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/guestlists/groups\" -d '{\n  \"wedding_account_id\": '\"$ACCT\"',\n  \"guests\": [{\n    \"first_name\": \"Mike\", \"family_name\": \"Smith\", \"relationship_type\": \"PRIMARY\",\n    \"source\": \"IOS\", \"email_address\": \"\", \"mobile_phone\": \"\", \"affiliation\": \"PRIMARY_FRIEND\",\n    \"tier\": \"A\", \"country_code\": \"US\", \"prefix\": \"\", \"middle_name\": \"\", \"suffix\": \"\",\n    \"home_phone\": \"\", \"address1\": \"\", \"address2\": \"\", \"city\": \"\", \"state_province\": \"\",\n    \"postal_code\": \"\", \"event_invitations\": [], \"tags\": []\n  }],\n  \"guest_group_affiliation\": \"PRIMARY_FRIEND\", \"guest_group_tier\": \"A\",\n  \"guest_group_uuid\": \"\", \"envelope_recipient\": \"\", \"invited\": true,\n  \"invitation_sent\": false, \"save_the_date_sent\": false,\n  \"rsvp_question_answers\": [], \"gift_count\": 0\n}' | jq '.data'\n```\n\n**Update a guest group's address** — read-modify-write via\n`PUT /v3/guestlists/groups/wedding-accounts/$ACCT/bulk/directory`. **Must**\npreserve each guest's existing `event_invitations` verbatim (a group that\nsends `event_invitations: []` wipes them, per `docs/zola-api-quirks.md` §5):\n\n```sh\n# 1. GET the directory (above), find the target group by guest_group_id,\n#    keep its `guests[]` array as-is except the address fields you're changing.\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/guestlists/groups/wedding-accounts/$ACCT/bulk/directory\" \\\n  -d '{\"updated_guest_groups\": [ <full-group-object-with-patched-guest-addresses> ]}' \\\n  | jq '.data'\n```\n\n**Remove a guest group** — `PUT /v3/guestlists/groups/wedding-accounts/$ACCT/delete`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/guestlists/groups/wedding-accounts/$ACCT/delete\" \\\n  -d '{\"wedding_account_id\": '\"$ACCT\"', \"guest_group_ids\": [<id>]}'\n```\n\n---\n\n## Seating (`src/tools/seating.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/seating-charts/summaries\" | jq '.'   # NOT wrapped in `data`\ncurl -sS \"${H[@]}\" \"$BASE/v3/seating-charts/<chart-uuid>\" | jq '.'\n```\n\n**Unseated guests** — `POST /v3/guestlists/directory/wedding-accounts/$ACCT`\n(same call as guests list), filter client-side for\n`.guests[].seating_chart_seat == null`.\n\n**Assign a seat** — `PUT /v3/seating-charts/seats`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/seating-charts/seats\" -d '{\n  \"guest_uuid\": \"<guest-uuid>\", \"seat_uuid\": \"<seat-uuid>\",\n  \"table_uuid\": \"<table-uuid>\", \"seating_chart_uuid\": \"<chart-uuid>\"\n}' | jq '.'\n```\n\n---\n\n## Inquiries (`src/tools/inquiries.ts`)\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/inquiries/unified-inquiries\" -d '{}' \\\n  | jq '.data[].inquiry_summaries[] | {inquiry_uuid, vendor_name: .vendor_card.vendor_name, unread, status_text}'\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/inquiries/<inquiry-uuid>/conversation\" | jq '.data.messages'\n\ncurl -sS \"${H[@]}\" -X PUT \"$BASE/v3/inquiries/<inquiry-uuid>/conversation/read\"\n```\n\n---\n\n## Events, RSVPs, gifts, registry (`src/tools/events.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/events/wedding-accounts/$ACCT/groups\" \\\n  | jq '.data[].events[]'   # event_entity_id, name, start_at, num_guests_*\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/events/track-rsvps\" | jq '.data.modules'\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/gift_tracker/$REG\" | jq '.data | del(.info_modules)'\n\ncurl -sS \"${H[@]}\" \"$BASE/v4/shop/registry?registry_id=$REG&updated_modules=true\" | jq '.data'\n```\n\n**Update an event** — read-modify-write: GET the groups above, find by\n`event_entity_id`, then `PUT /v3/websites/events/{event_id}` with the full\nevent object (patch only the fields you're changing; everything else —\n`type`, `uuid`, `wedding_account_id`, counts, `meal_options` — round-trips\nfrom the GET):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/websites/events/<event_id>\" -d '{\n  \"event_entity_id\": <event_id>, \"uuid\": \"<from-get>\", \"wedding_account_id\": '\"$ACCT\"',\n  \"type\": \"<from-get>\", \"name\": \"Reception\", \"start_at\": \"<from-get>\", \"end_at\": \"<from-get>\",\n  \"timezone\": \"<from-get>\", \"venue_name\": \"The Grand Hall\", \"address1\": \"\", \"address2\": \"\",\n  \"city\": \"Charlotte\", \"state_province\": \"NC\", \"postal_code\": \"\", \"country_code\": \"US\",\n  \"note\": \"\", \"attire\": \"Black tie\", \"collect_rsvps\": true, \"public\": <from-get>,\n  \"display_order\": 0, \"num_guests_attending\": <from-get>, \"num_guests_declined\": <from-get>,\n  \"num_guests_not_responded\": <from-get>, \"meal_options\": <from-get>,\n  \"rsvp_questions\": [], \"add_booked_vendor\": false\n}' | jq '.data'\n```\n\n**Event invitations (per-guest, read-modify-write)** — from\n`src/tools/event-invitations.ts`. Each guest carries an `event_invitations`\narray; invite = append `{\"event_id\":<id>,\"id\":null,\"rsvp_type\":\"NO_RESPONSE\"}`,\nuninvite = drop that element, **preserve every other element verbatim**\n(same wipe risk as the guest-address write above). Write through the same\n`PUT /v3/guestlists/groups/wedding-accounts/$ACCT/bulk/directory` used for\nguest writes, sending `{\"updated_guest_groups\": [...]}` with full group\nobjects (each guest keeps its full shape, only `event_invitations` patched).\n\n---\n\n## Discover / storefronts (`src/tools/discover.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v4/your-wedding\" | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/storefronts/search\" -d '{\n  \"taxonomy_node_id\": 2, \"city\": \"Charlotte\", \"state\": \"NC\",\n  \"limit\": 24, \"offset\": 0, \"facets\": {},\n  \"metro_types\": [\"HOME\",\"HOME_SERVICE\",\"AWAY\"], \"metros\": [],\n  \"exclude_inquired_storefronts\": false, \"exclude_booked_storefronts\": false,\n  \"boost_featured_storefronts\": false, \"suggested_vendors_for_inquiry_limit\": 12\n}' | jq '.data'\n# taxonomy_node_id: 1=Venues 2=Photographers 3=Florists 7=Planners 9=Bands/DJs\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/storefronts/<storefront-uuid>\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/favorites/\" | jq '.data'\n```\n\n---\n\n## Registry items (`src/tools/registry-items.ts`)\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/categories/<category_id>/entities\" \\\n  -d '{\"offset\":0,\"limit\":50,\"registry_id\":\"'\"$REG\"'\"}' | jq '.data'\n\n# Default collection id: `.data.default_collection_id` from the registry GET\n# above, or the first `groups[].modules[]` entry with `type == \"COLLECTION\"`.\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/registries/$REG/collections/<collection_id>\" \\\n  -d '{\"sku_id\":\"<sku>\",\"quantity\":1,\"most_wanted\":false,\"enable_group_gifting\":false}' \\\n  | jq '.data'\n\n# Full replace — all fields required:\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/registries/$REG/items/<collection_item_id>\" -d '{\n  \"quantity\": 2, \"group_gift\": false, \"marked_fulfilled\": false,\n  \"personal_note\": \"\", \"most_wanted\": true, \"collection_id\": \"<collection_id>\"\n}' | jq '.data'\n\ncurl -sS \"${H[@]}\" -X DELETE \"$BASE/v3/registries/$REG/items/<collection_item_id>\"\n```\n\n---\n\n## Website pages & settings (`src/tools/website.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/pages/wedding-accounts/full\" | jq '.data'\n\ncurl -sS \"${H[@]}\" -X PUT \"$BASE/v3/websites/pages/<page_id>/hidden/true\"  # or /false\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/websites/pages/wedding-accounts/$ACCT/reorder\" \\\n  -d '{\"ids\": [<page_id_1>, <page_id_2>, ...]}' | jq '.data'\n\n# Partial patch is fine here (unlike most write endpoints):\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/websites/pages-v2/<page_id>\" \\\n  -d '{\"page_id\": <page_id>, \"title\": \"Our Story\"}' | jq '.data'\n```\n\n**Wedding settings** — read via `GET /v3/users/me/context` → `.data.wedding`;\nwrite is read-modify-write (all fields required) to\n`PUT /v3/weddings/{wedding_id}`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/weddings/$WEDDING_ID\" -d '{\n  \"wedding_id\": '\"$WEDDING_ID\"', \"account_id\": '\"$ACCT\"',\n  \"slug\": \"<from-get>\", \"owner_first_name\": \"<from-get>\", \"owner_last_name\": \"<from-get>\",\n  \"partner_first_name\": \"<from-get>\", \"partner_last_name\": \"<from-get>\",\n  \"title\": \"Alex & Jordan\", \"wedding_date\": \"<from-get>\", \"hashtag\": \"#alexandjordan2026\",\n  \"enable_search_engine\": true, \"enable_search_zola\": true,\n  \"city\": \"Charlotte\", \"state_province\": \"NC\", \"guest_count\": 150\n}' | jq '.data'\n```\n\n---\n\n## Website theme & customization (`src/tools/website-theme.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/themes/current\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/website-customizations/context\" | jq '.data.current_style_customizations'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/themes/search\" \\\n  -d '{\"limit\":50,\"offset\":0,\"theme_layout_types\":[\"MULTI_PAGE\"]}' | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/themes/current\" \\\n  -d '{\"theme_key\":\"galata\",\"theme_layout_type\":\"MULTI_PAGE\"}' | jq '.data'\n```\n\n**Update colors/fonts** — `POST /v3/websites/website-customizations/context`.\n**Gotcha (see `docs/zola-api-quirks.md` §1–4):** changing `header_font`\n(i.e. `header_font_family_id`) wipes every *other* active color unless you\nre-send them in the same POST — GET the current state first and bundle its\nnon-null `accent_color` / `background_color` / `body.color` /\n`navigation_customization.background_color` into the body. `body_font`'s\n`font_family_id` only accepts `68` (Libre Baskerville) or `198` (Circular) —\nany other value 500s. `header_color` / `nav_font_color` are **not** writable\non this API at all (web-api-only, cookie+CSRF auth — out of scope here).\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/websites/website-customizations/context\" -d '{\n  \"accent_color\": \"C9A66B\",\n  \"background_color\": \"FFFFFF\",\n  \"body_font\": {\"color\": \"222222\"},\n  \"navigation_customization\": {\"background_color\": \"FFFFFF\"}\n}' | jq '.data'\n```\n\n---\n\n## Website content: FAQs / home sections / POIs / travel (`src/tools/website-content.ts`)\n\nAll four follow the same list/add/update/remove shape;\n`{wedding_account_id: $ACCT}` and an `_entity_id: 0` (create) or the real id\n(update) prefix every write body. Deletes share one path shape:\n`DELETE /v3/websites/{pages}/{entities}/{entity_id}/wedding-accounts/$ACCT`\nwhere `{pages}` is looked up per-type from\n`GET /v3/websites/pages/wedding-accounts/full` (`.data.{home,faq,poi,travel}_page.page_id`).\n\n```sh\n# FAQs\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/faqs/wedding-accounts/$ACCT\" | jq '.data'\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/websites/faqs\" -d '{\n  \"wedding_account_id\": '\"$ACCT\"', \"faq_entity_id\": 0,\n  \"question\": \"What is the dress code?\", \"answer\": \"Cocktail attire\", \"display_order\": 0\n}' | jq '.data'\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/websites/faqs/<faq_entity_id>\" -d '{\n  \"wedding_account_id\": '\"$ACCT\"', \"faq_entity_id\": <faq_entity_id>,\n  \"question\": \"...\", \"answer\": \"...\", \"display_order\": 0\n}' | jq '.data'\ncurl -sS \"${H[@]}\" -X DELETE \"$BASE/v3/websites/pages/<faq_page_id>/entities/<faq_entity_id>/wedding-accounts/$ACCT\"\n\n# Home page story sections — same shape, path `/v3/websites/home-sections[...]`,\n# id field `homepage_entity_id`, extra `hidden` boolean, `title`+`subtitle`+`description`.\n\n# Points of interest — `/v3/websites/points-of-interest[...]`, id field\n# `poi_entity_id`; fields: title, description, address1/2, city, state_province,\n# postal_code, country_code, latitude, longitude, google_place_id, contact_phone, url.\n\n# Travel items — `/v3/websites/travel[...]`, id field `travel_entity_id`;\n# fields: type (HOTEL|FLIGHT|TRAIN|BUS|CAR|OTHER), name, note, code,\n# address1/2, city, state_province, postal_code, country_code, latitude,\n# longitude, google_place_id, contact_number, email_address, url, source\n# (GOOGLE_PLACES|MANUAL), timezone, display_order.\n```\n\nHome-section/POI/travel `list`/`add`/`update`/`remove` all mirror the FAQ\ncalls above 1:1 (same verbs, same `wedding_account_id` prefix, same delete\nshape) — see `src/tools/website-content.ts` for the exact field names per\ntype if you need the full body.\n\n---\n\n## Invitations / card projects (`src/tools/invitations.ts`)\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/card-projects/search_request\" -d '{\n  \"completed\": false, \"limit\": 30, \"card_suite_uuids\": [], \"card_suite_ids\": [],\n  \"offset\": 0, \"fetch_customizations\": true, \"include_deleted\": false,\n  \"medium\": [\"PAPER\",\"MAGNET\",\"DIGITAL\"], \"single_sample\": false\n}' | jq '.data'\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/card-projects/<project_uuid>\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/card-projects/<project_uuid>/validate\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/card-projects/<project_uuid>/project-guest-groups\" | jq '.data'\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v4/card-catalog/suites/details/<suite_uuid>\" | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/card-catalog/search/faceted\" -d '{\n  \"lead_card_types\": [], \"include_updated_proof_module\": true, \"limit\": 50, \"offset\": 0,\n  \"digital_suite\": false, \"include_lead_card_type_metadata\": true,\n  \"lead_card_type\": \"INVITATION\", \"include_module\": true\n}' | jq '.data'\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/favorites/card-suites/\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/rsvps/wedding-accounts/$ACCT\" | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/card-projects\" -d '{\n  \"quantity\": 150, \"lead_variation_uuid\": \"<variation-uuid>\",\n  \"extra_customizable\": false, \"account_id\": '\"$ACCT\"', \"suite_uuid\": \"<suite-uuid>\"\n}' | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/card-projects/<project_uuid>\" -d '{\n  \"customizations\": {\"<customization_uuid>\": {\"variation_uuid\": \"<new-variation-uuid>\"}}\n}' | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/card-projects/<project_uuid>/project-guest-groups\" -d '{\n  \"guest_group_requests\": [{\"guest_group_id\": <id>, \"enabled\": true}]\n}' | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/card-templates/preview\" -d '{\n  \"variation_uuids\": [\"<variation-uuid>\"], \"customizable\": true,\n  \"substitutions\": {\"first_name\": \"Alex\", \"wedding_date\": \"2026-10-17\"}\n}' | jq '.data'\n```\n\n**QR code** — preview returns raw image bytes (Content-Type lies as\n`image/jpeg` even for a PNG — sniff magic bytes if scripting):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/card-projects/qrcode/preview\" \\\n  -H 'accept: */*' -d '{\"dimension\":\"MEDIUM\",\"url_type\":\"CUSTOM\",\"enabled\":true,\"url\":\"https://example.com\"}' \\\n  -o qrcode.png\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/card-projects/<project_uuid>/customization/page/<page_uuid>/qrcode\" -d '{\n  \"dimension\": \"MEDIUM\", \"url_type\": \"CUSTOM\", \"color\": \"000000\",\n  \"enabled\": true, \"url\": \"https://example.com\"\n}' | jq '.data'\n```\n\nFile v2.1.8:skill-card.md\n\n## Description:\n\nGuides agents in reading and updating Zola wedding-planning data through direct mobile API calls from a shell.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nZola account holders and developers use this skill to query or update wedding vendors, budgets, guests, RSVPs, registry, invitations, and website content without an MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: A leaked refresh token could expose the Zola account.\n\nMitigation: Treat ZOLA_REFRESH_TOKEN as a password, keep it out of shell history and logs, and revoke or rotate it if exposed.\n\nRisk: Direct API calls can change or delete live wedding data without adequate safeguards.\n\nMitigation: Require explicit confirmation of exact record names and IDs before guest, budget, registry, website, invitation, or delete operations.\n\nRisk: Partial writes may overwrite unrelated fields or remove guest event invitations.\n\nMitigation: Read the current record first and preserve unchanged fields when updating it.\n\n## Reference(s):\n\n- [Mobile API endpoint recipes](references/mobile-api-endpoints.md)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance]\n\n**Output Format:** [Markdown with inline shell commands and JSON examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Requires a Zola refresh token and authenticated access to the account.]\n\n## Skill Version(s):\n\n2.1.8 (source: ClawHub release)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v2.1.7: 4 files, 10472 bytes\n\nFiles: references/mobile-api-endpoints.md (18315b), skill-card.md (2054b), SKILL.md (5663b), _meta.json (127b)\n\nFile v2.1.7:SKILL.md\n\n---\nname: zola-api\ndescription: >-\n  Query or update Zola wedding-planning data (vendors, budget, guests, seating,\n  events/RSVPs, registry, gift tracker, inquiries, wedding website) straight\n  from a shell with curl against mobile-api.zola.com, instead of running the\n  zola-mcp server. Use when you want Zola data without the MCP, in a script,\n  or on a machine where the MCP isn't installed. Triggers on \"check Zola\",\n  \"Zola vendors/budget/guests/RSVP/seating/registry\", or any Zola wedding\n  data request that should hit the API directly.\n---\n\n# Zola mobile API via curl (no MCP)\n\nZola's mobile API (`mobile-api.zola.com` — the same surface the iOS/iPad app\nand `zola-mcp` use) is a plain Bearer-JWT REST API reachable directly from a\nserver or shell — no browser bridge needed. This skill shells out to `curl`\nwith the JWT in an `Authorization: Bearer` header, exactly as\n`zola-mcp`'s `src/client.ts` does.\n\n## One-time setup: get the refresh token\n\nYou need Zola's long-lived (~1 year) refresh JWT — the `usr` cookie from a\nsigned-in `zola.com` session. Same credential `zola-mcp` uses:\n\n```sh\n# Prefer the env var zola-mcp itself reads (check its .env first):\ngrep -h ZOLA_REFRESH_TOKEN ~/git/zola-mcp/.env 2>/dev/null\nexport ZOLA_REFRESH_TOKEN='eyJhbGciOi...'   # or export directly if you have it\n```\n\nIf you don't have it yet: sign into zola.com, open DevTools → Application →\nCookies → `https://www.zola.com`, copy the `usr` value. (`zola-mcp` also has a\nfetchproxy fallback that reads this cookie from a signed-in browser tab — see\nits README — but that's the MCP's path, not this skill's; this skill assumes\nyou already have the token in hand.)\n\n## Core call pattern\n\nEvery mobile-api call needs a short-lived (30 min) **session token**, minted\nfrom the refresh token, plus a fixed set of headers (CloudFront WAF requires\n`x-zola-session-id` on every request — omit it and you get a 403).\n\n```sh\nBASE=https://mobile-api.zola.com\nDEVICE_SESSION_ID=$(uuidgen | tr 'a-z' 'A-Z')   # one per \"session\"; reuse across calls\nUA='Zola/42.5.0 (iPad; iOS 26.4; Scale/2.0)'\n\n# 1. Mint a 30-min session token from the refresh token\nSESSION_TOKEN=$(curl -sS -X POST \"$BASE/v3/sessions/refresh\" \\\n  -H 'content-type: application/json' -H 'accept: application/json' \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" \\\n  -H \"user-agent: $UA\" \\\n  -d \"{\\\"token\\\":\\\"$ZOLA_REFRESH_TOKEN\\\"}\" | jq -r '.data.session_token')\n\n# 2. Every subsequent call reuses $SESSION_TOKEN until it expires (~30 min),\n#    then re-run step 1. All calls carry the same 4 headers:\ncurl -sS -X GET \"$BASE/v3/users/me/context\" \\\n  -H \"authorization: Bearer $SESSION_TOKEN\" \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" \\\n  -H \"user-agent: $UA\" | jq '.data'\n```\n\nWrap this in a shell function or export the 4 headers once — every recipe in\n`references/mobile-api-endpoints.md` reuses `$BASE`, `$SESSION_TOKEN`,\n`$DEVICE_SESSION_ID`, `$UA`. POST/PUT bodies additionally need\n`-H 'content-type: application/json'`.\n\n(The app also sends a fifth header, `x-zola-user-session-id`, derived from a\nclaim inside the session JWT — `zola-mcp` only attaches it when the decode\nsucceeds, so it's not load-bearing; every endpoint below works without it.)\n\n## The one rule: resolve context first\n\nMost write/list endpoints are scoped by numeric IDs, never inferred — call\n`GET /v3/users/me/context` once per session and keep the three IDs around:\n\n```sh\nCTX=$(curl -sS \"$BASE/v3/users/me/context\" -H \"authorization: Bearer $SESSION_TOKEN\" \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" -H \"user-agent: $UA\")\nACCT=$(jq -r '.data.wedding_account.wedding_account_id' <<<\"$CTX\")\nREG=$(jq -r '.data.registry.id' <<<\"$CTX\")\nWEDDING_ID=$(jq -r '.data.wedding.wedding_id' <<<\"$CTX\")\n```\n\n`ACCT` (`wedding_account_id`) feeds guest/event/website-content paths; `REG`\n(`registry_id`) feeds registry/gift-tracker paths.\n\n## Ready-to-run endpoints\n\n`references/mobile-api-endpoints.md` has real, ready-to-run `curl` + `jq`\nrecipes for every one of the 30 `zola-mcp` tools' underlying calls\n(vendors, budget, guests, seating, inquiries, events/RSVPs/gifts/registry,\ndiscover/storefronts, registry items, invitations/card-projects, website\npages, website theme/customization, website content). Transcribed straight\nfrom `src/tools/*.ts` — same paths, same request bodies.\n\n## Mutation gotcha: most writes are read-modify-write\n\nZola's write endpoints replace whole objects, not deltas — the same tools\nthat build a request first re-GET the current record and only patch the\nrequested fields, or a full-state write can wipe unrelated fields.\n`docs/zola-api-quirks.md` in this repo documents the worst offenders (a\n`header_font` write nulling every other website color; guest writes needing\nto preserve `event_invitations` verbatim or lose them). Follow the\nread-modify-write shape shown per-endpoint in the references file — don't\nsend a bare partial body to `PUT`/`POST` write endpoints.\n\n## Output / error contract\n\n- A 2xx body is `{\"data\": ...}` for nearly every endpoint — pipe to\n  `jq '.data'`.\n- `401` — session token expired; re-mint via `POST /v3/sessions/refresh`\n  (step 1 above) and retry once.\n- `429` — rate limited; back off ~2s and retry once.\n- Any other non-2xx — the body is a JSON error envelope; read it directly,\n  it is not redacted here (unlike the MCP, which redacts secrets from error\n  text — don't paste raw error bodies containing the session token anywhere\n  public).\n- This project (`zola-mcp`) is developed and maintained by AI (Claude).\n\nFile v2.1.7:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"zola-api\",\n  \"version\": \"2.1.7\",\n  \"publishedAt\": 1790992020018\n}\n\nFile v2.1.7:references/mobile-api-endpoints.md\n\n# Zola mobile-api endpoints (curl + jq)\n\nAll paths are relative to `$BASE=https://mobile-api.zola.com`. Every call\ncarries `authorization: Bearer $SESSION_TOKEN`, `x-zola-platform-type:\niphone_app`, `x-zola-session-id: $DEVICE_SESSION_ID`, `user-agent: $UA` (see\n`SKILL.md`); POST/PUT/DELETE-with-body also need `-H 'content-type:\napplication/json'`. `$ACCT` = `wedding_account_id`, `$REG` = `registry_id`,\n`$WEDDING_ID` = `wedding_id`, all three from `GET /v3/users/me/context` (see\n`SKILL.md`'s \"resolve context first\" section). Response envelope is `{\"data\": ...}`\nunless noted. Paths/bodies below are transcribed from `src/tools/*.ts` —\neach section names its source file.\n\nShorthand used below:\n\n```sh\nH=(-H \"authorization: Bearer $SESSION_TOKEN\" -H \"x-zola-platform-type: iphone_app\" \\\n   -H \"x-zola-session-id: $DEVICE_SESSION_ID\" -H \"user-agent: $UA\")\nHJ=(\"${H[@]}\" -H 'content-type: application/json')\n```\n\n---\n\n## Context (`src/client.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/users/me/context\" | jq '.data'\n# .data.user.id, .data.wedding_account.wedding_account_id,\n# .data.wedding.{wedding_id,wedding_date,slug}, .data.registry.id\n```\n\n---\n\n## Vendors (`src/tools/vendors.ts`)\n\n**List booked vendors** — `POST /v3/account-vendors/booked-list` body `{}`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/account-vendors/booked-list\" -d '{}' \\\n  | jq '.data.booked_vendors'\n```\n\n**Search vendors (typeahead)** — `POST /v3/reference-vendors/typeahead-taxonomy`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/reference-vendors/typeahead-taxonomy\" \\\n  -d '{\"query\":\"Acme Photography\",\"taxonomy_key\":\"wedding-photographers\"}' | jq '.data'\n# taxonomy_key default: wedding-venues. Other keys: wedding-planners, wedding-bands-djs, ...\n```\n\n**Book a vendor** — find an unbooked slot from `booked-list` for the\n`vendor_type`, then `PUT /v5/account-vendors/vendor` (note: **v5**, not v3):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v5/account-vendors/vendor\" -d '{\n  \"uuid\": \"<slot-uuid-from-booked-list>\", \"id\": 0, \"vendor_type\": \"PHOTOGRAPHER\",\n  \"booked\": true, \"booking_source\": \"BOOKED_VENDORS\",\n  \"price_cents\": 350000, \"event_date\": null,\n  \"sync_with_budget_tool_enabled\": true, \"facet_keys\": [],\n  \"reference_vendor_request\": {\n    \"id\": null, \"name\": \"Acme Photography\", \"email\": null, \"phone\": null,\n    \"address\": {\"city\": \"Charlotte\", \"state_province_region\": \"NC\"}\n  }\n}' | jq '.data'\n```\n\n**Update a booked vendor** — same `PUT /v5/account-vendors/vendor`, but\nread-modify-write: GET `booked-list`, find by `uuid`, keep `id`/`vendor_type`,\nonly patch the fields you're changing (name/city/state/email/price/date\ndefault to the current value).\n\n**Unbook a vendor** — `POST /v3/account-vendors/vendor/unbook`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/account-vendors/vendor/unbook\" \\\n  -d '{\"uuid\":\"<vendor-uuid>\"}'\n```\n\n---\n\n## Budget (`src/tools/budget.ts`)\n\n**Get budget** — `GET /v3/budgets`:\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/budgets\" | jq '{\n  budgeted_cents: .data.budgeted_cents, cost_cents: .data.cost_cents,\n  paid_cents: .data.paid_cents, balance_due_cents: .data.balance_due_cents,\n  items: [.data.taxonomy_nodes[].items[] | {uuid, title, cost_cents, paid_cents}]\n}'\n```\n\n**Update a budget item** — read-modify-write: `GET /v3/budgets`, find the item\nby `uuid`, then `PUT /v3/budgets/items` with every required field (server\nrejects a bare partial):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/budgets/items\" -d '{\n  \"item_uuid\": \"<uuid>\", \"taxonomy_node_uuid\": \"<from-get>\",\n  \"estimated_cost_cents\": <from-get>, \"actual_cost_cents\": 250000,\n  \"note\": \"Deposit paid\", \"item_type\": \"<from-get, e.g. VENUE>\",\n  \"title\": \"<from-get>\"\n}' | jq '.data'\n```\n\n---\n\n## Guests (`src/tools/guests.ts`)\n\n**List guests** — `POST /v3/guestlists/directory/wedding-accounts/$ACCT` body\n`{\"sort_by_name_asc\": true}`. Guest shape is **flat** (fields directly on the\nguest object, no `{guest:{...}}` wrapper):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/guestlists/directory/wedding-accounts/$ACCT\" \\\n  -d '{\"sort_by_name_asc\": true}' \\\n  | jq '.data.guest_groups[] | {guest_group_id, guests: [.guests[] | {guest_id, first_name, family_name, rsvp}]}'\n```\n\n**Add a guest group** — `POST /v3/guestlists/groups`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/guestlists/groups\" -d '{\n  \"wedding_account_id\": '\"$ACCT\"',\n  \"guests\": [{\n    \"first_name\": \"Mike\", \"family_name\": \"Smith\", \"relationship_type\": \"PRIMARY\",\n    \"source\": \"IOS\", \"email_address\": \"\", \"mobile_phone\": \"\", \"affiliation\": \"PRIMARY_FRIEND\",\n    \"tier\": \"A\", \"country_code\": \"US\", \"prefix\": \"\", \"middle_name\": \"\", \"suffix\": \"\",\n    \"home_phone\": \"\", \"address1\": \"\", \"address2\": \"\", \"city\": \"\", \"state_province\": \"\",\n    \"postal_code\": \"\", \"event_invitations\": [], \"tags\": []\n  }],\n  \"guest_group_affiliation\": \"PRIMARY_FRIEND\", \"guest_group_tier\": \"A\",\n  \"guest_group_uuid\": \"\", \"envelope_recipient\": \"\", \"invited\": true,\n  \"invitation_sent\": false, \"save_the_date_sent\": false,\n  \"rsvp_question_answers\": [], \"gift_count\": 0\n}' | jq '.data'\n```\n\n**Update a guest group's address** — read-modify-write via\n`PUT /v3/guestlists/groups/wedding-accounts/$ACCT/bulk/directory`. **Must**\npreserve each guest's existing `event_invitations` verbatim (a group that\nsends `event_invitations: []` wipes them, per `docs/zola-api-quirks.md` §5):\n\n```sh\n# 1. GET the directory (above), find the target group by guest_group_id,\n#    keep its `guests[]` array as-is except the address fields you're changing.\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/guestlists/groups/wedding-accounts/$ACCT/bulk/directory\" \\\n  -d '{\"updated_guest_groups\": [ <full-group-object-with-patched-guest-addresses> ]}' \\\n  | jq '.data'\n```\n\n**Remove a guest group** — `PUT /v3/guestlists/groups/wedding-accounts/$ACCT/delete`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/guestlists/groups/wedding-accounts/$ACCT/delete\" \\\n  -d '{\"wedding_account_id\": '\"$ACCT\"', \"guest_group_ids\": [<id>]}'\n```\n\n---\n\n## Seating (`src/tools/seating.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/seating-charts/summaries\" | jq '.'   # NOT wrapped in `data`\ncurl -sS \"${H[@]}\" \"$BASE/v3/seating-charts/<chart-uuid>\" | jq '.'\n```\n\n**Unseated guests** — `POST /v3/guestlists/directory/wedding-accounts/$ACCT`\n(same call as guests list), filter client-side for\n`.guests[].seating_chart_seat == null`.\n\n**Assign a seat** — `PUT /v3/seating-charts/seats`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/seating-charts/seats\" -d '{\n  \"guest_uuid\": \"<guest-uuid>\", \"seat_uuid\": \"<seat-uuid>\",\n  \"table_uuid\": \"<table-uuid>\", \"seating_chart_uuid\": \"<chart-uuid>\"\n}' | jq '.'\n```\n\n---\n\n## Inquiries (`src/tools/inquiries.ts`)\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/inquiries/unified-inquiries\" -d '{}' \\\n  | jq '.data[].inquiry_summaries[] | {inquiry_uuid, vendor_name: .vendor_card.vendor_name, unread, status_text}'\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/inquiries/<inquiry-uuid>/conversation\" | jq '.data.messages'\n\ncurl -sS \"${H[@]}\" -X PUT \"$BASE/v3/inquiries/<inquiry-uuid>/conversation/read\"\n```\n\n---\n\n## Events, RSVPs, gifts, registry (`src/tools/events.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/events/wedding-accounts/$ACCT/groups\" \\\n  | jq '.data[].events[]'   # event_entity_id, name, start_at, num_guests_*\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/events/track-rsvps\" | jq '.data.modules'\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/gift_tracker/$REG\" | jq '.data | del(.info_modules)'\n\ncurl -sS \"${H[@]}\" \"$BASE/v4/shop/registry?registry_id=$REG&updated_modules=true\" | jq '.data'\n```\n\n**Update an event** — read-modify-write: GET the groups above, find by\n`event_entity_id`, then `PUT /v3/websites/events/{event_id}` with the full\nevent object (patch only the fields you're changing; everything else —\n`type`, `uuid`, `wedding_account_id`, counts, `meal_options` — round-trips\nfrom the GET):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/websites/events/<event_id>\" -d '{\n  \"event_entity_id\": <event_id>, \"uuid\": \"<from-get>\", \"wedding_account_id\": '\"$ACCT\"',\n  \"type\": \"<from-get>\", \"name\": \"Reception\", \"start_at\": \"<from-get>\", \"end_at\": \"<from-get>\",\n  \"timezone\": \"<from-get>\", \"venue_name\": \"The Grand Hall\", \"address1\": \"\", \"address2\": \"\",\n  \"city\": \"Charlotte\", \"state_province\": \"NC\", \"postal_code\": \"\", \"country_code\": \"US\",\n  \"note\": \"\", \"attire\": \"Black tie\", \"collect_rsvps\": true, \"public\": <from-get>,\n  \"display_order\": 0, \"num_guests_attending\": <from-get>, \"num_guests_declined\": <from-get>,\n  \"num_guests_not_responded\": <from-get>, \"meal_options\": <from-get>,\n  \"rsvp_questions\": [], \"add_booked_vendor\": false\n}' | jq '.data'\n```\n\n**Event invitations (per-guest, read-modify-write)** — from\n`src/tools/event-invitations.ts`. Each guest carries an `event_invitations`\narray; invite = append `{\"event_id\":<id>,\"id\":null,\"rsvp_type\":\"NO_RESPONSE\"}`,\nuninvite = drop that element, **preserve every other element verbatim**\n(same wipe risk as the guest-address write above). Write through the same\n`PUT /v3/guestlists/groups/wedding-accounts/$ACCT/bulk/directory` used for\nguest writes, sending `{\"updated_guest_groups\": [...]}` with full group\nobjects (each guest keeps its full shape, only `event_invitations` patched).\n\n---\n\n## Discover / storefronts (`src/tools/discover.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v4/your-wedding\" | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/storefronts/search\" -d '{\n  \"taxonomy_node_id\": 2, \"city\": \"Charlotte\", \"state\": \"NC\",\n  \"limit\": 24, \"offset\": 0, \"facets\": {},\n  \"metro_types\": [\"HOME\",\"HOME_SERVICE\",\"AWAY\"], \"metros\": [],\n  \"exclude_inquired_storefronts\": false, \"exclude_booked_storefronts\": false,\n  \"boost_featured_storefronts\": false, \"suggested_vendors_for_inquiry_limit\": 12\n}' | jq '.data'\n# taxonomy_node_id: 1=Venues 2=Photographers 3=Florists 7=Planners 9=Bands/DJs\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/storefronts/<storefront-uuid>\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/favorites/\" | jq '.data'\n```\n\n---\n\n## Registry items (`src/tools/registry-items.ts`)\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/categories/<category_id>/entities\" \\\n  -d '{\"offset\":0,\"limit\":50,\"registry_id\":\"'\"$REG\"'\"}' | jq '.data'\n\n# Default collection id: `.data.default_collection_id` from the registry GET\n# above, or the first `groups[].modules[]` entry with `type == \"COLLECTION\"`.\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/registries/$REG/collections/<collection_id>\" \\\n  -d '{\"sku_id\":\"<sku>\",\"quantity\":1,\"most_wanted\":false,\"enable_group_gifting\":false}' \\\n  | jq '.data'\n\n# Full replace — all fields required:\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/registries/$REG/items/<collection_item_id>\" -d '{\n  \"quantity\": 2, \"group_gift\": false, \"marked_fulfilled\": false,\n  \"personal_note\": \"\", \"most_wanted\": true, \"collection_id\": \"<collection_id>\"\n}' | jq '.data'\n\ncurl -sS \"${H[@]}\" -X DELETE \"$BASE/v3/registries/$REG/items/<collection_item_id>\"\n```\n\n---\n\n## Website pages & settings (`src/tools/website.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/pages/wedding-accounts/full\" | jq '.data'\n\ncurl -sS \"${H[@]}\" -X PUT \"$BASE/v3/websites/pages/<page_id>/hidden/true\"  # or /false\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/websites/pages/wedding-accounts/$ACCT/reorder\" \\\n  -d '{\"ids\": [<page_id_1>, <page_id_2>, ...]}' | jq '.data'\n\n# Partial patch is fine here (unlike most write endpoints):\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/websites/pages-v2/<page_id>\" \\\n  -d '{\"page_id\": <page_id>, \"title\": \"Our Story\"}' | jq '.data'\n```\n\n**Wedding settings** — read via `GET /v3/users/me/context` → `.data.wedding`;\nwrite is read-modify-write (all fields required) to\n`PUT /v3/weddings/{wedding_id}`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/weddings/$WEDDING_ID\" -d '{\n  \"wedding_id\": '\"$WEDDING_ID\"', \"account_id\": '\"$ACCT\"',\n  \"slug\": \"<from-get>\", \"owner_first_name\": \"<from-get>\", \"owner_last_name\": \"<from-get>\",\n  \"partner_first_name\": \"<from-get>\", \"partner_last_name\": \"<from-get>\",\n  \"title\": \"Alex & Jordan\", \"wedding_date\": \"<from-get>\", \"hashtag\": \"#alexandjordan2026\",\n  \"enable_search_engine\": true, \"enable_search_zola\": true,\n  \"city\": \"Charlotte\", \"state_province\": \"NC\", \"guest_count\": 150\n}' | jq '.data'\n```\n\n---\n\n## Website theme & customization (`src/tools/website-theme.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/themes/current\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/website-customizations/context\" | jq '.data.current_style_customizations'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/themes/search\" \\\n  -d '{\"limit\":50,\"offset\":0,\"theme_layout_types\":[\"MULTI_PAGE\"]}' | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/themes/current\" \\\n  -d '{\"theme_key\":\"galata\",\"theme_layout_type\":\"MULTI_PAGE\"}' | jq '.data'\n```\n\n**Update colors/fonts** — `POST /v3/websites/website-customizations/context`.\n**Gotcha (see `docs/zola-api-quirks.md` §1–4):** changing `header_font`\n(i.e. `header_font_family_id`) wipes every *other* active color unless you\nre-send them in the same POST — GET the current state first and bundle its\nnon-null `accent_color` / `background_color` / `body.color` /\n`navigation_customization.background_color` into the body. `body_font`'s\n`font_family_id` only accepts `68` (Libre Baskerville) or `198` (Circular) —\nany other value 500s. `header_color` / `nav_font_color` are **not** writable\non this API at all (web-api-only, cookie+CSRF auth — out of scope here).\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/websites/website-customizations/context\" -d '{\n  \"accent_color\": \"C9A66B\",\n  \"background_color\": \"FFFFFF\",\n  \"body_font\": {\"color\": \"222222\"},\n  \"navigation_customization\": {\"background_color\": \"FFFFFF\"}\n}' | jq '.data'\n```\n\n---\n\n## Website content: FAQs / home sections / POIs / travel (`src/tools/website-content.ts`)\n\nAll four follow the same list/add/update/remove shape;\n`{wedding_account_id: $ACCT}` and an `_entity_id: 0` (create) or the real id\n(update) prefix every write body. Deletes share one path shape:\n`DELETE /v3/websites/{pages}/{entities}/{entity_id}/wedding-accounts/$ACCT`\nwhere `{pages}` is looked up per-type from\n`GET /v3/websites/pages/wedding-accounts/full` (`.data.{home,faq,poi,travel}_page.page_id`).\n\n```sh\n# FAQs\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/faqs/wedding-accounts/$ACCT\" | jq '.data'\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/websites/faqs\" -d '{\n  \"wedding_account_id\": '\"$ACCT\"', \"faq_entity_id\": 0,\n  \"question\": \"What is the dress code?\", \"answer\": \"Cocktail attire\", \"display_order\": 0\n}' | jq '.data'\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/websites/faqs/<faq_entity_id>\" -d '{\n  \"wedding_account_id\": '\"$ACCT\"', \"faq_entity_id\": <faq_entity_id>,\n  \"question\": \"...\", \"answer\": \"...\", \"display_order\": 0\n}' | jq '.data'\ncurl -sS \"${H[@]}\" -X DELETE \"$BASE/v3/websites/pages/<faq_page_id>/entities/<faq_entity_id>/wedding-accounts/$ACCT\"\n\n# Home page story sections — same shape, path `/v3/websites/home-sections[...]`,\n# id field `homepage_entity_id`, extra `hidden` boolean, `title`+`subtitle`+`description`.\n\n# Points of interest — `/v3/websites/points-of-interest[...]`, id field\n# `poi_entity_id`; fields: title, description, address1/2, city, state_province,\n# postal_code, country_code, latitude, longitude, google_place_id, contact_phone, url.\n\n# Travel items — `/v3/websites/travel[...]`, id field `travel_entity_id`;\n# fields: type (HOTEL|FLIGHT|TRAIN|BUS|CAR|OTHER), name, note, code,\n# address1/2, city, state_province, postal_code, country_code, latitude,\n# longitude, google_place_id, contact_number, email_address, url, source\n# (GOOGLE_PLACES|MANUAL), timezone, display_order.\n```\n\nHome-section/POI/travel `list`/`add`/`update`/`remove` all mirror the FAQ\ncalls above 1:1 (same verbs, same `wedding_account_id` prefix, same delete\nshape) — see `src/tools/website-content.ts` for the exact field names per\ntype if you need the full body.\n\n---\n\n## Invitations / card projects (`src/tools/invitations.ts`)\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/card-projects/search_request\" -d '{\n  \"completed\": false, \"limit\": 30, \"card_suite_uuids\": [], \"card_suite_ids\": [],\n  \"offset\": 0, \"fetch_customizations\": true, \"include_deleted\": false,\n  \"medium\": [\"PAPER\",\"MAGNET\",\"DIGITAL\"], \"single_sample\": false\n}' | jq '.data'\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/card-projects/<project_uuid>\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/card-projects/<project_uuid>/validate\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/card-projects/<project_uuid>/project-guest-groups\" | jq '.data'\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v4/card-catalog/suites/details/<suite_uuid>\" | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/card-catalog/search/faceted\" -d '{\n  \"lead_card_types\": [], \"include_updated_proof_module\": true, \"limit\": 50, \"offset\": 0,\n  \"digital_suite\": false, \"include_lead_card_type_metadata\": true,\n  \"lead_card_type\": \"INVITATION\", \"include_module\": true\n}' | jq '.data'\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/favorites/card-suites/\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/rsvps/wedding-accounts/$ACCT\" | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/card-projects\" -d '{\n  \"quantity\": 150, \"lead_variation_uuid\": \"<variation-uuid>\",\n  \"extra_customizable\": false, \"account_id\": '\"$ACCT\"', \"suite_uuid\": \"<suite-uuid>\"\n}' | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/card-projects/<project_uuid>\" -d '{\n  \"customizations\": {\"<customization_uuid>\": {\"variation_uuid\": \"<new-variation-uuid>\"}}\n}' | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/card-projects/<project_uuid>/project-guest-groups\" -d '{\n  \"guest_group_requests\": [{\"guest_group_id\": <id>, \"enabled\": true}]\n}' | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/card-templates/preview\" -d '{\n  \"variation_uuids\": [\"<variation-uuid>\"], \"customizable\": true,\n  \"substitutions\": {\"first_name\": \"Alex\", \"wedding_date\": \"2026-10-17\"}\n}' | jq '.data'\n```\n\n**QR code** — preview returns raw image bytes (Content-Type lies as\n`image/jpeg` even for a PNG — sniff magic bytes if scripting):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/card-projects/qrcode/preview\" \\\n  -H 'accept: */*' -d '{\"dimension\":\"MEDIUM\",\"url_type\":\"CUSTOM\",\"enabled\":true,\"url\":\"https://example.com\"}' \\\n  -o qrcode.png\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/card-projects/<project_uuid>/customization/page/<page_uuid>/qrcode\" -d '{\n  \"dimension\": \"MEDIUM\", \"url_type\": \"CUSTOM\", \"color\": \"000000\",\n  \"enabled\": true, \"url\": \"https://example.com\"\n}' | jq '.data'\n```\n\nFile v2.1.7:skill-card.md\n\n## Description:\n\nGuides agents in reading and updating Zola wedding-planning data through direct mobile API calls with curl, without an MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nZola account holders and developers use this skill to inspect or update wedding vendors, budgets, guests, events, registry items, invitations, and website content through direct API requests.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: A long-lived account refresh token or session token could be exposed in shell history, logs, or shared error output.\n\nMitigation: Treat tokens like passwords; avoid pasting them into logged shells and never share unredacted token-bearing output.\n\nRisk: Direct write, delete, booking, registry, budget, guest, invitation, or website changes may modify live account data unexpectedly.\n\nMitigation: Read current state, review a proposed diff, and obtain explicit user confirmation before each change.\n\nRisk: Replacing an entire record with a partial update can erase unrelated fields.\n\nMitigation: Read the complete current record and preserve unchanged fields when constructing updates.\n\n## Reference(s):\n\n- [Zola API skill release](https://clawhub.ai/chrischall/skills/zola-api)\n- [Mobile API endpoint recipes](references/mobile-api-endpoints.md)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Configuration guidance, Text guidance]\n\n**Output Format:** [Markdown with curl and jq examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [API responses may contain private wedding and account data.]\n\n## Skill Version(s):\n\n2.1.7 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v2.1.6: 4 files, 10454 bytes\n\nFiles: references/mobile-api-endpoints.md (18315b), skill-card.md (1934b), SKILL.md (5663b), _meta.json (127b)\n\nFile v2.1.6:SKILL.md\n\n---\nname: zola-api\ndescription: >-\n  Query or update Zola wedding-planning data (vendors, budget, guests, seating,\n  events/RSVPs, registry, gift tracker, inquiries, wedding website) straight\n  from a shell with curl against mobile-api.zola.com, instead of running the\n  zola-mcp server. Use when you want Zola data without the MCP, in a script,\n  or on a machine where the MCP isn't installed. Triggers on \"check Zola\",\n  \"Zola vendors/budget/guests/RSVP/seating/registry\", or any Zola wedding\n  data request that should hit the API directly.\n---\n\n# Zola mobile API via curl (no MCP)\n\nZola's mobile API (`mobile-api.zola.com` — the same surface the iOS/iPad app\nand `zola-mcp` use) is a plain Bearer-JWT REST API reachable directly from a\nserver or shell — no browser bridge needed. This skill shells out to `curl`\nwith the JWT in an `Authorization: Bearer` header, exactly as\n`zola-mcp`'s `src/client.ts` does.\n\n## One-time setup: get the refresh token\n\nYou need Zola's long-lived (~1 year) refresh JWT — the `usr` cookie from a\nsigned-in `zola.com` session. Same credential `zola-mcp` uses:\n\n```sh\n# Prefer the env var zola-mcp itself reads (check its .env first):\ngrep -h ZOLA_REFRESH_TOKEN ~/git/zola-mcp/.env 2>/dev/null\nexport ZOLA_REFRESH_TOKEN='eyJhbGciOi...'   # or export directly if you have it\n```\n\nIf you don't have it yet: sign into zola.com, open DevTools → Application →\nCookies → `https://www.zola.com`, copy the `usr` value. (`zola-mcp` also has a\nfetchproxy fallback that reads this cookie from a signed-in browser tab — see\nits README — but that's the MCP's path, not this skill's; this skill assumes\nyou already have the token in hand.)\n\n## Core call pattern\n\nEvery mobile-api call needs a short-lived (30 min) **session token**, minted\nfrom the refresh token, plus a fixed set of headers (CloudFront WAF requires\n`x-zola-session-id` on every request — omit it and you get a 403).\n\n```sh\nBASE=https://mobile-api.zola.com\nDEVICE_SESSION_ID=$(uuidgen | tr 'a-z' 'A-Z')   # one per \"session\"; reuse across calls\nUA='Zola/42.5.0 (iPad; iOS 26.4; Scale/2.0)'\n\n# 1. Mint a 30-min session token from the refresh token\nSESSION_TOKEN=$(curl -sS -X POST \"$BASE/v3/sessions/refresh\" \\\n  -H 'content-type: application/json' -H 'accept: application/json' \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" \\\n  -H \"user-agent: $UA\" \\\n  -d \"{\\\"token\\\":\\\"$ZOLA_REFRESH_TOKEN\\\"}\" | jq -r '.data.session_token')\n\n# 2. Every subsequent call reuses $SESSION_TOKEN until it expires (~30 min),\n#    then re-run step 1. All calls carry the same 4 headers:\ncurl -sS -X GET \"$BASE/v3/users/me/context\" \\\n  -H \"authorization: Bearer $SESSION_TOKEN\" \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" \\\n  -H \"user-agent: $UA\" | jq '.data'\n```\n\nWrap this in a shell function or export the 4 headers once — every recipe in\n`references/mobile-api-endpoints.md` reuses `$BASE`, `$SESSION_TOKEN`,\n`$DEVICE_SESSION_ID`, `$UA`. POST/PUT bodies additionally need\n`-H 'content-type: application/json'`.\n\n(The app also sends a fifth header, `x-zola-user-session-id`, derived from a\nclaim inside the session JWT — `zola-mcp` only attaches it when the decode\nsucceeds, so it's not load-bearing; every endpoint below works without it.)\n\n## The one rule: resolve context first\n\nMost write/list endpoints are scoped by numeric IDs, never inferred — call\n`GET /v3/users/me/context` once per session and keep the three IDs around:\n\n```sh\nCTX=$(curl -sS \"$BASE/v3/users/me/context\" -H \"authorization: Bearer $SESSION_TOKEN\" \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" -H \"user-agent: $UA\")\nACCT=$(jq -r '.data.wedding_account.wedding_account_id' <<<\"$CTX\")\nREG=$(jq -r '.data.registry.id' <<<\"$CTX\")\nWEDDING_ID=$(jq -r '.data.wedding.wedding_id' <<<\"$CTX\")\n```\n\n`ACCT` (`wedding_account_id`) feeds guest/event/website-content paths; `REG`\n(`registry_id`) feeds registry/gift-tracker paths.\n\n## Ready-to-run endpoints\n\n`references/mobile-api-endpoints.md` has real, ready-to-run `curl` + `jq`\nrecipes for every one of the 30 `zola-mcp` tools' underlying calls\n(vendors, budget, guests, seating, inquiries, events/RSVPs/gifts/registry,\ndiscover/storefronts, registry items, invitations/card-projects, website\npages, website theme/customization, website content). Transcribed straight\nfrom `src/tools/*.ts` — same paths, same request bodies.\n\n## Mutation gotcha: most writes are read-modify-write\n\nZola's write endpoints replace whole objects, not deltas — the same tools\nthat build a request first re-GET the current record and only patch the\nrequested fields, or a full-state write can wipe unrelated fields.\n`docs/zola-api-quirks.md` in this repo documents the worst offenders (a\n`header_font` write nulling every other website color; guest writes needing\nto preserve `event_invitations` verbatim or lose them). Follow the\nread-modify-write shape shown per-endpoint in the references file — don't\nsend a bare partial body to `PUT`/`POST` write endpoints.\n\n## Output / error contract\n\n- A 2xx body is `{\"data\": ...}` for nearly every endpoint — pipe to\n  `jq '.data'`.\n- `401` — session token expired; re-mint via `POST /v3/sessions/refresh`\n  (step 1 above) and retry once.\n- `429` — rate limited; back off ~2s and retry once.\n- Any other non-2xx — the body is a JSON error envelope; read it directly,\n  it is not redacted here (unlike the MCP, which redacts secrets from error\n  text — don't paste raw error bodies containing the session token anywhere\n  public).\n- This project (`zola-mcp`) is developed and maintained by AI (Claude).\n\nFile v2.1.6:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"zola-api\",\n  \"version\": \"2.1.6\",\n  \"publishedAt\": 1790956159691\n}\n\nFile v2.1.6:references/mobile-api-endpoints.md\n\n# Zola mobile-api endpoints (curl + jq)\n\nAll paths are relative to `$BASE=https://mobile-api.zola.com`. Every call\ncarries `authorization: Bearer $SESSION_TOKEN`, `x-zola-platform-type:\niphone_app`, `x-zola-session-id: $DEVICE_SESSION_ID`, `user-agent: $UA` (see\n`SKILL.md`); POST/PUT/DELETE-with-body also need `-H 'content-type:\napplication/json'`. `$ACCT` = `wedding_account_id`, `$REG` = `registry_id`,\n`$WEDDING_ID` = `wedding_id`, all three from `GET /v3/users/me/context` (see\n`SKILL.md`'s \"resolve context first\" section). Response envelope is `{\"data\": ...}`\nunless noted. Paths/bodies below are transcribed from `src/tools/*.ts` —\neach section names its source file.\n\nShorthand used below:\n\n```sh\nH=(-H \"authorization: Bearer $SESSION_TOKEN\" -H \"x-zola-platform-type: iphone_app\" \\\n   -H \"x-zola-session-id: $DEVICE_SESSION_ID\" -H \"user-agent: $UA\")\nHJ=(\"${H[@]}\" -H 'content-type: application/json')\n```\n\n---\n\n## Context (`src/client.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/users/me/context\" | jq '.data'\n# .data.user.id, .data.wedding_account.wedding_account_id,\n# .data.wedding.{wedding_id,wedding_date,slug}, .data.registry.id\n```\n\n---\n\n## Vendors (`src/tools/vendors.ts`)\n\n**List booked vendors** — `POST /v3/account-vendors/booked-list` body `{}`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/account-vendors/booked-list\" -d '{}' \\\n  | jq '.data.booked_vendors'\n```\n\n**Search vendors (typeahead)** — `POST /v3/reference-vendors/typeahead-taxonomy`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/reference-vendors/typeahead-taxonomy\" \\\n  -d '{\"query\":\"Acme Photography\",\"taxonomy_key\":\"wedding-photographers\"}' | jq '.data'\n# taxonomy_key default: wedding-venues. Other keys: wedding-planners, wedding-bands-djs, ...\n```\n\n**Book a vendor** — find an unbooked slot from `booked-list` for the\n`vendor_type`, then `PUT /v5/account-vendors/vendor` (note: **v5**, not v3):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v5/account-vendors/vendor\" -d '{\n  \"uuid\": \"<slot-uuid-from-booked-list>\", \"id\": 0, \"vendor_type\": \"PHOTOGRAPHER\",\n  \"booked\": true, \"booking_source\": \"BOOKED_VENDORS\",\n  \"price_cents\": 350000, \"event_date\": null,\n  \"sync_with_budget_tool_enabled\": true, \"facet_keys\": [],\n  \"reference_vendor_request\": {\n    \"id\": null, \"name\": \"Acme Photography\", \"email\": null, \"phone\": null,\n    \"address\": {\"city\": \"Charlotte\", \"state_province_region\": \"NC\"}\n  }\n}' | jq '.data'\n```\n\n**Update a booked vendor** — same `PUT /v5/account-vendors/vendor`, but\nread-modify-write: GET `booked-list`, find by `uuid`, keep `id`/`vendor_type`,\nonly patch the fields you're changing (name/city/state/email/price/date\ndefault to the current value).\n\n**Unbook a vendor** — `POST /v3/account-vendors/vendor/unbook`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/account-vendors/vendor/unbook\" \\\n  -d '{\"uuid\":\"<vendor-uuid>\"}'\n```\n\n---\n\n## Budget (`src/tools/budget.ts`)\n\n**Get budget** — `GET /v3/budgets`:\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/budgets\" | jq '{\n  budgeted_cents: .data.budgeted_cents, cost_cents: .data.cost_cents,\n  paid_cents: .data.paid_cents, balance_due_cents: .data.balance_due_cents,\n  items: [.data.taxonomy_nodes[].items[] | {uuid, title, cost_cents, paid_cents}]\n}'\n```\n\n**Update a budget item** — read-modify-write: `GET /v3/budgets`, find the item\nby `uuid`, then `PUT /v3/budgets/items` with every required field (server\nrejects a bare partial):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/budgets/items\" -d '{\n  \"item_uuid\": \"<uuid>\", \"taxonomy_node_uuid\": \"<from-get>\",\n  \"estimated_cost_cents\": <from-get>, \"actual_cost_cents\": 250000,\n  \"note\": \"Deposit paid\", \"item_type\": \"<from-get, e.g. VENUE>\",\n  \"title\": \"<from-get>\"\n}' | jq '.data'\n```\n\n---\n\n## Guests (`src/tools/guests.ts`)\n\n**List guests** — `POST /v3/guestlists/directory/wedding-accounts/$ACCT` body\n`{\"sort_by_name_asc\": true}`. Guest shape is **flat** (fields directly on the\nguest object, no `{guest:{...}}` wrapper):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/guestlists/directory/wedding-accounts/$ACCT\" \\\n  -d '{\"sort_by_name_asc\": true}' \\\n  | jq '.data.guest_groups[] | {guest_group_id, guests: [.guests[] | {guest_id, first_name, family_name, rsvp}]}'\n```\n\n**Add a guest group** — `POST /v3/guestlists/groups`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/guestlists/groups\" -d '{\n  \"wedding_account_id\": '\"$ACCT\"',\n  \"guests\": [{\n    \"first_name\": \"Mike\", \"family_name\": \"Smith\", \"relationship_type\": \"PRIMARY\",\n    \"source\": \"IOS\", \"email_address\": \"\", \"mobile_phone\": \"\", \"affiliation\": \"PRIMARY_FRIEND\",\n    \"tier\": \"A\", \"country_code\": \"US\", \"prefix\": \"\", \"middle_name\": \"\", \"suffix\": \"\",\n    \"home_phone\": \"\", \"address1\": \"\", \"address2\": \"\", \"city\": \"\", \"state_province\": \"\",\n    \"postal_code\": \"\", \"event_invitations\": [], \"tags\": []\n  }],\n  \"guest_group_affiliation\": \"PRIMARY_FRIEND\", \"guest_group_tier\": \"A\",\n  \"guest_group_uuid\": \"\", \"envelope_recipient\": \"\", \"invited\": true,\n  \"invitation_sent\": false, \"save_the_date_sent\": false,\n  \"rsvp_question_answers\": [], \"gift_count\": 0\n}' | jq '.data'\n```\n\n**Update a guest group's address** — read-modify-write via\n`PUT /v3/guestlists/groups/wedding-accounts/$ACCT/bulk/directory`. **Must**\npreserve each guest's existing `event_invitations` verbatim (a group that\nsends `event_invitations: []` wipes them, per `docs/zola-api-quirks.md` §5):\n\n```sh\n# 1. GET the directory (above), find the target group by guest_group_id,\n#    keep its `guests[]` array as-is except the address fields you're changing.\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/guestlists/groups/wedding-accounts/$ACCT/bulk/directory\" \\\n  -d '{\"updated_guest_groups\": [ <full-group-object-with-patched-guest-addresses> ]}' \\\n  | jq '.data'\n```\n\n**Remove a guest group** — `PUT /v3/guestlists/groups/wedding-accounts/$ACCT/delete`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/guestlists/groups/wedding-accounts/$ACCT/delete\" \\\n  -d '{\"wedding_account_id\": '\"$ACCT\"', \"guest_group_ids\": [<id>]}'\n```\n\n---\n\n## Seating (`src/tools/seating.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/seating-charts/summaries\" | jq '.'   # NOT wrapped in `data`\ncurl -sS \"${H[@]}\" \"$BASE/v3/seating-charts/<chart-uuid>\" | jq '.'\n```\n\n**Unseated guests** — `POST /v3/guestlists/directory/wedding-accounts/$ACCT`\n(same call as guests list), filter client-side for\n`.guests[].seating_chart_seat == null`.\n\n**Assign a seat** — `PUT /v3/seating-charts/seats`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/seating-charts/seats\" -d '{\n  \"guest_uuid\": \"<guest-uuid>\", \"seat_uuid\": \"<seat-uuid>\",\n  \"table_uuid\": \"<table-uuid>\", \"seating_chart_uuid\": \"<chart-uuid>\"\n}' | jq '.'\n```\n\n---\n\n## Inquiries (`src/tools/inquiries.ts`)\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/inquiries/unified-inquiries\" -d '{}' \\\n  | jq '.data[].inquiry_summaries[] | {inquiry_uuid, vendor_name: .vendor_card.vendor_name, unread, status_text}'\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/inquiries/<inquiry-uuid>/conversation\" | jq '.data.messages'\n\ncurl -sS \"${H[@]}\" -X PUT \"$BASE/v3/inquiries/<inquiry-uuid>/conversation/read\"\n```\n\n---\n\n## Events, RSVPs, gifts, registry (`src/tools/events.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/events/wedding-accounts/$ACCT/groups\" \\\n  | jq '.data[].events[]'   # event_entity_id, name, start_at, num_guests_*\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/events/track-rsvps\" | jq '.data.modules'\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/gift_tracker/$REG\" | jq '.data | del(.info_modules)'\n\ncurl -sS \"${H[@]}\" \"$BASE/v4/shop/registry?registry_id=$REG&updated_modules=true\" | jq '.data'\n```\n\n**Update an event** — read-modify-write: GET the groups above, find by\n`event_entity_id`, then `PUT /v3/websites/events/{event_id}` with the full\nevent object (patch only the fields you're changing; everything else —\n`type`, `uuid`, `wedding_account_id`, counts, `meal_options` — round-trips\nfrom the GET):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/websites/events/<event_id>\" -d '{\n  \"event_entity_id\": <event_id>, \"uuid\": \"<from-get>\", \"wedding_account_id\": '\"$ACCT\"',\n  \"type\": \"<from-get>\", \"name\": \"Reception\", \"start_at\": \"<from-get>\", \"end_at\": \"<from-get>\",\n  \"timezone\": \"<from-get>\", \"venue_name\": \"The Grand Hall\", \"address1\": \"\", \"address2\": \"\",\n  \"city\": \"Charlotte\", \"state_province\": \"NC\", \"postal_code\": \"\", \"country_code\": \"US\",\n  \"note\": \"\", \"attire\": \"Black tie\", \"collect_rsvps\": true, \"public\": <from-get>,\n  \"display_order\": 0, \"num_guests_attending\": <from-get>, \"num_guests_declined\": <from-get>,\n  \"num_guests_not_responded\": <from-get>, \"meal_options\": <from-get>,\n  \"rsvp_questions\": [], \"add_booked_vendor\": false\n}' | jq '.data'\n```\n\n**Event invitations (per-guest, read-modify-write)** — from\n`src/tools/event-invitations.ts`. Each guest carries an `event_invitations`\narray; invite = append `{\"event_id\":<id>,\"id\":null,\"rsvp_type\":\"NO_RESPONSE\"}`,\nuninvite = drop that element, **preserve every other element verbatim**\n(same wipe risk as the guest-address write above). Write through the same\n`PUT /v3/guestlists/groups/wedding-accounts/$ACCT/bulk/directory` used for\nguest writes, sending `{\"updated_guest_groups\": [...]}` with full group\nobjects (each guest keeps its full shape, only `event_invitations` patched).\n\n---\n\n## Discover / storefronts (`src/tools/discover.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v4/your-wedding\" | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/storefronts/search\" -d '{\n  \"taxonomy_node_id\": 2, \"city\": \"Charlotte\", \"state\": \"NC\",\n  \"limit\": 24, \"offset\": 0, \"facets\": {},\n  \"metro_types\": [\"HOME\",\"HOME_SERVICE\",\"AWAY\"], \"metros\": [],\n  \"exclude_inquired_storefronts\": false, \"exclude_booked_storefronts\": false,\n  \"boost_featured_storefronts\": false, \"suggested_vendors_for_inquiry_limit\": 12\n}' | jq '.data'\n# taxonomy_node_id: 1=Venues 2=Photographers 3=Florists 7=Planners 9=Bands/DJs\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/storefronts/<storefront-uuid>\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/favorites/\" | jq '.data'\n```\n\n---\n\n## Registry items (`src/tools/registry-items.ts`)\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/categories/<category_id>/entities\" \\\n  -d '{\"offset\":0,\"limit\":50,\"registry_id\":\"'\"$REG\"'\"}' | jq '.data'\n\n# Default collection id: `.data.default_collection_id` from the registry GET\n# above, or the first `groups[].modules[]` entry with `type == \"COLLECTION\"`.\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/registries/$REG/collections/<collection_id>\" \\\n  -d '{\"sku_id\":\"<sku>\",\"quantity\":1,\"most_wanted\":false,\"enable_group_gifting\":false}' \\\n  | jq '.data'\n\n# Full replace — all fields required:\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/registries/$REG/items/<collection_item_id>\" -d '{\n  \"quantity\": 2, \"group_gift\": false, \"marked_fulfilled\": false,\n  \"personal_note\": \"\", \"most_wanted\": true, \"collection_id\": \"<collection_id>\"\n}' | jq '.data'\n\ncurl -sS \"${H[@]}\" -X DELETE \"$BASE/v3/registries/$REG/items/<collection_item_id>\"\n```\n\n---\n\n## Website pages & settings (`src/tools/website.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/pages/wedding-accounts/full\" | jq '.data'\n\ncurl -sS \"${H[@]}\" -X PUT \"$BASE/v3/websites/pages/<page_id>/hidden/true\"  # or /false\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/websites/pages/wedding-accounts/$ACCT/reorder\" \\\n  -d '{\"ids\": [<page_id_1>, <page_id_2>, ...]}' | jq '.data'\n\n# Partial patch is fine here (unlike most write endpoints):\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/websites/pages-v2/<page_id>\" \\\n  -d '{\"page_id\": <page_id>, \"title\": \"Our Story\"}' | jq '.data'\n```\n\n**Wedding settings** — read via `GET /v3/users/me/context` → `.data.wedding`;\nwrite is read-modify-write (all fields required) to\n`PUT /v3/weddings/{wedding_id}`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/weddings/$WEDDING_ID\" -d '{\n  \"wedding_id\": '\"$WEDDING_ID\"', \"account_id\": '\"$ACCT\"',\n  \"slug\": \"<from-get>\", \"owner_first_name\": \"<from-get>\", \"owner_last_name\": \"<from-get>\",\n  \"partner_first_name\": \"<from-get>\", \"partner_last_name\": \"<from-get>\",\n  \"title\": \"Alex & Jordan\", \"wedding_date\": \"<from-get>\", \"hashtag\": \"#alexandjordan2026\",\n  \"enable_search_engine\": true, \"enable_search_zola\": true,\n  \"city\": \"Charlotte\", \"state_province\": \"NC\", \"guest_count\": 150\n}' | jq '.data'\n```\n\n---\n\n## Website theme & customization (`src/tools/website-theme.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/themes/current\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/website-customizations/context\" | jq '.data.current_style_customizations'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/themes/search\" \\\n  -d '{\"limit\":50,\"offset\":0,\"theme_layout_types\":[\"MULTI_PAGE\"]}' | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/themes/current\" \\\n  -d '{\"theme_key\":\"galata\",\"theme_layout_type\":\"MULTI_PAGE\"}' | jq '.data'\n```\n\n**Update colors/fonts** — `POST /v3/websites/website-customizations/context`.\n**Gotcha (see `docs/zola-api-quirks.md` §1–4):** changing `header_font`\n(i.e. `header_font_family_id`) wipes every *other* active color unless you\nre-send them in the same POST — GET the current state first and bundle its\nnon-null `accent_color` / `background_color` / `body.color` /\n`navigation_customization.background_color` into the body. `body_font`'s\n`font_family_id` only accepts `68` (Libre Baskerville) or `198` (Circular) —\nany other value 500s. `header_color` / `nav_font_color` are **not** writable\non this API at all (web-api-only, cookie+CSRF auth — out of scope here).\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/websites/website-customizations/context\" -d '{\n  \"accent_color\": \"C9A66B\",\n  \"background_color\": \"FFFFFF\",\n  \"body_font\": {\"color\": \"222222\"},\n  \"navigation_customization\": {\"background_color\": \"FFFFFF\"}\n}' | jq '.data'\n```\n\n---\n\n## Website content: FAQs / home sections / POIs / travel (`src/tools/website-content.ts`)\n\nAll four follow the same list/add/update/remove shape;\n`{wedding_account_id: $ACCT}` and an `_entity_id: 0` (create) or the real id\n(update) prefix every write body. Deletes share one path shape:\n`DELETE /v3/websites/{pages}/{entities}/{entity_id}/wedding-accounts/$ACCT`\nwhere `{pages}` is looked up per-type from\n`GET /v3/websites/pages/wedding-accounts/full` (`.data.{home,faq,poi,travel}_page.page_id`).\n\n```sh\n# FAQs\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/faqs/wedding-accounts/$ACCT\" | jq '.data'\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/websites/faqs\" -d '{\n  \"wedding_account_id\": '\"$ACCT\"', \"faq_entity_id\": 0,\n  \"question\": \"What is the dress code?\", \"answer\": \"Cocktail attire\", \"display_order\": 0\n}' | jq '.data'\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/websites/faqs/<faq_entity_id>\" -d '{\n  \"wedding_account_id\": '\"$ACCT\"', \"faq_entity_id\": <faq_entity_id>,\n  \"question\": \"...\", \"answer\": \"...\", \"display_order\": 0\n}' | jq '.data'\ncurl -sS \"${H[@]}\" -X DELETE \"$BASE/v3/websites/pages/<faq_page_id>/entities/<faq_entity_id>/wedding-accounts/$ACCT\"\n\n# Home page story sections — same shape, path `/v3/websites/home-sections[...]`,\n# id field `homepage_entity_id`, extra `hidden` boolean, `title`+`subtitle`+`description`.\n\n# Points of interest — `/v3/websites/points-of-interest[...]`, id field\n# `poi_entity_id`; fields: title, description, address1/2, city, state_province,\n# postal_code, country_code, latitude, longitude, google_place_id, contact_phone, url.\n\n# Travel items — `/v3/websites/travel[...]`, id field `travel_entity_id`;\n# fields: type (HOTEL|FLIGHT|TRAIN|BUS|CAR|OTHER), name, note, code,\n# address1/2, city, state_province, postal_code, country_code, latitude,\n# longitude, google_place_id, contact_number, email_address, url, source\n# (GOOGLE_PLACES|MANUAL), timezone, display_order.\n```\n\nHome-section/POI/travel `list`/`add`/`update`/`remove` all mirror the FAQ\ncalls above 1:1 (same verbs, same `wedding_account_id` prefix, same delete\nshape) — see `src/tools/website-content.ts` for the exact field names per\ntype if you need the full body.\n\n---\n\n## Invitations / card projects (`src/tools/invitations.ts`)\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/card-projects/search_request\" -d '{\n  \"completed\": false, \"limit\": 30, \"card_suite_uuids\": [], \"card_suite_ids\": [],\n  \"offset\": 0, \"fetch_customizations\": true, \"include_deleted\": false,\n  \"medium\": [\"PAPER\",\"MAGNET\",\"DIGITAL\"], \"single_sample\": false\n}' | jq '.data'\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/card-projects/<project_uuid>\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/card-projects/<project_uuid>/validate\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/card-projects/<project_uuid>/project-guest-groups\" | jq '.data'\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v4/card-catalog/suites/details/<suite_uuid>\" | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/card-catalog/search/faceted\" -d '{\n  \"lead_card_types\": [], \"include_updated_proof_module\": true, \"limit\": 50, \"offset\": 0,\n  \"digital_suite\": false, \"include_lead_card_type_metadata\": true,\n  \"lead_card_type\": \"INVITATION\", \"include_module\": true\n}' | jq '.data'\n\ncurl -sS \"${H[@]}\" \"$BASE/v3/favorites/card-suites/\" | jq '.data'\ncurl -sS \"${H[@]}\" \"$BASE/v3/websites/rsvps/wedding-accounts/$ACCT\" | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/card-projects\" -d '{\n  \"quantity\": 150, \"lead_variation_uuid\": \"<variation-uuid>\",\n  \"extra_customizable\": false, \"account_id\": '\"$ACCT\"', \"suite_uuid\": \"<suite-uuid>\"\n}' | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/card-projects/<project_uuid>\" -d '{\n  \"customizations\": {\"<customization_uuid>\": {\"variation_uuid\": \"<new-variation-uuid>\"}}\n}' | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/card-projects/<project_uuid>/project-guest-groups\" -d '{\n  \"guest_group_requests\": [{\"guest_group_id\": <id>, \"enabled\": true}]\n}' | jq '.data'\n\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/card-templates/preview\" -d '{\n  \"variation_uuids\": [\"<variation-uuid>\"], \"customizable\": true,\n  \"substitutions\": {\"first_name\": \"Alex\", \"wedding_date\": \"2026-10-17\"}\n}' | jq '.data'\n```\n\n**QR code** — preview returns raw image bytes (Content-Type lies as\n`image/jpeg` even for a PNG — sniff magic bytes if scripting):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/card-projects/qrcode/preview\" \\\n  -H 'accept: */*' -d '{\"dimension\":\"MEDIUM\",\"url_type\":\"CUSTOM\",\"enabled\":true,\"url\":\"https://example.com\"}' \\\n  -o qrcode.png\n\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/card-projects/<project_uuid>/customization/page/<page_uuid>/qrcode\" -d '{\n  \"dimension\": \"MEDIUM\", \"url_type\": \"CUSTOM\", \"color\": \"000000\",\n  \"enabled\": true, \"url\": \"https://example.com\"\n}' | jq '.data'\n```\n\nFile v2.1.6:skill-card.md\n\n## Description:\n\nGuides agents in querying and updating Zola wedding-planning data directly through shell-based mobile API requests without an MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and other Zola account holders use this skill to inspect or manage wedding vendors, budgets, guests, seating, RSVPs, registries, and website information from a shell when an MCP server is unavailable or unwanted.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: A long-lived Zola refresh token or session token can expose private account data if shared in chats or logs.\n\nMitigation: Treat tokens like passwords; keep them out of chats and logs, and review credential handling before use.\n\nRisk: Ready-to-run write and delete commands can change or remove real wedding, guest, budget, registry, and website data.\n\nMitigation: Confirm exact account IDs, target records, and payloads before each POST, PUT, or DELETE request; preserve existing fields in read-modify-write operations.\n\n## Reference(s):\n\n- [Zola API skill release](https://clawhub.ai/chrischall/skills/zola-api)\n- [Mobile API endpoints and examples](artifact/references/mobile-api-endpoints.md)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance]\n\n**Output Format:** [Markdown with curl and jq command examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Provides request examples and guidance for handling JSON API responses.]\n\n## Skill Version(s):\n\n2.1.6 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v2.1.5: 4 files, 10417 bytes\n\nFiles: references/mobile-api-endpoints.md (18315b), skill-card.md (1992b), SKILL.md (5663b), _meta.json (127b)\n\nFile v2.1.5:SKILL.md\n\n---\nname: zola-api\ndescription: >-\n  Query or update Zola wedding-planning data (vendors, budget, guests, seating,\n  events/RSVPs, registry, gift tracker, inquiries, wedding website) straight\n  from a shell with curl against mobile-api.zola.com, instead of running the\n  zola-mcp server. Use when you want Zola data without the MCP, in a script,\n  or on a machine where the MCP isn't installed. Triggers on \"check Zola\",\n  \"Zola vendors/budget/guests/RSVP/seating/registry\", or any Zola wedding\n  data request that should hit the API directly.\n---\n\n# Zola mobile API via curl (no MCP)\n\nZola's mobile API (`mobile-api.zola.com` — the same surface the iOS/iPad app\nand `zola-mcp` use) is a plain Bearer-JWT REST API reachable directly from a\nserver or shell — no browser bridge needed. This skill shells out to `curl`\nwith the JWT in an `Authorization: Bearer` header, exactly as\n`zola-mcp`'s `src/client.ts` does.\n\n## One-time setup: get the refresh token\n\nYou need Zola's long-lived (~1 year) refresh JWT — the `usr` cookie from a\nsigned-in `zola.com` session. Same credential `zola-mcp` uses:\n\n```sh\n# Prefer the env var zola-mcp itself reads (check its .env first):\ngrep -h ZOLA_REFRESH_TOKEN ~/git/zola-mcp/.env 2>/dev/null\nexport ZOLA_REFRESH_TOKEN='eyJhbGciOi...'   # or export directly if you have it\n```\n\nIf you don't have it yet: sign into zola.com, open DevTools → Application →\nCookies → `https://www.zola.com`, copy the `usr` value. (`zola-mcp` also has a\nfetchproxy fallback that reads this cookie from a signed-in browser tab — see\nits README — but that's the MCP's path, not this skill's; this skill assumes\nyou already have the token in hand.)\n\n## Core call pattern\n\nEvery mobile-api call needs a short-lived (30 min) **session token**, minted\nfrom the refresh token, plus a fixed set of headers (CloudFront WAF requires\n`x-zola-session-id` on every request — omit it and you get a 403).\n\n```sh\nBASE=https://mobile-api.zola.com\nDEVICE_SESSION_ID=$(uuidgen | tr 'a-z' 'A-Z')   # one per \"session\"; reuse across calls\nUA='Zola/42.5.0 (iPad; iOS 26.4; Scale/2.0)'\n\n# 1. Mint a 30-min session token from the refresh token\nSESSION_TOKEN=$(curl -sS -X POST \"$BASE/v3/sessions/refresh\" \\\n  -H 'content-type: application/json' -H 'accept: application/json' \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" \\\n  -H \"user-agent: $UA\" \\\n  -d \"{\\\"token\\\":\\\"$ZOLA_REFRESH_TOKEN\\\"}\" | jq -r '.data.session_token')\n\n# 2. Every subsequent call reuses $SESSION_TOKEN until it expires (~30 min),\n#    then re-run step 1. All calls carry the same 4 headers:\ncurl -sS -X GET \"$BASE/v3/users/me/context\" \\\n  -H \"authorization: Bearer $SESSION_TOKEN\" \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" \\\n  -H \"user-agent: $UA\" | jq '.data'\n```\n\nWrap this in a shell function or export the 4 headers once — every recipe in\n`references/mobile-api-endpoints.md` reuses `$BASE`, `$SESSION_TOKEN`,\n`$DEVICE_SESSION_ID`, `$UA`. POST/PUT bodies additionally need\n`-H 'content-type: application/json'`.\n\n(The app also sends a fifth header, `x-zola-user-session-id`, derived from a\nclaim inside the session JWT — `zola-mcp` only attaches it when the decode\nsucceeds, so it's not load-bearing; every endpoint below works without it.)\n\n## The one rule: resolve context first\n\nMost write/list endpoints are scoped by numeric IDs, never inferred — call\n`GET /v3/users/me/context` once per session and keep the three IDs around:\n\n```sh\nCTX=$(curl -sS \"$BASE/v3/users/me/context\" -H \"authorization: Bearer $SESSION_TOKEN\" \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" -H \"user-agent: $UA\")\nACCT=$(jq -r '.data.wedding_account.wedding_account_id' <<<\"$CTX\")\nREG=$(jq -r '.data.registry.id' <<<\"$CTX\")\nWEDDING_ID=$(jq -r '.data.wedding.wedding_id' <<<\"$CTX\")\n```\n\n`ACCT` (`wedding_account_id`) feeds guest/event/website-content paths; `REG`\n(`registry_id`) feeds registry/gift-tracker paths.\n\n## Ready-to-run endpoints\n\n`references/mobile-api-endpoints.md` has real, ready-to-run `curl` + `jq`\nrecipes for every one of the 30 `zola-mcp` tools' underlying calls\n(vendors, budget, guests, seating, inquiries, events/RSVPs/gifts/registry,\ndiscover/storefronts, registry items, invitations/card-projects, website\npages, website theme/customization, website content). Transcribed straight\nfrom `src/tools/*.ts` — same paths, same request bodies.\n\n## Mutation gotcha: most writes are read-modify-write\n\nZola's write endpoints replace whole objects, not deltas — the same tools\nthat build a request first re-GET the current record and only patch the\nrequested fields, or a full-state write can wipe unrelated fields.\n`docs/zola-api-quirks.md` in this repo documents the worst offenders (a\n`header_font` write nulling every other website color; guest writes needing\nto preserve `event_invitations` verbatim or lose them). Follow the\nread-modify-write shape shown per-endpoint in the references file — don't\nsend a bare partial body to `PUT`/`POST` write endpoints.\n\n## Output / error contract\n\n- A 2xx body is `{\"data\": ...}` for nearly every endpoint — pipe to\n  `jq '.data'`.\n- `401` — session token expired; re-mint via `POST /v3/sessions/refresh`\n  (step 1 above) and retry once.\n- `429` — rate limited; back off ~2s and retry once.\n- Any other non-2xx — the body is a JSON error envelope; read it directly,\n  it is not redacted here (unlike the MCP, which redacts secrets from error\n  text — don't paste raw error bodies containing the session token anywhere\n  public).\n- This project (`zola-mcp`) is developed and maintained by AI (Claude).\n\nFile v2.1.5:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"zola-api\",\n  \"version\": \"2.1.5\",\n  \"publishedAt\": 1790603830952\n}\n\nFile v2.1.5:references/mobile-api-endpoints.md\n\n# Zola mobile-api endpoints (curl + jq)\n\nAll paths are relative to `$BASE=https://mobile-api.zola.com`. Every call\ncarries `authorization: Bearer $SESSION_TOKEN`, `x-zola-platform-type:\niphone_app`, `x-zola-session-id: $DEVICE_SESSION_ID`, `user-agent: $UA` (see\n`SKILL.md`); POST/PUT/DELETE-with-body also need `-H 'content-type:\napplication/json'`. `$ACCT` = `wedding_account_id`, `$REG` = `registry_id`,\n`$WEDDING_ID` = `wedding_id`, all three from `GET /v3/users/me/context` (see\n`SKILL.md`'s \"resolve context first\" section). Response envelope is `{\"data\": ...}`\nunless noted. Paths/bodies below are transcribed from `src/tools/*.ts` —\neach section names its source file.\n\nShorthand used below:\n\n```sh\nH=(-H \"authorization: Bearer $SESSION_TOKEN\" -H \"x-zola-platform-type: iphone_app\" \\\n   -H \"x-zola-session-id: $DEVICE_SESSION_ID\" -H \"user-agent: $UA\")\nHJ=(\"${H[@]}\" -H 'content-type: application/json')\n```\n\n---\n\n## Context (`src/client.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/users/me/context\" | jq '.data'\n# .data.user.id, .data.wedding_account.wedding_account_id,\n# .data.wedding.{wedding_id,wedding_date,slug}, .data.registry.id\n```\n\n---\n\n## Vendors (`src/tools/vendors.ts`)\n\n**List booked vendors** — `POST /v3/account-vendors/booked-list` body `{}`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/account-vendors/booked-list\" -d '{}' \\\n  | jq '.data.booked_vendors'\n```\n\n**Search vendors (typeahead)** — `POST /v3/reference-vendors/typeahead-taxonomy`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/reference-vendors/typeahead-taxonomy\" \\\n  -d '{\"query\":\"Acme Photography\",\"taxonomy_key\":\"wedding-photographers\"}' | jq '.data'\n# taxonomy_key default: wedding-venues. Other keys: wedding-planners, wedding-bands-djs, ...\n```\n\n**Book a vendor** — find an unbooked slot from `booked-list` for the\n`vendor_type`, then `PUT /v5/account-vendors/vendor` (note: **v5**, not v3):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v5/account-vendors/vendor\" -d '{\n  \"uuid\": \"<slot-uuid-from-booked-list>\", \"id\": 0, \"vendor_type\": \"PHOTOGRAPHER\",\n  \"booked\": true, \"booking_source\": \"BOOKED_VENDORS\",\n  \"price_cents\": 350000, \"event_date\": null,\n  \"sync_with_budget_tool_enabled\": true, \"facet_keys\": [],\n  \"reference_vendor_request\": {\n    \"id\": null, \"name\": \"Acme Photography\", \"email\": null, \"phone\": null,\n    \"address\": {\"city\": \"Charlotte\", \"state_province_region\": \"NC\"}\n  }\n}' | jq '.data'\n```\n\n**Update a booked vendor** — same `PUT /v5/account-vendors/vendor`, but\nread-modify-write: GET `booked-list`, find by `uuid`, keep `id`/`vendor_type`,\nonly patch the fields you're changing (name/city/state/email/price/date\ndefault to the current value).\n\n**Unbook a vendor** — `POST /v3/account-vendors/vendor/unbook`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/account-vendors/vendor/unbook\" \\\n  -d '{\"uuid\":\"<vendor-uuid>\"}'\n```\n\n---\n\n## Budget (`src/tools/budget.ts`)\n\n**Get budget** — `GET /v3/budgets`:\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/budgets\" | jq '{\n  budgeted_cents: .data.budgeted_cents, cost_cents: .data.cost_cents,\n  paid_cents: .data.paid_cents, balance_due_cents: .data.balance_due_cents,\n  items: [.data.taxonomy_nodes[].items[] | {uuid, title, cost_cents, paid_cents}]\n}'\n```\n\n**Update a budget item** — read-modify-write: `GET /v3/budgets`, find the item\nby `uuid`, then `PUT /v3/budgets/items` with every required field (server\nrejects a bare partial):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v3/budgets/items\" -d '{\n  \"item_uuid\": \"<uuid>\", \"taxonomy_node_uuid\": \"<from-get>\",\n  \"estimated_cost_cents\": <from-get>, \"actual_cost_cents\": 250000,\n  \"note\": \"Deposit paid\", \"item_type\": \"<from-get, e.g. VENUE>\",\n  \"title\": \"<from-get>\"\n}' | jq '.data'\n```\n\n---\n\n## Guests (`src/tools/guests.ts`)\n\n**List guests** — `POST /v3/guestlists/directory/wedding-accounts/$ACCT` body\n`{\"sort_by_name_asc\": true}`. Guest shape is **flat** (fields directly on the\nguest object, no `{guest:{...}}` wrapper):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/guestlists/directory/wedding-accounts/$ACCT\" \\\n  -d '{\"sort_by_name_asc\": true}' \\\n  | jq '.data.guest_groups[] | {guest_group_id, guests: [.guests[] | {guest_id, first_name, family_name, rsvp}]}'\n```\n\n**Add a guest group** — `POST /v3/guestlists/groups`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/guestlists/groups\" -d '{\n  \"wedding_account_id\": '\"$ACCT\"',\n  \"guests\": [{\n    \"first_name\": \"Mike\", \"family_name\": \"Smith\", \"relationship_type\": \"PRIMARY\",\n    \"source\": \"IOS\", \"email_address\": \"\", \"mobile_phone\": \"\", \"affiliation\": \"PRIMARY_FRIEND\",\n    \"tier\": \"A\", \"country_code\": \"US\", \"prefix\": \"\", \"middle_name\": \"\", \"suffix\": \"\",\n    \"home_phone\": \"\", \"address1\": \"\", \"address2\": \"\", \"city\n\nArchive v2.1.4: 4 files, 10380 bytes\n\nFiles: references/mobile-api-endpoints.md (18315b), skill-card.md (1911b), SKILL.md (5663b), _meta.json (127b)\n\nArchive v2.1.3: 4 files, 10592 bytes\n\nFiles: references/mobile-api-endpoints.md (18315b), skill-card.md (2345b), SKILL.md (5663b), _meta.json (127b)\n\nArchive v2.1.2: 4 files, 10639 bytes\n\nFiles: references/mobile-api-endpoints.md (18315b), skill-card.md (2429b), SKILL.md (5663b), _meta.json (127b)\n\nArchive v2.1.1: 4 files, 10553 bytes\n\nFiles: references/mobile-api-endpoints.md (18315b), skill-card.md (2217b), SKILL.md (5663b), _meta.json (127b)","readmeExcerpt":"Skill: zola-api Owner: chrischall Summary: Query or update Zola wedding-planning data (vendors, budget, guests, seating, events/RSVPs, registry, gift tracker, inquiries, wedding website) straight from a shell with curl against mobile-api.zola.com, instead of running the zola-mcp server. Use when you want Zola data without the MCP, in a script, or on a machine where the MCP isn't installed. Triggers on \"check Zola\", \"","codeSnippets":[],"executableExamples":[{"language":"sh","snippet":"# Prefer the env var zola-mcp itself reads (check its .env first):\ngrep -h ZOLA_REFRESH_TOKEN ~/git/zola-mcp/.env 2>/dev/null\nexport ZOLA_REFRESH_TOKEN='eyJhbGciOi...'   # or export directly if you have it"},{"language":"sh","snippet":"curl -sS -X GET \"$BASE/v3/users/me/context\" \\\n  -H \"authorization: Bearer $SESSION_TOKEN\" \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" \\\n  -H \"user-agent: $UA\" | jq '.data'"},{"language":"sh","snippet":"BASE=https://mobile-api.zola.com\nDEVICE_SESSION_ID=$(uuidgen | tr 'a-z' 'A-Z')   # one per \"session\"; reuse across calls\nUA='Zola/42.5.0 (iPad; iOS 26.4; Scale/2.0)'\n\n# 1. Mint a 30-min session token from the refresh token\nSESSION_TOKEN=$(curl -sS -X POST \"$BASE/v3/sessions/refresh\" \\\n  -H 'content-type: application/json' -H 'accept: application/json' \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" \\\n  -H \"user-agent: $UA\" \\\n  -d \"{\\\"token\\\":\\\"$ZOLA_REFRESH_TOKEN\\\"}\" | jq -r '.data.session_token')\n\n# 2. Every subsequent call reuses $SESSION_TOKEN until it expires (~30 min),\n#    then re-run step 1. All calls carry the same 4 headers:\ncurl -sS -X GET \"$BASE/v3/users/me/context\" \\\n  -H \"authorization: Bearer $SESSION_TOKEN\" \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" \\\n  -H \"user-agent: $UA\" | jq '.data'"},{"language":"sh","snippet":"CTX=$(curl -sS \"$BASE/v3/users/me/context\" -H \"authorization: Bearer $SESSION_TOKEN\" \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" -H \"user-agent: $UA\")\nACCT=$(jq -r '.data.wedding_account.wedding_account_id' <<<\"$CTX\")\nREG=$(jq -r '.data.registry.id' <<<\"$CTX\")\nWEDDING_ID=$(jq -r '.data.wedding.wedding_id' <<<\"$CTX\")"},{"language":"sh","snippet":"H=(-H \"authorization: Bearer $SESSION_TOKEN\" -H \"x-zola-platform-type: iphone_app\" \\\n   -H \"x-zola-session-id: $DEVICE_SESSION_ID\" -H \"user-agent: $UA\")\nHJ=(\"${H[@]}\" -H 'content-type: application/json')"},{"language":"sh","snippet":"curl -sS \"${H[@]}\" \"$BASE/v3/users/me/context\" | jq '.data'"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: zola-api\ndescription: >-\n  Query or update Zola wedding-planning data (vendors, budget, guests, seating,\n  events/RSVPs, registry, gift tracker, inquiries, wedding website) straight\n  from a shell with curl against mobile-api.zola.com, instead of running the\n  zola-mcp server. Use when you want Zola data without the MCP, in a script,\n  or on a machine where the MCP isn't installed. Triggers on \"check Zola\",\n  \"Zola vendors/budget/guests/RSVP/seating/registry\", or any Zola wedding\n  data request that should hit the API directly.\n---\n\n# Zola mobile API via curl (no MCP)\n\nZola's mobile API (`mobile-api.zola.com` — the same surface the iOS/iPad app\nand `zola-mcp` use) is a plain Bearer-JWT REST API reachable directly from a\nserver or shell — no browser bridge needed. This skill shells out to `curl`\nwith the JWT in an `Authorization: Bearer` header, exactly as\n`zola-mcp`'s `src/client.ts` does.\n\n## One-time setup: get the refresh token\n\nYou need Zola's long-lived (~1 year) refresh JWT — the `usr` cookie from a\nsigned-in `zola.com` session. Same credential `zola-mcp` uses:\n\n```sh\n# Prefer the env var zola-mcp itself reads (check its .env first):\ngrep -h ZOLA_REFRESH_TOKEN ~/git/zola-mcp/.env 2>/dev/null\nexport ZOLA_REFRESH_TOKEN='eyJhbGciOi...'   # or export directly if you have it\n```\n\nIf you don't have it yet: sign into zola.com, open DevTools → Application →\nCookies → `https://www.zola.com`, copy the `usr` value. (`zola-mcp` also has a\nfetchproxy fallback that reads this cookie from a signed-in browser tab — see\nits README — but that's the MCP's path, not this skill's; this skill assumes\nyou already have the token in hand.)\n\n## Core call pattern\n\nEvery mobile-api call needs a short-lived (30 min) **session token**, minted\nfrom the refresh token, plus a fixed set of headers (CloudFront WAF requires\n`x-zola-session-id` on every request — omit it and you get a 403).\n\n```sh\nBASE=https://mobile-api.zola.com\nDEVICE_SESSION_ID=$(uuidgen | tr 'a-z' 'A-Z')   # one per \"session\"; reuse across calls\nUA='Zola/42.5.0 (iPad; iOS 26.4; Scale/2.0)'\n\n# 1. Mint a 30-min session token from the refresh token\nSESSION_TOKEN=$(curl -sS -X POST \"$BASE/v3/sessions/refresh\" \\\n  -H 'content-type: application/json' -H 'accept: application/json' \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" \\\n  -H \"user-agent: $UA\" \\\n  -d \"{\\\"token\\\":\\\"$ZOLA_REFRESH_TOKEN\\\"}\" | jq -r '.data.session_token')\n\n# 2. Every subsequent call reuses $SESSION_TOKEN until it expires (~30 min),\n#    then re-run step 1. All calls carry the same 4 headers:\ncurl -sS -X GET \"$BASE/v3/users/me/context\" \\\n  -H \"authorization: Bearer $SESSION_TOKEN\" \\\n  -H \"x-zola-platform-type: iphone_app\" -H \"x-zola-session-id: $DEVICE_SESSION_ID\" \\\n  -H \"user-agent: $UA\" | jq '.data'\n```\n\nWrap this in a shell function or export the 4 headers once — every recipe in\n`references/mobile-api-endpoints.md` reuses `$BASE`, `$SESSION_TOKEN`,\n`$DEVICE_SESSION_ID`, `$UA`. POST/PUT bodies "},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"zola-api\",\n  \"version\": \"2.1.10\",\n  \"publishedAt\": 1791588409449\n}"},{"path":"references/mobile-api-endpoints.md","content":"# Zola mobile-api endpoints (curl + jq)\n\nAll paths are relative to `$BASE=https://mobile-api.zola.com`. Every call\ncarries `authorization: Bearer $SESSION_TOKEN`, `x-zola-platform-type:\niphone_app`, `x-zola-session-id: $DEVICE_SESSION_ID`, `user-agent: $UA` (see\n`SKILL.md`); POST/PUT/DELETE-with-body also need `-H 'content-type:\napplication/json'`. `$ACCT` = `wedding_account_id`, `$REG` = `registry_id`,\n`$WEDDING_ID` = `wedding_id`, all three from `GET /v3/users/me/context` (see\n`SKILL.md`'s \"resolve context first\" section). Response envelope is `{\"data\": ...}`\nunless noted. Paths/bodies below are transcribed from `src/tools/*.ts` —\neach section names its source file.\n\nShorthand used below:\n\n```sh\nH=(-H \"authorization: Bearer $SESSION_TOKEN\" -H \"x-zola-platform-type: iphone_app\" \\\n   -H \"x-zola-session-id: $DEVICE_SESSION_ID\" -H \"user-agent: $UA\")\nHJ=(\"${H[@]}\" -H 'content-type: application/json')\n```\n\n---\n\n## Context (`src/client.ts`)\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/users/me/context\" | jq '.data'\n# .data.user.id, .data.wedding_account.wedding_account_id,\n# .data.wedding.{wedding_id,wedding_date,slug}, .data.registry.id\n```\n\n---\n\n## Vendors (`src/tools/vendors.ts`)\n\n**List booked vendors** — `POST /v3/account-vendors/booked-list` body `{}`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/account-vendors/booked-list\" -d '{}' \\\n  | jq '.data.booked_vendors'\n```\n\n**Search vendors (typeahead)** — `POST /v3/reference-vendors/typeahead-taxonomy`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/reference-vendors/typeahead-taxonomy\" \\\n  -d '{\"query\":\"Acme Photography\",\"taxonomy_key\":\"wedding-photographers\"}' | jq '.data'\n# taxonomy_key default: wedding-venues. Other keys: wedding-planners, wedding-bands-djs, ...\n```\n\n**Book a vendor** — find an unbooked slot from `booked-list` for the\n`vendor_type`, then `PUT /v5/account-vendors/vendor` (note: **v5**, not v3):\n\n```sh\ncurl -sS \"${HJ[@]}\" -X PUT \"$BASE/v5/account-vendors/vendor\" -d '{\n  \"uuid\": \"<slot-uuid-from-booked-list>\", \"id\": 0, \"vendor_type\": \"PHOTOGRAPHER\",\n  \"booked\": true, \"booking_source\": \"BOOKED_VENDORS\",\n  \"price_cents\": 350000, \"event_date\": null,\n  \"sync_with_budget_tool_enabled\": true, \"facet_keys\": [],\n  \"reference_vendor_request\": {\n    \"id\": null, \"name\": \"Acme Photography\", \"email\": null, \"phone\": null,\n    \"address\": {\"city\": \"Charlotte\", \"state_province_region\": \"NC\"}\n  }\n}' | jq '.data'\n```\n\n**Update a booked vendor** — same `PUT /v5/account-vendors/vendor`, but\nread-modify-write: GET `booked-list`, find by `uuid`, keep `id`/`vendor_type`,\nonly patch the fields you're changing (name/city/state/email/price/date\ndefault to the current value).\n\n**Unbook a vendor** — `POST /v3/account-vendors/vendor/unbook`:\n\n```sh\ncurl -sS \"${HJ[@]}\" -X POST \"$BASE/v3/account-vendors/vendor/unbook\" \\\n  -d '{\"uuid\":\"<vendor-uuid>\"}'\n```\n\n---\n\n## Budget (`src/tools/budget.ts`)\n\n**Get budget** — `GET /v3/budgets`:\n\n```sh\ncurl -sS \"${H[@]}\" \"$BASE/v3/budgets\" | jq '{\n  budgeted_cents: .data.budgeted_cents, c"},{"path":"skill-card.md","content":"## Description:\n\nGuides agents in querying and updating Zola wedding-planning data directly from a shell using authenticated API requests, without the MCP server.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nZola account holders and developers use this skill to inspect or manage wedding-planning records, including guests, budgets, events, RSVPs, registries, and websites, from a shell without an MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The long-lived Zola refresh token effectively grants account access and can leak through chats, logs, or shared terminals.\n\nMitigation: Keep tokens out of chats, logs, and committed files; handle them temporarily and rotate any exposed token.\n\nRisk: Write and delete examples can change live wedding data, and incomplete update bodies can erase unrelated fields.\n\nMitigation: Back up or export data, confirm each production change, and read the current record before sending a complete update body.\n\n## Reference(s):\n\n- [Zola API endpoint recipes](references/mobile-api-endpoints.md)\n- [ClawHub skill page](https://clawhub.ai/chrischall/skills/zola-api)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell command examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [API responses may contain private wedding-planning information.]\n\n## Skill Version(s):\n\n2.1.10 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":1364,"uniquenessScore":44,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T18:15:17.713Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-10T18:15:17.713Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T21:48:02.286Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}