@itm-studio/partner-sdk 0.2.5 → 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 +474 -158
- 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
|
});
|
|
@@ -90,6 +90,139 @@ export function createITMClientForRequest(request: Request) {
|
|
|
90
90
|
|
|
91
91
|
If you also send `input.compliance`, the backend keeps the values you provide and backfills missing `ipAddress` and `userAgent` from the forwarded headers when available.
|
|
92
92
|
|
|
93
|
+
## Money Amounts
|
|
94
|
+
|
|
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.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
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",
|
|
130
|
+
currency: currencyCode,
|
|
131
|
+
minimumFractionDigits: maximumFractionDigits,
|
|
132
|
+
maximumFractionDigits,
|
|
133
|
+
}).format(amount);
|
|
134
|
+
}
|
|
135
|
+
```
|
|
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
|
+
|
|
93
226
|
## Available Queries
|
|
94
227
|
|
|
95
228
|
### `getPartnerMomentsForBrand`
|
|
@@ -100,10 +233,10 @@ Get paginated moments (events) for your brand.
|
|
|
100
233
|
const { getPartnerMomentsForBrand } = await itm.query({
|
|
101
234
|
getPartnerMomentsForBrand: {
|
|
102
235
|
__args: {
|
|
103
|
-
take: 20,
|
|
104
|
-
cursor: nextCursor,
|
|
105
|
-
status:
|
|
106
|
-
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)
|
|
107
240
|
},
|
|
108
241
|
moments: { uid: true, name: true, startDate: true },
|
|
109
242
|
totalCount: true,
|
|
@@ -113,6 +246,60 @@ const { getPartnerMomentsForBrand } = await itm.query({
|
|
|
113
246
|
});
|
|
114
247
|
```
|
|
115
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
|
+
|
|
116
303
|
### `getPartnerMoment`
|
|
117
304
|
|
|
118
305
|
Get a single moment by slug.
|
|
@@ -120,7 +307,7 @@ Get a single moment by slug.
|
|
|
120
307
|
```typescript
|
|
121
308
|
const { getPartnerMoment } = await itm.query({
|
|
122
309
|
getPartnerMoment: {
|
|
123
|
-
__args: { slug:
|
|
310
|
+
__args: { slug: "my-event-slug" },
|
|
124
311
|
uid: true,
|
|
125
312
|
name: true,
|
|
126
313
|
startDate: true,
|
|
@@ -148,7 +335,7 @@ Get approved public lineup entries for a moment. Use `getPartnerMoment` first if
|
|
|
148
335
|
```typescript
|
|
149
336
|
const { getPartnerMoment } = await itm.query({
|
|
150
337
|
getPartnerMoment: {
|
|
151
|
-
__args: { slug:
|
|
338
|
+
__args: { slug: "my-event-slug" },
|
|
152
339
|
uid: true,
|
|
153
340
|
lineupDisplayLabel: true,
|
|
154
341
|
},
|
|
@@ -162,7 +349,7 @@ if (momentUid) {
|
|
|
162
349
|
__args: {
|
|
163
350
|
momentUid,
|
|
164
351
|
take: 50,
|
|
165
|
-
lineupType:
|
|
352
|
+
lineupType: "DJ", // optional: LINEUP | CO_HOST | PROMOTER | DJ | SPEAKER | VENDOR
|
|
166
353
|
},
|
|
167
354
|
lineups: {
|
|
168
355
|
uid: true,
|
|
@@ -182,7 +369,10 @@ if (momentUid) {
|
|
|
182
369
|
},
|
|
183
370
|
});
|
|
184
371
|
|
|
185
|
-
console.log(
|
|
372
|
+
console.log(
|
|
373
|
+
getPartnerMoment?.lineupDisplayLabel,
|
|
374
|
+
getPublicMomentLineups.lineups
|
|
375
|
+
);
|
|
186
376
|
}
|
|
187
377
|
```
|
|
188
378
|
|
|
@@ -197,7 +387,7 @@ Get public lineup invitation details by response token. This is useful if you bu
|
|
|
197
387
|
```typescript
|
|
198
388
|
const { getMomentLineupByToken } = await itm.query({
|
|
199
389
|
getMomentLineupByToken: {
|
|
200
|
-
__args: { token:
|
|
390
|
+
__args: { token: "lineup-response-token" },
|
|
201
391
|
artistName: true,
|
|
202
392
|
momentName: true,
|
|
203
393
|
brandName: true,
|
|
@@ -208,7 +398,7 @@ const { getMomentLineupByToken } = await itm.query({
|
|
|
208
398
|
});
|
|
209
399
|
```
|
|
210
400
|
|
|
211
|
-
### `
|
|
401
|
+
### `getPartnerMomentStats`
|
|
212
402
|
|
|
213
403
|
Get cached partner-facing counts for a moment owned by the authenticated partner's brand.
|
|
214
404
|
|
|
@@ -218,9 +408,9 @@ Get cached partner-facing counts for a moment owned by the authenticated partner
|
|
|
218
408
|
- `ticketRsvpRequestsCount` counts ticket RSVP requests.
|
|
219
409
|
|
|
220
410
|
```typescript
|
|
221
|
-
const {
|
|
222
|
-
|
|
223
|
-
__args: { momentSlug:
|
|
411
|
+
const { getPartnerMomentStats } = await itm.query({
|
|
412
|
+
getPartnerMomentStats: {
|
|
413
|
+
__args: { momentSlug: "my-event-slug" },
|
|
224
414
|
ticketsCount: true,
|
|
225
415
|
waitlistCount: true,
|
|
226
416
|
externalRsvpCount: true,
|
|
@@ -229,7 +419,56 @@ const { getMomentStats } = await itm.query({
|
|
|
229
419
|
});
|
|
230
420
|
```
|
|
231
421
|
|
|
232
|
-
> **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`).
|
|
233
472
|
|
|
234
473
|
### `getPartnerTicketsForMoment`
|
|
235
474
|
|
|
@@ -239,8 +478,8 @@ Get paginated tickets for a moment.
|
|
|
239
478
|
const { getPartnerTicketsForMoment } = await itm.query({
|
|
240
479
|
getPartnerTicketsForMoment: {
|
|
241
480
|
__args: {
|
|
242
|
-
momentSlug:
|
|
243
|
-
checkedIn: true,
|
|
481
|
+
momentSlug: "my-event-slug",
|
|
482
|
+
checkedIn: true, // optional: filter by check-in status
|
|
244
483
|
take: 50,
|
|
245
484
|
},
|
|
246
485
|
tickets: {
|
|
@@ -266,7 +505,7 @@ Get paginated customers (users with their tickets) for a moment.
|
|
|
266
505
|
```typescript
|
|
267
506
|
const { getPartnerCustomersForMoment } = await itm.query({
|
|
268
507
|
getPartnerCustomersForMoment: {
|
|
269
|
-
__args: { momentSlug:
|
|
508
|
+
__args: { momentSlug: "my-event-slug", take: 50 },
|
|
270
509
|
customers: {
|
|
271
510
|
user: { uid: true, name: true, primaryEmail: true, phoneNumber: true },
|
|
272
511
|
tickets: { uid: true, price: true, redeemedAt: true },
|
|
@@ -285,7 +524,7 @@ Get a moment collection by slug. Collections group related moments together (e.g
|
|
|
285
524
|
```typescript
|
|
286
525
|
const { getPartnerMomentCollection } = await itm.query({
|
|
287
526
|
getPartnerMomentCollection: {
|
|
288
|
-
__args: { slug:
|
|
527
|
+
__args: { slug: "summer-series" },
|
|
289
528
|
uid: true,
|
|
290
529
|
name: true,
|
|
291
530
|
description: true,
|
|
@@ -326,10 +565,10 @@ You can filter moments within a collection using `queryFilters`:
|
|
|
326
565
|
```typescript
|
|
327
566
|
const { getPartnerMomentCollection } = await itm.query({
|
|
328
567
|
getPartnerMomentCollection: {
|
|
329
|
-
__args: { slug:
|
|
568
|
+
__args: { slug: "summer-series" },
|
|
330
569
|
name: true,
|
|
331
570
|
moments: {
|
|
332
|
-
__args: { queryFilters: [
|
|
571
|
+
__args: { queryFilters: ["upcoming"] },
|
|
333
572
|
uid: true,
|
|
334
573
|
name: true,
|
|
335
574
|
startDate: true,
|
|
@@ -387,7 +626,7 @@ Filter by moment to get echoes scoped to a specific event:
|
|
|
387
626
|
```typescript
|
|
388
627
|
const { getPartnerPublicEchoes } = await itm.query({
|
|
389
628
|
getPartnerPublicEchoes: {
|
|
390
|
-
__args: { momentSlug:
|
|
629
|
+
__args: { momentSlug: "my-event-slug", take: 10 },
|
|
391
630
|
echoes: {
|
|
392
631
|
uid: true,
|
|
393
632
|
type: true,
|
|
@@ -421,8 +660,8 @@ const { respondToMomentLineup } = await itm.mutation({
|
|
|
421
660
|
respondToMomentLineup: {
|
|
422
661
|
__args: {
|
|
423
662
|
input: {
|
|
424
|
-
token:
|
|
425
|
-
status:
|
|
663
|
+
token: "lineup-response-token",
|
|
664
|
+
status: "APPROVED", // APPROVED or REJECTED
|
|
426
665
|
},
|
|
427
666
|
},
|
|
428
667
|
status: true,
|
|
@@ -446,10 +685,10 @@ Use OTP subscribe when you want the user to confirm the phone number before the
|
|
|
446
685
|
|
|
447
686
|
### Flow Summary
|
|
448
687
|
|
|
449
|
-
| Mode
|
|
450
|
-
|
|
451
|
-
| Direct | `createPartnerBrandSubscription` with `requireOtpVerification: false` or omitted | `verificationRequired: false` and `subscription` populated
|
|
452
|
-
| 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` |
|
|
453
692
|
|
|
454
693
|
### Input Behavior
|
|
455
694
|
|
|
@@ -484,27 +723,27 @@ The field is part of the generated SDK input types:
|
|
|
484
723
|
import type {
|
|
485
724
|
CreatePartnerBrandSubscriptionInput,
|
|
486
725
|
PostSubscriptionMessageInput,
|
|
487
|
-
} from
|
|
726
|
+
} from "@itm-studio/partner-sdk";
|
|
488
727
|
|
|
489
728
|
const welcomeMessage: PostSubscriptionMessageInput = {
|
|
490
729
|
sms: {
|
|
491
|
-
body:
|
|
492
|
-
mediaUrls: [
|
|
730
|
+
body: "Welcome to Acme. Reply STOP to opt out.",
|
|
731
|
+
mediaUrls: ["https://cdn.example.com/welcome-banner.png"],
|
|
493
732
|
},
|
|
494
733
|
email: {
|
|
495
|
-
subject:
|
|
496
|
-
body:
|
|
734
|
+
subject: "Welcome to Acme",
|
|
735
|
+
body: "Thanks for subscribing. Your first update is on the way.",
|
|
497
736
|
},
|
|
498
737
|
push: {
|
|
499
|
-
title:
|
|
500
|
-
body:
|
|
738
|
+
title: "Welcome to Acme",
|
|
739
|
+
body: "Tap to see what is dropping this week.",
|
|
501
740
|
},
|
|
502
741
|
};
|
|
503
742
|
|
|
504
743
|
const input: CreatePartnerBrandSubscriptionInput = {
|
|
505
|
-
phoneNumber:
|
|
506
|
-
fullName:
|
|
507
|
-
email:
|
|
744
|
+
phoneNumber: "+14155550111",
|
|
745
|
+
fullName: "Jane Doe",
|
|
746
|
+
email: "jane@example.com",
|
|
508
747
|
postSubscriptionMessage: welcomeMessage,
|
|
509
748
|
};
|
|
510
749
|
```
|
|
@@ -514,21 +753,21 @@ await itm.mutation({
|
|
|
514
753
|
createPartnerBrandSubscription: {
|
|
515
754
|
__args: {
|
|
516
755
|
input: {
|
|
517
|
-
phoneNumber:
|
|
518
|
-
fullName:
|
|
519
|
-
email:
|
|
756
|
+
phoneNumber: "+14155550111",
|
|
757
|
+
fullName: "Jane Doe",
|
|
758
|
+
email: "jane@example.com",
|
|
520
759
|
postSubscriptionMessage: {
|
|
521
760
|
sms: {
|
|
522
|
-
body:
|
|
523
|
-
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"],
|
|
524
763
|
},
|
|
525
764
|
email: {
|
|
526
|
-
subject:
|
|
527
|
-
body:
|
|
765
|
+
subject: "Welcome to Acme",
|
|
766
|
+
body: "Hi Jane,\n\nThanks for subscribing — here is what to expect next...",
|
|
528
767
|
},
|
|
529
768
|
push: {
|
|
530
|
-
title:
|
|
531
|
-
body:
|
|
769
|
+
title: "Welcome to Acme",
|
|
770
|
+
body: "Tap to see what is dropping this week.",
|
|
532
771
|
},
|
|
533
772
|
},
|
|
534
773
|
},
|
|
@@ -541,20 +780,20 @@ await itm.mutation({
|
|
|
541
780
|
|
|
542
781
|
**Direct vs OTP timing:**
|
|
543
782
|
|
|
544
|
-
| Mode
|
|
545
|
-
|
|
546
|
-
| Direct subscribe | Initial `createPartnerBrandSubscription` call
|
|
547
|
-
| 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 |
|
|
548
787
|
|
|
549
788
|
Do not pass `postSubscriptionMessage` to `verifyPartnerBrandSubscription`; that mutation only accepts `verificationToken` and `code`.
|
|
550
789
|
|
|
551
790
|
**Channel inputs:**
|
|
552
791
|
|
|
553
|
-
| Channel | Required
|
|
554
|
-
|
|
555
|
-
| `sms`
|
|
556
|
-
| `email` | `subject`, `body` | —
|
|
557
|
-
| `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. |
|
|
558
797
|
|
|
559
798
|
**Delivery semantics:**
|
|
560
799
|
|
|
@@ -578,10 +817,10 @@ await itm.mutation({
|
|
|
578
817
|
createPartnerBrandSubscription: {
|
|
579
818
|
__args: {
|
|
580
819
|
input: {
|
|
581
|
-
phoneNumber:
|
|
820
|
+
phoneNumber: "+14155550111",
|
|
582
821
|
postSubscriptionMessage: {
|
|
583
822
|
sms: {
|
|
584
|
-
body:
|
|
823
|
+
body: "You are subscribed to Acme updates. Reply STOP to opt out.",
|
|
585
824
|
},
|
|
586
825
|
},
|
|
587
826
|
},
|
|
@@ -599,16 +838,16 @@ const { createPartnerBrandSubscription } = await itm.mutation({
|
|
|
599
838
|
createPartnerBrandSubscription: {
|
|
600
839
|
__args: {
|
|
601
840
|
input: {
|
|
602
|
-
phoneNumber:
|
|
841
|
+
phoneNumber: "+14155550112",
|
|
603
842
|
requireOtpVerification: true,
|
|
604
|
-
email:
|
|
843
|
+
email: "jane@example.com",
|
|
605
844
|
postSubscriptionMessage: {
|
|
606
845
|
sms: {
|
|
607
|
-
body:
|
|
846
|
+
body: "Thanks for joining Acme. Reply STOP to opt out.",
|
|
608
847
|
},
|
|
609
848
|
email: {
|
|
610
|
-
subject:
|
|
611
|
-
body:
|
|
849
|
+
subject: "You are subscribed",
|
|
850
|
+
body: "Your subscription is confirmed. We will send updates here.",
|
|
612
851
|
},
|
|
613
852
|
},
|
|
614
853
|
},
|
|
@@ -624,7 +863,7 @@ await itm.mutation({
|
|
|
624
863
|
__args: {
|
|
625
864
|
input: {
|
|
626
865
|
verificationToken: createPartnerBrandSubscription.verificationToken!,
|
|
627
|
-
code:
|
|
866
|
+
code: "123456",
|
|
628
867
|
},
|
|
629
868
|
},
|
|
630
869
|
uid: true,
|
|
@@ -647,17 +886,17 @@ const { createPartnerBrandSubscription } = await itm.mutation({
|
|
|
647
886
|
createPartnerBrandSubscription: {
|
|
648
887
|
__args: {
|
|
649
888
|
input: {
|
|
650
|
-
phoneNumber:
|
|
651
|
-
fullName:
|
|
652
|
-
email:
|
|
653
|
-
listNames: [
|
|
889
|
+
phoneNumber: "+14155550111",
|
|
890
|
+
fullName: "Jane Doe",
|
|
891
|
+
email: "jane@example.com",
|
|
892
|
+
listNames: ["VIP", "Newsletter"],
|
|
654
893
|
compliance: {
|
|
655
|
-
pageUrl:
|
|
656
|
-
submissionMethod:
|
|
894
|
+
pageUrl: "https://example.com/signup",
|
|
895
|
+
submissionMethod: "partner_sdk",
|
|
657
896
|
},
|
|
658
897
|
postSubscriptionMessage: {
|
|
659
898
|
sms: {
|
|
660
|
-
body:
|
|
899
|
+
body: "Welcome to Acme updates. Reply STOP to opt out.",
|
|
661
900
|
},
|
|
662
901
|
},
|
|
663
902
|
},
|
|
@@ -694,12 +933,12 @@ const { createPartnerBrandSubscription } = await itm.mutation({
|
|
|
694
933
|
createPartnerBrandSubscription: {
|
|
695
934
|
__args: {
|
|
696
935
|
input: {
|
|
697
|
-
phoneNumber:
|
|
936
|
+
phoneNumber: "+14155550112",
|
|
698
937
|
requireOtpVerification: true,
|
|
699
|
-
fullName:
|
|
938
|
+
fullName: "Otp User",
|
|
700
939
|
compliance: {
|
|
701
|
-
pageUrl:
|
|
702
|
-
submissionMethod:
|
|
940
|
+
pageUrl: "https://example.com/otp-signup",
|
|
941
|
+
submissionMethod: "partner_sdk",
|
|
703
942
|
},
|
|
704
943
|
},
|
|
705
944
|
},
|
|
@@ -709,18 +948,17 @@ const { createPartnerBrandSubscription } = await itm.mutation({
|
|
|
709
948
|
});
|
|
710
949
|
|
|
711
950
|
if (!createPartnerBrandSubscription.verificationToken) {
|
|
712
|
-
throw new Error(
|
|
951
|
+
throw new Error("Expected OTP verification to be required");
|
|
713
952
|
}
|
|
714
953
|
|
|
715
954
|
// Collect the SMS code from the user in your UI or API flow.
|
|
716
|
-
const code =
|
|
955
|
+
const code = "123456";
|
|
717
956
|
|
|
718
957
|
const { verifyPartnerBrandSubscription } = await itm.mutation({
|
|
719
958
|
verifyPartnerBrandSubscription: {
|
|
720
959
|
__args: {
|
|
721
960
|
input: {
|
|
722
|
-
verificationToken:
|
|
723
|
-
createPartnerBrandSubscription.verificationToken,
|
|
961
|
+
verificationToken: createPartnerBrandSubscription.verificationToken,
|
|
724
962
|
code,
|
|
725
963
|
},
|
|
726
964
|
},
|
|
@@ -746,17 +984,17 @@ This is the recommended mode when:
|
|
|
746
984
|
This is the full two-step flow most integrators will want to implement from a server route or server action.
|
|
747
985
|
|
|
748
986
|
```typescript
|
|
749
|
-
import { createITMPartnerClient } from
|
|
987
|
+
import { createITMPartnerClient } from "@itm-studio/partner-sdk";
|
|
750
988
|
|
|
751
989
|
function createRequestScopedITM(request: Request) {
|
|
752
990
|
return createITMPartnerClient({
|
|
753
991
|
token: process.env.ITM_PARTNER_TOKEN!,
|
|
754
992
|
headers: () => ({
|
|
755
|
-
...(request.headers.get(
|
|
756
|
-
? {
|
|
993
|
+
...(request.headers.get("x-forwarded-for")
|
|
994
|
+
? { "x-forwarded-for": request.headers.get("x-forwarded-for")! }
|
|
757
995
|
: {}),
|
|
758
|
-
...(request.headers.get(
|
|
759
|
-
? {
|
|
996
|
+
...(request.headers.get("user-agent")
|
|
997
|
+
? { "user-agent": request.headers.get("user-agent")! }
|
|
760
998
|
: {}),
|
|
761
999
|
}),
|
|
762
1000
|
});
|
|
@@ -769,24 +1007,24 @@ export async function startOtpSubscription(request: Request) {
|
|
|
769
1007
|
createPartnerBrandSubscription: {
|
|
770
1008
|
__args: {
|
|
771
1009
|
input: {
|
|
772
|
-
phoneNumber:
|
|
1010
|
+
phoneNumber: "+14155550112",
|
|
773
1011
|
requireOtpVerification: true,
|
|
774
|
-
fullName:
|
|
775
|
-
email:
|
|
776
|
-
listNames: [
|
|
1012
|
+
fullName: "Otp User",
|
|
1013
|
+
email: "otp@example.com",
|
|
1014
|
+
listNames: ["Members", "SMS"],
|
|
777
1015
|
compliance: {
|
|
778
|
-
pageUrl:
|
|
779
|
-
submissionMethod:
|
|
1016
|
+
pageUrl: "https://example.com/join",
|
|
1017
|
+
submissionMethod: "partner_sdk",
|
|
780
1018
|
submittedAt: new Date().toISOString(),
|
|
781
|
-
timezone:
|
|
1019
|
+
timezone: "America/New_York",
|
|
782
1020
|
},
|
|
783
1021
|
postSubscriptionMessage: {
|
|
784
1022
|
sms: {
|
|
785
|
-
body:
|
|
1023
|
+
body: "You are subscribed. Reply STOP to opt out.",
|
|
786
1024
|
},
|
|
787
1025
|
email: {
|
|
788
|
-
subject:
|
|
789
|
-
body:
|
|
1026
|
+
subject: "Welcome",
|
|
1027
|
+
body: "Your subscription is confirmed.",
|
|
790
1028
|
},
|
|
791
1029
|
},
|
|
792
1030
|
},
|
|
@@ -801,13 +1039,13 @@ export async function startOtpSubscription(request: Request) {
|
|
|
801
1039
|
|
|
802
1040
|
if (!createPartnerBrandSubscription.verificationRequired) {
|
|
803
1041
|
return {
|
|
804
|
-
status:
|
|
1042
|
+
status: "subscribed",
|
|
805
1043
|
subscriptionUid: createPartnerBrandSubscription.subscription?.uid ?? null,
|
|
806
1044
|
};
|
|
807
1045
|
}
|
|
808
1046
|
|
|
809
1047
|
return {
|
|
810
|
-
status:
|
|
1048
|
+
status: "otp_required",
|
|
811
1049
|
verificationToken: createPartnerBrandSubscription.verificationToken,
|
|
812
1050
|
};
|
|
813
1051
|
}
|
|
@@ -815,7 +1053,7 @@ export async function startOtpSubscription(request: Request) {
|
|
|
815
1053
|
export async function finishOtpSubscription(
|
|
816
1054
|
request: Request,
|
|
817
1055
|
verificationToken: string,
|
|
818
|
-
code: string
|
|
1056
|
+
code: string
|
|
819
1057
|
) {
|
|
820
1058
|
const itm = createRequestScopedITM(request);
|
|
821
1059
|
|
|
@@ -856,7 +1094,7 @@ Get a presigned S3 URL to upload a file directly.
|
|
|
856
1094
|
```typescript
|
|
857
1095
|
const { getPartnerUploadUrl } = await itm.mutation({
|
|
858
1096
|
getPartnerUploadUrl: {
|
|
859
|
-
__args: { filename:
|
|
1097
|
+
__args: { filename: "event-cover", fileExtension: "jpg" },
|
|
860
1098
|
url: true,
|
|
861
1099
|
filename: true,
|
|
862
1100
|
},
|
|
@@ -864,9 +1102,9 @@ const { getPartnerUploadUrl } = await itm.mutation({
|
|
|
864
1102
|
|
|
865
1103
|
// Upload the file to S3 using the presigned URL
|
|
866
1104
|
await fetch(getPartnerUploadUrl.url, {
|
|
867
|
-
method:
|
|
1105
|
+
method: "PUT",
|
|
868
1106
|
body: fileBuffer, // Buffer, Blob, or ReadableStream
|
|
869
|
-
headers: {
|
|
1107
|
+
headers: { "Content-Type": "image/jpeg" },
|
|
870
1108
|
});
|
|
871
1109
|
```
|
|
872
1110
|
|
|
@@ -878,7 +1116,7 @@ After uploading, register the asset so it's linked to your brand.
|
|
|
878
1116
|
const { createPartnerMediaAsset } = await itm.mutation({
|
|
879
1117
|
createPartnerMediaAsset: {
|
|
880
1118
|
__args: {
|
|
881
|
-
mimetype:
|
|
1119
|
+
mimetype: "image/jpeg",
|
|
882
1120
|
filename: getPartnerUploadUrl.filename, // from Step 1
|
|
883
1121
|
dimensions: { width: 1920, height: 1080 },
|
|
884
1122
|
},
|
|
@@ -900,37 +1138,38 @@ const { createPartnerMoment } = await itm.mutation({
|
|
|
900
1138
|
createPartnerMoment: {
|
|
901
1139
|
__args: {
|
|
902
1140
|
input: {
|
|
903
|
-
name:
|
|
904
|
-
description:
|
|
905
|
-
blurb:
|
|
906
|
-
startDate:
|
|
907
|
-
endDate:
|
|
908
|
-
timezone:
|
|
909
|
-
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,
|
|
910
1149
|
coverImageUid: createPartnerMediaAsset.uid, // from upload flow
|
|
911
1150
|
venue: {
|
|
912
|
-
name:
|
|
913
|
-
city:
|
|
914
|
-
country:
|
|
915
|
-
address:
|
|
1151
|
+
name: "Rooftop Bar",
|
|
1152
|
+
city: "New York",
|
|
1153
|
+
country: "US",
|
|
1154
|
+
address: "123 Main St",
|
|
916
1155
|
},
|
|
917
|
-
category:
|
|
918
|
-
subcategory:
|
|
1156
|
+
category: "PARTY",
|
|
1157
|
+
subcategory: "ROOFTOP_PARTY",
|
|
919
1158
|
ticketTiers: [
|
|
920
1159
|
{
|
|
921
|
-
name:
|
|
922
|
-
price: 2500,
|
|
1160
|
+
name: "General Admission",
|
|
1161
|
+
price: 2500, // $25.00 in cents
|
|
923
1162
|
supply: 200,
|
|
924
1163
|
maxPerUser: 4,
|
|
925
|
-
currencyCode:
|
|
1164
|
+
currencyCode: "USD",
|
|
926
1165
|
},
|
|
927
1166
|
{
|
|
928
|
-
name:
|
|
929
|
-
price: 7500,
|
|
1167
|
+
name: "VIP",
|
|
1168
|
+
price: 7500, // $75.00 in cents
|
|
930
1169
|
supply: 50,
|
|
931
1170
|
maxPerUser: 2,
|
|
932
|
-
currencyCode:
|
|
933
|
-
description:
|
|
1171
|
+
currencyCode: "USD",
|
|
1172
|
+
description: "Includes open bar",
|
|
934
1173
|
},
|
|
935
1174
|
],
|
|
936
1175
|
},
|
|
@@ -941,6 +1180,7 @@ const { createPartnerMoment } = await itm.mutation({
|
|
|
941
1180
|
startDate: true,
|
|
942
1181
|
endDate: true,
|
|
943
1182
|
status: true,
|
|
1183
|
+
capacity: true,
|
|
944
1184
|
coverImage: { url: true },
|
|
945
1185
|
venue: { name: true, city: true },
|
|
946
1186
|
ticketTiers: {
|
|
@@ -954,19 +1194,93 @@ const { createPartnerMoment } = await itm.mutation({
|
|
|
954
1194
|
});
|
|
955
1195
|
```
|
|
956
1196
|
|
|
957
|
-
**Required fields:** `name`, `description`, `
|
|
1197
|
+
**Required fields:** `name`, `description`, `startDate`, `endDate`, `timezone`, `type`
|
|
958
1198
|
|
|
959
1199
|
**Moment types:**
|
|
1200
|
+
|
|
960
1201
|
- `IRL` — in-person event (requires `venue`)
|
|
961
|
-
- `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
|
+
```
|
|
1242
|
+
|
|
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.
|
|
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.
|
|
962
1252
|
|
|
963
|
-
|
|
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
|
+
```
|
|
964
1278
|
|
|
965
1279
|
### Complete Upload + Create Moment Flow
|
|
966
1280
|
|
|
967
1281
|
```typescript
|
|
968
|
-
import { createITMPartnerClient } from
|
|
969
|
-
import { readFile } from
|
|
1282
|
+
import { createITMPartnerClient } from "@itm-studio/partner-sdk";
|
|
1283
|
+
import { readFile } from "fs/promises";
|
|
970
1284
|
|
|
971
1285
|
const itm = createITMPartnerClient({
|
|
972
1286
|
token: process.env.ITM_PARTNER_TOKEN!,
|
|
@@ -975,25 +1289,25 @@ const itm = createITMPartnerClient({
|
|
|
975
1289
|
// 1. Get presigned upload URL
|
|
976
1290
|
const { getPartnerUploadUrl } = await itm.mutation({
|
|
977
1291
|
getPartnerUploadUrl: {
|
|
978
|
-
__args: { filename:
|
|
1292
|
+
__args: { filename: "event-cover", fileExtension: "jpg" },
|
|
979
1293
|
url: true,
|
|
980
1294
|
filename: true,
|
|
981
1295
|
},
|
|
982
1296
|
});
|
|
983
1297
|
|
|
984
1298
|
// 2. Upload file to S3
|
|
985
|
-
const file = await readFile(
|
|
1299
|
+
const file = await readFile("./event-cover.jpg");
|
|
986
1300
|
await fetch(getPartnerUploadUrl.url, {
|
|
987
|
-
method:
|
|
1301
|
+
method: "PUT",
|
|
988
1302
|
body: file,
|
|
989
|
-
headers: {
|
|
1303
|
+
headers: { "Content-Type": "image/jpeg" },
|
|
990
1304
|
});
|
|
991
1305
|
|
|
992
1306
|
// 3. Register the uploaded asset
|
|
993
1307
|
const { createPartnerMediaAsset } = await itm.mutation({
|
|
994
1308
|
createPartnerMediaAsset: {
|
|
995
1309
|
__args: {
|
|
996
|
-
mimetype:
|
|
1310
|
+
mimetype: "image/jpeg",
|
|
997
1311
|
filename: getPartnerUploadUrl.filename,
|
|
998
1312
|
dimensions: { width: 1920, height: 1080 },
|
|
999
1313
|
},
|
|
@@ -1006,26 +1320,26 @@ const { createPartnerMoment } = await itm.mutation({
|
|
|
1006
1320
|
createPartnerMoment: {
|
|
1007
1321
|
__args: {
|
|
1008
1322
|
input: {
|
|
1009
|
-
name:
|
|
1010
|
-
description:
|
|
1011
|
-
blurb:
|
|
1012
|
-
startDate:
|
|
1013
|
-
endDate:
|
|
1014
|
-
timezone:
|
|
1015
|
-
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",
|
|
1016
1330
|
coverImageUid: createPartnerMediaAsset.uid,
|
|
1017
1331
|
venue: {
|
|
1018
|
-
name:
|
|
1019
|
-
city:
|
|
1020
|
-
country:
|
|
1332
|
+
name: "The Venue",
|
|
1333
|
+
city: "New York",
|
|
1334
|
+
country: "US",
|
|
1021
1335
|
},
|
|
1022
1336
|
ticketTiers: [
|
|
1023
1337
|
{
|
|
1024
|
-
name:
|
|
1338
|
+
name: "General Admission",
|
|
1025
1339
|
price: 2500,
|
|
1026
1340
|
supply: 100,
|
|
1027
1341
|
maxPerUser: 4,
|
|
1028
|
-
currencyCode:
|
|
1342
|
+
currencyCode: "USD",
|
|
1029
1343
|
},
|
|
1030
1344
|
],
|
|
1031
1345
|
},
|
|
@@ -1037,7 +1351,9 @@ const { createPartnerMoment } = await itm.mutation({
|
|
|
1037
1351
|
},
|
|
1038
1352
|
});
|
|
1039
1353
|
|
|
1040
|
-
console.log(
|
|
1354
|
+
console.log(
|
|
1355
|
+
`Created: ${createPartnerMoment.slug} (${createPartnerMoment.status})`
|
|
1356
|
+
);
|
|
1041
1357
|
```
|
|
1042
1358
|
|
|
1043
1359
|
## Pagination
|
|
@@ -1050,7 +1366,7 @@ let cursor: string | null = null;
|
|
|
1050
1366
|
do {
|
|
1051
1367
|
const { getPartnerTicketsForMoment } = await itm.query({
|
|
1052
1368
|
getPartnerTicketsForMoment: {
|
|
1053
|
-
__args: { momentSlug:
|
|
1369
|
+
__args: { momentSlug: "my-event", cursor, take: 50 },
|
|
1054
1370
|
tickets: { uid: true, user: { name: true } },
|
|
1055
1371
|
hasNextPage: true,
|
|
1056
1372
|
nextCursor: true,
|
|
@@ -1073,7 +1389,7 @@ For staging or local development:
|
|
|
1073
1389
|
```typescript
|
|
1074
1390
|
const itm = createITMPartnerClient({
|
|
1075
1391
|
token: process.env.ITM_PARTNER_TOKEN!,
|
|
1076
|
-
baseUrl:
|
|
1392
|
+
baseUrl: "https://staging-api.itm.studio/graphql",
|
|
1077
1393
|
});
|
|
1078
1394
|
```
|
|
1079
1395
|
|
|
@@ -1133,12 +1449,12 @@ npm run schema:publish # Publish schema to registry
|
|
|
1133
1449
|
|
|
1134
1450
|
## CI/CD
|
|
1135
1451
|
|
|
1136
|
-
| Job
|
|
1137
|
-
|
|
1138
|
-
| `build-and-test` | Every push & PR | Install, generate, lint, test, build
|
|
1139
|
-
| `schema-check`
|
|
1140
|
-
| `schema-publish` | Push to main
|
|
1141
|
-
| `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 |
|
|
1142
1458
|
|
|
1143
1459
|
## Adding New Partner Endpoints
|
|
1144
1460
|
|