npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@itm-studio/partner-sdk

v0.2.6

Published

TypeScript SDK for the ITM Partner API — typed GraphQL client with zero introspection required

Readme

@itm-studio/partner-sdk

Type-safe TypeScript SDK for the ITM Partner API. Built with genql — full autocomplete, zero introspection required.

CI Schema Registry

A full implementation example can be found here: demo-events-sdk-integration

Install

npm install @itm-studio/partner-sdk

Quick 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.

  • ticketsCount counts confirmed tickets only.
  • waitlistCount counts active waitlist entries.
  • externalRsvpCount counts external RSVPs.
  • ticketRsvpRequestsCount counts ticket RSVP requests.
const { getMomentStats } = await itm.query({
  getMomentStats: {
    __args: { momentSlug: 'my-event-slug' },
    ticketsCount: true,
    waitlistCount: true,
    externalRsvpCount: true,
    ticketRsvpRequestsCount: true,
  },
});

Note: getMomentStats returns null if 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: 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.
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 like pageUrl, submissionMethod, submittedAt, timezone, and geo/browser/device details.
  • compliance.ipAddress and compliance.userAgent: optional. If you omit them and forward x-forwarded-for and user-agent, the backend backfills those values for you.
  • postSubscriptionMessage: optional welcome message delivered automatically after the subscription becomes active. The SDK exposes this as PostSubscriptionMessageInput and accepts it on CreatePartnerBrandSubscriptionInput. See Post-Subscription Welcome Message.

OTP Flow Step By Step

  1. Call createPartnerBrandSubscription with requireOtpVerification: true.
  2. Store the returned verificationToken in the user session or another short-lived server-side state container associated with the current signup attempt.
  3. Collect the SMS code from the user.
  4. Call verifyPartnerBrandSubscription with that verificationToken and code.

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 verifyPartnerBrandSubscription succeeds, 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 postSubscriptionMessage on the first createPartnerBrandSubscription call. In OTP mode, the message is buffered with the pending session and delivered after verifyPartnerBrandSubscription. 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 requireOtpVerification or set it to false to create the subscription immediately.
  • Set requireOtpVerification: true to start the OTP flow. The backend sends the code to the provided phone number and returns a verificationToken.
  • If you pass fullName, include both first and last name.
  • Pass postSubscriptionMessage here 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 verificationToken only 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 listNames during 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 moment

createPartnerMoment

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 (requires venue)
  • 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 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 for schema governance.

# 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