Setting the file. One moment.
Seed Events · Wix Vibe Headless · wix/skills · Skills Docs
ContentsBack to the top of the page Wix Blog
This file
Number 20.47
Position 47 of 126
Type JavaScript
Size 12 KB
Lines 251 references/events/seed/ seed-events.js
JavaScript · 251 lines · 12 KB
14 // // setupEvents installs the Wix Events app first (installEventsApp) — base44 sites may not have it.
15 //
16 // // STEP 1: create each event as a DRAFT (TICKETING = paid tiers | RSVP = free built-in form)
17 // const ev = await seed.createEvent(ctx, {
18 // title: "Summer Synth Festival", shortDescription: "One night of analog sound.",
19 // type: "TICKETING", startDate: "2026-10-01T03:30:00.000Z", endDate: "2026-10-01T07:00:00.000Z",
20 // timeZoneId: "America/Los_Angeles",
21 // location: { name: "The Echo Lot", type: "VENUE", address: { addressLine: "120 Harbor St", city: "Seattle", subdivision: "US-WA", postalCode: "98101", country: "US" } },
22 // });
23 // // STEP 2 (TICKETING only): add ticket tiers BEFORE publish
24 // const tiers = await seed.createTicketTiers(ctx, ev.id, [{ name: "General Admission", price: "65.00", initialLimit: 200 }]);
25 // // STEP 3: publish (one-way)
26 // await seed.publishEvent(ctx, ev.id);
27 // // STEP 4 (optional): group by format/track
28 // const cats = await seed.createEventCategories(ctx, ["Talks"]);
29 // await seed.assignEventsToCategory(ctx, cats[0].id, [ev.id]);
30 // // Attach images (optional): import the url to Wix Media first (events binds by file id), then patch.
31 // const file = await seed.importImage(ctx, imageUrl); // → { id, url } (Wix Media file id + wixstatic url)
32 // await seed.setEventMainImage(ctx, { eventId: ev.id, id: file.id, url: file.url, height: 1024, width: 1024, altText: ev.slug });
33 //
34 // If any call fails with a shape the caller didn't expect, fall back to the wix-docs skill
35 // (search + read the live Wix API reference) — never guess. Source recipe (authoritative):
36 // wix-headless/references/inline-recipes/setup-events.md.
37
38 const API = "https://www.wixapis.com" ;
39 // Wix Events app id — installEventsApp installs it before seeding (base44 sites may not have it).
40 const EVENTS_APP_ID = "140603ad-af8d-84a5-2c80-a0f60cb47351" ;
41
42 async function req ( ctx , path , { method = "POST" , body } = {}) {
43 const res = await fetch ( API + path, {
44 method,
45 headers: {
46 Authorization: `Bearer ${ ctx . token }` ,
47 "wix-site-id" : ctx.siteId,
48 "Content-Type" : "application/json" ,
49 },
50 body: body ? JSON . stringify (body) : undefined ,
51 });
52 const json = await res. json (). catch (() => ({}));
53 if ( ! res.ok) throw new Error ( `${ method } ${ path } -> ${ res . status }: ${ JSON . stringify ( json ). slice ( 0 , 400 ) }` );
54 return json;
55 }
56
57 // {type:"TICKETING"|"RSVP", ...} -> Wix registration block. TICKETING carries a tickets{} config;
58 // RSVP carries an rsvp{responseType} and seeds NO form fields (name+email form is built-in).
59 // initialType is IMMUTABLE after create — set from the request, never plan to convert.
60 function buildRegistration ( ev ) {
61 if (ev.type === "TICKETING" ) {
62 return {
63 initialType: "TICKETING" ,
64 tickets: {
65 ticketLimitPerOrder: ev.ticketLimitPerOrder ?? 8 , // per recipe (example value)
66 currency: ev.currency ?? "USD" , // setupEvents threads the site currency; USD only as last-resort fallback
67 reservationDurationInMinutes: ev.reservationDurationInMinutes ?? 20 , // per recipe (example value)
68 },
69 };
70 }
71 return {
72 initialType: "RSVP" ,
73 rsvp: { responseType: ev.rsvpResponseType ?? "YES_ONLY" }, // "YES_ONLY" | "YES_AND_NO"
74 };
75 }
76
77 // ---- exported operations ----
78
79 /**
80 * STEP 1 — create ONE event as a draft (no bulk endpoint; loop for multiple events).
81 * @param ev { title, shortDescription?, type:"TICKETING"|"RSVP",
82 * startDate, endDate, timeZoneId, showTimeZone?, // dates are ISO-8601 UTC and MUST be in the future
83 * location, // {name,type:"VENUE",address:{…}} | {name,type:"ONLINE"} | {locationTbd:true,name}
84 * ticketLimitPerOrder?, currency?, reservationDurationInMinutes?, // TICKETING only
85 * rsvpResponseType? } // RSVP only
86 * @returns { id, slug } (id feeds STEP 2/3; slug is the URL identifier — do NOT confuse with id)
87 */
88 async function createEvent ( ctx , ev ) {
89 const body = {
90 draft: true ,
91 event: {
92 title: ev.title,
93 shortDescription: ev.shortDescription,
94 location: ev.location,
95 dateAndTimeSettings: {
96 startDate: ev.startDate,
97 endDate: ev.endDate,
98 timeZoneId: ev.timeZoneId,
99 showTimeZone: ev.showTimeZone ?? true , // per recipe (example value)
100 },
101 registration: buildRegistration (ev),
102 },
103 fields: [ "DETAILS" , "TEXTS" , "REGISTRATION" , "URLS" ],
104 };
105 const r = await req (ctx, "/events/v3/events" , { body });
106 return { id: r.event?.id, slug: r.event?.slug };
107 }
108
109 /**
110 * STEP 2 — create ticket definitions for a TICKETING event (skip for RSVP). Must run BEFORE publish.
111 * Tiers for one event are independent — fired as one parallel batch.
112 * @param tiers [{ name (<=30 chars), description?, price (decimal STRING, e.g. "65.00"),
113 * currency?, initialLimit? (omit for unlimited), feeType? ("FEE_INCLUDED"|"FEE_ADDED_AT_CHECKOUT"|"NO_FEE") }]
114 * @returns [{ id }] (frontend lists tiers and reserves by ticketDefinition id)
115 */
116 async function createTicketTiers ( ctx , eventId , tiers ) {
117 return Promise . all (tiers. map ( async ( t ) => {
118 const body = {
119 ticketDefinition: {
120 eventId,
121 name: t.name,
122 description: t.description,
123 ... (t.initialLimit != null ? { initialLimit: t.initialLimit } : {}), // omit => unlimited
124 pricingMethod: { fixedPrice: { value: String (t.price), currency: t.currency ?? "USD" } }, // value = decimal string; currency = site currency (threaded by setupEvents), USD only as fallback
125 feeType: t.feeType ?? "FEE_INCLUDED" , // per recipe (default)
126 },
127 fields: [ "SALES_DETAILS" ],
128 };
129 const r = await req (ctx, "/events/v3/ticket-definitions" , { body });
130 return { id: r.ticketDefinition?.id };
131 }));
132 }
133
134 // STEP 3 — publish the event (one-way). For a TICKETING event, publish only AFTER its tiers exist.
135 async function publishEvent ( ctx , eventId ) {
136 return req (ctx, `/events/v3/events/${ eventId }/publish` , { body: {} });
137 }
138
139 // STEP 4 (optional) — group events by a format/track. Categories are the v1 API (NOT v3). One call each.
140 async function createEventCategories ( ctx , names ) {
141 const out = [];
142 for ( const name of names) {
143 const r = await req (ctx, "/events/v1/categories" , { body: { category: { name } } });
144 out. push ({ id: r.category?.id, name });
145 }
146 return out;
147 }
148
149 // Assign events to a category. Path is /{categoryId}/events (NOT /assign); body key is `eventId` (array).
150 async function assignEventsToCategory ( ctx , categoryId , eventIds ) {
151 return req (ctx, `/events/v1/categories/${ categoryId }/events` , { body: { eventId: eventIds } });
152 }
153
154 // Import an external image URL into Wix Media → { id, url }. Events binds mainImage by the Wix Media
155 // file **id**, NOT a url — an external url (e.g. a base44 generate_image result) MUST be imported
156 // first; the raw url as the id stores (200) but renders nothing. id = wixstatic file id, url = the
157 // permanent wixstatic url.
158 async function importImage ( ctx , url , displayName = "image.png" ) {
159 const r = await req (ctx, "/site-media/v1/files/import" , { body: { url, mimeType: "image/png" , displayName } });
160 const f = r.file || r;
161 if ( ! f?.id) throw new Error ( `import-file returned no file id: ${ JSON . stringify ( r ). slice ( 0 , 200 ) }` );
162 return { id: f.id, url: f.url };
163 }
164
165 // Attach images (optional). mainImage is an Image OBJECT; height/width are REQUIRED or it won't
166 // render. Events V3 uses NO revision — partial PATCH keyed by event.id. Works before OR after publish.
167 // item: { eventId, id, url, height, width, altText } (id = the WixMedia image id, from importImage)
168 async function setEventMainImage ( ctx , item ) {
169 return req (ctx, `/events/v3/events/${ item . eventId }` , {
170 method: "PATCH" ,
171 body: {
172 event: {
173 id: item.eventId,
174 mainImage: { id: item.id, url: item.url, height: item.height, width: item.width, altText: item.altText },
175 },
176 fields: [ "DETAILS" ], // mainImage reads back only under DETAILS
177 },
178 });
179 }
180
181 /**
182 * ONE-CALL seed: per event create DRAFT → (ticketed) add tiers → publish, then create + assign
183 * named categories and attach main images — in the correct order, keeping created ids in memory
184 * (no hand-threading of event ids across exec calls). DEFAULT path.
185 * @param plan {{ events: [{
186 * ...createEvent fields (title, shortDescription?, type, startDate, endDate, timeZoneId, location, …),
187 * ticketTiers?: [{ name, price, initialLimit?, … }], // TICKETING only; omit to skip
188 * category?: string, // category NAME; resolved to id + assigned
189 * imageUrl?: string // a plain image url; imported to Wix Media here, optional
190 * }] }}
191 */
192 // Install the Wix Events app so seeding self-provisions — base44 sites aren't guaranteed to have it
193 // (there's no separate Setup step here, unlike the wix-headless recipe this was ported from). Idempotent:
194 // re-installing an already-installed app returns 200 (verified), so it's safe to call unconditionally.
195 async function installEventsApp ( ctx ) {
196 return req (ctx, "/apps-installer-service/v1/app-instance/install" , { body: {
197 tenant: { tenantType: "SITE" , id: ctx.siteId },
198 appInstance: { appDefId: EVENTS_APP_ID , enabled: true },
199 } });
200 }
201
202 // Ticket prices are in the SITE's currency. The ticket-definitions API REQUIRES `currency` and does
203 // not infer it (omitting it 400s), and it does NOT fall back to the site currency — whatever you pass
204 // is used verbatim. So resolve the site's payment currency once and thread it through, instead of a
205 // hardcoded default that would mis-price tickets on a non-USD site (e.g. USD tickets on a EUR/ILS site).
206 async function getSiteCurrency ( ctx ) {
207 try {
208 const r = await req (ctx, "/site-properties/v4/properties" , { method: "GET" });
209 return r?.properties?.paymentCurrency || "USD" ;
210 } catch { return "USD" ; }
211 }
212
213 async function setupEvents ( ctx , { events = [] } = {}) {
214 await installEventsApp (ctx);
215 const siteCurrency = await getSiteCurrency (ctx);
216 const created = [];
217 for ( const ev of events) {
218 const e = await createEvent (ctx, { ... ev, currency: ev.currency ?? siteCurrency });
219 const tiers = ev.ticketTiers?. length
220 ? await createTicketTiers (ctx, e.id, ev.ticketTiers. map (( t ) => ({ ... t, currency: t.currency ?? siteCurrency })))
221 : [];
222 await publishEvent (ctx, e.id);
223 created. push ({ ... e, category: ev.category, imageUrl: ev.imageUrl, ticketCount: tiers. length });
224 }
225 const names = [ ...new Set (created. map (( e ) => e.category). filter (Boolean))];
226 const cats = names. length ? await createEventCategories (ctx, names) : [];
227 for ( const c of cats) {
228 const eventIds = created. filter (( e ) => e.category === c.name). map (( e ) => e.id);
229 await assignEventsToCategory (ctx, c.id, eventIds);
230 }
231 let imagesAttached = 0 ;
232 for ( const e of created) {
233 if ( ! e.imageUrl) continue ;
234 try {
235 const file = await importImage (ctx, e.imageUrl, `${ e . slug || "event"}.png` ); // → Wix Media file id
236 await setEventMainImage (ctx, { eventId: e.id, id: file.id, url: file.url, height: 1024 , width: 1024 , altText: e.slug });
237 imagesAttached ++ ;
238 } catch { /* never block on image failure — leave the event image-less */ }
239 }
240 return {
241 events: created. map (( e ) => ({ id: e.id, slug: e.slug, ticketCount: e.ticketCount, category: e.category ?? null })),
242 categories: cats,
243 imagesAttached,
244 };
245 }
246
247 module . exports = {
248 setupEvents,
249 createEvent, createTicketTiers, publishEvent,
250 installEventsApp, getSiteCurrency, createEventCategories, assignEventsToCategory, importImage, setEventMainImage,
251 };