@itm-studio/partner-sdk 0.2.6 → 0.2.7
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 +462 -170
- package/dist/index.d.mts +618 -9
- package/dist/index.d.ts +618 -9
- package/dist/index.js +1233 -383
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1233 -383
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/schema/partner.graphql +460 -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,6 +134,95 @@ 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
|
|
|
119
228
|
### `getPartnerMomentsForBrand`
|
|
@@ -124,10 +233,10 @@ Get paginated moments (events) for your brand.
|
|
|
124
233
|
const { getPartnerMomentsForBrand } = await itm.query({
|
|
125
234
|
getPartnerMomentsForBrand: {
|
|
126
235
|
__args: {
|
|
127
|
-
take: 20,
|
|
128
|
-
cursor: nextCursor,
|
|
129
|
-
status:
|
|
130
|
-
sortOrder:
|
|
236
|
+
take: 20, // items per page (default: 50, omit to use default)
|
|
237
|
+
cursor: nextCursor, // from previous response
|
|
238
|
+
status: "UPCOMING", // UPCOMING | LIVE | ENDED
|
|
239
|
+
sortOrder: "ASC", // ASC | DESC by start date (default: DESC)
|
|
131
240
|
},
|
|
132
241
|
moments: { uid: true, name: true, startDate: true },
|
|
133
242
|
totalCount: true,
|
|
@@ -137,6 +246,60 @@ const { getPartnerMomentsForBrand } = await itm.query({
|
|
|
137
246
|
});
|
|
138
247
|
```
|
|
139
248
|
|
|
249
|
+
### `getPartnerDrops`
|
|
250
|
+
|
|
251
|
+
Get paginated brand drops for the brand associated with your partner token.
|
|
252
|
+
Use this for partner-hosted drop grids, release pages, and filtered merch or
|
|
253
|
+
music drop listings.
|
|
254
|
+
|
|
255
|
+
Supported filters:
|
|
256
|
+
|
|
257
|
+
- `search`: case-insensitive match across drop name, subtitle, and slug.
|
|
258
|
+
- `isPublic`: filter public or private drops.
|
|
259
|
+
- `ctaType`: filter by `EXTERNAL_URL`, `GATED_LINK`, `MUSIC_LINK`, or `SUBSCRIBE`.
|
|
260
|
+
- `cursor` and `take`: use `nextCursor` while `hasNextPage` is true.
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
const { getPartnerDrops } = await itm.query({
|
|
264
|
+
getPartnerDrops: {
|
|
265
|
+
__args: {
|
|
266
|
+
take: 20,
|
|
267
|
+
cursor: nextCursor,
|
|
268
|
+
search: "release",
|
|
269
|
+
isPublic: true,
|
|
270
|
+
ctaType: "EXTERNAL_URL", // EXTERNAL_URL | GATED_LINK | MUSIC_LINK | SUBSCRIBE
|
|
271
|
+
},
|
|
272
|
+
drops: {
|
|
273
|
+
uid: true,
|
|
274
|
+
name: true,
|
|
275
|
+
slug: true,
|
|
276
|
+
isPublic: true,
|
|
277
|
+
ctaType: true,
|
|
278
|
+
ctaUrl: true,
|
|
279
|
+
captureName: true,
|
|
280
|
+
captureEmail: true,
|
|
281
|
+
coverImage: { url: true, mimeType: true },
|
|
282
|
+
coverImageMobile: { url: true, mimeType: true },
|
|
283
|
+
musicLink: {
|
|
284
|
+
title: true,
|
|
285
|
+
artistName: true,
|
|
286
|
+
releaseType: true,
|
|
287
|
+
sourceUrl: true,
|
|
288
|
+
odesliPageUrl: true,
|
|
289
|
+
thumbnailUrl: true,
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
totalCount: true,
|
|
293
|
+
hasNextPage: true,
|
|
294
|
+
nextCursor: true,
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The response is always scoped by the partner token; do not pass a brand ID.
|
|
300
|
+
`coverImageMobile` and `musicLink` can be `null`, so handle those as optional in
|
|
301
|
+
rendering code.
|
|
302
|
+
|
|
140
303
|
### `getPartnerMoment`
|
|
141
304
|
|
|
142
305
|
Get a single moment by slug.
|
|
@@ -144,7 +307,7 @@ Get a single moment by slug.
|
|
|
144
307
|
```typescript
|
|
145
308
|
const { getPartnerMoment } = await itm.query({
|
|
146
309
|
getPartnerMoment: {
|
|
147
|
-
__args: { slug:
|
|
310
|
+
__args: { slug: "my-event-slug" },
|
|
148
311
|
uid: true,
|
|
149
312
|
name: true,
|
|
150
313
|
startDate: true,
|
|
@@ -172,7 +335,7 @@ Get approved public lineup entries for a moment. Use `getPartnerMoment` first if
|
|
|
172
335
|
```typescript
|
|
173
336
|
const { getPartnerMoment } = await itm.query({
|
|
174
337
|
getPartnerMoment: {
|
|
175
|
-
__args: { slug:
|
|
338
|
+
__args: { slug: "my-event-slug" },
|
|
176
339
|
uid: true,
|
|
177
340
|
lineupDisplayLabel: true,
|
|
178
341
|
},
|
|
@@ -186,7 +349,7 @@ if (momentUid) {
|
|
|
186
349
|
__args: {
|
|
187
350
|
momentUid,
|
|
188
351
|
take: 50,
|
|
189
|
-
lineupType:
|
|
352
|
+
lineupType: "DJ", // optional: LINEUP | CO_HOST | PROMOTER | DJ | SPEAKER | VENDOR
|
|
190
353
|
},
|
|
191
354
|
lineups: {
|
|
192
355
|
uid: true,
|
|
@@ -206,7 +369,10 @@ if (momentUid) {
|
|
|
206
369
|
},
|
|
207
370
|
});
|
|
208
371
|
|
|
209
|
-
console.log(
|
|
372
|
+
console.log(
|
|
373
|
+
getPartnerMoment?.lineupDisplayLabel,
|
|
374
|
+
getPublicMomentLineups.lineups
|
|
375
|
+
);
|
|
210
376
|
}
|
|
211
377
|
```
|
|
212
378
|
|
|
@@ -221,7 +387,7 @@ Get public lineup invitation details by response token. This is useful if you bu
|
|
|
221
387
|
```typescript
|
|
222
388
|
const { getMomentLineupByToken } = await itm.query({
|
|
223
389
|
getMomentLineupByToken: {
|
|
224
|
-
__args: { token:
|
|
390
|
+
__args: { token: "lineup-response-token" },
|
|
225
391
|
artistName: true,
|
|
226
392
|
momentName: true,
|
|
227
393
|
brandName: true,
|
|
@@ -232,7 +398,7 @@ const { getMomentLineupByToken } = await itm.query({
|
|
|
232
398
|
});
|
|
233
399
|
```
|
|
234
400
|
|
|
235
|
-
### `
|
|
401
|
+
### `getPartnerMomentStats`
|
|
236
402
|
|
|
237
403
|
Get cached partner-facing counts for a moment owned by the authenticated partner's brand.
|
|
238
404
|
|
|
@@ -242,9 +408,9 @@ Get cached partner-facing counts for a moment owned by the authenticated partner
|
|
|
242
408
|
- `ticketRsvpRequestsCount` counts ticket RSVP requests.
|
|
243
409
|
|
|
244
410
|
```typescript
|
|
245
|
-
const {
|
|
246
|
-
|
|
247
|
-
__args: { momentSlug:
|
|
411
|
+
const { getPartnerMomentStats } = await itm.query({
|
|
412
|
+
getPartnerMomentStats: {
|
|
413
|
+
__args: { momentSlug: "my-event-slug" },
|
|
248
414
|
ticketsCount: true,
|
|
249
415
|
waitlistCount: true,
|
|
250
416
|
externalRsvpCount: true,
|
|
@@ -253,7 +419,56 @@ const { getMomentStats } = await itm.query({
|
|
|
253
419
|
});
|
|
254
420
|
```
|
|
255
421
|
|
|
256
|
-
> **Note:** `
|
|
422
|
+
> **Note:** `getPartnerMomentStats` returns `null` if the moment does not exist or is not owned by the authenticated partner's brand.
|
|
423
|
+
|
|
424
|
+
### `getPartnerTicketTiersForMoment`
|
|
425
|
+
|
|
426
|
+
Get paginated, partner-facing ticket tiers for a moment. Use this for pricing,
|
|
427
|
+
inventory, and dynamic catalog views without selecting the full public `Moment`
|
|
428
|
+
payload.
|
|
429
|
+
|
|
430
|
+
Provide either `momentSlug` or `momentUid`. If `isActive` is omitted, the backend
|
|
431
|
+
returns active tiers only; pass `isActive: false` only when you intentionally need
|
|
432
|
+
inactive tiers.
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
const { getPartnerTicketTiersForMoment } = await itm.query({
|
|
436
|
+
getPartnerTicketTiersForMoment: {
|
|
437
|
+
__args: {
|
|
438
|
+
input: {
|
|
439
|
+
momentSlug: "my-event-slug",
|
|
440
|
+
take: 20,
|
|
441
|
+
search: "general",
|
|
442
|
+
isPaid: true,
|
|
443
|
+
minPrice: 1000, // cents
|
|
444
|
+
maxPrice: 5000, // cents
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
ticketTiers: {
|
|
448
|
+
uid: true,
|
|
449
|
+
name: true,
|
|
450
|
+
label: true,
|
|
451
|
+
labelColor: true,
|
|
452
|
+
price: true, // cents
|
|
453
|
+
supply: true,
|
|
454
|
+
remainingSupply: true,
|
|
455
|
+
fillRate: true, // 0-1 sold-through ratio
|
|
456
|
+
isActive: true,
|
|
457
|
+
isPrivate: true,
|
|
458
|
+
slotDate: true,
|
|
459
|
+
slotDurationHours: true,
|
|
460
|
+
},
|
|
461
|
+
totalCount: true,
|
|
462
|
+
hasNextPage: true,
|
|
463
|
+
nextCursor: true,
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Supported filters include `ticketTierUids`, `search`, `isActive`, `isPrivate`,
|
|
469
|
+
`isPaid`, exact/min/max `price`, exact/min/max `supply`, exact/min/max
|
|
470
|
+
`remainingSupply`, min/max `fillRate`, admission-window bounds (`startsAt`,
|
|
471
|
+
`endsAt`), and slot date bounds (`slotDateStart`, `slotDateEnd`).
|
|
257
472
|
|
|
258
473
|
### `getPartnerTicketsForMoment`
|
|
259
474
|
|
|
@@ -263,8 +478,8 @@ Get paginated tickets for a moment.
|
|
|
263
478
|
const { getPartnerTicketsForMoment } = await itm.query({
|
|
264
479
|
getPartnerTicketsForMoment: {
|
|
265
480
|
__args: {
|
|
266
|
-
momentSlug:
|
|
267
|
-
checkedIn: true,
|
|
481
|
+
momentSlug: "my-event-slug",
|
|
482
|
+
checkedIn: true, // optional: filter by check-in status
|
|
268
483
|
take: 50,
|
|
269
484
|
},
|
|
270
485
|
tickets: {
|
|
@@ -290,7 +505,7 @@ Get paginated customers (users with their tickets) for a moment.
|
|
|
290
505
|
```typescript
|
|
291
506
|
const { getPartnerCustomersForMoment } = await itm.query({
|
|
292
507
|
getPartnerCustomersForMoment: {
|
|
293
|
-
__args: { momentSlug:
|
|
508
|
+
__args: { momentSlug: "my-event-slug", take: 50 },
|
|
294
509
|
customers: {
|
|
295
510
|
user: { uid: true, name: true, primaryEmail: true, phoneNumber: true },
|
|
296
511
|
tickets: { uid: true, price: true, redeemedAt: true },
|
|
@@ -309,7 +524,7 @@ Get a moment collection by slug. Collections group related moments together (e.g
|
|
|
309
524
|
```typescript
|
|
310
525
|
const { getPartnerMomentCollection } = await itm.query({
|
|
311
526
|
getPartnerMomentCollection: {
|
|
312
|
-
__args: { slug:
|
|
527
|
+
__args: { slug: "summer-series" },
|
|
313
528
|
uid: true,
|
|
314
529
|
name: true,
|
|
315
530
|
description: true,
|
|
@@ -350,10 +565,10 @@ You can filter moments within a collection using `queryFilters`:
|
|
|
350
565
|
```typescript
|
|
351
566
|
const { getPartnerMomentCollection } = await itm.query({
|
|
352
567
|
getPartnerMomentCollection: {
|
|
353
|
-
__args: { slug:
|
|
568
|
+
__args: { slug: "summer-series" },
|
|
354
569
|
name: true,
|
|
355
570
|
moments: {
|
|
356
|
-
__args: { queryFilters: [
|
|
571
|
+
__args: { queryFilters: ["upcoming"] },
|
|
357
572
|
uid: true,
|
|
358
573
|
name: true,
|
|
359
574
|
startDate: true,
|
|
@@ -411,7 +626,7 @@ Filter by moment to get echoes scoped to a specific event:
|
|
|
411
626
|
```typescript
|
|
412
627
|
const { getPartnerPublicEchoes } = await itm.query({
|
|
413
628
|
getPartnerPublicEchoes: {
|
|
414
|
-
__args: { momentSlug:
|
|
629
|
+
__args: { momentSlug: "my-event-slug", take: 10 },
|
|
415
630
|
echoes: {
|
|
416
631
|
uid: true,
|
|
417
632
|
type: true,
|
|
@@ -445,8 +660,8 @@ const { respondToMomentLineup } = await itm.mutation({
|
|
|
445
660
|
respondToMomentLineup: {
|
|
446
661
|
__args: {
|
|
447
662
|
input: {
|
|
448
|
-
token:
|
|
449
|
-
status:
|
|
663
|
+
token: "lineup-response-token",
|
|
664
|
+
status: "APPROVED", // APPROVED or REJECTED
|
|
450
665
|
},
|
|
451
666
|
},
|
|
452
667
|
status: true,
|
|
@@ -470,10 +685,10 @@ Use OTP subscribe when you want the user to confirm the phone number before the
|
|
|
470
685
|
|
|
471
686
|
### Flow Summary
|
|
472
687
|
|
|
473
|
-
| Mode
|
|
474
|
-
|
|
475
|
-
| Direct | `createPartnerBrandSubscription` with `requireOtpVerification: false` or omitted | `verificationRequired: false` and `subscription` populated
|
|
476
|
-
| OTP
|
|
688
|
+
| Mode | First mutation | First response | Second mutation |
|
|
689
|
+
| ------ | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------------------------------- |
|
|
690
|
+
| Direct | `createPartnerBrandSubscription` with `requireOtpVerification: false` or omitted | `verificationRequired: false` and `subscription` populated | None |
|
|
691
|
+
| OTP | `createPartnerBrandSubscription` with `requireOtpVerification: true` | `verificationRequired: true`, `verificationToken`, `subscription: null` | `verifyPartnerBrandSubscription` |
|
|
477
692
|
|
|
478
693
|
### Input Behavior
|
|
479
694
|
|
|
@@ -508,27 +723,27 @@ The field is part of the generated SDK input types:
|
|
|
508
723
|
import type {
|
|
509
724
|
CreatePartnerBrandSubscriptionInput,
|
|
510
725
|
PostSubscriptionMessageInput,
|
|
511
|
-
} from
|
|
726
|
+
} from "@itm-studio/partner-sdk";
|
|
512
727
|
|
|
513
728
|
const welcomeMessage: PostSubscriptionMessageInput = {
|
|
514
729
|
sms: {
|
|
515
|
-
body:
|
|
516
|
-
mediaUrls: [
|
|
730
|
+
body: "Welcome to Acme. Reply STOP to opt out.",
|
|
731
|
+
mediaUrls: ["https://cdn.example.com/welcome-banner.png"],
|
|
517
732
|
},
|
|
518
733
|
email: {
|
|
519
|
-
subject:
|
|
520
|
-
body:
|
|
734
|
+
subject: "Welcome to Acme",
|
|
735
|
+
body: "Thanks for subscribing. Your first update is on the way.",
|
|
521
736
|
},
|
|
522
737
|
push: {
|
|
523
|
-
title:
|
|
524
|
-
body:
|
|
738
|
+
title: "Welcome to Acme",
|
|
739
|
+
body: "Tap to see what is dropping this week.",
|
|
525
740
|
},
|
|
526
741
|
};
|
|
527
742
|
|
|
528
743
|
const input: CreatePartnerBrandSubscriptionInput = {
|
|
529
|
-
phoneNumber:
|
|
530
|
-
fullName:
|
|
531
|
-
email:
|
|
744
|
+
phoneNumber: "+14155550111",
|
|
745
|
+
fullName: "Jane Doe",
|
|
746
|
+
email: "jane@example.com",
|
|
532
747
|
postSubscriptionMessage: welcomeMessage,
|
|
533
748
|
};
|
|
534
749
|
```
|
|
@@ -538,21 +753,21 @@ await itm.mutation({
|
|
|
538
753
|
createPartnerBrandSubscription: {
|
|
539
754
|
__args: {
|
|
540
755
|
input: {
|
|
541
|
-
phoneNumber:
|
|
542
|
-
fullName:
|
|
543
|
-
email:
|
|
756
|
+
phoneNumber: "+14155550111",
|
|
757
|
+
fullName: "Jane Doe",
|
|
758
|
+
email: "jane@example.com",
|
|
544
759
|
postSubscriptionMessage: {
|
|
545
760
|
sms: {
|
|
546
|
-
body:
|
|
547
|
-
mediaUrls: [
|
|
761
|
+
body: "Welcome to Acme — your first drop ships Friday. Reply STOP to opt out.",
|
|
762
|
+
mediaUrls: ["https://cdn.example.com/welcome-banner.png"],
|
|
548
763
|
},
|
|
549
764
|
email: {
|
|
550
|
-
subject:
|
|
551
|
-
body:
|
|
765
|
+
subject: "Welcome to Acme",
|
|
766
|
+
body: "Hi Jane,\n\nThanks for subscribing — here is what to expect next...",
|
|
552
767
|
},
|
|
553
768
|
push: {
|
|
554
|
-
title:
|
|
555
|
-
body:
|
|
769
|
+
title: "Welcome to Acme",
|
|
770
|
+
body: "Tap to see what is dropping this week.",
|
|
556
771
|
},
|
|
557
772
|
},
|
|
558
773
|
},
|
|
@@ -565,20 +780,20 @@ await itm.mutation({
|
|
|
565
780
|
|
|
566
781
|
**Direct vs OTP timing:**
|
|
567
782
|
|
|
568
|
-
| Mode
|
|
569
|
-
|
|
570
|
-
| Direct subscribe | Initial `createPartnerBrandSubscription` call
|
|
571
|
-
| OTP subscribe
|
|
783
|
+
| Mode | Where to pass `postSubscriptionMessage` | When delivery starts |
|
|
784
|
+
| ---------------- | --------------------------------------------------------------------------------- | ------------------------------------------------ |
|
|
785
|
+
| Direct subscribe | Initial `createPartnerBrandSubscription` call | After the subscription is created or reactivated |
|
|
786
|
+
| OTP subscribe | Initial `createPartnerBrandSubscription` call with `requireOtpVerification: true` | After `verifyPartnerBrandSubscription` succeeds |
|
|
572
787
|
|
|
573
788
|
Do not pass `postSubscriptionMessage` to `verifyPartnerBrandSubscription`; that mutation only accepts `verificationToken` and `code`.
|
|
574
789
|
|
|
575
790
|
**Channel inputs:**
|
|
576
791
|
|
|
577
|
-
| Channel | Required
|
|
578
|
-
|
|
579
|
-
| `sms`
|
|
580
|
-
| `email` | `subject`, `body` | —
|
|
581
|
-
| `push`
|
|
792
|
+
| Channel | Required | Optional | Notes |
|
|
793
|
+
| ------- | ----------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
794
|
+
| `sms` | `body` | `mediaUrls` (string array of image URLs for MMS) | Delivered to `phoneNumber`. |
|
|
795
|
+
| `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. |
|
|
796
|
+
| `push` | `title`, `body` | — | Skipped if the user has no push tokens or has opted out of brand messages. |
|
|
582
797
|
|
|
583
798
|
**Delivery semantics:**
|
|
584
799
|
|
|
@@ -602,10 +817,10 @@ await itm.mutation({
|
|
|
602
817
|
createPartnerBrandSubscription: {
|
|
603
818
|
__args: {
|
|
604
819
|
input: {
|
|
605
|
-
phoneNumber:
|
|
820
|
+
phoneNumber: "+14155550111",
|
|
606
821
|
postSubscriptionMessage: {
|
|
607
822
|
sms: {
|
|
608
|
-
body:
|
|
823
|
+
body: "You are subscribed to Acme updates. Reply STOP to opt out.",
|
|
609
824
|
},
|
|
610
825
|
},
|
|
611
826
|
},
|
|
@@ -623,16 +838,16 @@ const { createPartnerBrandSubscription } = await itm.mutation({
|
|
|
623
838
|
createPartnerBrandSubscription: {
|
|
624
839
|
__args: {
|
|
625
840
|
input: {
|
|
626
|
-
phoneNumber:
|
|
841
|
+
phoneNumber: "+14155550112",
|
|
627
842
|
requireOtpVerification: true,
|
|
628
|
-
email:
|
|
843
|
+
email: "jane@example.com",
|
|
629
844
|
postSubscriptionMessage: {
|
|
630
845
|
sms: {
|
|
631
|
-
body:
|
|
846
|
+
body: "Thanks for joining Acme. Reply STOP to opt out.",
|
|
632
847
|
},
|
|
633
848
|
email: {
|
|
634
|
-
subject:
|
|
635
|
-
body:
|
|
849
|
+
subject: "You are subscribed",
|
|
850
|
+
body: "Your subscription is confirmed. We will send updates here.",
|
|
636
851
|
},
|
|
637
852
|
},
|
|
638
853
|
},
|
|
@@ -648,7 +863,7 @@ await itm.mutation({
|
|
|
648
863
|
__args: {
|
|
649
864
|
input: {
|
|
650
865
|
verificationToken: createPartnerBrandSubscription.verificationToken!,
|
|
651
|
-
code:
|
|
866
|
+
code: "123456",
|
|
652
867
|
},
|
|
653
868
|
},
|
|
654
869
|
uid: true,
|
|
@@ -671,17 +886,17 @@ const { createPartnerBrandSubscription } = await itm.mutation({
|
|
|
671
886
|
createPartnerBrandSubscription: {
|
|
672
887
|
__args: {
|
|
673
888
|
input: {
|
|
674
|
-
phoneNumber:
|
|
675
|
-
fullName:
|
|
676
|
-
email:
|
|
677
|
-
listNames: [
|
|
889
|
+
phoneNumber: "+14155550111",
|
|
890
|
+
fullName: "Jane Doe",
|
|
891
|
+
email: "jane@example.com",
|
|
892
|
+
listNames: ["VIP", "Newsletter"],
|
|
678
893
|
compliance: {
|
|
679
|
-
pageUrl:
|
|
680
|
-
submissionMethod:
|
|
894
|
+
pageUrl: "https://example.com/signup",
|
|
895
|
+
submissionMethod: "partner_sdk",
|
|
681
896
|
},
|
|
682
897
|
postSubscriptionMessage: {
|
|
683
898
|
sms: {
|
|
684
|
-
body:
|
|
899
|
+
body: "Welcome to Acme updates. Reply STOP to opt out.",
|
|
685
900
|
},
|
|
686
901
|
},
|
|
687
902
|
},
|
|
@@ -718,12 +933,12 @@ const { createPartnerBrandSubscription } = await itm.mutation({
|
|
|
718
933
|
createPartnerBrandSubscription: {
|
|
719
934
|
__args: {
|
|
720
935
|
input: {
|
|
721
|
-
phoneNumber:
|
|
936
|
+
phoneNumber: "+14155550112",
|
|
722
937
|
requireOtpVerification: true,
|
|
723
|
-
fullName:
|
|
938
|
+
fullName: "Otp User",
|
|
724
939
|
compliance: {
|
|
725
|
-
pageUrl:
|
|
726
|
-
submissionMethod:
|
|
940
|
+
pageUrl: "https://example.com/otp-signup",
|
|
941
|
+
submissionMethod: "partner_sdk",
|
|
727
942
|
},
|
|
728
943
|
},
|
|
729
944
|
},
|
|
@@ -733,18 +948,17 @@ const { createPartnerBrandSubscription } = await itm.mutation({
|
|
|
733
948
|
});
|
|
734
949
|
|
|
735
950
|
if (!createPartnerBrandSubscription.verificationToken) {
|
|
736
|
-
throw new Error(
|
|
951
|
+
throw new Error("Expected OTP verification to be required");
|
|
737
952
|
}
|
|
738
953
|
|
|
739
954
|
// Collect the SMS code from the user in your UI or API flow.
|
|
740
|
-
const code =
|
|
955
|
+
const code = "123456";
|
|
741
956
|
|
|
742
957
|
const { verifyPartnerBrandSubscription } = await itm.mutation({
|
|
743
958
|
verifyPartnerBrandSubscription: {
|
|
744
959
|
__args: {
|
|
745
960
|
input: {
|
|
746
|
-
verificationToken:
|
|
747
|
-
createPartnerBrandSubscription.verificationToken,
|
|
961
|
+
verificationToken: createPartnerBrandSubscription.verificationToken,
|
|
748
962
|
code,
|
|
749
963
|
},
|
|
750
964
|
},
|
|
@@ -770,17 +984,17 @@ This is the recommended mode when:
|
|
|
770
984
|
This is the full two-step flow most integrators will want to implement from a server route or server action.
|
|
771
985
|
|
|
772
986
|
```typescript
|
|
773
|
-
import { createITMPartnerClient } from
|
|
987
|
+
import { createITMPartnerClient } from "@itm-studio/partner-sdk";
|
|
774
988
|
|
|
775
989
|
function createRequestScopedITM(request: Request) {
|
|
776
990
|
return createITMPartnerClient({
|
|
777
991
|
token: process.env.ITM_PARTNER_TOKEN!,
|
|
778
992
|
headers: () => ({
|
|
779
|
-
...(request.headers.get(
|
|
780
|
-
? {
|
|
993
|
+
...(request.headers.get("x-forwarded-for")
|
|
994
|
+
? { "x-forwarded-for": request.headers.get("x-forwarded-for")! }
|
|
781
995
|
: {}),
|
|
782
|
-
...(request.headers.get(
|
|
783
|
-
? {
|
|
996
|
+
...(request.headers.get("user-agent")
|
|
997
|
+
? { "user-agent": request.headers.get("user-agent")! }
|
|
784
998
|
: {}),
|
|
785
999
|
}),
|
|
786
1000
|
});
|
|
@@ -793,24 +1007,24 @@ export async function startOtpSubscription(request: Request) {
|
|
|
793
1007
|
createPartnerBrandSubscription: {
|
|
794
1008
|
__args: {
|
|
795
1009
|
input: {
|
|
796
|
-
phoneNumber:
|
|
1010
|
+
phoneNumber: "+14155550112",
|
|
797
1011
|
requireOtpVerification: true,
|
|
798
|
-
fullName:
|
|
799
|
-
email:
|
|
800
|
-
listNames: [
|
|
1012
|
+
fullName: "Otp User",
|
|
1013
|
+
email: "otp@example.com",
|
|
1014
|
+
listNames: ["Members", "SMS"],
|
|
801
1015
|
compliance: {
|
|
802
|
-
pageUrl:
|
|
803
|
-
submissionMethod:
|
|
1016
|
+
pageUrl: "https://example.com/join",
|
|
1017
|
+
submissionMethod: "partner_sdk",
|
|
804
1018
|
submittedAt: new Date().toISOString(),
|
|
805
|
-
timezone:
|
|
1019
|
+
timezone: "America/New_York",
|
|
806
1020
|
},
|
|
807
1021
|
postSubscriptionMessage: {
|
|
808
1022
|
sms: {
|
|
809
|
-
body:
|
|
1023
|
+
body: "You are subscribed. Reply STOP to opt out.",
|
|
810
1024
|
},
|
|
811
1025
|
email: {
|
|
812
|
-
subject:
|
|
813
|
-
body:
|
|
1026
|
+
subject: "Welcome",
|
|
1027
|
+
body: "Your subscription is confirmed.",
|
|
814
1028
|
},
|
|
815
1029
|
},
|
|
816
1030
|
},
|
|
@@ -825,13 +1039,13 @@ export async function startOtpSubscription(request: Request) {
|
|
|
825
1039
|
|
|
826
1040
|
if (!createPartnerBrandSubscription.verificationRequired) {
|
|
827
1041
|
return {
|
|
828
|
-
status:
|
|
1042
|
+
status: "subscribed",
|
|
829
1043
|
subscriptionUid: createPartnerBrandSubscription.subscription?.uid ?? null,
|
|
830
1044
|
};
|
|
831
1045
|
}
|
|
832
1046
|
|
|
833
1047
|
return {
|
|
834
|
-
status:
|
|
1048
|
+
status: "otp_required",
|
|
835
1049
|
verificationToken: createPartnerBrandSubscription.verificationToken,
|
|
836
1050
|
};
|
|
837
1051
|
}
|
|
@@ -839,7 +1053,7 @@ export async function startOtpSubscription(request: Request) {
|
|
|
839
1053
|
export async function finishOtpSubscription(
|
|
840
1054
|
request: Request,
|
|
841
1055
|
verificationToken: string,
|
|
842
|
-
code: string
|
|
1056
|
+
code: string
|
|
843
1057
|
) {
|
|
844
1058
|
const itm = createRequestScopedITM(request);
|
|
845
1059
|
|
|
@@ -880,7 +1094,7 @@ Get a presigned S3 URL to upload a file directly.
|
|
|
880
1094
|
```typescript
|
|
881
1095
|
const { getPartnerUploadUrl } = await itm.mutation({
|
|
882
1096
|
getPartnerUploadUrl: {
|
|
883
|
-
__args: { filename:
|
|
1097
|
+
__args: { filename: "event-cover", fileExtension: "jpg" },
|
|
884
1098
|
url: true,
|
|
885
1099
|
filename: true,
|
|
886
1100
|
},
|
|
@@ -888,9 +1102,9 @@ const { getPartnerUploadUrl } = await itm.mutation({
|
|
|
888
1102
|
|
|
889
1103
|
// Upload the file to S3 using the presigned URL
|
|
890
1104
|
await fetch(getPartnerUploadUrl.url, {
|
|
891
|
-
method:
|
|
1105
|
+
method: "PUT",
|
|
892
1106
|
body: fileBuffer, // Buffer, Blob, or ReadableStream
|
|
893
|
-
headers: {
|
|
1107
|
+
headers: { "Content-Type": "image/jpeg" },
|
|
894
1108
|
});
|
|
895
1109
|
```
|
|
896
1110
|
|
|
@@ -902,7 +1116,7 @@ After uploading, register the asset so it's linked to your brand.
|
|
|
902
1116
|
const { createPartnerMediaAsset } = await itm.mutation({
|
|
903
1117
|
createPartnerMediaAsset: {
|
|
904
1118
|
__args: {
|
|
905
|
-
mimetype:
|
|
1119
|
+
mimetype: "image/jpeg",
|
|
906
1120
|
filename: getPartnerUploadUrl.filename, // from Step 1
|
|
907
1121
|
dimensions: { width: 1920, height: 1080 },
|
|
908
1122
|
},
|
|
@@ -924,37 +1138,38 @@ const { createPartnerMoment } = await itm.mutation({
|
|
|
924
1138
|
createPartnerMoment: {
|
|
925
1139
|
__args: {
|
|
926
1140
|
input: {
|
|
927
|
-
name:
|
|
928
|
-
description:
|
|
929
|
-
blurb:
|
|
930
|
-
startDate:
|
|
931
|
-
endDate:
|
|
932
|
-
timezone:
|
|
933
|
-
type:
|
|
1141
|
+
name: "Summer Rooftop Party",
|
|
1142
|
+
description: "A rooftop party in NYC",
|
|
1143
|
+
blurb: "Join us on the roof!",
|
|
1144
|
+
startDate: "2026-07-15T20:00:00.000Z",
|
|
1145
|
+
endDate: "2026-07-16T02:00:00.000Z",
|
|
1146
|
+
timezone: "America/New_York",
|
|
1147
|
+
type: "IRL",
|
|
1148
|
+
capacity: 250,
|
|
934
1149
|
coverImageUid: createPartnerMediaAsset.uid, // from upload flow
|
|
935
1150
|
venue: {
|
|
936
|
-
name:
|
|
937
|
-
city:
|
|
938
|
-
country:
|
|
939
|
-
address:
|
|
1151
|
+
name: "Rooftop Bar",
|
|
1152
|
+
city: "New York",
|
|
1153
|
+
country: "US",
|
|
1154
|
+
address: "123 Main St",
|
|
940
1155
|
},
|
|
941
|
-
category:
|
|
942
|
-
subcategory:
|
|
1156
|
+
category: "PARTY",
|
|
1157
|
+
subcategory: "ROOFTOP_PARTY",
|
|
943
1158
|
ticketTiers: [
|
|
944
1159
|
{
|
|
945
|
-
name:
|
|
946
|
-
price: 2500,
|
|
1160
|
+
name: "General Admission",
|
|
1161
|
+
price: 2500, // $25.00 in cents
|
|
947
1162
|
supply: 200,
|
|
948
1163
|
maxPerUser: 4,
|
|
949
|
-
currencyCode:
|
|
1164
|
+
currencyCode: "USD",
|
|
950
1165
|
},
|
|
951
1166
|
{
|
|
952
|
-
name:
|
|
953
|
-
price: 7500,
|
|
1167
|
+
name: "VIP",
|
|
1168
|
+
price: 7500, // $75.00 in cents
|
|
954
1169
|
supply: 50,
|
|
955
1170
|
maxPerUser: 2,
|
|
956
|
-
currencyCode:
|
|
957
|
-
description:
|
|
1171
|
+
currencyCode: "USD",
|
|
1172
|
+
description: "Includes open bar",
|
|
958
1173
|
},
|
|
959
1174
|
],
|
|
960
1175
|
},
|
|
@@ -965,6 +1180,7 @@ const { createPartnerMoment } = await itm.mutation({
|
|
|
965
1180
|
startDate: true,
|
|
966
1181
|
endDate: true,
|
|
967
1182
|
status: true,
|
|
1183
|
+
capacity: true,
|
|
968
1184
|
coverImage: { url: true },
|
|
969
1185
|
venue: { name: true, city: true },
|
|
970
1186
|
ticketTiers: {
|
|
@@ -978,19 +1194,93 @@ const { createPartnerMoment } = await itm.mutation({
|
|
|
978
1194
|
});
|
|
979
1195
|
```
|
|
980
1196
|
|
|
981
|
-
**Required fields:** `name`, `description`, `
|
|
1197
|
+
**Required fields:** `name`, `description`, `startDate`, `endDate`, `timezone`, `type`
|
|
982
1198
|
|
|
983
1199
|
**Moment types:**
|
|
1200
|
+
|
|
984
1201
|
- `IRL` — in-person event (requires `venue`)
|
|
985
|
-
- `DIGITAL` — online event (no venue needed)
|
|
1202
|
+
- `DIGITAL` — online event (no venue needed; accepts `onlineConfig`)
|
|
1203
|
+
- `HYBRID` — venue plus online access. When creating ticket tiers for a hybrid moment, set each tier's `attendanceMode` to `IN_PERSON` or `ONLINE`.
|
|
1204
|
+
|
|
1205
|
+
For digital or hybrid moments, pass `onlineConfig` when you want ITM to store the meeting details:
|
|
1206
|
+
|
|
1207
|
+
```typescript
|
|
1208
|
+
await itm.mutation({
|
|
1209
|
+
createPartnerMoment: {
|
|
1210
|
+
__args: {
|
|
1211
|
+
input: {
|
|
1212
|
+
name: "Hybrid Show",
|
|
1213
|
+
description: "Attend in person or online",
|
|
1214
|
+
startDate: "2026-07-15T20:00:00.000Z",
|
|
1215
|
+
endDate: "2026-07-15T23:00:00.000Z",
|
|
1216
|
+
timezone: "America/New_York",
|
|
1217
|
+
type: "HYBRID",
|
|
1218
|
+
onlineConfig: {
|
|
1219
|
+
provider: "ZOOM", // GOOGLE_MEET | MS_TEAMS | OTHER | ZOOM
|
|
1220
|
+
meetingUrl: "https://zoom.us/j/123456789",
|
|
1221
|
+
meetingPasscode: "club",
|
|
1222
|
+
},
|
|
1223
|
+
venue: { name: "Main Room", city: "New York", country: "US" },
|
|
1224
|
+
ticketTiers: [
|
|
1225
|
+
{
|
|
1226
|
+
name: "Online Admission",
|
|
1227
|
+
price: 1500,
|
|
1228
|
+
supply: 300,
|
|
1229
|
+
maxPerUser: 4,
|
|
1230
|
+
currencyCode: "USD",
|
|
1231
|
+
attendanceMode: "ONLINE",
|
|
1232
|
+
},
|
|
1233
|
+
],
|
|
1234
|
+
},
|
|
1235
|
+
},
|
|
1236
|
+
uid: true,
|
|
1237
|
+
type: true,
|
|
1238
|
+
ticketTiers: { uid: true, attendanceMode: true },
|
|
1239
|
+
},
|
|
1240
|
+
});
|
|
1241
|
+
```
|
|
986
1242
|
|
|
987
1243
|
**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
1244
|
|
|
1245
|
+
### `editPartnerTicketTiersForMoment`
|
|
1246
|
+
|
|
1247
|
+
Bulk edit partner-owned ticket tiers for a moment. Provide either `momentSlug` or
|
|
1248
|
+
`momentUid`, then target specific tiers by `ticketTierUid`.
|
|
1249
|
+
|
|
1250
|
+
This endpoint intentionally supports only `price` and `supply` updates. Use
|
|
1251
|
+
`getPartnerTicketTiersForMoment` first if you need to discover tier UIDs.
|
|
1252
|
+
|
|
1253
|
+
```typescript
|
|
1254
|
+
const { editPartnerTicketTiersForMoment } = await itm.mutation({
|
|
1255
|
+
editPartnerTicketTiersForMoment: {
|
|
1256
|
+
__args: {
|
|
1257
|
+
input: {
|
|
1258
|
+
momentSlug: "my-event-slug",
|
|
1259
|
+
updates: [
|
|
1260
|
+
{
|
|
1261
|
+
ticketTierUid: "tier-uid",
|
|
1262
|
+
price: 3000, // cents
|
|
1263
|
+
supply: 120,
|
|
1264
|
+
},
|
|
1265
|
+
],
|
|
1266
|
+
},
|
|
1267
|
+
},
|
|
1268
|
+
uid: true,
|
|
1269
|
+
name: true,
|
|
1270
|
+
price: true,
|
|
1271
|
+
supply: true,
|
|
1272
|
+
remainingSupply: true,
|
|
1273
|
+
fillRate: true,
|
|
1274
|
+
isActive: true,
|
|
1275
|
+
},
|
|
1276
|
+
});
|
|
1277
|
+
```
|
|
1278
|
+
|
|
989
1279
|
### Complete Upload + Create Moment Flow
|
|
990
1280
|
|
|
991
1281
|
```typescript
|
|
992
|
-
import { createITMPartnerClient } from
|
|
993
|
-
import { readFile } from
|
|
1282
|
+
import { createITMPartnerClient } from "@itm-studio/partner-sdk";
|
|
1283
|
+
import { readFile } from "fs/promises";
|
|
994
1284
|
|
|
995
1285
|
const itm = createITMPartnerClient({
|
|
996
1286
|
token: process.env.ITM_PARTNER_TOKEN!,
|
|
@@ -999,25 +1289,25 @@ const itm = createITMPartnerClient({
|
|
|
999
1289
|
// 1. Get presigned upload URL
|
|
1000
1290
|
const { getPartnerUploadUrl } = await itm.mutation({
|
|
1001
1291
|
getPartnerUploadUrl: {
|
|
1002
|
-
__args: { filename:
|
|
1292
|
+
__args: { filename: "event-cover", fileExtension: "jpg" },
|
|
1003
1293
|
url: true,
|
|
1004
1294
|
filename: true,
|
|
1005
1295
|
},
|
|
1006
1296
|
});
|
|
1007
1297
|
|
|
1008
1298
|
// 2. Upload file to S3
|
|
1009
|
-
const file = await readFile(
|
|
1299
|
+
const file = await readFile("./event-cover.jpg");
|
|
1010
1300
|
await fetch(getPartnerUploadUrl.url, {
|
|
1011
|
-
method:
|
|
1301
|
+
method: "PUT",
|
|
1012
1302
|
body: file,
|
|
1013
|
-
headers: {
|
|
1303
|
+
headers: { "Content-Type": "image/jpeg" },
|
|
1014
1304
|
});
|
|
1015
1305
|
|
|
1016
1306
|
// 3. Register the uploaded asset
|
|
1017
1307
|
const { createPartnerMediaAsset } = await itm.mutation({
|
|
1018
1308
|
createPartnerMediaAsset: {
|
|
1019
1309
|
__args: {
|
|
1020
|
-
mimetype:
|
|
1310
|
+
mimetype: "image/jpeg",
|
|
1021
1311
|
filename: getPartnerUploadUrl.filename,
|
|
1022
1312
|
dimensions: { width: 1920, height: 1080 },
|
|
1023
1313
|
},
|
|
@@ -1030,26 +1320,26 @@ const { createPartnerMoment } = await itm.mutation({
|
|
|
1030
1320
|
createPartnerMoment: {
|
|
1031
1321
|
__args: {
|
|
1032
1322
|
input: {
|
|
1033
|
-
name:
|
|
1034
|
-
description:
|
|
1035
|
-
blurb:
|
|
1036
|
-
startDate:
|
|
1037
|
-
endDate:
|
|
1038
|
-
timezone:
|
|
1039
|
-
type:
|
|
1323
|
+
name: "My Event",
|
|
1324
|
+
description: "An amazing event",
|
|
1325
|
+
blurb: "Come join us!",
|
|
1326
|
+
startDate: "2026-07-15T20:00:00.000Z",
|
|
1327
|
+
endDate: "2026-07-16T02:00:00.000Z",
|
|
1328
|
+
timezone: "America/New_York",
|
|
1329
|
+
type: "IRL",
|
|
1040
1330
|
coverImageUid: createPartnerMediaAsset.uid,
|
|
1041
1331
|
venue: {
|
|
1042
|
-
name:
|
|
1043
|
-
city:
|
|
1044
|
-
country:
|
|
1332
|
+
name: "The Venue",
|
|
1333
|
+
city: "New York",
|
|
1334
|
+
country: "US",
|
|
1045
1335
|
},
|
|
1046
1336
|
ticketTiers: [
|
|
1047
1337
|
{
|
|
1048
|
-
name:
|
|
1338
|
+
name: "General Admission",
|
|
1049
1339
|
price: 2500,
|
|
1050
1340
|
supply: 100,
|
|
1051
1341
|
maxPerUser: 4,
|
|
1052
|
-
currencyCode:
|
|
1342
|
+
currencyCode: "USD",
|
|
1053
1343
|
},
|
|
1054
1344
|
],
|
|
1055
1345
|
},
|
|
@@ -1061,7 +1351,9 @@ const { createPartnerMoment } = await itm.mutation({
|
|
|
1061
1351
|
},
|
|
1062
1352
|
});
|
|
1063
1353
|
|
|
1064
|
-
console.log(
|
|
1354
|
+
console.log(
|
|
1355
|
+
`Created: ${createPartnerMoment.slug} (${createPartnerMoment.status})`
|
|
1356
|
+
);
|
|
1065
1357
|
```
|
|
1066
1358
|
|
|
1067
1359
|
## Pagination
|
|
@@ -1074,7 +1366,7 @@ let cursor: string | null = null;
|
|
|
1074
1366
|
do {
|
|
1075
1367
|
const { getPartnerTicketsForMoment } = await itm.query({
|
|
1076
1368
|
getPartnerTicketsForMoment: {
|
|
1077
|
-
__args: { momentSlug:
|
|
1369
|
+
__args: { momentSlug: "my-event", cursor, take: 50 },
|
|
1078
1370
|
tickets: { uid: true, user: { name: true } },
|
|
1079
1371
|
hasNextPage: true,
|
|
1080
1372
|
nextCursor: true,
|
|
@@ -1097,7 +1389,7 @@ For staging or local development:
|
|
|
1097
1389
|
```typescript
|
|
1098
1390
|
const itm = createITMPartnerClient({
|
|
1099
1391
|
token: process.env.ITM_PARTNER_TOKEN!,
|
|
1100
|
-
baseUrl:
|
|
1392
|
+
baseUrl: "https://staging-api.itm.studio/graphql",
|
|
1101
1393
|
});
|
|
1102
1394
|
```
|
|
1103
1395
|
|
|
@@ -1157,12 +1449,12 @@ npm run schema:publish # Publish schema to registry
|
|
|
1157
1449
|
|
|
1158
1450
|
## CI/CD
|
|
1159
1451
|
|
|
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`
|
|
1452
|
+
| Job | Trigger | What it does |
|
|
1453
|
+
| ---------------- | --------------- | --------------------------------------------- |
|
|
1454
|
+
| `build-and-test` | Every push & PR | Install, generate, lint, test, build |
|
|
1455
|
+
| `schema-check` | PRs only | Validates schema changes won't break partners |
|
|
1456
|
+
| `schema-publish` | Push to main | Publishes schema to Hive registry |
|
|
1457
|
+
| `publish` | Push to main | Auto-bumps patch version and publishes to NPM |
|
|
1166
1458
|
|
|
1167
1459
|
## Adding New Partner Endpoints
|
|
1168
1460
|
|