npm package: @itm-studio/partner-sdk
Description: TypeScript SDK for the ITM Partner API — typed GraphQL client with zero introspection required
Author: ITM Studio
Latest version: 0.2.1

# @itm-studio/partner-sdk

Type-safe TypeScript SDK for the ITM Partner API. Built with [genql](https://genql.dev/) — full autocomplete, zero introspection required.

[![CI](https://github.com/itm-studio/itm-partner-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/itm-studio/itm-partner-sdk/actions/workflows/ci.yml)
[![Schema Registry](https://img.shields.io/badge/schema-GraphQL%20Hive-blue)](https://app.graphql-hive.com/itm/itm-partner-api/production)

A full implementation example can be found here: [demo-events-sdk-integration](https://github.com/itm-studio/demo-events-sdk-integration)

## Install

```bash
npm install @itm-studio/partner-sdk
```

## Quick Start

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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`:

```typescript
const { getPartnerMomentCollection } = await itm.query({
  getPartnerMomentCollection: {
    __args: { slug: 'summer-series' },
    name: true,
    moments: {
      __args: { queryFilters: ['upcoming'] },
      uid: true,
      name: true,
      startDate: true,
    },
  },
});
```

> **Note:** `getPartnerMomentCollection` returns `null` if 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 `momentSlug` is provided, returns **moment-level** echoes for that moment.
- If `momentSlug` is omitted, returns **brand-level** echoes.

```typescript
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:

```typescript
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.

```typescript
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.

```typescript
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 moment
```

### `createPartnerMoment`

Create a moment (event) with optional venue and ticket tiers in one atomic operation.

```typescript
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 (requires `venue`)
- `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

```typescript
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.

```typescript
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:

```typescript
const itm = createITMPartnerClient({
  token: process.env.ITM_PARTNER_TOKEN!,
  baseUrl: 'https://staging-api.itm.studio/graphql',
});
```

## Development

```bash
npm install
npm run generate   # Generate typed client from schema
npm test           # Run tests
npm run build      # Build for publishing
```

## Architecture

```
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 via `x-partner-api-key` header

## Schema Governance (Hive)

This SDK uses [GraphQL Hive](https://the-guild.dev/graphql/hive) for schema governance.

- **On PRs**: `schema:check` runs automatically to detect breaking changes before merge
- **On push to main**: `schema:publish` updates the registry so the schema history is tracked
- **Dashboard**: [app.graphql-hive.com/itm/itm-partner-api/production](https://app.graphql-hive.com/itm/itm-partner-api/production)

```bash
# Local usage (requires HIVE_TOKEN env var)
npm run schema:check    # Check for breaking changes
npm run schema:publish  # Publish schema to registry
```

## CI/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:

1. Add the query/mutation to `schema/partner.graphql` with only the fields partners should access
2. Run `npm run generate` to regenerate the typed client
3. Add tests in `tests/queries.test.ts`
4. Open a PR — Hive will validate the schema change isn't breaking
5. After merge, the schema is auto-published to Hive and a new SDK version can be released