@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 +543 -170
- package/dist/index.d.mts +733 -9
- package/dist/index.d.ts +733 -9
- package/dist/index.js +1404 -427
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1404 -427
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/schema/partner.graphql +532 -7
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
|
|
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:
|
|
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
|
|
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(
|
|
80
|
-
const userAgent = request.headers.get(
|
|
79
|
+
const forwardedFor = request.headers.get("x-forwarded-for");
|
|
80
|
+
const userAgent = request.headers.get("user-agent");
|
|
81
81
|
|
|
82
82
|
return {
|
|
83
|
-
...(forwardedFor ? {
|
|
84
|
-
...(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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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([
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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,
|
|
128
|
-
cursor: nextCursor,
|
|
129
|
-
status:
|
|
130
|
-
sortOrder:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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:
|
|
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
|
-
### `
|
|
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 {
|
|
246
|
-
|
|
247
|
-
__args: { momentSlug:
|
|
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:** `
|
|
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:
|
|
267
|
-
checkedIn: true,
|
|
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:
|
|
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:
|
|
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:
|
|
649
|
+
__args: { slug: "summer-series" },
|
|
354
650
|
name: true,
|
|
355
651
|
moments: {
|
|
356
|
-
__args: { queryFilters: [
|
|
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:
|
|
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:
|
|
449
|
-
status:
|
|
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
|
|
474
|
-
|
|
475
|
-
| Direct | `createPartnerBrandSubscription` with `requireOtpVerification: false` or omitted | `verificationRequired: false` and `subscription` populated
|
|
476
|
-
| OTP
|
|
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
|
|
807
|
+
} from "@itm-studio/partner-sdk";
|
|
512
808
|
|
|
513
809
|
const welcomeMessage: PostSubscriptionMessageInput = {
|
|
514
810
|
sms: {
|
|
515
|
-
body:
|
|
516
|
-
mediaUrls: [
|
|
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:
|
|
520
|
-
body:
|
|
815
|
+
subject: "Welcome to Acme",
|
|
816
|
+
body: "Thanks for subscribing. Your first update is on the way.",
|
|
521
817
|
},
|
|
522
818
|
push: {
|
|
523
|
-
title:
|
|
524
|
-
body:
|
|
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:
|
|
530
|
-
fullName:
|
|
531
|
-
email:
|
|
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:
|
|
542
|
-
fullName:
|
|
543
|
-
email:
|
|
837
|
+
phoneNumber: "+14155550111",
|
|
838
|
+
fullName: "Jane Doe",
|
|
839
|
+
email: "jane@example.com",
|
|
544
840
|
postSubscriptionMessage: {
|
|
545
841
|
sms: {
|
|
546
|
-
body:
|
|
547
|
-
mediaUrls: [
|
|
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:
|
|
551
|
-
body:
|
|
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:
|
|
555
|
-
body:
|
|
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
|
|
569
|
-
|
|
570
|
-
| Direct subscribe | Initial `createPartnerBrandSubscription` call
|
|
571
|
-
| OTP subscribe
|
|
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
|
|
578
|
-
|
|
579
|
-
| `sms`
|
|
580
|
-
| `email` | `subject`, `body` | —
|
|
581
|
-
| `push`
|
|
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:
|
|
901
|
+
phoneNumber: "+14155550111",
|
|
606
902
|
postSubscriptionMessage: {
|
|
607
903
|
sms: {
|
|
608
|
-
body:
|
|
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:
|
|
922
|
+
phoneNumber: "+14155550112",
|
|
627
923
|
requireOtpVerification: true,
|
|
628
|
-
email:
|
|
924
|
+
email: "jane@example.com",
|
|
629
925
|
postSubscriptionMessage: {
|
|
630
926
|
sms: {
|
|
631
|
-
body:
|
|
927
|
+
body: "Thanks for joining Acme. Reply STOP to opt out.",
|
|
632
928
|
},
|
|
633
929
|
email: {
|
|
634
|
-
subject:
|
|
635
|
-
body:
|
|
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:
|
|
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:
|
|
675
|
-
fullName:
|
|
676
|
-
email:
|
|
677
|
-
listNames: [
|
|
970
|
+
phoneNumber: "+14155550111",
|
|
971
|
+
fullName: "Jane Doe",
|
|
972
|
+
email: "jane@example.com",
|
|
973
|
+
listNames: ["VIP", "Newsletter"],
|
|
678
974
|
compliance: {
|
|
679
|
-
pageUrl:
|
|
680
|
-
submissionMethod:
|
|
975
|
+
pageUrl: "https://example.com/signup",
|
|
976
|
+
submissionMethod: "partner_sdk",
|
|
681
977
|
},
|
|
682
978
|
postSubscriptionMessage: {
|
|
683
979
|
sms: {
|
|
684
|
-
body:
|
|
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:
|
|
1017
|
+
phoneNumber: "+14155550112",
|
|
722
1018
|
requireOtpVerification: true,
|
|
723
|
-
fullName:
|
|
1019
|
+
fullName: "Otp User",
|
|
724
1020
|
compliance: {
|
|
725
|
-
pageUrl:
|
|
726
|
-
submissionMethod:
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
780
|
-
? {
|
|
1074
|
+
...(request.headers.get("x-forwarded-for")
|
|
1075
|
+
? { "x-forwarded-for": request.headers.get("x-forwarded-for")! }
|
|
781
1076
|
: {}),
|
|
782
|
-
...(request.headers.get(
|
|
783
|
-
? {
|
|
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:
|
|
1091
|
+
phoneNumber: "+14155550112",
|
|
797
1092
|
requireOtpVerification: true,
|
|
798
|
-
fullName:
|
|
799
|
-
email:
|
|
800
|
-
listNames: [
|
|
1093
|
+
fullName: "Otp User",
|
|
1094
|
+
email: "otp@example.com",
|
|
1095
|
+
listNames: ["Members", "SMS"],
|
|
801
1096
|
compliance: {
|
|
802
|
-
pageUrl:
|
|
803
|
-
submissionMethod:
|
|
1097
|
+
pageUrl: "https://example.com/join",
|
|
1098
|
+
submissionMethod: "partner_sdk",
|
|
804
1099
|
submittedAt: new Date().toISOString(),
|
|
805
|
-
timezone:
|
|
1100
|
+
timezone: "America/New_York",
|
|
806
1101
|
},
|
|
807
1102
|
postSubscriptionMessage: {
|
|
808
1103
|
sms: {
|
|
809
|
-
body:
|
|
1104
|
+
body: "You are subscribed. Reply STOP to opt out.",
|
|
810
1105
|
},
|
|
811
1106
|
email: {
|
|
812
|
-
subject:
|
|
813
|
-
body:
|
|
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:
|
|
1123
|
+
status: "subscribed",
|
|
829
1124
|
subscriptionUid: createPartnerBrandSubscription.subscription?.uid ?? null,
|
|
830
1125
|
};
|
|
831
1126
|
}
|
|
832
1127
|
|
|
833
1128
|
return {
|
|
834
|
-
status:
|
|
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:
|
|
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:
|
|
1186
|
+
method: "PUT",
|
|
892
1187
|
body: fileBuffer, // Buffer, Blob, or ReadableStream
|
|
893
|
-
headers: {
|
|
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:
|
|
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:
|
|
928
|
-
description:
|
|
929
|
-
blurb:
|
|
930
|
-
startDate:
|
|
931
|
-
endDate:
|
|
932
|
-
timezone:
|
|
933
|
-
type:
|
|
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:
|
|
937
|
-
city:
|
|
938
|
-
country:
|
|
939
|
-
address:
|
|
1232
|
+
name: "Rooftop Bar",
|
|
1233
|
+
city: "New York",
|
|
1234
|
+
country: "US",
|
|
1235
|
+
address: "123 Main St",
|
|
940
1236
|
},
|
|
941
|
-
category:
|
|
942
|
-
subcategory:
|
|
1237
|
+
category: "PARTY",
|
|
1238
|
+
subcategory: "ROOFTOP_PARTY",
|
|
943
1239
|
ticketTiers: [
|
|
944
1240
|
{
|
|
945
|
-
name:
|
|
946
|
-
price: 2500,
|
|
1241
|
+
name: "General Admission",
|
|
1242
|
+
price: 2500, // $25.00 in cents
|
|
947
1243
|
supply: 200,
|
|
948
1244
|
maxPerUser: 4,
|
|
949
|
-
currencyCode:
|
|
1245
|
+
currencyCode: "USD",
|
|
950
1246
|
},
|
|
951
1247
|
{
|
|
952
|
-
name:
|
|
953
|
-
price: 7500,
|
|
1248
|
+
name: "VIP",
|
|
1249
|
+
price: 7500, // $75.00 in cents
|
|
954
1250
|
supply: 50,
|
|
955
1251
|
maxPerUser: 2,
|
|
956
|
-
currencyCode:
|
|
957
|
-
description:
|
|
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`, `
|
|
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
|
|
993
|
-
import { readFile } from
|
|
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:
|
|
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(
|
|
1380
|
+
const file = await readFile("./event-cover.jpg");
|
|
1010
1381
|
await fetch(getPartnerUploadUrl.url, {
|
|
1011
|
-
method:
|
|
1382
|
+
method: "PUT",
|
|
1012
1383
|
body: file,
|
|
1013
|
-
headers: {
|
|
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:
|
|
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:
|
|
1034
|
-
description:
|
|
1035
|
-
blurb:
|
|
1036
|
-
startDate:
|
|
1037
|
-
endDate:
|
|
1038
|
-
timezone:
|
|
1039
|
-
type:
|
|
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:
|
|
1043
|
-
city:
|
|
1044
|
-
country:
|
|
1413
|
+
name: "The Venue",
|
|
1414
|
+
city: "New York",
|
|
1415
|
+
country: "US",
|
|
1045
1416
|
},
|
|
1046
1417
|
ticketTiers: [
|
|
1047
1418
|
{
|
|
1048
|
-
name:
|
|
1419
|
+
name: "General Admission",
|
|
1049
1420
|
price: 2500,
|
|
1050
1421
|
supply: 100,
|
|
1051
1422
|
maxPerUser: 4,
|
|
1052
|
-
currencyCode:
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
|
1161
|
-
|
|
1162
|
-
| `build-and-test` | Every push & PR | Install, generate, lint, test, build
|
|
1163
|
-
| `schema-check`
|
|
1164
|
-
| `schema-publish` | Push to main
|
|
1165
|
-
| `publish`
|
|
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
|
|