@itm-studio/partner-sdk 0.2.6 → 0.2.8

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/README.md CHANGED
@@ -16,7 +16,7 @@ npm install @itm-studio/partner-sdk
16
16
  ## Quick Start
17
17
 
18
18
  ```typescript
19
- import { createITMPartnerClient } from '@itm-studio/partner-sdk';
19
+ import { createITMPartnerClient } from "@itm-studio/partner-sdk";
20
20
 
21
21
  const itm = createITMPartnerClient({
22
22
  token: process.env.ITM_PARTNER_TOKEN!,
@@ -25,7 +25,7 @@ const itm = createITMPartnerClient({
25
25
  // Get all upcoming moments for your brand
26
26
  const { getPartnerMomentsForBrand } = await itm.query({
27
27
  getPartnerMomentsForBrand: {
28
- __args: { status: 'UPCOMING', take: 10 },
28
+ __args: { status: "UPCOMING", take: 10 },
29
29
  moments: {
30
30
  uid: true,
31
31
  name: true,
@@ -70,18 +70,18 @@ Keep the partner token on your server. Do not expose it from browser code or cli
70
70
  For request-scoped mutations such as partner brand subscriptions, create the client inside the incoming request and forward `x-forwarded-for` and `user-agent` so the backend can enrich compliance metadata when those fields are omitted from `input.compliance`.
71
71
 
72
72
  ```typescript
73
- import { createITMPartnerClient } from '@itm-studio/partner-sdk';
73
+ import { createITMPartnerClient } from "@itm-studio/partner-sdk";
74
74
 
75
75
  export function createITMClientForRequest(request: Request) {
76
76
  return createITMPartnerClient({
77
77
  token: process.env.ITM_PARTNER_TOKEN!,
78
78
  headers: () => {
79
- const forwardedFor = request.headers.get('x-forwarded-for');
80
- const userAgent = request.headers.get('user-agent');
79
+ const forwardedFor = request.headers.get("x-forwarded-for");
80
+ const userAgent = request.headers.get("user-agent");
81
81
 
82
82
  return {
83
- ...(forwardedFor ? { 'x-forwarded-for': forwardedFor } : {}),
84
- ...(userAgent ? { 'user-agent': userAgent } : {}),
83
+ ...(forwardedFor ? { "x-forwarded-for": forwardedFor } : {}),
84
+ ...(userAgent ? { "user-agent": userAgent } : {}),
85
85
  };
86
86
  },
87
87
  });
@@ -92,21 +92,41 @@ If you also send `input.compliance`, the backend keeps the values you provide an
92
92
 
93
93
  ## Money Amounts
94
94
 
95
- Partner API price values are always encoded as `amount * 100`. Divide by `100` before rendering a major-unit display amount (dollars for USD, euros for EUR, yen for JPY, etc.).
96
-
97
- This convention also applies to zero-decimal currencies such as JPY and KRW. The rendering conversion is always `price / 100`; zero-decimal status only controls whether the formatted display shows fractional digits. For example, `2500` should be displayed as `JPY 25`, not `JPY 2,500`.
95
+ Dynamic-pricing price values use the currency's integer minor unit: cents for
96
+ USD and EUR, and whole units for zero-decimal currencies such as JPY and KRW.
97
+ Do not multiply zero-decimal dynamic-pricing amounts by 100. Older partner tier
98
+ read/edit fields retain the backend's historical cents representation; use the
99
+ dynamic-pricing API for price changes on enrolled tiers.
98
100
 
99
101
  ```typescript
100
- const ZERO_DECIMAL_CURRENCIES = new Set(['BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', 'UGX', 'VND', 'VUV', 'XAF', 'XOF', 'XPF']);
101
-
102
- function formatPartnerPrice(priceInCents: number, currencyCode: string) {
103
- if (priceInCents === 0) return 'Free';
104
-
105
- const amount = priceInCents / 100;
106
- const maximumFractionDigits = ZERO_DECIMAL_CURRENCIES.has(currencyCode) ? 0 : 2;
107
-
108
- return new Intl.NumberFormat('en-US', {
109
- style: 'currency',
102
+ const ZERO_DECIMAL_CURRENCIES = new Set([
103
+ "BIF",
104
+ "CLP",
105
+ "DJF",
106
+ "GNF",
107
+ "JPY",
108
+ "KMF",
109
+ "KRW",
110
+ "MGA",
111
+ "PYG",
112
+ "RWF",
113
+ "UGX",
114
+ "VND",
115
+ "VUV",
116
+ "XAF",
117
+ "XOF",
118
+ "XPF",
119
+ ]);
120
+
121
+ function formatPartnerPrice(priceInMinorUnits: number, currencyCode: string) {
122
+ if (priceInMinorUnits === 0) return "Free";
123
+
124
+ const zeroDecimal = ZERO_DECIMAL_CURRENCIES.has(currencyCode);
125
+ const amount = zeroDecimal ? priceInMinorUnits : priceInMinorUnits / 100;
126
+ const maximumFractionDigits = zeroDecimal ? 0 : 2;
127
+
128
+ return new Intl.NumberFormat("en-US", {
129
+ style: "currency",
110
130
  currency: currencyCode,
111
131
  minimumFractionDigits: maximumFractionDigits,
112
132
  maximumFractionDigits,
@@ -114,8 +134,125 @@ function formatPartnerPrice(priceInCents: number, currencyCode: string) {
114
134
  }
115
135
  ```
116
136
 
137
+ ## Dynamic Pricing
138
+
139
+ Dynamic pricing is a controlled actuator, not a hosted rules engine. A brand
140
+ admin first enables a moment policy and enrolls paid tiers with minimum and
141
+ maximum prices. The partner then reads current context and explicitly chooses
142
+ which enrolled tiers to update.
143
+
144
+ Always fetch fresh context immediately before an apply. Admission windows,
145
+ policy version, bounds, configured supply, and current price form the stable
146
+ pricing concurrency contract. Live sales do not invalidate
147
+ `pricingContextVersion`. Each update must separately declare whether it is
148
+ inventory-independent or provide a remaining-supply/fill-rate range checked
149
+ under the price-write lock. The entire batch is atomic.
150
+
151
+ ```typescript
152
+ const { getPartnerMomentDynamicPricingContext: context } = await itm.query({
153
+ getPartnerMomentDynamicPricingContext: {
154
+ __args: { input: { momentUid } },
155
+ policy: { version: true, enabled: true },
156
+ tiers: {
157
+ ticketTierUid: true,
158
+ currentPrice: true,
159
+ minPrice: true,
160
+ maxPrice: true,
161
+ eligible: true,
162
+ pricingContextVersion: true,
163
+ supply: true,
164
+ remainingSupply: true,
165
+ fillRateBps: true,
166
+ admissionWindows: { startsAt: true, endsAt: true },
167
+ },
168
+ },
169
+ });
170
+
171
+ const tier = context.tiers.find((candidate) => candidate.eligible)!;
172
+ const { applyPartnerMomentDynamicPricing: batch } = await itm.mutation({
173
+ applyPartnerMomentDynamicPricing: {
174
+ __args: {
175
+ input: {
176
+ momentUid,
177
+ mode: "APPLY", // use PREVIEW to validate without changing prices
178
+ idempotencyKey: crypto.randomUUID(),
179
+ policyVersion: context.policy.version,
180
+ reason: "60% fill threshold reached",
181
+ strategyKey: "capacity-step",
182
+ strategyVersion: "1.0.0",
183
+ updates: [
184
+ {
185
+ ticketTierUid: tier.ticketTierUid,
186
+ newPrice: tier.currentPrice + 500,
187
+ expectedPrice: tier.currentPrice,
188
+ expectedPricingContextVersion: tier.pricingContextVersion,
189
+ inventoryGuard: {
190
+ mode: "RANGE",
191
+ metric: "FILL_RATE_BPS",
192
+ minimum: 6000,
193
+ maximum: 6999,
194
+ },
195
+ },
196
+ ],
197
+ },
198
+ },
199
+ uid: true,
200
+ status: true,
201
+ failureCode: true,
202
+ failureMessage: true,
203
+ items: {
204
+ ticketTierUid: true,
205
+ status: true,
206
+ previousPrice: true,
207
+ resultingPrice: true,
208
+ failureCode: true,
209
+ },
210
+ },
211
+ });
212
+ ```
213
+
214
+ Reuse an idempotency key only for the identical request. `PROCESSING` can be
215
+ polled with `getPartnerDynamicPricingBatch`; terminal statuses are `PREVIEWED`,
216
+ `APPLIED`, `NO_CHANGES`, `REJECTED`, `CONFLICTED`, and `FAILED`. Legacy
217
+ `editPartnerTicketTiersForMoment` price edits are rejected for tiers enrolled
218
+ in an enabled dynamic pricing policy.
219
+
220
+ Use `{ mode: 'INDEPENDENT' }` only when the strategy intentionally does not
221
+ depend on live capacity. Range bounds are inclusive; fill rate uses integer
222
+ basis points (`6000` = 60%). Refresh after `validUntil`. Because price is stored
223
+ per tier, every upcoming admission window for a managed tier must fall within
224
+ the configured horizon before that tier is eligible.
225
+
117
226
  ## Available Queries
118
227
 
228
+ ### `getPartnerBrand`
229
+
230
+ Get details for the brand associated with the partner token. Takes no
231
+ arguments — the brand is resolved from the authenticated token, and the
232
+ response is always non-null.
233
+
234
+ ```typescript
235
+ const { getPartnerBrand } = await itm.query({
236
+ getPartnerBrand: {
237
+ uid: true,
238
+ name: true,
239
+ slug: true,
240
+ bio: true,
241
+ publicUrl: true,
242
+ customDomain: true,
243
+ customDomainVerifiedAt: true,
244
+ spotifyUrl: true,
245
+ youtubeEmbedUrl: true,
246
+ },
247
+ });
248
+ ```
249
+
250
+ - `publicUrl` is the brand's hosted page on ITM.
251
+ - `customDomain` and `customDomainVerifiedAt` describe the brand's white-label
252
+ domain, if one is configured; `customDomainVerifiedAt` is `null` until the
253
+ domain is verified.
254
+ - `bio`, `spotifyUrl`, and `youtubeEmbedUrl` are `null` when unset.
255
+
119
256
  ### `getPartnerMomentsForBrand`
120
257
 
121
258
  Get paginated moments (events) for your brand.
@@ -124,10 +261,10 @@ Get paginated moments (events) for your brand.
124
261
  const { getPartnerMomentsForBrand } = await itm.query({
125
262
  getPartnerMomentsForBrand: {
126
263
  __args: {
127
- take: 20, // items per page (default: 50, omit to use default)
128
- cursor: nextCursor, // from previous response
129
- status: 'UPCOMING', // UPCOMING | LIVE | ENDED
130
- sortOrder: 'ASC', // ASC | DESC by start date (default: DESC)
264
+ take: 20, // items per page (default: 50, omit to use default)
265
+ cursor: nextCursor, // from previous response
266
+ status: "UPCOMING", // UPCOMING | LIVE | ENDED
267
+ sortOrder: "ASC", // ASC | DESC by start date (default: DESC)
131
268
  },
132
269
  moments: { uid: true, name: true, startDate: true },
133
270
  totalCount: true,
@@ -137,6 +274,60 @@ const { getPartnerMomentsForBrand } = await itm.query({
137
274
  });
138
275
  ```
139
276
 
277
+ ### `getPartnerDrops`
278
+
279
+ Get paginated brand drops for the brand associated with your partner token.
280
+ Use this for partner-hosted drop grids, release pages, and filtered merch or
281
+ music drop listings.
282
+
283
+ Supported filters:
284
+
285
+ - `search`: case-insensitive match across drop name, subtitle, and slug.
286
+ - `isPublic`: filter public or private drops.
287
+ - `ctaType`: filter by `EXTERNAL_URL`, `GATED_LINK`, `MUSIC_LINK`, or `SUBSCRIBE`.
288
+ - `cursor` and `take`: use `nextCursor` while `hasNextPage` is true.
289
+
290
+ ```typescript
291
+ const { getPartnerDrops } = await itm.query({
292
+ getPartnerDrops: {
293
+ __args: {
294
+ take: 20,
295
+ cursor: nextCursor,
296
+ search: "release",
297
+ isPublic: true,
298
+ ctaType: "EXTERNAL_URL", // EXTERNAL_URL | GATED_LINK | MUSIC_LINK | SUBSCRIBE
299
+ },
300
+ drops: {
301
+ uid: true,
302
+ name: true,
303
+ slug: true,
304
+ isPublic: true,
305
+ ctaType: true,
306
+ ctaUrl: true,
307
+ captureName: true,
308
+ captureEmail: true,
309
+ coverImage: { url: true, mimeType: true },
310
+ coverImageMobile: { url: true, mimeType: true },
311
+ musicLink: {
312
+ title: true,
313
+ artistName: true,
314
+ releaseType: true,
315
+ sourceUrl: true,
316
+ odesliPageUrl: true,
317
+ thumbnailUrl: true,
318
+ },
319
+ },
320
+ totalCount: true,
321
+ hasNextPage: true,
322
+ nextCursor: true,
323
+ },
324
+ });
325
+ ```
326
+
327
+ The response is always scoped by the partner token; do not pass a brand ID.
328
+ `coverImageMobile` and `musicLink` can be `null`, so handle those as optional in
329
+ rendering code.
330
+
140
331
  ### `getPartnerMoment`
141
332
 
142
333
  Get a single moment by slug.
@@ -144,7 +335,7 @@ Get a single moment by slug.
144
335
  ```typescript
145
336
  const { getPartnerMoment } = await itm.query({
146
337
  getPartnerMoment: {
147
- __args: { slug: 'my-event-slug' },
338
+ __args: { slug: "my-event-slug" },
148
339
  uid: true,
149
340
  name: true,
150
341
  startDate: true,
@@ -172,7 +363,7 @@ Get approved public lineup entries for a moment. Use `getPartnerMoment` first if
172
363
  ```typescript
173
364
  const { getPartnerMoment } = await itm.query({
174
365
  getPartnerMoment: {
175
- __args: { slug: 'my-event-slug' },
366
+ __args: { slug: "my-event-slug" },
176
367
  uid: true,
177
368
  lineupDisplayLabel: true,
178
369
  },
@@ -186,7 +377,7 @@ if (momentUid) {
186
377
  __args: {
187
378
  momentUid,
188
379
  take: 50,
189
- lineupType: 'DJ', // optional: LINEUP | CO_HOST | PROMOTER | DJ | SPEAKER | VENDOR
380
+ lineupType: "DJ", // optional: LINEUP | CO_HOST | PROMOTER | DJ | SPEAKER | VENDOR
190
381
  },
191
382
  lineups: {
192
383
  uid: true,
@@ -206,7 +397,10 @@ if (momentUid) {
206
397
  },
207
398
  });
208
399
 
209
- console.log(getPartnerMoment?.lineupDisplayLabel, getPublicMomentLineups.lineups);
400
+ console.log(
401
+ getPartnerMoment?.lineupDisplayLabel,
402
+ getPublicMomentLineups.lineups
403
+ );
210
404
  }
211
405
  ```
212
406
 
@@ -221,7 +415,7 @@ Get public lineup invitation details by response token. This is useful if you bu
221
415
  ```typescript
222
416
  const { getMomentLineupByToken } = await itm.query({
223
417
  getMomentLineupByToken: {
224
- __args: { token: 'lineup-response-token' },
418
+ __args: { token: "lineup-response-token" },
225
419
  artistName: true,
226
420
  momentName: true,
227
421
  brandName: true,
@@ -232,7 +426,7 @@ const { getMomentLineupByToken } = await itm.query({
232
426
  });
233
427
  ```
234
428
 
235
- ### `getMomentStats`
429
+ ### `getPartnerMomentStats`
236
430
 
237
431
  Get cached partner-facing counts for a moment owned by the authenticated partner's brand.
238
432
 
@@ -242,9 +436,9 @@ Get cached partner-facing counts for a moment owned by the authenticated partner
242
436
  - `ticketRsvpRequestsCount` counts ticket RSVP requests.
243
437
 
244
438
  ```typescript
245
- const { getMomentStats } = await itm.query({
246
- getMomentStats: {
247
- __args: { momentSlug: 'my-event-slug' },
439
+ const { getPartnerMomentStats } = await itm.query({
440
+ getPartnerMomentStats: {
441
+ __args: { momentSlug: "my-event-slug" },
248
442
  ticketsCount: true,
249
443
  waitlistCount: true,
250
444
  externalRsvpCount: true,
@@ -253,7 +447,109 @@ const { getMomentStats } = await itm.query({
253
447
  });
254
448
  ```
255
449
 
256
- > **Note:** `getMomentStats` returns `null` if the moment does not exist or is not owned by the authenticated partner's brand.
450
+ > **Note:** `getPartnerMomentStats` returns `null` if the moment does not exist or is not owned by the authenticated partner's brand.
451
+
452
+ ### `getPartnerMomentAttendanceStats`
453
+
454
+ Get a cached attendance snapshot for a moment owned by the authenticated
455
+ partner's brand. Counts can be up to five minutes old; `generatedAt` reports
456
+ when the snapshot was produced.
457
+
458
+ Look up the moment by exactly one of:
459
+
460
+ - `momentUid` — the ITM moment UID
461
+ - `momentSlug` — the ITM moment slug
462
+ - `externalPlatform` + `externalId` — the source-platform identifier for
463
+ imported events (for example `EVENTBRITE`); the two must be provided together
464
+
465
+ ```typescript
466
+ const { getPartnerMomentAttendanceStats } = await itm.query({
467
+ getPartnerMomentAttendanceStats: {
468
+ __args: {
469
+ input: { externalPlatform: "EVENTBRITE", externalId: "1234567890" },
470
+ },
471
+ momentUid: true,
472
+ momentSlug: true,
473
+ generatedAt: true,
474
+ tickets: {
475
+ total: true,
476
+ checkedIn: true,
477
+ tiers: {
478
+ tierUid: true,
479
+ name: true,
480
+ label: true,
481
+ total: true,
482
+ checkedIn: true,
483
+ },
484
+ },
485
+ guestlist: {
486
+ total: true,
487
+ checkedIn: true,
488
+ unlabeled: { total: true, checkedIn: true },
489
+ labels: { labelUid: true, name: true, total: true, checkedIn: true },
490
+ },
491
+ },
492
+ });
493
+ ```
494
+
495
+ - `tickets` counts confirmed tickets, overall and per ticket tier.
496
+ - `guestlist` counts guest-list entries, split into `unlabeled` and per-label
497
+ buckets.
498
+
499
+ > **Note:** Returns `null` if no moment matches the lookup or the moment is not
500
+ > owned by the authenticated partner's brand. The request fails with a
501
+ > validation error when an identifier is an empty string, when
502
+ > `externalPlatform` and `externalId` are not provided together, when more than
503
+ > one lookup key is given, or when an external ID matches multiple moments.
504
+
505
+ ### `getPartnerTicketTiersForMoment`
506
+
507
+ Get paginated, partner-facing ticket tiers for a moment. Use this for pricing,
508
+ inventory, and dynamic catalog views without selecting the full public `Moment`
509
+ payload.
510
+
511
+ Provide either `momentSlug` or `momentUid`. If `isActive` is omitted, the backend
512
+ returns active tiers only; pass `isActive: false` only when you intentionally need
513
+ inactive tiers.
514
+
515
+ ```typescript
516
+ const { getPartnerTicketTiersForMoment } = await itm.query({
517
+ getPartnerTicketTiersForMoment: {
518
+ __args: {
519
+ input: {
520
+ momentSlug: "my-event-slug",
521
+ take: 20,
522
+ search: "general",
523
+ isPaid: true,
524
+ minPrice: 1000, // cents
525
+ maxPrice: 5000, // cents
526
+ },
527
+ },
528
+ ticketTiers: {
529
+ uid: true,
530
+ name: true,
531
+ label: true,
532
+ labelColor: true,
533
+ price: true, // cents
534
+ supply: true,
535
+ remainingSupply: true,
536
+ fillRate: true, // 0-1 sold-through ratio
537
+ isActive: true,
538
+ isPrivate: true,
539
+ slotDate: true,
540
+ slotDurationHours: true,
541
+ },
542
+ totalCount: true,
543
+ hasNextPage: true,
544
+ nextCursor: true,
545
+ },
546
+ });
547
+ ```
548
+
549
+ Supported filters include `ticketTierUids`, `search`, `isActive`, `isPrivate`,
550
+ `isPaid`, exact/min/max `price`, exact/min/max `supply`, exact/min/max
551
+ `remainingSupply`, min/max `fillRate`, admission-window bounds (`startsAt`,
552
+ `endsAt`), and slot date bounds (`slotDateStart`, `slotDateEnd`).
257
553
 
258
554
  ### `getPartnerTicketsForMoment`
259
555
 
@@ -263,8 +559,8 @@ Get paginated tickets for a moment.
263
559
  const { getPartnerTicketsForMoment } = await itm.query({
264
560
  getPartnerTicketsForMoment: {
265
561
  __args: {
266
- momentSlug: 'my-event-slug',
267
- checkedIn: true, // optional: filter by check-in status
562
+ momentSlug: "my-event-slug",
563
+ checkedIn: true, // optional: filter by check-in status
268
564
  take: 50,
269
565
  },
270
566
  tickets: {
@@ -290,7 +586,7 @@ Get paginated customers (users with their tickets) for a moment.
290
586
  ```typescript
291
587
  const { getPartnerCustomersForMoment } = await itm.query({
292
588
  getPartnerCustomersForMoment: {
293
- __args: { momentSlug: 'my-event-slug', take: 50 },
589
+ __args: { momentSlug: "my-event-slug", take: 50 },
294
590
  customers: {
295
591
  user: { uid: true, name: true, primaryEmail: true, phoneNumber: true },
296
592
  tickets: { uid: true, price: true, redeemedAt: true },
@@ -309,7 +605,7 @@ Get a moment collection by slug. Collections group related moments together (e.g
309
605
  ```typescript
310
606
  const { getPartnerMomentCollection } = await itm.query({
311
607
  getPartnerMomentCollection: {
312
- __args: { slug: 'summer-series' },
608
+ __args: { slug: "summer-series" },
313
609
  uid: true,
314
610
  name: true,
315
611
  description: true,
@@ -350,10 +646,10 @@ You can filter moments within a collection using `queryFilters`:
350
646
  ```typescript
351
647
  const { getPartnerMomentCollection } = await itm.query({
352
648
  getPartnerMomentCollection: {
353
- __args: { slug: 'summer-series' },
649
+ __args: { slug: "summer-series" },
354
650
  name: true,
355
651
  moments: {
356
- __args: { queryFilters: ['upcoming'] },
652
+ __args: { queryFilters: ["upcoming"] },
357
653
  uid: true,
358
654
  name: true,
359
655
  startDate: true,
@@ -411,7 +707,7 @@ Filter by moment to get echoes scoped to a specific event:
411
707
  ```typescript
412
708
  const { getPartnerPublicEchoes } = await itm.query({
413
709
  getPartnerPublicEchoes: {
414
- __args: { momentSlug: 'my-event-slug', take: 10 },
710
+ __args: { momentSlug: "my-event-slug", take: 10 },
415
711
  echoes: {
416
712
  uid: true,
417
713
  type: true,
@@ -445,8 +741,8 @@ const { respondToMomentLineup } = await itm.mutation({
445
741
  respondToMomentLineup: {
446
742
  __args: {
447
743
  input: {
448
- token: 'lineup-response-token',
449
- status: 'APPROVED', // APPROVED or REJECTED
744
+ token: "lineup-response-token",
745
+ status: "APPROVED", // APPROVED or REJECTED
450
746
  },
451
747
  },
452
748
  status: true,
@@ -470,10 +766,10 @@ Use OTP subscribe when you want the user to confirm the phone number before the
470
766
 
471
767
  ### Flow Summary
472
768
 
473
- | Mode | First mutation | First response | Second mutation |
474
- |---|---|---|---|
475
- | Direct | `createPartnerBrandSubscription` with `requireOtpVerification: false` or omitted | `verificationRequired: false` and `subscription` populated | None |
476
- | OTP | `createPartnerBrandSubscription` with `requireOtpVerification: true` | `verificationRequired: true`, `verificationToken`, `subscription: null` | `verifyPartnerBrandSubscription` |
769
+ | Mode | First mutation | First response | Second mutation |
770
+ | ------ | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------------------------------- |
771
+ | Direct | `createPartnerBrandSubscription` with `requireOtpVerification: false` or omitted | `verificationRequired: false` and `subscription` populated | None |
772
+ | OTP | `createPartnerBrandSubscription` with `requireOtpVerification: true` | `verificationRequired: true`, `verificationToken`, `subscription: null` | `verifyPartnerBrandSubscription` |
477
773
 
478
774
  ### Input Behavior
479
775
 
@@ -508,27 +804,27 @@ The field is part of the generated SDK input types:
508
804
  import type {
509
805
  CreatePartnerBrandSubscriptionInput,
510
806
  PostSubscriptionMessageInput,
511
- } from '@itm-studio/partner-sdk';
807
+ } from "@itm-studio/partner-sdk";
512
808
 
513
809
  const welcomeMessage: PostSubscriptionMessageInput = {
514
810
  sms: {
515
- body: 'Welcome to Acme. Reply STOP to opt out.',
516
- mediaUrls: ['https://cdn.example.com/welcome-banner.png'],
811
+ body: "Welcome to Acme. Reply STOP to opt out.",
812
+ mediaUrls: ["https://cdn.example.com/welcome-banner.png"],
517
813
  },
518
814
  email: {
519
- subject: 'Welcome to Acme',
520
- body: 'Thanks for subscribing. Your first update is on the way.',
815
+ subject: "Welcome to Acme",
816
+ body: "Thanks for subscribing. Your first update is on the way.",
521
817
  },
522
818
  push: {
523
- title: 'Welcome to Acme',
524
- body: 'Tap to see what is dropping this week.',
819
+ title: "Welcome to Acme",
820
+ body: "Tap to see what is dropping this week.",
525
821
  },
526
822
  };
527
823
 
528
824
  const input: CreatePartnerBrandSubscriptionInput = {
529
- phoneNumber: '+14155550111',
530
- fullName: 'Jane Doe',
531
- email: 'jane@example.com',
825
+ phoneNumber: "+14155550111",
826
+ fullName: "Jane Doe",
827
+ email: "jane@example.com",
532
828
  postSubscriptionMessage: welcomeMessage,
533
829
  };
534
830
  ```
@@ -538,21 +834,21 @@ await itm.mutation({
538
834
  createPartnerBrandSubscription: {
539
835
  __args: {
540
836
  input: {
541
- phoneNumber: '+14155550111',
542
- fullName: 'Jane Doe',
543
- email: 'jane@example.com',
837
+ phoneNumber: "+14155550111",
838
+ fullName: "Jane Doe",
839
+ email: "jane@example.com",
544
840
  postSubscriptionMessage: {
545
841
  sms: {
546
- body: 'Welcome to Acme — your first drop ships Friday. Reply STOP to opt out.',
547
- mediaUrls: ['https://cdn.example.com/welcome-banner.png'],
842
+ body: "Welcome to Acme — your first drop ships Friday. Reply STOP to opt out.",
843
+ mediaUrls: ["https://cdn.example.com/welcome-banner.png"],
548
844
  },
549
845
  email: {
550
- subject: 'Welcome to Acme',
551
- body: 'Hi Jane,\n\nThanks for subscribing — here is what to expect next...',
846
+ subject: "Welcome to Acme",
847
+ body: "Hi Jane,\n\nThanks for subscribing — here is what to expect next...",
552
848
  },
553
849
  push: {
554
- title: 'Welcome to Acme',
555
- body: 'Tap to see what is dropping this week.',
850
+ title: "Welcome to Acme",
851
+ body: "Tap to see what is dropping this week.",
556
852
  },
557
853
  },
558
854
  },
@@ -565,20 +861,20 @@ await itm.mutation({
565
861
 
566
862
  **Direct vs OTP timing:**
567
863
 
568
- | Mode | Where to pass `postSubscriptionMessage` | When delivery starts |
569
- |---|---|---|
570
- | Direct subscribe | Initial `createPartnerBrandSubscription` call | After the subscription is created or reactivated |
571
- | OTP subscribe | Initial `createPartnerBrandSubscription` call with `requireOtpVerification: true` | After `verifyPartnerBrandSubscription` succeeds |
864
+ | Mode | Where to pass `postSubscriptionMessage` | When delivery starts |
865
+ | ---------------- | --------------------------------------------------------------------------------- | ------------------------------------------------ |
866
+ | Direct subscribe | Initial `createPartnerBrandSubscription` call | After the subscription is created or reactivated |
867
+ | OTP subscribe | Initial `createPartnerBrandSubscription` call with `requireOtpVerification: true` | After `verifyPartnerBrandSubscription` succeeds |
572
868
 
573
869
  Do not pass `postSubscriptionMessage` to `verifyPartnerBrandSubscription`; that mutation only accepts `verificationToken` and `code`.
574
870
 
575
871
  **Channel inputs:**
576
872
 
577
- | Channel | Required | Optional | Notes |
578
- |---|---|---|---|
579
- | `sms` | `body` | `mediaUrls` (string array of image URLs for MMS) | Delivered to `phoneNumber`. |
580
- | `email` | `subject`, `body` | — | `body` is plain text or markdown, rendered via React Email. Skipped if the user has no primary email or the brand has no configured email alias. |
581
- | `push` | `title`, `body` | — | Skipped if the user has no push tokens or has opted out of brand messages. |
873
+ | Channel | Required | Optional | Notes |
874
+ | ------- | ----------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
875
+ | `sms` | `body` | `mediaUrls` (string array of image URLs for MMS) | Delivered to `phoneNumber`. |
876
+ | `email` | `subject`, `body` | — | `body` is plain text or markdown, rendered via React Email. Skipped if the user has no primary email or the brand has no configured email alias. |
877
+ | `push` | `title`, `body` | — | Skipped if the user has no push tokens or has opted out of brand messages. |
582
878
 
583
879
  **Delivery semantics:**
584
880
 
@@ -602,10 +898,10 @@ await itm.mutation({
602
898
  createPartnerBrandSubscription: {
603
899
  __args: {
604
900
  input: {
605
- phoneNumber: '+14155550111',
901
+ phoneNumber: "+14155550111",
606
902
  postSubscriptionMessage: {
607
903
  sms: {
608
- body: 'You are subscribed to Acme updates. Reply STOP to opt out.',
904
+ body: "You are subscribed to Acme updates. Reply STOP to opt out.",
609
905
  },
610
906
  },
611
907
  },
@@ -623,16 +919,16 @@ const { createPartnerBrandSubscription } = await itm.mutation({
623
919
  createPartnerBrandSubscription: {
624
920
  __args: {
625
921
  input: {
626
- phoneNumber: '+14155550112',
922
+ phoneNumber: "+14155550112",
627
923
  requireOtpVerification: true,
628
- email: 'jane@example.com',
924
+ email: "jane@example.com",
629
925
  postSubscriptionMessage: {
630
926
  sms: {
631
- body: 'Thanks for joining Acme. Reply STOP to opt out.',
927
+ body: "Thanks for joining Acme. Reply STOP to opt out.",
632
928
  },
633
929
  email: {
634
- subject: 'You are subscribed',
635
- body: 'Your subscription is confirmed. We will send updates here.',
930
+ subject: "You are subscribed",
931
+ body: "Your subscription is confirmed. We will send updates here.",
636
932
  },
637
933
  },
638
934
  },
@@ -648,7 +944,7 @@ await itm.mutation({
648
944
  __args: {
649
945
  input: {
650
946
  verificationToken: createPartnerBrandSubscription.verificationToken!,
651
- code: '123456',
947
+ code: "123456",
652
948
  },
653
949
  },
654
950
  uid: true,
@@ -671,17 +967,17 @@ const { createPartnerBrandSubscription } = await itm.mutation({
671
967
  createPartnerBrandSubscription: {
672
968
  __args: {
673
969
  input: {
674
- phoneNumber: '+14155550111',
675
- fullName: 'Jane Doe',
676
- email: 'jane@example.com',
677
- listNames: ['VIP', 'Newsletter'],
970
+ phoneNumber: "+14155550111",
971
+ fullName: "Jane Doe",
972
+ email: "jane@example.com",
973
+ listNames: ["VIP", "Newsletter"],
678
974
  compliance: {
679
- pageUrl: 'https://example.com/signup',
680
- submissionMethod: 'partner_sdk',
975
+ pageUrl: "https://example.com/signup",
976
+ submissionMethod: "partner_sdk",
681
977
  },
682
978
  postSubscriptionMessage: {
683
979
  sms: {
684
- body: 'Welcome to Acme updates. Reply STOP to opt out.',
980
+ body: "Welcome to Acme updates. Reply STOP to opt out.",
685
981
  },
686
982
  },
687
983
  },
@@ -718,12 +1014,12 @@ const { createPartnerBrandSubscription } = await itm.mutation({
718
1014
  createPartnerBrandSubscription: {
719
1015
  __args: {
720
1016
  input: {
721
- phoneNumber: '+14155550112',
1017
+ phoneNumber: "+14155550112",
722
1018
  requireOtpVerification: true,
723
- fullName: 'Otp User',
1019
+ fullName: "Otp User",
724
1020
  compliance: {
725
- pageUrl: 'https://example.com/otp-signup',
726
- submissionMethod: 'partner_sdk',
1021
+ pageUrl: "https://example.com/otp-signup",
1022
+ submissionMethod: "partner_sdk",
727
1023
  },
728
1024
  },
729
1025
  },
@@ -733,18 +1029,17 @@ const { createPartnerBrandSubscription } = await itm.mutation({
733
1029
  });
734
1030
 
735
1031
  if (!createPartnerBrandSubscription.verificationToken) {
736
- throw new Error('Expected OTP verification to be required');
1032
+ throw new Error("Expected OTP verification to be required");
737
1033
  }
738
1034
 
739
1035
  // Collect the SMS code from the user in your UI or API flow.
740
- const code = '123456';
1036
+ const code = "123456";
741
1037
 
742
1038
  const { verifyPartnerBrandSubscription } = await itm.mutation({
743
1039
  verifyPartnerBrandSubscription: {
744
1040
  __args: {
745
1041
  input: {
746
- verificationToken:
747
- createPartnerBrandSubscription.verificationToken,
1042
+ verificationToken: createPartnerBrandSubscription.verificationToken,
748
1043
  code,
749
1044
  },
750
1045
  },
@@ -770,17 +1065,17 @@ This is the recommended mode when:
770
1065
  This is the full two-step flow most integrators will want to implement from a server route or server action.
771
1066
 
772
1067
  ```typescript
773
- import { createITMPartnerClient } from '@itm-studio/partner-sdk';
1068
+ import { createITMPartnerClient } from "@itm-studio/partner-sdk";
774
1069
 
775
1070
  function createRequestScopedITM(request: Request) {
776
1071
  return createITMPartnerClient({
777
1072
  token: process.env.ITM_PARTNER_TOKEN!,
778
1073
  headers: () => ({
779
- ...(request.headers.get('x-forwarded-for')
780
- ? { 'x-forwarded-for': request.headers.get('x-forwarded-for')! }
1074
+ ...(request.headers.get("x-forwarded-for")
1075
+ ? { "x-forwarded-for": request.headers.get("x-forwarded-for")! }
781
1076
  : {}),
782
- ...(request.headers.get('user-agent')
783
- ? { 'user-agent': request.headers.get('user-agent')! }
1077
+ ...(request.headers.get("user-agent")
1078
+ ? { "user-agent": request.headers.get("user-agent")! }
784
1079
  : {}),
785
1080
  }),
786
1081
  });
@@ -793,24 +1088,24 @@ export async function startOtpSubscription(request: Request) {
793
1088
  createPartnerBrandSubscription: {
794
1089
  __args: {
795
1090
  input: {
796
- phoneNumber: '+14155550112',
1091
+ phoneNumber: "+14155550112",
797
1092
  requireOtpVerification: true,
798
- fullName: 'Otp User',
799
- email: 'otp@example.com',
800
- listNames: ['Members', 'SMS'],
1093
+ fullName: "Otp User",
1094
+ email: "otp@example.com",
1095
+ listNames: ["Members", "SMS"],
801
1096
  compliance: {
802
- pageUrl: 'https://example.com/join',
803
- submissionMethod: 'partner_sdk',
1097
+ pageUrl: "https://example.com/join",
1098
+ submissionMethod: "partner_sdk",
804
1099
  submittedAt: new Date().toISOString(),
805
- timezone: 'America/New_York',
1100
+ timezone: "America/New_York",
806
1101
  },
807
1102
  postSubscriptionMessage: {
808
1103
  sms: {
809
- body: 'You are subscribed. Reply STOP to opt out.',
1104
+ body: "You are subscribed. Reply STOP to opt out.",
810
1105
  },
811
1106
  email: {
812
- subject: 'Welcome',
813
- body: 'Your subscription is confirmed.',
1107
+ subject: "Welcome",
1108
+ body: "Your subscription is confirmed.",
814
1109
  },
815
1110
  },
816
1111
  },
@@ -825,13 +1120,13 @@ export async function startOtpSubscription(request: Request) {
825
1120
 
826
1121
  if (!createPartnerBrandSubscription.verificationRequired) {
827
1122
  return {
828
- status: 'subscribed',
1123
+ status: "subscribed",
829
1124
  subscriptionUid: createPartnerBrandSubscription.subscription?.uid ?? null,
830
1125
  };
831
1126
  }
832
1127
 
833
1128
  return {
834
- status: 'otp_required',
1129
+ status: "otp_required",
835
1130
  verificationToken: createPartnerBrandSubscription.verificationToken,
836
1131
  };
837
1132
  }
@@ -839,7 +1134,7 @@ export async function startOtpSubscription(request: Request) {
839
1134
  export async function finishOtpSubscription(
840
1135
  request: Request,
841
1136
  verificationToken: string,
842
- code: string,
1137
+ code: string
843
1138
  ) {
844
1139
  const itm = createRequestScopedITM(request);
845
1140
 
@@ -880,7 +1175,7 @@ Get a presigned S3 URL to upload a file directly.
880
1175
  ```typescript
881
1176
  const { getPartnerUploadUrl } = await itm.mutation({
882
1177
  getPartnerUploadUrl: {
883
- __args: { filename: 'event-cover', fileExtension: 'jpg' },
1178
+ __args: { filename: "event-cover", fileExtension: "jpg" },
884
1179
  url: true,
885
1180
  filename: true,
886
1181
  },
@@ -888,9 +1183,9 @@ const { getPartnerUploadUrl } = await itm.mutation({
888
1183
 
889
1184
  // Upload the file to S3 using the presigned URL
890
1185
  await fetch(getPartnerUploadUrl.url, {
891
- method: 'PUT',
1186
+ method: "PUT",
892
1187
  body: fileBuffer, // Buffer, Blob, or ReadableStream
893
- headers: { 'Content-Type': 'image/jpeg' },
1188
+ headers: { "Content-Type": "image/jpeg" },
894
1189
  });
895
1190
  ```
896
1191
 
@@ -902,7 +1197,7 @@ After uploading, register the asset so it's linked to your brand.
902
1197
  const { createPartnerMediaAsset } = await itm.mutation({
903
1198
  createPartnerMediaAsset: {
904
1199
  __args: {
905
- mimetype: 'image/jpeg',
1200
+ mimetype: "image/jpeg",
906
1201
  filename: getPartnerUploadUrl.filename, // from Step 1
907
1202
  dimensions: { width: 1920, height: 1080 },
908
1203
  },
@@ -924,37 +1219,38 @@ const { createPartnerMoment } = await itm.mutation({
924
1219
  createPartnerMoment: {
925
1220
  __args: {
926
1221
  input: {
927
- name: 'Summer Rooftop Party',
928
- description: 'A rooftop party in NYC',
929
- blurb: 'Join us on the roof!',
930
- startDate: '2026-07-15T20:00:00.000Z',
931
- endDate: '2026-07-16T02:00:00.000Z',
932
- timezone: 'America/New_York',
933
- type: 'IRL',
1222
+ name: "Summer Rooftop Party",
1223
+ description: "A rooftop party in NYC",
1224
+ blurb: "Join us on the roof!",
1225
+ startDate: "2026-07-15T20:00:00.000Z",
1226
+ endDate: "2026-07-16T02:00:00.000Z",
1227
+ timezone: "America/New_York",
1228
+ type: "IRL",
1229
+ capacity: 250,
934
1230
  coverImageUid: createPartnerMediaAsset.uid, // from upload flow
935
1231
  venue: {
936
- name: 'Rooftop Bar',
937
- city: 'New York',
938
- country: 'US',
939
- address: '123 Main St',
1232
+ name: "Rooftop Bar",
1233
+ city: "New York",
1234
+ country: "US",
1235
+ address: "123 Main St",
940
1236
  },
941
- category: 'PARTY',
942
- subcategory: 'ROOFTOP_PARTY',
1237
+ category: "PARTY",
1238
+ subcategory: "ROOFTOP_PARTY",
943
1239
  ticketTiers: [
944
1240
  {
945
- name: 'General Admission',
946
- price: 2500, // $25.00 in cents
1241
+ name: "General Admission",
1242
+ price: 2500, // $25.00 in cents
947
1243
  supply: 200,
948
1244
  maxPerUser: 4,
949
- currencyCode: 'USD',
1245
+ currencyCode: "USD",
950
1246
  },
951
1247
  {
952
- name: 'VIP',
953
- price: 7500, // $75.00 in cents
1248
+ name: "VIP",
1249
+ price: 7500, // $75.00 in cents
954
1250
  supply: 50,
955
1251
  maxPerUser: 2,
956
- currencyCode: 'USD',
957
- description: 'Includes open bar',
1252
+ currencyCode: "USD",
1253
+ description: "Includes open bar",
958
1254
  },
959
1255
  ],
960
1256
  },
@@ -965,6 +1261,7 @@ const { createPartnerMoment } = await itm.mutation({
965
1261
  startDate: true,
966
1262
  endDate: true,
967
1263
  status: true,
1264
+ capacity: true,
968
1265
  coverImage: { url: true },
969
1266
  venue: { name: true, city: true },
970
1267
  ticketTiers: {
@@ -978,19 +1275,93 @@ const { createPartnerMoment } = await itm.mutation({
978
1275
  });
979
1276
  ```
980
1277
 
981
- **Required fields:** `name`, `description`, `blurb`, `startDate`, `endDate`, `timezone`, `type`
1278
+ **Required fields:** `name`, `description`, `startDate`, `endDate`, `timezone`, `type`
982
1279
 
983
1280
  **Moment types:**
1281
+
984
1282
  - `IRL` — in-person event (requires `venue`)
985
- - `DIGITAL` — online event (no venue needed)
1283
+ - `DIGITAL` — online event (no venue needed; accepts `onlineConfig`)
1284
+ - `HYBRID` — venue plus online access. When creating ticket tiers for a hybrid moment, set each tier's `attendanceMode` to `IN_PERSON` or `ONLINE`.
1285
+
1286
+ For digital or hybrid moments, pass `onlineConfig` when you want ITM to store the meeting details:
1287
+
1288
+ ```typescript
1289
+ await itm.mutation({
1290
+ createPartnerMoment: {
1291
+ __args: {
1292
+ input: {
1293
+ name: "Hybrid Show",
1294
+ description: "Attend in person or online",
1295
+ startDate: "2026-07-15T20:00:00.000Z",
1296
+ endDate: "2026-07-15T23:00:00.000Z",
1297
+ timezone: "America/New_York",
1298
+ type: "HYBRID",
1299
+ onlineConfig: {
1300
+ provider: "ZOOM", // GOOGLE_MEET | MS_TEAMS | OTHER | ZOOM
1301
+ meetingUrl: "https://zoom.us/j/123456789",
1302
+ meetingPasscode: "club",
1303
+ },
1304
+ venue: { name: "Main Room", city: "New York", country: "US" },
1305
+ ticketTiers: [
1306
+ {
1307
+ name: "Online Admission",
1308
+ price: 1500,
1309
+ supply: 300,
1310
+ maxPerUser: 4,
1311
+ currencyCode: "USD",
1312
+ attendanceMode: "ONLINE",
1313
+ },
1314
+ ],
1315
+ },
1316
+ },
1317
+ uid: true,
1318
+ type: true,
1319
+ ticketTiers: { uid: true, attendanceMode: true },
1320
+ },
1321
+ });
1322
+ ```
986
1323
 
987
1324
  **Ticket tier pricing:** `price` is in **cents** (e.g. `2500` = $25.00 for USD). Use `0` for free tiers. See [Money Amounts](#money-amounts) for display formatting, including zero-decimal currencies.
988
1325
 
1326
+ ### `editPartnerTicketTiersForMoment`
1327
+
1328
+ Bulk edit partner-owned ticket tiers for a moment. Provide either `momentSlug` or
1329
+ `momentUid`, then target specific tiers by `ticketTierUid`.
1330
+
1331
+ This endpoint intentionally supports only `price` and `supply` updates. Use
1332
+ `getPartnerTicketTiersForMoment` first if you need to discover tier UIDs.
1333
+
1334
+ ```typescript
1335
+ const { editPartnerTicketTiersForMoment } = await itm.mutation({
1336
+ editPartnerTicketTiersForMoment: {
1337
+ __args: {
1338
+ input: {
1339
+ momentSlug: "my-event-slug",
1340
+ updates: [
1341
+ {
1342
+ ticketTierUid: "tier-uid",
1343
+ price: 3000, // cents
1344
+ supply: 120,
1345
+ },
1346
+ ],
1347
+ },
1348
+ },
1349
+ uid: true,
1350
+ name: true,
1351
+ price: true,
1352
+ supply: true,
1353
+ remainingSupply: true,
1354
+ fillRate: true,
1355
+ isActive: true,
1356
+ },
1357
+ });
1358
+ ```
1359
+
989
1360
  ### Complete Upload + Create Moment Flow
990
1361
 
991
1362
  ```typescript
992
- import { createITMPartnerClient } from '@itm-studio/partner-sdk';
993
- import { readFile } from 'fs/promises';
1363
+ import { createITMPartnerClient } from "@itm-studio/partner-sdk";
1364
+ import { readFile } from "fs/promises";
994
1365
 
995
1366
  const itm = createITMPartnerClient({
996
1367
  token: process.env.ITM_PARTNER_TOKEN!,
@@ -999,25 +1370,25 @@ const itm = createITMPartnerClient({
999
1370
  // 1. Get presigned upload URL
1000
1371
  const { getPartnerUploadUrl } = await itm.mutation({
1001
1372
  getPartnerUploadUrl: {
1002
- __args: { filename: 'event-cover', fileExtension: 'jpg' },
1373
+ __args: { filename: "event-cover", fileExtension: "jpg" },
1003
1374
  url: true,
1004
1375
  filename: true,
1005
1376
  },
1006
1377
  });
1007
1378
 
1008
1379
  // 2. Upload file to S3
1009
- const file = await readFile('./event-cover.jpg');
1380
+ const file = await readFile("./event-cover.jpg");
1010
1381
  await fetch(getPartnerUploadUrl.url, {
1011
- method: 'PUT',
1382
+ method: "PUT",
1012
1383
  body: file,
1013
- headers: { 'Content-Type': 'image/jpeg' },
1384
+ headers: { "Content-Type": "image/jpeg" },
1014
1385
  });
1015
1386
 
1016
1387
  // 3. Register the uploaded asset
1017
1388
  const { createPartnerMediaAsset } = await itm.mutation({
1018
1389
  createPartnerMediaAsset: {
1019
1390
  __args: {
1020
- mimetype: 'image/jpeg',
1391
+ mimetype: "image/jpeg",
1021
1392
  filename: getPartnerUploadUrl.filename,
1022
1393
  dimensions: { width: 1920, height: 1080 },
1023
1394
  },
@@ -1030,26 +1401,26 @@ const { createPartnerMoment } = await itm.mutation({
1030
1401
  createPartnerMoment: {
1031
1402
  __args: {
1032
1403
  input: {
1033
- name: 'My Event',
1034
- description: 'An amazing event',
1035
- blurb: 'Come join us!',
1036
- startDate: '2026-07-15T20:00:00.000Z',
1037
- endDate: '2026-07-16T02:00:00.000Z',
1038
- timezone: 'America/New_York',
1039
- type: 'IRL',
1404
+ name: "My Event",
1405
+ description: "An amazing event",
1406
+ blurb: "Come join us!",
1407
+ startDate: "2026-07-15T20:00:00.000Z",
1408
+ endDate: "2026-07-16T02:00:00.000Z",
1409
+ timezone: "America/New_York",
1410
+ type: "IRL",
1040
1411
  coverImageUid: createPartnerMediaAsset.uid,
1041
1412
  venue: {
1042
- name: 'The Venue',
1043
- city: 'New York',
1044
- country: 'US',
1413
+ name: "The Venue",
1414
+ city: "New York",
1415
+ country: "US",
1045
1416
  },
1046
1417
  ticketTiers: [
1047
1418
  {
1048
- name: 'General Admission',
1419
+ name: "General Admission",
1049
1420
  price: 2500,
1050
1421
  supply: 100,
1051
1422
  maxPerUser: 4,
1052
- currencyCode: 'USD',
1423
+ currencyCode: "USD",
1053
1424
  },
1054
1425
  ],
1055
1426
  },
@@ -1061,7 +1432,9 @@ const { createPartnerMoment } = await itm.mutation({
1061
1432
  },
1062
1433
  });
1063
1434
 
1064
- console.log(`Created: ${createPartnerMoment.slug} (${createPartnerMoment.status})`);
1435
+ console.log(
1436
+ `Created: ${createPartnerMoment.slug} (${createPartnerMoment.status})`
1437
+ );
1065
1438
  ```
1066
1439
 
1067
1440
  ## Pagination
@@ -1074,7 +1447,7 @@ let cursor: string | null = null;
1074
1447
  do {
1075
1448
  const { getPartnerTicketsForMoment } = await itm.query({
1076
1449
  getPartnerTicketsForMoment: {
1077
- __args: { momentSlug: 'my-event', cursor, take: 50 },
1450
+ __args: { momentSlug: "my-event", cursor, take: 50 },
1078
1451
  tickets: { uid: true, user: { name: true } },
1079
1452
  hasNextPage: true,
1080
1453
  nextCursor: true,
@@ -1097,7 +1470,7 @@ For staging or local development:
1097
1470
  ```typescript
1098
1471
  const itm = createITMPartnerClient({
1099
1472
  token: process.env.ITM_PARTNER_TOKEN!,
1100
- baseUrl: 'https://staging-api.itm.studio/graphql',
1473
+ baseUrl: "https://staging-api.itm.studio/graphql",
1101
1474
  });
1102
1475
  ```
1103
1476
 
@@ -1157,12 +1530,12 @@ npm run schema:publish # Publish schema to registry
1157
1530
 
1158
1531
  ## CI/CD
1159
1532
 
1160
- | Job | Trigger | What it does |
1161
- |---|---|---|
1162
- | `build-and-test` | Every push & PR | Install, generate, lint, test, build |
1163
- | `schema-check` | PRs only | Validates schema changes won't break partners |
1164
- | `schema-publish` | Push to main | Publishes schema to Hive registry |
1165
- | `publish` | Push to main | Auto-bumps patch version and publishes to NPM |
1533
+ | Job | Trigger | What it does |
1534
+ | ---------------- | --------------- | --------------------------------------------- |
1535
+ | `build-and-test` | Every push & PR | Install, generate, lint, test, build |
1536
+ | `schema-check` | PRs only | Validates schema changes won't break partners |
1537
+ | `schema-publish` | Push to main | Publishes schema to Hive registry |
1538
+ | `publish` | Push to main | Auto-bumps patch version and publishes to NPM |
1166
1539
 
1167
1540
  ## Adding New Partner Endpoints
1168
1541