@itm-studio/partner-sdk
v0.2.1
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,
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.
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,
coverImage: {
url: true,
mimeType: true,
},
ticketTiers: {
name: true,
price: true,
soldOut: true,
},
},
});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(). All mutations use the same authentication — your brand is determined from the token.
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). Use 0 for free tiers.
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
