zola-mcp 1.4.4 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bundle.js +128 -8
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/server.json +2 -2
- package/skills/zola-api/SKILL.md +120 -0
- package/skills/zola-api/references/mobile-api-endpoints.md +444 -0
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "Zola wedding planning tools for Claude Code",
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.5.0"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "Zola",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "Zola wedding planning tools for Claude — vendors, budget, guests, seating, events, registry, inquiries, and more via MCP",
|
|
18
|
-
"version": "1.
|
|
18
|
+
"version": "1.5.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Chall"
|
|
21
21
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zola",
|
|
3
3
|
"displayName": "Zola",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.5.0",
|
|
5
5
|
"description": "Zola wedding planning tools for Claude — vendors, budget, guests, seating, events, registry, inquiries, and more via MCP",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Chall",
|
package/dist/bundle.js
CHANGED
|
@@ -34960,6 +34960,7 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
34960
34960
|
"capture_request_header",
|
|
34961
34961
|
"capture_redirect",
|
|
34962
34962
|
"read_indexed_db",
|
|
34963
|
+
"read_dom",
|
|
34963
34964
|
"download"
|
|
34964
34965
|
]);
|
|
34965
34966
|
|
|
@@ -35261,6 +35262,44 @@ function assertIndexedDbScopesArray(value, label) {
|
|
|
35261
35262
|
}
|
|
35262
35263
|
}
|
|
35263
35264
|
}
|
|
35265
|
+
var DOM_SELECTOR_RE = /^[^-]{1,512}$/;
|
|
35266
|
+
var DOM_ATTRIBUTE_RE = /^[A-Za-z_:][A-Za-z0-9_:.\-]{0,127}$/;
|
|
35267
|
+
function assertDomSelectorsArray(value, label) {
|
|
35268
|
+
if (!Array.isArray(value)) {
|
|
35269
|
+
throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
|
|
35270
|
+
}
|
|
35271
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35272
|
+
for (let i = 0; i < value.length; i++) {
|
|
35273
|
+
const entry = value[i];
|
|
35274
|
+
assertObject(entry, `${label}[${i}]`);
|
|
35275
|
+
if (entry.name === void 0) {
|
|
35276
|
+
throw new ProtocolError(`${label}[${i}].name: missing`);
|
|
35277
|
+
}
|
|
35278
|
+
if (entry.selector === void 0) {
|
|
35279
|
+
throw new ProtocolError(`${label}[${i}].selector: missing`);
|
|
35280
|
+
}
|
|
35281
|
+
if (typeof entry.name !== "string" || !SCOPE_KEY_RE.test(entry.name)) {
|
|
35282
|
+
throw new ProtocolError(`${label}[${i}].name: invalid ${JSON.stringify(entry.name)}`);
|
|
35283
|
+
}
|
|
35284
|
+
if (typeof entry.selector !== "string" || !DOM_SELECTOR_RE.test(entry.selector)) {
|
|
35285
|
+
throw new ProtocolError(`${label}[${i}].selector: invalid ${JSON.stringify(entry.selector)}`);
|
|
35286
|
+
}
|
|
35287
|
+
if (entry.attribute !== void 0) {
|
|
35288
|
+
if (typeof entry.attribute !== "string" || !DOM_ATTRIBUTE_RE.test(entry.attribute)) {
|
|
35289
|
+
throw new ProtocolError(`${label}[${i}].attribute: invalid ${JSON.stringify(entry.attribute)}`);
|
|
35290
|
+
}
|
|
35291
|
+
}
|
|
35292
|
+
if (seen.has(entry.name)) {
|
|
35293
|
+
throw new ProtocolError(`${label}: duplicate name ${JSON.stringify(entry.name)}`);
|
|
35294
|
+
}
|
|
35295
|
+
seen.add(entry.name);
|
|
35296
|
+
for (const k of Object.keys(entry)) {
|
|
35297
|
+
if (k !== "name" && k !== "selector" && k !== "attribute") {
|
|
35298
|
+
throw new ProtocolError(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
|
|
35299
|
+
}
|
|
35300
|
+
}
|
|
35301
|
+
}
|
|
35302
|
+
}
|
|
35264
35303
|
function validateFrame(raw) {
|
|
35265
35304
|
assertObject(raw, "frame");
|
|
35266
35305
|
const t = raw.type;
|
|
@@ -35336,6 +35375,9 @@ function validateHello(raw) {
|
|
|
35336
35375
|
if (raw.sessionStoragePointers !== void 0) {
|
|
35337
35376
|
assertStoragePointersArray(raw.sessionStoragePointers, "hello.sessionStoragePointers", raw.sessionStorageKeys);
|
|
35338
35377
|
}
|
|
35378
|
+
if (raw.domSelectors !== void 0) {
|
|
35379
|
+
assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
|
|
35380
|
+
}
|
|
35339
35381
|
assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
|
|
35340
35382
|
assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
|
|
35341
35383
|
assertBase64(raw.sessionNonce, "hello.sessionNonce");
|
|
@@ -35570,6 +35612,21 @@ function validateInnerRequest(raw) {
|
|
|
35570
35612
|
}
|
|
35571
35613
|
return raw;
|
|
35572
35614
|
}
|
|
35615
|
+
if (raw.op === "read_dom") {
|
|
35616
|
+
assertObject(raw.init, "inner.init");
|
|
35617
|
+
if (raw.init.origin === void 0)
|
|
35618
|
+
throw new ProtocolError("inner.init.origin: missing");
|
|
35619
|
+
if (raw.init.names === void 0)
|
|
35620
|
+
throw new ProtocolError("inner.init.names: missing");
|
|
35621
|
+
assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
|
|
35622
|
+
assertNonEmptyKeyArray(raw.init.names, "inner.init.names");
|
|
35623
|
+
for (const k of Object.keys(raw.init)) {
|
|
35624
|
+
if (k !== "origin" && k !== "names") {
|
|
35625
|
+
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on read_dom`);
|
|
35626
|
+
}
|
|
35627
|
+
}
|
|
35628
|
+
return raw;
|
|
35629
|
+
}
|
|
35573
35630
|
if (raw.op === "download") {
|
|
35574
35631
|
assertObject(raw.init, "inner.init");
|
|
35575
35632
|
if (raw.init.url === void 0) {
|
|
@@ -35595,7 +35652,7 @@ function validateInnerRequest(raw) {
|
|
|
35595
35652
|
}
|
|
35596
35653
|
return raw;
|
|
35597
35654
|
}
|
|
35598
|
-
throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "download"; got ${JSON.stringify(raw.op)}`);
|
|
35655
|
+
throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "read_dom", "download"; got ${JSON.stringify(raw.op)}`);
|
|
35599
35656
|
}
|
|
35600
35657
|
function assertNonEmptyKeyArray(value, label) {
|
|
35601
35658
|
if (!Array.isArray(value)) {
|
|
@@ -35680,6 +35737,13 @@ function validateInnerResponse(raw) {
|
|
|
35680
35737
|
assertObject(raw.values, "inner.values");
|
|
35681
35738
|
return raw;
|
|
35682
35739
|
}
|
|
35740
|
+
if (op === "read_dom") {
|
|
35741
|
+
if (raw.values === void 0) {
|
|
35742
|
+
throw new ProtocolError("inner.values: missing on read_dom response");
|
|
35743
|
+
}
|
|
35744
|
+
assertStringMap(raw.values, "inner.values");
|
|
35745
|
+
return raw;
|
|
35746
|
+
}
|
|
35683
35747
|
if (op === "download") {
|
|
35684
35748
|
assertObject(raw.value, "inner.value");
|
|
35685
35749
|
assertString(raw.value.path, "inner.value.path");
|
|
@@ -36019,6 +36083,13 @@ async function buildServerHello(opts) {
|
|
|
36019
36083
|
jsonPointer: d.jsonPointer
|
|
36020
36084
|
}));
|
|
36021
36085
|
}
|
|
36086
|
+
if (opts.domSelectors && opts.domSelectors.length > 0) {
|
|
36087
|
+
hello.domSelectors = opts.domSelectors.map((d) => ({
|
|
36088
|
+
name: d.name,
|
|
36089
|
+
selector: d.selector,
|
|
36090
|
+
...d.attribute !== void 0 ? { attribute: d.attribute } : {}
|
|
36091
|
+
}));
|
|
36092
|
+
}
|
|
36022
36093
|
return hello;
|
|
36023
36094
|
}
|
|
36024
36095
|
|
|
@@ -36108,7 +36179,8 @@ async function startHost(opts) {
|
|
|
36108
36179
|
captureHeaders: opts.ownCaptureHeaders,
|
|
36109
36180
|
indexedDbScopes: opts.ownIndexedDbScopes,
|
|
36110
36181
|
localStoragePointers: opts.ownLocalStoragePointers,
|
|
36111
|
-
sessionStoragePointers: opts.ownSessionStoragePointers
|
|
36182
|
+
sessionStoragePointers: opts.ownSessionStoragePointers,
|
|
36183
|
+
domSelectors: opts.ownDomSelectors
|
|
36112
36184
|
});
|
|
36113
36185
|
const ownSessionNonce = fromB64(ownHello.sessionNonce);
|
|
36114
36186
|
let extensionWs = null;
|
|
@@ -36343,6 +36415,7 @@ async function startPeer(opts) {
|
|
|
36343
36415
|
sessionStorageKeys: opts.sessionStorageKeys,
|
|
36344
36416
|
captureHeaders: opts.captureHeaders,
|
|
36345
36417
|
indexedDbScopes: opts.indexedDbScopes,
|
|
36418
|
+
domSelectors: opts.domSelectors,
|
|
36346
36419
|
localStoragePointers: opts.localStoragePointers,
|
|
36347
36420
|
sessionStoragePointers: opts.sessionStoragePointers
|
|
36348
36421
|
});
|
|
@@ -36750,6 +36823,11 @@ var FetchproxyServer = class {
|
|
|
36750
36823
|
key: d.key,
|
|
36751
36824
|
jsonPointer: d.jsonPointer
|
|
36752
36825
|
})),
|
|
36826
|
+
domSelectors: (opts.domSelectors ?? []).map((d) => ({
|
|
36827
|
+
name: d.name,
|
|
36828
|
+
selector: d.selector,
|
|
36829
|
+
...d.attribute !== void 0 ? { attribute: d.attribute } : {}
|
|
36830
|
+
})),
|
|
36753
36831
|
// 0.8.0+: timer + lazy-revive default to ON. Every realty MCP
|
|
36754
36832
|
// adapter was about to set these to the same numbers anyway; the
|
|
36755
36833
|
// back-door is `0` (explicit opt-out) if a caller genuinely wants
|
|
@@ -36870,6 +36948,7 @@ var FetchproxyServer = class {
|
|
|
36870
36948
|
ownIndexedDbScopes: this.opts.indexedDbScopes,
|
|
36871
36949
|
ownLocalStoragePointers: this.opts.localStoragePointers,
|
|
36872
36950
|
ownSessionStoragePointers: this.opts.sessionStoragePointers,
|
|
36951
|
+
ownDomSelectors: this.opts.domSelectors,
|
|
36873
36952
|
onPairCode: this.opts.onPairCode
|
|
36874
36953
|
});
|
|
36875
36954
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
@@ -36897,7 +36976,8 @@ var FetchproxyServer = class {
|
|
|
36897
36976
|
captureHeaders: this.opts.captureHeaders,
|
|
36898
36977
|
indexedDbScopes: this.opts.indexedDbScopes,
|
|
36899
36978
|
localStoragePointers: this.opts.localStoragePointers,
|
|
36900
|
-
sessionStoragePointers: this.opts.sessionStoragePointers
|
|
36979
|
+
sessionStoragePointers: this.opts.sessionStoragePointers,
|
|
36980
|
+
domSelectors: this.opts.domSelectors
|
|
36901
36981
|
});
|
|
36902
36982
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
36903
36983
|
this.peerHandle.onRenegotiate(() => {
|
|
@@ -37872,6 +37952,46 @@ var FetchproxyServer = class {
|
|
|
37872
37952
|
await this.sendInnerFrame(inner);
|
|
37873
37953
|
return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
|
|
37874
37954
|
}
|
|
37955
|
+
/**
|
|
37956
|
+
* 1.4.0+: read declared DOM values from the user's signed-in tab.
|
|
37957
|
+
* Requires `'read_dom'` in capabilities AND every requested `name` to
|
|
37958
|
+
* match a declared `domSelectors` entry. The extension reads each
|
|
37959
|
+
* declared selector from the matched tab's DOM (isolated-world
|
|
37960
|
+
* `querySelector`, value or attribute) — no page-JS execution.
|
|
37961
|
+
*
|
|
37962
|
+
* Returns a `Record<string, string>` of `name → value`, with names
|
|
37963
|
+
* whose element (or attribute) was absent omitted. Throws
|
|
37964
|
+
* `FetchproxyProtocolError` on bridge failures and a plain `Error` on
|
|
37965
|
+
* developer mistakes (undeclared capability, undeclared name).
|
|
37966
|
+
*/
|
|
37967
|
+
async readDom(opts) {
|
|
37968
|
+
if (!this.opts.capabilities.includes("read_dom")) {
|
|
37969
|
+
throw new Error('FetchproxyServer.readDom(): MCP did not declare "read_dom" in capabilities');
|
|
37970
|
+
}
|
|
37971
|
+
await this.ensureConnected();
|
|
37972
|
+
this.throwIfPendingPair();
|
|
37973
|
+
if (!Array.isArray(opts.names) || opts.names.length === 0) {
|
|
37974
|
+
throw new Error("FetchproxyServer.readDom: opts.names must be a non-empty array");
|
|
37975
|
+
}
|
|
37976
|
+
this.assertScopeSubset(opts.names, this.opts.domSelectors.map((d) => d.name), "domSelectors");
|
|
37977
|
+
if (opts.subdomain !== void 0)
|
|
37978
|
+
assertSubdomainLabel(opts.subdomain);
|
|
37979
|
+
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
37980
|
+
const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
|
|
37981
|
+
const origin = `https://${host}`;
|
|
37982
|
+
const id = this.nextRequestId++;
|
|
37983
|
+
const inner = {
|
|
37984
|
+
type: "request",
|
|
37985
|
+
id,
|
|
37986
|
+
op: "read_dom",
|
|
37987
|
+
init: { origin, names: [...opts.names] }
|
|
37988
|
+
};
|
|
37989
|
+
const pending = new Promise((resolve, reject) => {
|
|
37990
|
+
this.pendingStorage.set(id, { resolve, reject });
|
|
37991
|
+
});
|
|
37992
|
+
await this.sendInnerFrame(inner);
|
|
37993
|
+
return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
|
|
37994
|
+
}
|
|
37875
37995
|
assertScopeSubset(requested, declared, label) {
|
|
37876
37996
|
const undeclared = undeclaredKeys(requested, declared);
|
|
37877
37997
|
if (undeclared.length > 0) {
|
|
@@ -37943,7 +38063,7 @@ var FetchproxyServer = class {
|
|
|
37943
38063
|
if (storageCb) {
|
|
37944
38064
|
this.pendingStorage.delete(inner.id);
|
|
37945
38065
|
if (inner.ok) {
|
|
37946
|
-
if ((inner.op === "read_local_storage" || inner.op === "read_session_storage") && inner.values) {
|
|
38066
|
+
if ((inner.op === "read_local_storage" || inner.op === "read_session_storage" || inner.op === "read_dom") && inner.values) {
|
|
37947
38067
|
storageCb.resolve({ ...inner.values });
|
|
37948
38068
|
} else {
|
|
37949
38069
|
storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
|
|
@@ -38268,7 +38388,7 @@ var BootstrapDisabledError = class extends Error {
|
|
|
38268
38388
|
// package.json
|
|
38269
38389
|
var package_default = {
|
|
38270
38390
|
name: "zola-mcp",
|
|
38271
|
-
version: "1.
|
|
38391
|
+
version: "1.5.0",
|
|
38272
38392
|
mcpName: "io.github.chrischall/zola-mcp",
|
|
38273
38393
|
description: "Zola wedding MCP server for Claude",
|
|
38274
38394
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -38314,7 +38434,7 @@ var package_default = {
|
|
|
38314
38434
|
"test:watch": "vitest"
|
|
38315
38435
|
},
|
|
38316
38436
|
dependencies: {
|
|
38317
|
-
"@chrischall/mcp-utils": "^0.
|
|
38437
|
+
"@chrischall/mcp-utils": "^0.13.0",
|
|
38318
38438
|
"@fetchproxy/bootstrap": "^1.3.0",
|
|
38319
38439
|
"@fetchproxy/server": "^1.3.0",
|
|
38320
38440
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -38325,7 +38445,7 @@ var package_default = {
|
|
|
38325
38445
|
"@types/node": "^26.0.0",
|
|
38326
38446
|
"@vitest/coverage-v8": "^4.1.8",
|
|
38327
38447
|
esbuild: "^0.28.0",
|
|
38328
|
-
typescript: "^
|
|
38448
|
+
typescript: "^7.0.2",
|
|
38329
38449
|
vitest: "^4.1.8"
|
|
38330
38450
|
}
|
|
38331
38451
|
};
|
|
@@ -40495,7 +40615,7 @@ function registerEventInvitationTools(server) {
|
|
|
40495
40615
|
}
|
|
40496
40616
|
|
|
40497
40617
|
// src/index.ts
|
|
40498
|
-
var VERSION = "1.
|
|
40618
|
+
var VERSION = "1.5.0";
|
|
40499
40619
|
await runMcp({
|
|
40500
40620
|
name: "zola-mcp",
|
|
40501
40621
|
version: VERSION,
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { registerWebsiteThemeTools } from './tools/website-theme.js';
|
|
|
12
12
|
import { registerRegistryItemTools } from './tools/registry-items.js';
|
|
13
13
|
import { registerInvitationTools } from './tools/invitations.js';
|
|
14
14
|
import { registerEventInvitationTools } from './tools/event-invitations.js';
|
|
15
|
-
const VERSION = '1.
|
|
15
|
+
const VERSION = '1.5.0'; // x-release-please-version
|
|
16
16
|
await runMcp({
|
|
17
17
|
name: 'zola-mcp',
|
|
18
18
|
version: VERSION,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zola-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/zola-mcp",
|
|
5
5
|
"description": "Zola wedding MCP server for Claude",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"test:watch": "vitest"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@chrischall/mcp-utils": "^0.
|
|
49
|
+
"@chrischall/mcp-utils": "^0.13.0",
|
|
50
50
|
"@fetchproxy/bootstrap": "^1.3.0",
|
|
51
51
|
"@fetchproxy/server": "^1.3.0",
|
|
52
52
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"@types/node": "^26.0.0",
|
|
58
58
|
"@vitest/coverage-v8": "^4.1.8",
|
|
59
59
|
"esbuild": "^0.28.0",
|
|
60
|
-
"typescript": "^
|
|
60
|
+
"typescript": "^7.0.2",
|
|
61
61
|
"vitest": "^4.1.8"
|
|
62
62
|
}
|
|
63
63
|
}
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/zola-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.5.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "zola-mcp",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.5.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zola-api
|
|
3
|
+
description: >-
|
|
4
|
+
Query or update Zola wedding-planning data (vendors, budget, guests, seating,
|
|
5
|
+
events/RSVPs, registry, gift tracker, inquiries, wedding website) straight
|
|
6
|
+
from a shell with curl against mobile-api.zola.com, instead of running the
|
|
7
|
+
zola-mcp server. Use when you want Zola data without the MCP, in a script,
|
|
8
|
+
or on a machine where the MCP isn't installed. Triggers on "check Zola",
|
|
9
|
+
"Zola vendors/budget/guests/RSVP/seating/registry", or any Zola wedding
|
|
10
|
+
data request that should hit the API directly.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Zola mobile API via curl (no MCP)
|
|
14
|
+
|
|
15
|
+
Zola's mobile API (`mobile-api.zola.com` — the same surface the iOS/iPad app
|
|
16
|
+
and `zola-mcp` use) is a plain Bearer-JWT REST API reachable directly from a
|
|
17
|
+
server or shell — no browser bridge needed. This skill shells out to `curl`
|
|
18
|
+
with the JWT in an `Authorization: Bearer` header, exactly as
|
|
19
|
+
`zola-mcp`'s `src/client.ts` does.
|
|
20
|
+
|
|
21
|
+
## One-time setup: get the refresh token
|
|
22
|
+
|
|
23
|
+
You need Zola's long-lived (~1 year) refresh JWT — the `usr` cookie from a
|
|
24
|
+
signed-in `zola.com` session. Same credential `zola-mcp` uses:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
# Prefer the env var zola-mcp itself reads (check its .env first):
|
|
28
|
+
grep -h ZOLA_REFRESH_TOKEN ~/git/zola-mcp/.env 2>/dev/null
|
|
29
|
+
export ZOLA_REFRESH_TOKEN='eyJhbGciOi...' # or export directly if you have it
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
If you don't have it yet: sign into zola.com, open DevTools → Application →
|
|
33
|
+
Cookies → `https://www.zola.com`, copy the `usr` value. (`zola-mcp` also has a
|
|
34
|
+
fetchproxy fallback that reads this cookie from a signed-in browser tab — see
|
|
35
|
+
its README — but that's the MCP's path, not this skill's; this skill assumes
|
|
36
|
+
you already have the token in hand.)
|
|
37
|
+
|
|
38
|
+
## Core call pattern
|
|
39
|
+
|
|
40
|
+
Every mobile-api call needs a short-lived (30 min) **session token**, minted
|
|
41
|
+
from the refresh token, plus a fixed set of headers (CloudFront WAF requires
|
|
42
|
+
`x-zola-session-id` on every request — omit it and you get a 403).
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
BASE=https://mobile-api.zola.com
|
|
46
|
+
DEVICE_SESSION_ID=$(uuidgen | tr 'a-z' 'A-Z') # one per "session"; reuse across calls
|
|
47
|
+
UA='Zola/42.5.0 (iPad; iOS 26.4; Scale/2.0)'
|
|
48
|
+
|
|
49
|
+
# 1. Mint a 30-min session token from the refresh token
|
|
50
|
+
SESSION_TOKEN=$(curl -sS -X POST "$BASE/v3/sessions/refresh" \
|
|
51
|
+
-H 'content-type: application/json' -H 'accept: application/json' \
|
|
52
|
+
-H "x-zola-platform-type: iphone_app" -H "x-zola-session-id: $DEVICE_SESSION_ID" \
|
|
53
|
+
-H "user-agent: $UA" \
|
|
54
|
+
-d "{\"token\":\"$ZOLA_REFRESH_TOKEN\"}" | jq -r '.data.session_token')
|
|
55
|
+
|
|
56
|
+
# 2. Every subsequent call reuses $SESSION_TOKEN until it expires (~30 min),
|
|
57
|
+
# then re-run step 1. All calls carry the same 4 headers:
|
|
58
|
+
curl -sS -X GET "$BASE/v3/users/me/context" \
|
|
59
|
+
-H "authorization: Bearer $SESSION_TOKEN" \
|
|
60
|
+
-H "x-zola-platform-type: iphone_app" -H "x-zola-session-id: $DEVICE_SESSION_ID" \
|
|
61
|
+
-H "user-agent: $UA" | jq '.data'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Wrap this in a shell function or export the 4 headers once — every recipe in
|
|
65
|
+
`references/mobile-api-endpoints.md` reuses `$BASE`, `$SESSION_TOKEN`,
|
|
66
|
+
`$DEVICE_SESSION_ID`, `$UA`. POST/PUT bodies additionally need
|
|
67
|
+
`-H 'content-type: application/json'`.
|
|
68
|
+
|
|
69
|
+
(The app also sends a fifth header, `x-zola-user-session-id`, derived from a
|
|
70
|
+
claim inside the session JWT — `zola-mcp` only attaches it when the decode
|
|
71
|
+
succeeds, so it's not load-bearing; every endpoint below works without it.)
|
|
72
|
+
|
|
73
|
+
## The one rule: resolve context first
|
|
74
|
+
|
|
75
|
+
Most write/list endpoints are scoped by numeric IDs, never inferred — call
|
|
76
|
+
`GET /v3/users/me/context` once per session and keep the three IDs around:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
CTX=$(curl -sS "$BASE/v3/users/me/context" -H "authorization: Bearer $SESSION_TOKEN" \
|
|
80
|
+
-H "x-zola-platform-type: iphone_app" -H "x-zola-session-id: $DEVICE_SESSION_ID" -H "user-agent: $UA")
|
|
81
|
+
ACCT=$(jq -r '.data.wedding_account.wedding_account_id' <<<"$CTX")
|
|
82
|
+
REG=$(jq -r '.data.registry.id' <<<"$CTX")
|
|
83
|
+
WEDDING_ID=$(jq -r '.data.wedding.wedding_id' <<<"$CTX")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`ACCT` (`wedding_account_id`) feeds guest/event/website-content paths; `REG`
|
|
87
|
+
(`registry_id`) feeds registry/gift-tracker paths.
|
|
88
|
+
|
|
89
|
+
## Ready-to-run endpoints
|
|
90
|
+
|
|
91
|
+
`references/mobile-api-endpoints.md` has real, ready-to-run `curl` + `jq`
|
|
92
|
+
recipes for every one of the 30 `zola-mcp` tools' underlying calls
|
|
93
|
+
(vendors, budget, guests, seating, inquiries, events/RSVPs/gifts/registry,
|
|
94
|
+
discover/storefronts, registry items, invitations/card-projects, website
|
|
95
|
+
pages, website theme/customization, website content). Transcribed straight
|
|
96
|
+
from `src/tools/*.ts` — same paths, same request bodies.
|
|
97
|
+
|
|
98
|
+
## Mutation gotcha: most writes are read-modify-write
|
|
99
|
+
|
|
100
|
+
Zola's write endpoints replace whole objects, not deltas — the same tools
|
|
101
|
+
that build a request first re-GET the current record and only patch the
|
|
102
|
+
requested fields, or a full-state write can wipe unrelated fields.
|
|
103
|
+
`docs/zola-api-quirks.md` in this repo documents the worst offenders (a
|
|
104
|
+
`header_font` write nulling every other website color; guest writes needing
|
|
105
|
+
to preserve `event_invitations` verbatim or lose them). Follow the
|
|
106
|
+
read-modify-write shape shown per-endpoint in the references file — don't
|
|
107
|
+
send a bare partial body to `PUT`/`POST` write endpoints.
|
|
108
|
+
|
|
109
|
+
## Output / error contract
|
|
110
|
+
|
|
111
|
+
- A 2xx body is `{"data": ...}` for nearly every endpoint — pipe to
|
|
112
|
+
`jq '.data'`.
|
|
113
|
+
- `401` — session token expired; re-mint via `POST /v3/sessions/refresh`
|
|
114
|
+
(step 1 above) and retry once.
|
|
115
|
+
- `429` — rate limited; back off ~2s and retry once.
|
|
116
|
+
- Any other non-2xx — the body is a JSON error envelope; read it directly,
|
|
117
|
+
it is not redacted here (unlike the MCP, which redacts secrets from error
|
|
118
|
+
text — don't paste raw error bodies containing the session token anywhere
|
|
119
|
+
public).
|
|
120
|
+
- This project (`zola-mcp`) is developed and maintained by AI (Claude).
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
# Zola mobile-api endpoints (curl + jq)
|
|
2
|
+
|
|
3
|
+
All paths are relative to `$BASE=https://mobile-api.zola.com`. Every call
|
|
4
|
+
carries `authorization: Bearer $SESSION_TOKEN`, `x-zola-platform-type:
|
|
5
|
+
iphone_app`, `x-zola-session-id: $DEVICE_SESSION_ID`, `user-agent: $UA` (see
|
|
6
|
+
`SKILL.md`); POST/PUT/DELETE-with-body also need `-H 'content-type:
|
|
7
|
+
application/json'`. `$ACCT` = `wedding_account_id`, `$REG` = `registry_id`,
|
|
8
|
+
`$WEDDING_ID` = `wedding_id`, all three from `GET /v3/users/me/context` (see
|
|
9
|
+
`SKILL.md`'s "resolve context first" section). Response envelope is `{"data": ...}`
|
|
10
|
+
unless noted. Paths/bodies below are transcribed from `src/tools/*.ts` —
|
|
11
|
+
each section names its source file.
|
|
12
|
+
|
|
13
|
+
Shorthand used below:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
H=(-H "authorization: Bearer $SESSION_TOKEN" -H "x-zola-platform-type: iphone_app" \
|
|
17
|
+
-H "x-zola-session-id: $DEVICE_SESSION_ID" -H "user-agent: $UA")
|
|
18
|
+
HJ=("${H[@]}" -H 'content-type: application/json')
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Context (`src/client.ts`)
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
curl -sS "${H[@]}" "$BASE/v3/users/me/context" | jq '.data'
|
|
27
|
+
# .data.user.id, .data.wedding_account.wedding_account_id,
|
|
28
|
+
# .data.wedding.{wedding_id,wedding_date,slug}, .data.registry.id
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Vendors (`src/tools/vendors.ts`)
|
|
34
|
+
|
|
35
|
+
**List booked vendors** — `POST /v3/account-vendors/booked-list` body `{}`:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
curl -sS "${HJ[@]}" -X POST "$BASE/v3/account-vendors/booked-list" -d '{}' \
|
|
39
|
+
| jq '.data.booked_vendors'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Search vendors (typeahead)** — `POST /v3/reference-vendors/typeahead-taxonomy`:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
curl -sS "${HJ[@]}" -X POST "$BASE/v3/reference-vendors/typeahead-taxonomy" \
|
|
46
|
+
-d '{"query":"Acme Photography","taxonomy_key":"wedding-photographers"}' | jq '.data'
|
|
47
|
+
# taxonomy_key default: wedding-venues. Other keys: wedding-planners, wedding-bands-djs, ...
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Book a vendor** — find an unbooked slot from `booked-list` for the
|
|
51
|
+
`vendor_type`, then `PUT /v5/account-vendors/vendor` (note: **v5**, not v3):
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
curl -sS "${HJ[@]}" -X PUT "$BASE/v5/account-vendors/vendor" -d '{
|
|
55
|
+
"uuid": "<slot-uuid-from-booked-list>", "id": 0, "vendor_type": "PHOTOGRAPHER",
|
|
56
|
+
"booked": true, "booking_source": "BOOKED_VENDORS",
|
|
57
|
+
"price_cents": 350000, "event_date": null,
|
|
58
|
+
"sync_with_budget_tool_enabled": true, "facet_keys": [],
|
|
59
|
+
"reference_vendor_request": {
|
|
60
|
+
"id": null, "name": "Acme Photography", "email": null, "phone": null,
|
|
61
|
+
"address": {"city": "Charlotte", "state_province_region": "NC"}
|
|
62
|
+
}
|
|
63
|
+
}' | jq '.data'
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Update a booked vendor** — same `PUT /v5/account-vendors/vendor`, but
|
|
67
|
+
read-modify-write: GET `booked-list`, find by `uuid`, keep `id`/`vendor_type`,
|
|
68
|
+
only patch the fields you're changing (name/city/state/email/price/date
|
|
69
|
+
default to the current value).
|
|
70
|
+
|
|
71
|
+
**Unbook a vendor** — `POST /v3/account-vendors/vendor/unbook`:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
curl -sS "${HJ[@]}" -X POST "$BASE/v3/account-vendors/vendor/unbook" \
|
|
75
|
+
-d '{"uuid":"<vendor-uuid>"}'
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Budget (`src/tools/budget.ts`)
|
|
81
|
+
|
|
82
|
+
**Get budget** — `GET /v3/budgets`:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
curl -sS "${H[@]}" "$BASE/v3/budgets" | jq '{
|
|
86
|
+
budgeted_cents: .data.budgeted_cents, cost_cents: .data.cost_cents,
|
|
87
|
+
paid_cents: .data.paid_cents, balance_due_cents: .data.balance_due_cents,
|
|
88
|
+
items: [.data.taxonomy_nodes[].items[] | {uuid, title, cost_cents, paid_cents}]
|
|
89
|
+
}'
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Update a budget item** — read-modify-write: `GET /v3/budgets`, find the item
|
|
93
|
+
by `uuid`, then `PUT /v3/budgets/items` with every required field (server
|
|
94
|
+
rejects a bare partial):
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
curl -sS "${HJ[@]}" -X PUT "$BASE/v3/budgets/items" -d '{
|
|
98
|
+
"item_uuid": "<uuid>", "taxonomy_node_uuid": "<from-get>",
|
|
99
|
+
"estimated_cost_cents": <from-get>, "actual_cost_cents": 250000,
|
|
100
|
+
"note": "Deposit paid", "item_type": "<from-get, e.g. VENUE>",
|
|
101
|
+
"title": "<from-get>"
|
|
102
|
+
}' | jq '.data'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Guests (`src/tools/guests.ts`)
|
|
108
|
+
|
|
109
|
+
**List guests** — `POST /v3/guestlists/directory/wedding-accounts/$ACCT` body
|
|
110
|
+
`{"sort_by_name_asc": true}`. Guest shape is **flat** (fields directly on the
|
|
111
|
+
guest object, no `{guest:{...}}` wrapper):
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
curl -sS "${HJ[@]}" -X POST "$BASE/v3/guestlists/directory/wedding-accounts/$ACCT" \
|
|
115
|
+
-d '{"sort_by_name_asc": true}' \
|
|
116
|
+
| jq '.data.guest_groups[] | {guest_group_id, guests: [.guests[] | {guest_id, first_name, family_name, rsvp}]}'
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Add a guest group** — `POST /v3/guestlists/groups`:
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
curl -sS "${HJ[@]}" -X POST "$BASE/v3/guestlists/groups" -d '{
|
|
123
|
+
"wedding_account_id": '"$ACCT"',
|
|
124
|
+
"guests": [{
|
|
125
|
+
"first_name": "Mike", "family_name": "Smith", "relationship_type": "PRIMARY",
|
|
126
|
+
"source": "IOS", "email_address": "", "mobile_phone": "", "affiliation": "PRIMARY_FRIEND",
|
|
127
|
+
"tier": "A", "country_code": "US", "prefix": "", "middle_name": "", "suffix": "",
|
|
128
|
+
"home_phone": "", "address1": "", "address2": "", "city": "", "state_province": "",
|
|
129
|
+
"postal_code": "", "event_invitations": [], "tags": []
|
|
130
|
+
}],
|
|
131
|
+
"guest_group_affiliation": "PRIMARY_FRIEND", "guest_group_tier": "A",
|
|
132
|
+
"guest_group_uuid": "", "envelope_recipient": "", "invited": true,
|
|
133
|
+
"invitation_sent": false, "save_the_date_sent": false,
|
|
134
|
+
"rsvp_question_answers": [], "gift_count": 0
|
|
135
|
+
}' | jq '.data'
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Update a guest group's address** — read-modify-write via
|
|
139
|
+
`PUT /v3/guestlists/groups/wedding-accounts/$ACCT/bulk/directory`. **Must**
|
|
140
|
+
preserve each guest's existing `event_invitations` verbatim (a group that
|
|
141
|
+
sends `event_invitations: []` wipes them, per `docs/zola-api-quirks.md` §5):
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
# 1. GET the directory (above), find the target group by guest_group_id,
|
|
145
|
+
# keep its `guests[]` array as-is except the address fields you're changing.
|
|
146
|
+
curl -sS "${HJ[@]}" -X PUT "$BASE/v3/guestlists/groups/wedding-accounts/$ACCT/bulk/directory" \
|
|
147
|
+
-d '{"updated_guest_groups": [ <full-group-object-with-patched-guest-addresses> ]}' \
|
|
148
|
+
| jq '.data'
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Remove a guest group** — `PUT /v3/guestlists/groups/wedding-accounts/$ACCT/delete`:
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
curl -sS "${HJ[@]}" -X PUT "$BASE/v3/guestlists/groups/wedding-accounts/$ACCT/delete" \
|
|
155
|
+
-d '{"wedding_account_id": '"$ACCT"', "guest_group_ids": [<id>]}'
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Seating (`src/tools/seating.ts`)
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
curl -sS "${H[@]}" "$BASE/v3/seating-charts/summaries" | jq '.' # NOT wrapped in `data`
|
|
164
|
+
curl -sS "${H[@]}" "$BASE/v3/seating-charts/<chart-uuid>" | jq '.'
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Unseated guests** — `POST /v3/guestlists/directory/wedding-accounts/$ACCT`
|
|
168
|
+
(same call as guests list), filter client-side for
|
|
169
|
+
`.guests[].seating_chart_seat == null`.
|
|
170
|
+
|
|
171
|
+
**Assign a seat** — `PUT /v3/seating-charts/seats`:
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
curl -sS "${HJ[@]}" -X PUT "$BASE/v3/seating-charts/seats" -d '{
|
|
175
|
+
"guest_uuid": "<guest-uuid>", "seat_uuid": "<seat-uuid>",
|
|
176
|
+
"table_uuid": "<table-uuid>", "seating_chart_uuid": "<chart-uuid>"
|
|
177
|
+
}' | jq '.'
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Inquiries (`src/tools/inquiries.ts`)
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
curl -sS "${HJ[@]}" -X POST "$BASE/v3/inquiries/unified-inquiries" -d '{}' \
|
|
186
|
+
| jq '.data[].inquiry_summaries[] | {inquiry_uuid, vendor_name: .vendor_card.vendor_name, unread, status_text}'
|
|
187
|
+
|
|
188
|
+
curl -sS "${H[@]}" "$BASE/v3/inquiries/<inquiry-uuid>/conversation" | jq '.data.messages'
|
|
189
|
+
|
|
190
|
+
curl -sS "${H[@]}" -X PUT "$BASE/v3/inquiries/<inquiry-uuid>/conversation/read"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Events, RSVPs, gifts, registry (`src/tools/events.ts`)
|
|
196
|
+
|
|
197
|
+
```sh
|
|
198
|
+
curl -sS "${H[@]}" "$BASE/v3/websites/events/wedding-accounts/$ACCT/groups" \
|
|
199
|
+
| jq '.data[].events[]' # event_entity_id, name, start_at, num_guests_*
|
|
200
|
+
|
|
201
|
+
curl -sS "${H[@]}" "$BASE/v3/websites/events/track-rsvps" | jq '.data.modules'
|
|
202
|
+
|
|
203
|
+
curl -sS "${H[@]}" "$BASE/v3/gift_tracker/$REG" | jq '.data | del(.info_modules)'
|
|
204
|
+
|
|
205
|
+
curl -sS "${H[@]}" "$BASE/v4/shop/registry?registry_id=$REG&updated_modules=true" | jq '.data'
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Update an event** — read-modify-write: GET the groups above, find by
|
|
209
|
+
`event_entity_id`, then `PUT /v3/websites/events/{event_id}` with the full
|
|
210
|
+
event object (patch only the fields you're changing; everything else —
|
|
211
|
+
`type`, `uuid`, `wedding_account_id`, counts, `meal_options` — round-trips
|
|
212
|
+
from the GET):
|
|
213
|
+
|
|
214
|
+
```sh
|
|
215
|
+
curl -sS "${HJ[@]}" -X PUT "$BASE/v3/websites/events/<event_id>" -d '{
|
|
216
|
+
"event_entity_id": <event_id>, "uuid": "<from-get>", "wedding_account_id": '"$ACCT"',
|
|
217
|
+
"type": "<from-get>", "name": "Reception", "start_at": "<from-get>", "end_at": "<from-get>",
|
|
218
|
+
"timezone": "<from-get>", "venue_name": "The Grand Hall", "address1": "", "address2": "",
|
|
219
|
+
"city": "Charlotte", "state_province": "NC", "postal_code": "", "country_code": "US",
|
|
220
|
+
"note": "", "attire": "Black tie", "collect_rsvps": true, "public": <from-get>,
|
|
221
|
+
"display_order": 0, "num_guests_attending": <from-get>, "num_guests_declined": <from-get>,
|
|
222
|
+
"num_guests_not_responded": <from-get>, "meal_options": <from-get>,
|
|
223
|
+
"rsvp_questions": [], "add_booked_vendor": false
|
|
224
|
+
}' | jq '.data'
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Event invitations (per-guest, read-modify-write)** — from
|
|
228
|
+
`src/tools/event-invitations.ts`. Each guest carries an `event_invitations`
|
|
229
|
+
array; invite = append `{"event_id":<id>,"id":null,"rsvp_type":"NO_RESPONSE"}`,
|
|
230
|
+
uninvite = drop that element, **preserve every other element verbatim**
|
|
231
|
+
(same wipe risk as the guest-address write above). Write through the same
|
|
232
|
+
`PUT /v3/guestlists/groups/wedding-accounts/$ACCT/bulk/directory` used for
|
|
233
|
+
guest writes, sending `{"updated_guest_groups": [...]}` with full group
|
|
234
|
+
objects (each guest keeps its full shape, only `event_invitations` patched).
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Discover / storefronts (`src/tools/discover.ts`)
|
|
239
|
+
|
|
240
|
+
```sh
|
|
241
|
+
curl -sS "${H[@]}" "$BASE/v4/your-wedding" | jq '.data'
|
|
242
|
+
|
|
243
|
+
curl -sS "${HJ[@]}" -X POST "$BASE/v3/storefronts/search" -d '{
|
|
244
|
+
"taxonomy_node_id": 2, "city": "Charlotte", "state": "NC",
|
|
245
|
+
"limit": 24, "offset": 0, "facets": {},
|
|
246
|
+
"metro_types": ["HOME","HOME_SERVICE","AWAY"], "metros": [],
|
|
247
|
+
"exclude_inquired_storefronts": false, "exclude_booked_storefronts": false,
|
|
248
|
+
"boost_featured_storefronts": false, "suggested_vendors_for_inquiry_limit": 12
|
|
249
|
+
}' | jq '.data'
|
|
250
|
+
# taxonomy_node_id: 1=Venues 2=Photographers 3=Florists 7=Planners 9=Bands/DJs
|
|
251
|
+
|
|
252
|
+
curl -sS "${H[@]}" "$BASE/v3/storefronts/<storefront-uuid>" | jq '.data'
|
|
253
|
+
curl -sS "${H[@]}" "$BASE/v3/favorites/" | jq '.data'
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Registry items (`src/tools/registry-items.ts`)
|
|
259
|
+
|
|
260
|
+
```sh
|
|
261
|
+
curl -sS "${HJ[@]}" -X POST "$BASE/v3/categories/<category_id>/entities" \
|
|
262
|
+
-d '{"offset":0,"limit":50,"registry_id":"'"$REG"'"}' | jq '.data'
|
|
263
|
+
|
|
264
|
+
# Default collection id: `.data.default_collection_id` from the registry GET
|
|
265
|
+
# above, or the first `groups[].modules[]` entry with `type == "COLLECTION"`.
|
|
266
|
+
curl -sS "${HJ[@]}" -X POST "$BASE/v3/registries/$REG/collections/<collection_id>" \
|
|
267
|
+
-d '{"sku_id":"<sku>","quantity":1,"most_wanted":false,"enable_group_gifting":false}' \
|
|
268
|
+
| jq '.data'
|
|
269
|
+
|
|
270
|
+
# Full replace — all fields required:
|
|
271
|
+
curl -sS "${HJ[@]}" -X PUT "$BASE/v3/registries/$REG/items/<collection_item_id>" -d '{
|
|
272
|
+
"quantity": 2, "group_gift": false, "marked_fulfilled": false,
|
|
273
|
+
"personal_note": "", "most_wanted": true, "collection_id": "<collection_id>"
|
|
274
|
+
}' | jq '.data'
|
|
275
|
+
|
|
276
|
+
curl -sS "${H[@]}" -X DELETE "$BASE/v3/registries/$REG/items/<collection_item_id>"
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Website pages & settings (`src/tools/website.ts`)
|
|
282
|
+
|
|
283
|
+
```sh
|
|
284
|
+
curl -sS "${H[@]}" "$BASE/v3/websites/pages/wedding-accounts/full" | jq '.data'
|
|
285
|
+
|
|
286
|
+
curl -sS "${H[@]}" -X PUT "$BASE/v3/websites/pages/<page_id>/hidden/true" # or /false
|
|
287
|
+
|
|
288
|
+
curl -sS "${HJ[@]}" -X PUT "$BASE/v3/websites/pages/wedding-accounts/$ACCT/reorder" \
|
|
289
|
+
-d '{"ids": [<page_id_1>, <page_id_2>, ...]}' | jq '.data'
|
|
290
|
+
|
|
291
|
+
# Partial patch is fine here (unlike most write endpoints):
|
|
292
|
+
curl -sS "${HJ[@]}" -X PUT "$BASE/v3/websites/pages-v2/<page_id>" \
|
|
293
|
+
-d '{"page_id": <page_id>, "title": "Our Story"}' | jq '.data'
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Wedding settings** — read via `GET /v3/users/me/context` → `.data.wedding`;
|
|
297
|
+
write is read-modify-write (all fields required) to
|
|
298
|
+
`PUT /v3/weddings/{wedding_id}`:
|
|
299
|
+
|
|
300
|
+
```sh
|
|
301
|
+
curl -sS "${HJ[@]}" -X PUT "$BASE/v3/weddings/$WEDDING_ID" -d '{
|
|
302
|
+
"wedding_id": '"$WEDDING_ID"', "account_id": '"$ACCT"',
|
|
303
|
+
"slug": "<from-get>", "owner_first_name": "<from-get>", "owner_last_name": "<from-get>",
|
|
304
|
+
"partner_first_name": "<from-get>", "partner_last_name": "<from-get>",
|
|
305
|
+
"title": "Alex & Jordan", "wedding_date": "<from-get>", "hashtag": "#alexandjordan2026",
|
|
306
|
+
"enable_search_engine": true, "enable_search_zola": true,
|
|
307
|
+
"city": "Charlotte", "state_province": "NC", "guest_count": 150
|
|
308
|
+
}' | jq '.data'
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Website theme & customization (`src/tools/website-theme.ts`)
|
|
314
|
+
|
|
315
|
+
```sh
|
|
316
|
+
curl -sS "${H[@]}" "$BASE/v3/themes/current" | jq '.data'
|
|
317
|
+
curl -sS "${H[@]}" "$BASE/v3/websites/website-customizations/context" | jq '.data.current_style_customizations'
|
|
318
|
+
|
|
319
|
+
curl -sS "${HJ[@]}" -X POST "$BASE/v3/themes/search" \
|
|
320
|
+
-d '{"limit":50,"offset":0,"theme_layout_types":["MULTI_PAGE"]}' | jq '.data'
|
|
321
|
+
|
|
322
|
+
curl -sS "${HJ[@]}" -X PUT "$BASE/v3/themes/current" \
|
|
323
|
+
-d '{"theme_key":"galata","theme_layout_type":"MULTI_PAGE"}' | jq '.data'
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Update colors/fonts** — `POST /v3/websites/website-customizations/context`.
|
|
327
|
+
**Gotcha (see `docs/zola-api-quirks.md` §1–4):** changing `header_font`
|
|
328
|
+
(i.e. `header_font_family_id`) wipes every *other* active color unless you
|
|
329
|
+
re-send them in the same POST — GET the current state first and bundle its
|
|
330
|
+
non-null `accent_color` / `background_color` / `body.color` /
|
|
331
|
+
`navigation_customization.background_color` into the body. `body_font`'s
|
|
332
|
+
`font_family_id` only accepts `68` (Libre Baskerville) or `198` (Circular) —
|
|
333
|
+
any other value 500s. `header_color` / `nav_font_color` are **not** writable
|
|
334
|
+
on this API at all (web-api-only, cookie+CSRF auth — out of scope here).
|
|
335
|
+
|
|
336
|
+
```sh
|
|
337
|
+
curl -sS "${HJ[@]}" -X POST "$BASE/v3/websites/website-customizations/context" -d '{
|
|
338
|
+
"accent_color": "C9A66B",
|
|
339
|
+
"background_color": "FFFFFF",
|
|
340
|
+
"body_font": {"color": "222222"},
|
|
341
|
+
"navigation_customization": {"background_color": "FFFFFF"}
|
|
342
|
+
}' | jq '.data'
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## Website content: FAQs / home sections / POIs / travel (`src/tools/website-content.ts`)
|
|
348
|
+
|
|
349
|
+
All four follow the same list/add/update/remove shape;
|
|
350
|
+
`{wedding_account_id: $ACCT}` and an `_entity_id: 0` (create) or the real id
|
|
351
|
+
(update) prefix every write body. Deletes share one path shape:
|
|
352
|
+
`DELETE /v3/websites/{pages}/{entities}/{entity_id}/wedding-accounts/$ACCT`
|
|
353
|
+
where `{pages}` is looked up per-type from
|
|
354
|
+
`GET /v3/websites/pages/wedding-accounts/full` (`.data.{home,faq,poi,travel}_page.page_id`).
|
|
355
|
+
|
|
356
|
+
```sh
|
|
357
|
+
# FAQs
|
|
358
|
+
curl -sS "${H[@]}" "$BASE/v3/websites/faqs/wedding-accounts/$ACCT" | jq '.data'
|
|
359
|
+
curl -sS "${HJ[@]}" -X POST "$BASE/v3/websites/faqs" -d '{
|
|
360
|
+
"wedding_account_id": '"$ACCT"', "faq_entity_id": 0,
|
|
361
|
+
"question": "What is the dress code?", "answer": "Cocktail attire", "display_order": 0
|
|
362
|
+
}' | jq '.data'
|
|
363
|
+
curl -sS "${HJ[@]}" -X PUT "$BASE/v3/websites/faqs/<faq_entity_id>" -d '{
|
|
364
|
+
"wedding_account_id": '"$ACCT"', "faq_entity_id": <faq_entity_id>,
|
|
365
|
+
"question": "...", "answer": "...", "display_order": 0
|
|
366
|
+
}' | jq '.data'
|
|
367
|
+
curl -sS "${H[@]}" -X DELETE "$BASE/v3/websites/pages/<faq_page_id>/entities/<faq_entity_id>/wedding-accounts/$ACCT"
|
|
368
|
+
|
|
369
|
+
# Home page story sections — same shape, path `/v3/websites/home-sections[...]`,
|
|
370
|
+
# id field `homepage_entity_id`, extra `hidden` boolean, `title`+`subtitle`+`description`.
|
|
371
|
+
|
|
372
|
+
# Points of interest — `/v3/websites/points-of-interest[...]`, id field
|
|
373
|
+
# `poi_entity_id`; fields: title, description, address1/2, city, state_province,
|
|
374
|
+
# postal_code, country_code, latitude, longitude, google_place_id, contact_phone, url.
|
|
375
|
+
|
|
376
|
+
# Travel items — `/v3/websites/travel[...]`, id field `travel_entity_id`;
|
|
377
|
+
# fields: type (HOTEL|FLIGHT|TRAIN|BUS|CAR|OTHER), name, note, code,
|
|
378
|
+
# address1/2, city, state_province, postal_code, country_code, latitude,
|
|
379
|
+
# longitude, google_place_id, contact_number, email_address, url, source
|
|
380
|
+
# (GOOGLE_PLACES|MANUAL), timezone, display_order.
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Home-section/POI/travel `list`/`add`/`update`/`remove` all mirror the FAQ
|
|
384
|
+
calls above 1:1 (same verbs, same `wedding_account_id` prefix, same delete
|
|
385
|
+
shape) — see `src/tools/website-content.ts` for the exact field names per
|
|
386
|
+
type if you need the full body.
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## Invitations / card projects (`src/tools/invitations.ts`)
|
|
391
|
+
|
|
392
|
+
```sh
|
|
393
|
+
curl -sS "${HJ[@]}" -X POST "$BASE/v3/card-projects/search_request" -d '{
|
|
394
|
+
"completed": false, "limit": 30, "card_suite_uuids": [], "card_suite_ids": [],
|
|
395
|
+
"offset": 0, "fetch_customizations": true, "include_deleted": false,
|
|
396
|
+
"medium": ["PAPER","MAGNET","DIGITAL"], "single_sample": false
|
|
397
|
+
}' | jq '.data'
|
|
398
|
+
|
|
399
|
+
curl -sS "${H[@]}" "$BASE/v3/card-projects/<project_uuid>" | jq '.data'
|
|
400
|
+
curl -sS "${H[@]}" "$BASE/v3/card-projects/<project_uuid>/validate" | jq '.data'
|
|
401
|
+
curl -sS "${H[@]}" "$BASE/v3/card-projects/<project_uuid>/project-guest-groups" | jq '.data'
|
|
402
|
+
curl -sS "${HJ[@]}" -X POST "$BASE/v4/card-catalog/suites/details/<suite_uuid>" | jq '.data'
|
|
403
|
+
|
|
404
|
+
curl -sS "${HJ[@]}" -X POST "$BASE/v3/card-catalog/search/faceted" -d '{
|
|
405
|
+
"lead_card_types": [], "include_updated_proof_module": true, "limit": 50, "offset": 0,
|
|
406
|
+
"digital_suite": false, "include_lead_card_type_metadata": true,
|
|
407
|
+
"lead_card_type": "INVITATION", "include_module": true
|
|
408
|
+
}' | jq '.data'
|
|
409
|
+
|
|
410
|
+
curl -sS "${H[@]}" "$BASE/v3/favorites/card-suites/" | jq '.data'
|
|
411
|
+
curl -sS "${H[@]}" "$BASE/v3/websites/rsvps/wedding-accounts/$ACCT" | jq '.data'
|
|
412
|
+
|
|
413
|
+
curl -sS "${HJ[@]}" -X POST "$BASE/v3/card-projects" -d '{
|
|
414
|
+
"quantity": 150, "lead_variation_uuid": "<variation-uuid>",
|
|
415
|
+
"extra_customizable": false, "account_id": '"$ACCT"', "suite_uuid": "<suite-uuid>"
|
|
416
|
+
}' | jq '.data'
|
|
417
|
+
|
|
418
|
+
curl -sS "${HJ[@]}" -X PUT "$BASE/v3/card-projects/<project_uuid>" -d '{
|
|
419
|
+
"customizations": {"<customization_uuid>": {"variation_uuid": "<new-variation-uuid>"}}
|
|
420
|
+
}' | jq '.data'
|
|
421
|
+
|
|
422
|
+
curl -sS "${HJ[@]}" -X PUT "$BASE/v3/card-projects/<project_uuid>/project-guest-groups" -d '{
|
|
423
|
+
"guest_group_requests": [{"guest_group_id": <id>, "enabled": true}]
|
|
424
|
+
}' | jq '.data'
|
|
425
|
+
|
|
426
|
+
curl -sS "${HJ[@]}" -X POST "$BASE/v3/card-templates/preview" -d '{
|
|
427
|
+
"variation_uuids": ["<variation-uuid>"], "customizable": true,
|
|
428
|
+
"substitutions": {"first_name": "Alex", "wedding_date": "2026-10-17"}
|
|
429
|
+
}' | jq '.data'
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**QR code** — preview returns raw image bytes (Content-Type lies as
|
|
433
|
+
`image/jpeg` even for a PNG — sniff magic bytes if scripting):
|
|
434
|
+
|
|
435
|
+
```sh
|
|
436
|
+
curl -sS "${HJ[@]}" -X PUT "$BASE/v3/card-projects/qrcode/preview" \
|
|
437
|
+
-H 'accept: */*' -d '{"dimension":"MEDIUM","url_type":"CUSTOM","enabled":true,"url":"https://example.com"}' \
|
|
438
|
+
-o qrcode.png
|
|
439
|
+
|
|
440
|
+
curl -sS "${HJ[@]}" -X PUT "$BASE/v3/card-projects/<project_uuid>/customization/page/<page_uuid>/qrcode" -d '{
|
|
441
|
+
"dimension": "MEDIUM", "url_type": "CUSTOM", "color": "000000",
|
|
442
|
+
"enabled": true, "url": "https://example.com"
|
|
443
|
+
}' | jq '.data'
|
|
444
|
+
```
|