@itm-studio/partner-sdk
v0.2.6
Published
TypeScript SDK for the ITM Partner API — typed GraphQL client with zero introspection required
Maintainers
Readme
@itm-studio/partner-sdk
Type-safe TypeScript SDK for the ITM Partner API. Built with genql — full autocomplete, zero introspection required.
A full implementation example can be found here: demo-events-sdk-integration
Install
npm install @itm-studio/partner-sdkQuick Start
import { createITMPartnerClient } from '@itm-studio/partner-sdk';
const itm = createITMPartnerClient({
token: process.env.ITM_PARTNER_TOKEN!,
});
// Get all upcoming moments for your brand
const { getPartnerMomentsForBrand } = await itm.query({
getPartnerMomentsForBrand: {
__args: { status: 'UPCOMING', take: 10 },
moments: {
uid: true,
name: true,
slug: true,
startDate: true,
endDate: true,
status: true,
timezone: true,
externalUrl: true,
lineupDisplayLabel: true,
coverImage: {
url: true,
mimeType: true,
},
venue: {
name: true,
city: true,
country: true,
},
ticketTiers: {
name: true,
price: true,
currency: { code: true, symbol: true },
soldOut: true,
},
},
totalCount: true,
hasNextPage: true,
nextCursor: true,
},
});Authentication
Get your partner API token from the ITM backstage dashboard: Settings > Partner API.
The token is sent as the x-partner-api-key header on every request. Your brand is determined automatically from the token — no need to pass brand IDs.
Keep the partner token on your server. Do not expose it from browser code or client-side mobile code.
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.
import { createITMPartnerClient } from '@itm-studio/partner-sdk';
export function createITMClientForRequest(request: Request) {
return createITMPartnerClient({
token: process.env.ITM_PARTNER_TOKEN!,
headers: () => {
const forwardedFor = request.headers.get('x-forwarded-for');
const userAgent = request.headers.get('user-agent');
return {
...(forwardedFor ? { 'x-forwarded-for': forwardedFor } : {}),
...(userAgent ? { 'user-agent': userAgent } : {}),
};
},
});
}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.
Money Amounts
Partner API price values are always encoded as amount * 100. Divide by 100 before rendering a major-unit display amount (dollars for USD, euros for EUR, yen for JPY, etc.).
This convention also applies to zero-decimal currencies such as JPY and KRW. The rendering conversion is always price / 100; zero-decimal status only controls whether the formatted display shows fractional digits. For example, 2500 should be displayed as JPY 25, not JPY 2,500.
const ZERO_DECIMAL_CURRENCIES = new Set(['BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', 'UGX', 'VND', 'VUV', 'XAF', 'XOF', 'XPF']);
function formatPartnerPrice(priceInCents: number, currencyCode: string) {
if (priceInCents === 0) return 'Free';
const amount = priceInCents / 100;
const maximumFractionDigits = ZERO_DECIMAL_CURRENCIES.has(currencyCode) ? 0 : 2;
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currencyCode,
minimumFractionDigits: maximumFractionDigits,
maximumFractionDigits,
}).format(amount);
}Available Queries
getPartnerMomentsForBrand
Get paginated moments (events) for your brand.
const { getPartnerMomentsForBrand } = await itm.query({
getPartnerMomentsForBrand: {
__args: {
take: 20, // items per page (default: 50, omit to use default)
cursor: nextCursor, // from previous response
status: 'UPCOMING', // UPCOMING | LIVE | ENDED
sortOrder: 'ASC', // ASC | DESC by start date (default: DESC)
},
moments: { uid: true, name: true, startDate: true },
totalCount: true,
hasNextPage: true,
nextCursor: true,
},
});getPartnerMoment
Get a single moment by slug.
const { getPartnerMoment } = await itm.query({
getPartnerMoment: {
__args: { slug: 'my-event-slug' },
uid: true,
name: true,
startDate: true,
endDate: true,
timezone: true,
externalUrl: true,
lineupDisplayLabel: true,
coverImage: {
url: true,
mimeType: true,
},
ticketTiers: {
name: true,
price: true,
soldOut: true,
},
},
});getPublicMomentLineups
Get approved public lineup entries for a moment. Use getPartnerMoment first if you only have the moment slug; this query takes the moment uid.
const { getPartnerMoment } = await itm.query({
getPartnerMoment: {
__args: { slug: 'my-event-slug' },
uid: true,
lineupDisplayLabel: true,
},
});
const momentUid = getPartnerMoment?.uid;
if (momentUid) {
const { getPublicMomentLineups } = await itm.query({
getPublicMomentLineups: {
__args: {
momentUid,
take: 50,
lineupType: 'DJ', // optional: LINEUP | CO_HOST | PROMOTER | DJ | SPEAKER | VENDOR
},
lineups: {
uid: true,
name: true,
status: true,
lineupType: true,
sortOrder: true,
url: true,
image: {
url: true,
mimeType: true,
},
},
totalCount: true,
hasNextPage: true,
nextCursor: true,
},
});
console.log(getPartnerMoment?.lineupDisplayLabel, getPublicMomentLineups.lineups);
}Only approved lineup entries are returned. momentUid, cursor, take, and lineupType map directly to the backend query args; use nextCursor as cursor while hasNextPage is true. The public lineup query is throttled to 30 requests per minute, so cache or batch page loads if you render it in high-traffic public pages.
lineupDisplayLabel is available on Moment so your UI can label the section as LINEUP, CO_HOSTS, DJS, PROMOTERS, SPEAKERS, or VENDORS.
getMomentLineupByToken
Get public lineup invitation details by response token. This is useful if you build a custom artist or collaborator response page.
const { getMomentLineupByToken } = await itm.query({
getMomentLineupByToken: {
__args: { token: 'lineup-response-token' },
artistName: true,
momentName: true,
brandName: true,
status: true,
lineupType: true,
alreadyResponded: true,
},
});getMomentStats
Get cached partner-facing counts for a moment owned by the authenticated partner's brand.
ticketsCountcounts confirmed tickets only.waitlistCountcounts active waitlist entries.externalRsvpCountcounts external RSVPs.ticketRsvpRequestsCountcounts ticket RSVP requests.
const { getMomentStats } = await itm.query({
getMomentStats: {
__args: { momentSlug: 'my-event-slug' },
ticketsCount: true,
waitlistCount: true,
externalRsvpCount: true,
ticketRsvpRequestsCount: true,
},
});Note:
getMomentStatsreturnsnullif the moment does not exist or is not owned by the authenticated partner's brand.
getPartnerTicketsForMoment
Get paginated tickets for a moment.
const { getPartnerTicketsForMoment } = await itm.query({
getPartnerTicketsForMoment: {
__args: {
momentSlug: 'my-event-slug',
checkedIn: true, // optional: filter by check-in status
take: 50,
},
tickets: {
uid: true,
price: true,
redeemedAt: true,
hasExpired: true,
user: { uid: true, name: true, primaryEmail: true },
ticketTier: { name: true, price: true },
payment: { amount: true, status: true },
},
totalCount: true,
hasNextPage: true,
nextCursor: true,
},
});getPartnerCustomersForMoment
Get paginated customers (users with their tickets) for a moment.
const { getPartnerCustomersForMoment } = await itm.query({
getPartnerCustomersForMoment: {
__args: { momentSlug: 'my-event-slug', take: 50 },
customers: {
user: { uid: true, name: true, primaryEmail: true, phoneNumber: true },
tickets: { uid: true, price: true, redeemedAt: true },
},
totalCount: true,
hasNextPage: true,
nextCursor: true,
},
});getPartnerMomentCollection
Get a moment collection by slug. Collections group related moments together (e.g. a summer series, a festival lineup). Returns the collection with its moments and links.
const { getPartnerMomentCollection } = await itm.query({
getPartnerMomentCollection: {
__args: { slug: 'summer-series' },
uid: true,
name: true,
description: true,
subLabel: true,
availableQueryFilters: true,
moments: {
uid: true,
name: true,
slug: true,
startDate: true,
endDate: true,
status: true,
timezone: true,
externalUrl: true,
coverImage: {
url: true,
mimeType: true,
},
venue: { name: true, city: true },
ticketTiers: {
name: true,
price: true,
currency: { code: true, symbol: true },
soldOut: true,
},
},
links: {
uid: true,
url: true,
label: true,
},
},
});You can filter moments within a collection using queryFilters:
const { getPartnerMomentCollection } = await itm.query({
getPartnerMomentCollection: {
__args: { slug: 'summer-series' },
name: true,
moments: {
__args: { queryFilters: ['upcoming'] },
uid: true,
name: true,
startDate: true,
},
},
});Note:
getPartnerMomentCollectionreturnsnullif no collection matches the given slug.
getPartnerPublicEchoes
Get public echoes (content) for your brand. Echoes are content units — links, posts, images, videos, galleries, audio, polls, forms, and third-party embeds (Spotify, Instagram, TikTok).
- If
momentSlugis provided, returns moment-level echoes for that moment. - If
momentSlugis omitted, returns brand-level echoes.
const { getPartnerPublicEchoes } = await itm.query({
getPartnerPublicEchoes: {
__args: { take: 20 },
echoes: {
uid: true,
type: true,
name: true,
slug: true,
description: true,
publishTime: true,
externalLink: true,
mediaAsset: {
uid: true,
mimeType: true,
s3Key: true,
dimensions: { width: true, height: true },
placeholderUrl: true,
blurhash: true,
},
echoMedia: {
uid: true,
mimeType: true,
s3Key: true,
dimensions: { width: true, height: true },
},
moment: { uid: true, name: true, slug: true },
},
totalCount: true,
hasNextPage: true,
nextCursor: true,
},
});Filter by moment to get echoes scoped to a specific event:
const { getPartnerPublicEchoes } = await itm.query({
getPartnerPublicEchoes: {
__args: { momentSlug: 'my-event-slug', take: 10 },
echoes: {
uid: true,
type: true,
name: true,
postBody: true,
externalLink: true,
thirdPartyEchoType: true,
thirdPartyEchoConfig: true,
},
totalCount: true,
hasNextPage: true,
nextCursor: true,
},
});Echo types: LINK | POST | IMAGE | VIDEO | GALLERY | MERCH | AUDIO | QUESTION | POLL | UPLOAD | TOKEN | MOMENT | FORM | THIRD_PARTY
Third-party embed types: SPOTIFY | INSTAGRAM | TIKTOK — use thirdPartyEchoType and thirdPartyEchoConfig for embed details.
Available Mutations
The SDK supports write operations via client.mutation(). Partner mutations use the same authentication — your brand is determined from the partner token.
respondToMomentLineup
Submit a public lineup invitation response by token.
const { respondToMomentLineup } = await itm.mutation({
respondToMomentLineup: {
__args: {
input: {
token: 'lineup-response-token',
status: 'APPROVED', // APPROVED or REJECTED
},
},
status: true,
success: true,
},
});Admin-only lineup creation and ordering are not part of the partner-token SDK surface. Use backstage for managing lineup entries.
Partner Brand Subscription Guide
The partner brand subscription API supports two integration modes:
- Direct subscribe: create the subscription immediately in one mutation.
- OTP subscribe: send an SMS code first, then confirm with a second mutation.
Use direct subscribe when your product already has the right consent flow and you do not need to prove possession of the phone number in real time.
Use OTP subscribe when you want the user to confirm the phone number before the brand subscription is created.
Flow Summary
| Mode | First mutation | First response | Second mutation |
|---|---|---|---|
| Direct | createPartnerBrandSubscription with requireOtpVerification: false or omitted | verificationRequired: false and subscription populated | None |
| OTP | createPartnerBrandSubscription with requireOtpVerification: true | verificationRequired: true, verificationToken, subscription: null | verifyPartnerBrandSubscription |
Input Behavior
phoneNumber: send an E.164 phone number such as+14155551234.fullName: optional, but if you send it, include both first and last name.email: optional and attached to the subscribed user if provided.listNames: optional brand list names. Missing lists are created automatically and the subscribed user is attached to them.compliance: optional metadata about how consent was collected. This is the right place for fields likepageUrl,submissionMethod,submittedAt,timezone, and geo/browser/device details.compliance.ipAddressandcompliance.userAgent: optional. If you omit them and forwardx-forwarded-foranduser-agent, the backend backfills those values for you.postSubscriptionMessage: optional welcome message delivered automatically after the subscription becomes active. The SDK exposes this asPostSubscriptionMessageInputand accepts it onCreatePartnerBrandSubscriptionInput. See Post-Subscription Welcome Message.
OTP Flow Step By Step
- Call
createPartnerBrandSubscriptionwithrequireOtpVerification: true. - Store the returned
verificationTokenin the user session or another short-lived server-side state container associated with the current signup attempt. - Collect the SMS code from the user.
- Call
verifyPartnerBrandSubscriptionwith thatverificationTokenandcode.
verificationToken is brand-scoped. A token created under one partner token cannot be verified by another brand's partner token.
If the verification token is invalid or expired, verification fails and the subscription is not created.
Post-Subscription Welcome Message
postSubscriptionMessage lets you fan out a welcome touch across SMS, email, and push the moment a subscription becomes active. The backend dispatches each channel asynchronously after the subscription is created (direct mode) or verified (OTP mode), so the mutation still returns synchronously — delivery happens in the background.
Channels are independent. Provide any combination of sms, email, and push. Channels you omit are skipped.
The field is part of the generated SDK input types:
import type {
CreatePartnerBrandSubscriptionInput,
PostSubscriptionMessageInput,
} from '@itm-studio/partner-sdk';
const welcomeMessage: PostSubscriptionMessageInput = {
sms: {
body: 'Welcome to Acme. Reply STOP to opt out.',
mediaUrls: ['https://cdn.example.com/welcome-banner.png'],
},
email: {
subject: 'Welcome to Acme',
body: 'Thanks for subscribing. Your first update is on the way.',
},
push: {
title: 'Welcome to Acme',
body: 'Tap to see what is dropping this week.',
},
};
const input: CreatePartnerBrandSubscriptionInput = {
phoneNumber: '+14155550111',
fullName: 'Jane Doe',
email: '[email protected]',
postSubscriptionMessage: welcomeMessage,
};await itm.mutation({
createPartnerBrandSubscription: {
__args: {
input: {
phoneNumber: '+14155550111',
fullName: 'Jane Doe',
email: '[email protected]',
postSubscriptionMessage: {
sms: {
body: 'Welcome to Acme — your first drop ships Friday. Reply STOP to opt out.',
mediaUrls: ['https://cdn.example.com/welcome-banner.png'],
},
email: {
subject: 'Welcome to Acme',
body: 'Hi Jane,\n\nThanks for subscribing — here is what to expect next...',
},
push: {
title: 'Welcome to Acme',
body: 'Tap to see what is dropping this week.',
},
},
},
},
verificationRequired: true,
subscription: { uid: true },
},
});Direct vs OTP timing:
| Mode | Where to pass postSubscriptionMessage | When delivery starts |
|---|---|---|
| Direct subscribe | Initial createPartnerBrandSubscription call | After the subscription is created or reactivated |
| OTP subscribe | Initial createPartnerBrandSubscription call with requireOtpVerification: true | After verifyPartnerBrandSubscription succeeds |
Do not pass postSubscriptionMessage to verifyPartnerBrandSubscription; that mutation only accepts verificationToken and code.
Channel inputs:
| Channel | Required | Optional | Notes |
|---|---|---|---|
| sms | body | mediaUrls (string array of image URLs for MMS) | Delivered to phoneNumber. |
| 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. |
| push | title, body | — | Skipped if the user has no push tokens or has opted out of brand messages. |
Delivery semantics:
- The message is sent at most once per active subscription period. The backend uses an atomic per-channel claim plus an idempotency key on each underlying task, so retrying the mutation (for example after a transient network failure) will not duplicate sends.
- The message is delivered when the subscription is newly created or reactivated — re-subscribing an already-active user is a no-op.
- In OTP mode, the message fires after
verifyPartnerBrandSubscriptionsucceeds, not when the OTP is requested. - Channel failures are isolated: if email cannot be sent because the user has no primary email, SMS and push are still attempted.
- Delivery is asynchronous. A successful mutation means the subscription request was accepted; it does not mean each SMS, email, or push provider has already completed delivery.
Recommended usage:
- Pass
postSubscriptionMessageon the firstcreatePartnerBrandSubscriptioncall. In OTP mode, the message is buffered with the pending session and delivered afterverifyPartnerBrandSubscription. You do not repeat it on the verify call. - For a marketing-style first SMS, prefer including a clear opt-out instruction (e.g.
Reply STOP to opt out) alongside any branded copy. - For the email channel, your brand must have an email alias configured on the backend. Confirm this in backstage before relying on the email channel in production.
- Keep message content deterministic for a signup attempt. Because delivery is idempotent per active subscription period, retrying the same create call is safe; changing the message between retries should not be used as an update mechanism.
Minimal SMS-only example:
await itm.mutation({
createPartnerBrandSubscription: {
__args: {
input: {
phoneNumber: '+14155550111',
postSubscriptionMessage: {
sms: {
body: 'You are subscribed to Acme updates. Reply STOP to opt out.',
},
},
},
},
verificationRequired: true,
subscription: { uid: true },
},
});OTP example with buffered welcome message:
const { createPartnerBrandSubscription } = await itm.mutation({
createPartnerBrandSubscription: {
__args: {
input: {
phoneNumber: '+14155550112',
requireOtpVerification: true,
email: '[email protected]',
postSubscriptionMessage: {
sms: {
body: 'Thanks for joining Acme. Reply STOP to opt out.',
},
email: {
subject: 'You are subscribed',
body: 'Your subscription is confirmed. We will send updates here.',
},
},
},
},
verificationRequired: true,
verificationToken: true,
},
});
// Later, after collecting the SMS code from the user:
await itm.mutation({
verifyPartnerBrandSubscription: {
__args: {
input: {
verificationToken: createPartnerBrandSubscription.verificationToken!,
code: '123456',
},
},
uid: true,
isActive: true,
},
});createPartnerBrandSubscription
Subscribe a phone number to the authenticated partner brand.
- Omit
requireOtpVerificationor set it tofalseto create the subscription immediately. - Set
requireOtpVerification: trueto start the OTP flow. The backend sends the code to the provided phone number and returns averificationToken. - If you pass
fullName, include both first and last name. - Pass
postSubscriptionMessagehere if you want SMS, email, or push sent after activation. This field is accepted only on the create mutation, including OTP starts.
const { createPartnerBrandSubscription } = await itm.mutation({
createPartnerBrandSubscription: {
__args: {
input: {
phoneNumber: '+14155550111',
fullName: 'Jane Doe',
email: '[email protected]',
listNames: ['VIP', 'Newsletter'],
compliance: {
pageUrl: 'https://example.com/signup',
submissionMethod: 'partner_sdk',
},
postSubscriptionMessage: {
sms: {
body: 'Welcome to Acme updates. Reply STOP to opt out.',
},
},
},
},
verificationRequired: true,
verificationToken: true,
subscription: {
uid: true,
isActive: true,
subscribedAt: true,
},
},
});
if (!createPartnerBrandSubscription.verificationRequired) {
console.log(createPartnerBrandSubscription.subscription?.uid);
}When verificationRequired is false, subscription is populated and verificationToken is null.
This is the recommended mode when:
- You are subscribing from an already-authenticated server flow.
- You already have consent and do not need phone possession verification.
- You want a single network round trip.
verifyPartnerBrandSubscription
Complete a subscription created with requireOtpVerification: true.
const { createPartnerBrandSubscription } = await itm.mutation({
createPartnerBrandSubscription: {
__args: {
input: {
phoneNumber: '+14155550112',
requireOtpVerification: true,
fullName: 'Otp User',
compliance: {
pageUrl: 'https://example.com/otp-signup',
submissionMethod: 'partner_sdk',
},
},
},
verificationRequired: true,
verificationToken: true,
},
});
if (!createPartnerBrandSubscription.verificationToken) {
throw new Error('Expected OTP verification to be required');
}
// Collect the SMS code from the user in your UI or API flow.
const code = '123456';
const { verifyPartnerBrandSubscription } = await itm.mutation({
verifyPartnerBrandSubscription: {
__args: {
input: {
verificationToken:
createPartnerBrandSubscription.verificationToken,
code,
},
},
uid: true,
isActive: true,
subscribedAt: true,
},
});
console.log(verifyPartnerBrandSubscription.uid);When OTP is required, createPartnerBrandSubscription returns verificationRequired: true, subscription: null, and a non-null verificationToken. Pass that token into verifyPartnerBrandSubscription together with the one-time code sent to the phone number.
This is the recommended mode when:
- The subscription starts from a public signup form.
- You want to prove the user controls the submitted phone number.
- Your compliance flow requires explicit OTP confirmation before the subscription is created.
Full OTP Example
This is the full two-step flow most integrators will want to implement from a server route or server action.
import { createITMPartnerClient } from '@itm-studio/partner-sdk';
function createRequestScopedITM(request: Request) {
return createITMPartnerClient({
token: process.env.ITM_PARTNER_TOKEN!,
headers: () => ({
...(request.headers.get('x-forwarded-for')
? { 'x-forwarded-for': request.headers.get('x-forwarded-for')! }
: {}),
...(request.headers.get('user-agent')
? { 'user-agent': request.headers.get('user-agent')! }
: {}),
}),
});
}
export async function startOtpSubscription(request: Request) {
const itm = createRequestScopedITM(request);
const { createPartnerBrandSubscription } = await itm.mutation({
createPartnerBrandSubscription: {
__args: {
input: {
phoneNumber: '+14155550112',
requireOtpVerification: true,
fullName: 'Otp User',
email: '[email protected]',
listNames: ['Members', 'SMS'],
compliance: {
pageUrl: 'https://example.com/join',
submissionMethod: 'partner_sdk',
submittedAt: new Date().toISOString(),
timezone: 'America/New_York',
},
postSubscriptionMessage: {
sms: {
body: 'You are subscribed. Reply STOP to opt out.',
},
email: {
subject: 'Welcome',
body: 'Your subscription is confirmed.',
},
},
},
},
verificationRequired: true,
verificationToken: true,
subscription: {
uid: true,
},
},
});
if (!createPartnerBrandSubscription.verificationRequired) {
return {
status: 'subscribed',
subscriptionUid: createPartnerBrandSubscription.subscription?.uid ?? null,
};
}
return {
status: 'otp_required',
verificationToken: createPartnerBrandSubscription.verificationToken,
};
}
export async function finishOtpSubscription(
request: Request,
verificationToken: string,
code: string,
) {
const itm = createRequestScopedITM(request);
const { verifyPartnerBrandSubscription } = await itm.mutation({
verifyPartnerBrandSubscription: {
__args: {
input: {
verificationToken,
code,
},
},
uid: true,
isActive: true,
subscribedAt: true,
},
});
return verifyPartnerBrandSubscription;
}Common Integration Notes
- Create the ITM client inside the incoming request when you need compliance backfill from headers.
- Persist the OTP
verificationTokenonly for the duration of the signup flow. Treat it as short-lived state, not a durable identifier. - Do not attempt to verify an OTP token with a different partner token than the one that created it.
- If you are building a browser form, post to your own backend first, then have your backend call the ITM Partner SDK. Do not call the SDK directly from the browser.
- If you need to associate the user with CRM-style segments on subscribe, send
listNamesduring the initial create call. Those lists are created on demand.
Media Upload Flow
Uploading media (e.g. cover images) is a two-step process:
Step 1: getPartnerUploadUrl
Get a presigned S3 URL to upload a file directly.
const { getPartnerUploadUrl } = await itm.mutation({
getPartnerUploadUrl: {
__args: { filename: 'event-cover', fileExtension: 'jpg' },
url: true,
filename: true,
},
});
// Upload the file to S3 using the presigned URL
await fetch(getPartnerUploadUrl.url, {
method: 'PUT',
body: fileBuffer, // Buffer, Blob, or ReadableStream
headers: { 'Content-Type': 'image/jpeg' },
});Step 2: createPartnerMediaAsset
After uploading, register the asset so it's linked to your brand.
const { createPartnerMediaAsset } = await itm.mutation({
createPartnerMediaAsset: {
__args: {
mimetype: 'image/jpeg',
filename: getPartnerUploadUrl.filename, // from Step 1
dimensions: { width: 1920, height: 1080 },
},
uid: true,
url: true,
mimeType: true,
},
});
// Use createPartnerMediaAsset.uid as coverImageUid when creating a momentcreatePartnerMoment
Create a moment (event) with optional venue and ticket tiers in one atomic operation.
const { createPartnerMoment } = await itm.mutation({
createPartnerMoment: {
__args: {
input: {
name: 'Summer Rooftop Party',
description: 'A rooftop party in NYC',
blurb: 'Join us on the roof!',
startDate: '2026-07-15T20:00:00.000Z',
endDate: '2026-07-16T02:00:00.000Z',
timezone: 'America/New_York',
type: 'IRL',
coverImageUid: createPartnerMediaAsset.uid, // from upload flow
venue: {
name: 'Rooftop Bar',
city: 'New York',
country: 'US',
address: '123 Main St',
},
category: 'PARTY',
subcategory: 'ROOFTOP_PARTY',
ticketTiers: [
{
name: 'General Admission',
price: 2500, // $25.00 in cents
supply: 200,
maxPerUser: 4,
currencyCode: 'USD',
},
{
name: 'VIP',
price: 7500, // $75.00 in cents
supply: 50,
maxPerUser: 2,
currencyCode: 'USD',
description: 'Includes open bar',
},
],
},
},
uid: true,
name: true,
slug: true,
startDate: true,
endDate: true,
status: true,
coverImage: { url: true },
venue: { name: true, city: true },
ticketTiers: {
uid: true,
name: true,
price: true,
currency: { code: true, symbol: true },
soldOut: true,
},
},
});Required fields: name, description, blurb, startDate, endDate, timezone, type
Moment types:
IRL— in-person event (requiresvenue)DIGITAL— online event (no venue needed)
Ticket tier pricing: price is in cents (e.g. 2500 = $25.00 for USD). Use 0 for free tiers. See Money Amounts for display formatting, including zero-decimal currencies.
Complete Upload + Create Moment Flow
import { createITMPartnerClient } from '@itm-studio/partner-sdk';
import { readFile } from 'fs/promises';
const itm = createITMPartnerClient({
token: process.env.ITM_PARTNER_TOKEN!,
});
// 1. Get presigned upload URL
const { getPartnerUploadUrl } = await itm.mutation({
getPartnerUploadUrl: {
__args: { filename: 'event-cover', fileExtension: 'jpg' },
url: true,
filename: true,
},
});
// 2. Upload file to S3
const file = await readFile('./event-cover.jpg');
await fetch(getPartnerUploadUrl.url, {
method: 'PUT',
body: file,
headers: { 'Content-Type': 'image/jpeg' },
});
// 3. Register the uploaded asset
const { createPartnerMediaAsset } = await itm.mutation({
createPartnerMediaAsset: {
__args: {
mimetype: 'image/jpeg',
filename: getPartnerUploadUrl.filename,
dimensions: { width: 1920, height: 1080 },
},
uid: true,
},
});
// 4. Create the moment with the uploaded cover image
const { createPartnerMoment } = await itm.mutation({
createPartnerMoment: {
__args: {
input: {
name: 'My Event',
description: 'An amazing event',
blurb: 'Come join us!',
startDate: '2026-07-15T20:00:00.000Z',
endDate: '2026-07-16T02:00:00.000Z',
timezone: 'America/New_York',
type: 'IRL',
coverImageUid: createPartnerMediaAsset.uid,
venue: {
name: 'The Venue',
city: 'New York',
country: 'US',
},
ticketTiers: [
{
name: 'General Admission',
price: 2500,
supply: 100,
maxPerUser: 4,
currencyCode: 'USD',
},
],
},
},
uid: true,
slug: true,
status: true,
ticketTiers: { uid: true, name: true },
},
});
console.log(`Created: ${createPartnerMoment.slug} (${createPartnerMoment.status})`);Pagination
All list endpoints use cursor-based pagination. The default page size is 50 items if take is not specified.
let cursor: string | null = null;
do {
const { getPartnerTicketsForMoment } = await itm.query({
getPartnerTicketsForMoment: {
__args: { momentSlug: 'my-event', cursor, take: 50 },
tickets: { uid: true, user: { name: true } },
hasNextPage: true,
nextCursor: true,
totalCount: true,
},
});
for (const ticket of getPartnerTicketsForMoment.tickets) {
console.log(ticket.user.name);
}
cursor = getPartnerTicketsForMoment.nextCursor ?? null;
} while (cursor);Custom Base URL
For staging or local development:
const itm = createITMPartnerClient({
token: process.env.ITM_PARTNER_TOKEN!,
baseUrl: 'https://staging-api.itm.studio/graphql',
});Development
npm install
npm run generate # Generate typed client from schema
npm test # Run tests
npm run build # Build for publishingArchitecture
itm-backend (NestJS) itm-partner-sdk
┌─────────────────────┐ ┌────────────────────────┐
│ Full GraphQL Schema │ │ schema/partner.graphql │
│ (8000+ lines) │ ── export ──▶ │ (partner subset only) │
│ │ │ │
│ BrandPartnerGuard │ │ genql │
│ x-partner-api-key │ │ │ │
│ Authorization Bearer │ │ ▼ │
└─────────────────────┘ │ src/generated/ (typed) │
▲ │ │ │
│ │ ▼ │
│ x-partner-api-key │ createITMPartnerClient │
└──────────────────────────────│ │
└────────────────────────┘
│
▼
┌────────────────────────┐
│ GraphQL Hive │
│ Schema Registry │
│ Breaking change guard │
└────────────────────────┘- Partner schema is a curated subset of the full backend schema — only partner-safe endpoints
- genql generates a fully typed client from the schema at build time — no runtime introspection
- GraphQL Hive validates schema changes on every PR and blocks breaking changes
- Authentication uses the existing
BrandPartnerGuard— token sent viax-partner-api-keyheader
Schema Governance (Hive)
This SDK uses GraphQL Hive for schema governance.
- On PRs:
schema:checkruns automatically to detect breaking changes before merge - On push to main:
schema:publishupdates the registry so the schema history is tracked - Dashboard: app.graphql-hive.com/itm/itm-partner-api/production
# Local usage (requires HIVE_TOKEN env var)
npm run schema:check # Check for breaking changes
npm run schema:publish # Publish schema to registryCI/CD
| Job | Trigger | What it does |
|---|---|---|
| build-and-test | Every push & PR | Install, generate, lint, test, build |
| schema-check | PRs only | Validates schema changes won't break partners |
| schema-publish | Push to main | Publishes schema to Hive registry |
| publish | Push to main | Auto-bumps patch version and publishes to NPM |
Adding New Partner Endpoints
When exposing a new backend endpoint to partners:
- Add the query/mutation to
schema/partner.graphqlwith only the fields partners should access - Run
npm run generateto regenerate the typed client - Add tests in
tests/queries.test.ts - Open a PR — Hive will validate the schema change isn't breaking
- After merge, the schema is auto-published to Hive and a new SDK version can be released
