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

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.

The SDK enforces this boundary and supports Node.js server runtimes only. Imports from browsers, React Native, Web or Service Workers, edge runtimes, and Electron processes fail before an authenticated client can be created. Send client-side requests to your own Node backend, then return only the data the client needs.

In Next.js, also mark the module that reads the token as server-only so an accidental Client Component import fails during the application build:

import "server-only";
import { createITMPartnerClient } from "@itm-studio/partner-sdk";

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.

Brand Agent session authentication (first-party only)

First-party ITM agents authenticate with a short-lived Brand Agent session instead of a partner API token. This mode is not available to external integrators: it requires workload identity (e.g. a Vercel OIDC token) trusted by the ITM backend and a backend-registered agent session, and only the capability-scoped operations granted to the session (CREATE_MOMENT, SEND_EMAIL, READ_MOMENT_ANALYTICS) will authorize. Everything else stays partner-token-only, and a request carrying both a partner API key and an agent session is rejected as ambiguous.

import { createITMPartnerClient } from "@itm-studio/partner-sdk";

const itm = createITMPartnerClient({
  agentSession: {
    // Minted fresh for every request; never cached or persisted.
    workloadToken: () => getWorkloadOidcToken(),
    sessionUid,
    bootstrapToken, // optional one-time session bootstrap
  },
});

The brand is always derived server-side from the session — this mode never carries a brand identifier or a partner API key. Agent-session mutations must supply toolCallId on their inputs, plus idempotencyKey where it is not already required (CreatePartnerMomentInput; SendPartnerAgentEmailInput requires idempotencyKey for every caller). Partner-token callers must omit the agent-only fields (toolCallId, and idempotencyKey on CreatePartnerMomentInput).

Brand Agent access-token authentication (first-party only)

A third mode exists for the one moment in the session lifecycle when no agent session exists yet: fetching a conversation thread's seed context while a new session is being created. It authenticates with workload identity plus the short-lived agent access token the caller presented, and it authorizes only the operations guarded for it (currently getAgentThreadSeed, which takes no arguments — the admin, brand, and thread scope all come from the token's verified claims).

const itm = createITMPartnerClient({
  agentAccess: {
    // Minted fresh for every request; never cached or persisted.
    workloadToken: () => getWorkloadOidcToken(),
    // The raw bearer from the inbound session-create request.
    accessToken,
  },
});

const { getAgentThreadSeed } = await itm.query({
  getAgentThreadSeed: {
    seed: true,
    includedMessages: true,
    totalMessages: true,
    truncated: true,
  },
});

Exactly one of token, agentSession, or agentAccess must be provided; combining modes is rejected.

Money Amounts

Dynamic-pricing price values use the currency's integer minor unit: cents for USD and EUR, and whole units for zero-decimal currencies such as JPY and KRW. Do not multiply zero-decimal dynamic-pricing amounts by 100. Older partner tier read/edit fields retain the backend's historical cents representation; use the dynamic-pricing API for price changes on enrolled tiers.

const ZERO_DECIMAL_CURRENCIES = new Set([
  "BIF",
  "CLP",
  "DJF",
  "GNF",
  "JPY",
  "KMF",
  "KRW",
  "MGA",
  "PYG",
  "RWF",
  "UGX",
  "VND",
  "VUV",
  "XAF",
  "XOF",
  "XPF",
]);

function formatPartnerPrice(priceInMinorUnits: number, currencyCode: string) {
  if (priceInMinorUnits === 0) return "Free";

  const zeroDecimal = ZERO_DECIMAL_CURRENCIES.has(currencyCode);
  const amount = zeroDecimal ? priceInMinorUnits : priceInMinorUnits / 100;
  const maximumFractionDigits = zeroDecimal ? 0 : 2;

  return new Intl.NumberFormat("en-US", {
    style: "currency",
    currency: currencyCode,
    minimumFractionDigits: maximumFractionDigits,
    maximumFractionDigits,
  }).format(amount);
}

Dynamic Pricing

Dynamic pricing is a controlled actuator, not a hosted rules engine. A brand admin first enables a moment policy and enrolls paid tiers with minimum and maximum prices. The partner then reads current context and explicitly chooses which enrolled tiers to update.

Always fetch fresh context immediately before an apply. Admission windows, policy version, bounds, configured supply, and current price form the stable pricing concurrency contract. Live sales do not invalidate pricingContextVersion. Each update must separately declare whether it is inventory-independent or provide a remaining-supply/fill-rate range checked under the price-write lock. The entire batch is atomic.

const { getPartnerMomentDynamicPricingContext: context } = await itm.query({
  getPartnerMomentDynamicPricingContext: {
    __args: { input: { momentUid } },
    policy: { version: true, enabled: true },
    tiers: {
      ticketTierUid: true,
      currentPrice: true,
      minPrice: true,
      maxPrice: true,
      eligible: true,
      pricingContextVersion: true,
      supply: true,
      remainingSupply: true,
      fillRateBps: true,
      admissionWindows: { startsAt: true, endsAt: true },
    },
  },
});

const tier = context.tiers.find((candidate) => candidate.eligible)!;
const { applyPartnerMomentDynamicPricing: batch } = await itm.mutation({
  applyPartnerMomentDynamicPricing: {
    __args: {
      input: {
        momentUid,
        mode: "APPLY", // use PREVIEW to validate without changing prices
        idempotencyKey: crypto.randomUUID(),
        policyVersion: context.policy.version,
        reason: "60% fill threshold reached",
        strategyKey: "capacity-step",
        strategyVersion: "1.0.0",
        updates: [
          {
            ticketTierUid: tier.ticketTierUid,
            newPrice: tier.currentPrice + 500,
            expectedPrice: tier.currentPrice,
            expectedPricingContextVersion: tier.pricingContextVersion,
            inventoryGuard: {
              mode: "RANGE",
              metric: "FILL_RATE_BPS",
              minimum: 6000,
              maximum: 6999,
            },
          },
        ],
      },
    },
    uid: true,
    status: true,
    failureCode: true,
    failureMessage: true,
    items: {
      ticketTierUid: true,
      status: true,
      previousPrice: true,
      resultingPrice: true,
      failureCode: true,
    },
  },
});

Reuse an idempotency key only for the identical request. PROCESSING can be polled with getPartnerDynamicPricingBatch; terminal statuses are PREVIEWED, APPLIED, NO_CHANGES, REJECTED, CONFLICTED, and FAILED. Legacy editPartnerTicketTiersForMoment price edits are rejected for tiers enrolled in an enabled dynamic pricing policy.

Use { mode: 'INDEPENDENT' } only when the strategy intentionally does not depend on live capacity. Range bounds are inclusive; fill rate uses integer basis points (6000 = 60%). Refresh after validUntil. Because price is stored per tier, every upcoming admission window for a managed tier must fall within the configured horizon before that tier is eligible.

AgentMail

Queue one to ten personalized plain-text emails from the authenticated brand's dedicated AgentMail inbox. Each item is a separate single-recipient message. The mutation returns after the durable batch is accepted; query the batch for final per-message provider status.

const queued = await client.mutation({
  sendPartnerAgentEmail: {
    __args: {
      input: {
        idempotencyKey: "venue-outreach-2026-08-13",
        messages: [
          {
            to: "[email protected]",
            subject: "October show inquiry",
            text: "Would October 10 work for a 200-person show?",
          },
        ],
      },
    },
    uid: true,
    status: true,
    acceptedCount: true,
    messages: { uid: true, recipient: true, status: true },
  },
});

const status = await client.query({
  getPartnerAgentEmailBatch: {
    __args: { batchUid: queued.sendPartnerAgentEmail.uid },
    status: true,
    messages: {
      recipient: true,
      status: true,
      agentMailMessageId: true,
      lastErrorCode: true,
    },
  },
});

Reuse an idempotency key only for the exact same logical batch. SENT means AgentMail accepted the message; it does not prove inbox delivery.

Available Queries

getPartnerBrand

Get details for the brand associated with the partner token. Takes no arguments — the brand is resolved from the authenticated token, and the response is always non-null.

const { getPartnerBrand } = await itm.query({
  getPartnerBrand: {
    uid: true,
    name: true,
    slug: true,
    bio: true,
    publicUrl: true,
    customDomain: true,
    customDomainVerifiedAt: true,
    spotifyUrl: true,
    youtubeEmbedUrl: true,
  },
});
  • publicUrl is the brand's hosted page on ITM.
  • customDomain and customDomainVerifiedAt describe the brand's white-label domain, if one is configured; customDomainVerifiedAt is null until the domain is verified.
  • bio, spotifyUrl, and youtubeEmbedUrl are null when unset.

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
      filters: {
        search: "summer launch", // case-insensitive name or slug match
        includePrivate: true, // include private moments owned by your brand
      },
      sortOrder: "ASC", // ASC | DESC by start date (default: DESC)
    },
    moments: { uid: true, name: true, startDate: true },
    totalCount: true,
    hasNextPage: true,
    nextCursor: true,
  },
});

By default, only public moments are returned. Set filters.includePrivate to true to return both public and private moments owned by the brand associated with the partner token. Omitting filters, or setting includePrivate to false, preserves the public-only behavior. filters.search matches moment names and slugs case-insensitively, is limited to 100 characters, and composes with visibility, status, sorting, and cursor pagination.

getPartnerDrops

Get paginated brand drops for the brand associated with your partner token. Use this for partner-hosted drop grids, release pages, and filtered merch or music drop listings.

Supported filters:

  • search: case-insensitive match across drop name, subtitle, and slug.
  • isPublic: filter public or private drops.
  • ctaType: filter by EXTERNAL_URL, GATED_LINK, MUSIC_LINK, or SUBSCRIBE.
  • cursor and take: use nextCursor while hasNextPage is true.
const { getPartnerDrops } = await itm.query({
  getPartnerDrops: {
    __args: {
      take: 20,
      cursor: nextCursor,
      search: "release",
      isPublic: true,
      ctaType: "EXTERNAL_URL", // EXTERNAL_URL | GATED_LINK | MUSIC_LINK | SUBSCRIBE
    },
    drops: {
      uid: true,
      name: true,
      slug: true,
      isPublic: true,
      ctaType: true,
      ctaUrl: true,
      captureName: true,
      captureEmail: true,
      coverImage: { url: true, mimeType: true },
      coverImageMobile: { url: true, mimeType: true },
      musicLink: {
        title: true,
        artistName: true,
        releaseType: true,
        sourceUrl: true,
        odesliPageUrl: true,
        thumbnailUrl: true,
      },
    },
    totalCount: true,
    hasNextPage: true,
    nextCursor: true,
  },
});

The response is always scoped by the partner token; do not pass a brand ID. coverImageMobile and musicLink can be null, so handle those as optional in rendering code.

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,
  },
});

getPartnerMomentStats

Get cached partner-facing counts for a moment owned by the authenticated partner's brand. Counts can be up to five minutes old.

  • ticketsCount counts confirmed tickets only.
  • generatedAt identifies when this cached count set was computed.
  • paidTicketsCount counts confirmed tickets with a materialized price above zero.
  • freeTicketsCount counts confirmed tickets with a zero materialized price.
  • waitlistCount counts active waitlist entries.
  • externalRsvpCount counts external RSVPs.
  • ticketRsvpRequestsCount counts ticket RSVP requests.
const { getPartnerMomentStats } = await itm.query({
  getPartnerMomentStats: {
    __args: { momentSlug: "my-event-slug" },
    generatedAt: true,
    ticketsCount: true,
    paidTicketsCount: true,
    freeTicketsCount: true,
    waitlistCount: true,
    externalRsvpCount: true,
    ticketRsvpRequestsCount: true,
  },
});

Note: getPartnerMomentStats returns null if the moment does not exist or is not owned by the authenticated partner's brand.

getPartnerMomentAttendanceStats

Get a cached attendance snapshot for a moment owned by the authenticated partner's brand. Counts can be up to five minutes old; generatedAt reports when the snapshot was produced.

Look up the moment by exactly one of:

  • momentUid — the ITM moment UID
  • momentSlug — the ITM moment slug
  • externalPlatform + externalId — the source-platform identifier for imported events (for example EVENTBRITE); the two must be provided together
const { getPartnerMomentAttendanceStats } = await itm.query({
  getPartnerMomentAttendanceStats: {
    __args: {
      input: { externalPlatform: "EVENTBRITE", externalId: "1234567890" },
    },
    momentUid: true,
    momentSlug: true,
    generatedAt: true,
    tickets: {
      total: true,
      checkedIn: true,
      tiers: {
        tierUid: true,
        name: true,
        label: true,
        total: true,
        checkedIn: true,
      },
    },
    guestlist: {
      total: true,
      checkedIn: true,
      unlabeled: { total: true, checkedIn: true },
      labels: { labelUid: true, name: true, total: true, checkedIn: true },
    },
  },
});
  • tickets counts confirmed tickets, overall and per ticket tier.
  • guestlist counts guest-list entries, split into unlabeled and per-label buckets.

Note: Returns null if no moment matches the lookup or the moment is not owned by the authenticated partner's brand. The request fails with a validation error when an identifier is an empty string, when externalPlatform and externalId are not provided together, when more than one lookup key is given, or when an external ID matches multiple moments.

getPartnerTicketTiersForMoment

Get paginated, partner-facing ticket tiers for a moment. Use this for pricing, inventory, and dynamic catalog views without selecting the full public Moment payload.

Provide either momentSlug or momentUid. If isActive is omitted, the backend returns active tiers only; pass isActive: false only when you intentionally need inactive tiers.

const { getPartnerTicketTiersForMoment } = await itm.query({
  getPartnerTicketTiersForMoment: {
    __args: {
      input: {
        momentSlug: "my-event-slug",
        take: 20,
        search: "general",
        isPaid: true,
        minPrice: 1000, // cents
        maxPrice: 5000, // cents
      },
    },
    ticketTiers: {
      uid: true,
      name: true,
      label: true,
      labelColor: true,
      price: true, // cents
      supply: true,
      remainingSupply: true,
      fillRate: true, // 0-1 sold-through ratio
      isActive: true,
      isPrivate: true,
      slotDate: true,
      slotDurationHours: true,
    },
    totalCount: true,
    hasNextPage: true,
    nextCursor: true,
  },
});

Supported filters include ticketTierUids, search, isActive, isPrivate, isPaid, exact/min/max price, exact/min/max supply, exact/min/max remainingSupply, min/max fillRate, admission-window bounds (startsAt, endsAt), and slot date bounds (slotDateStart, slotDateEnd).

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,
  },
});

getPartnerAudience

Get the authenticated brand's customer audience with explicit filters, one deterministic sort, and opaque cursor pagination. Call this only from a trusted Node.js server: the partner token and returned customer data must never be embedded in a browser/mobile bundle or written to application logs.

const { getPartnerAudience } = await itm.query({
  getPartnerAudience: {
    __args: {
      input: {
        filters: {
          search: "[email protected]",
          emailConsentStatuses: ["SUBSCRIBED"],
          momentUids: ["moment-uid"],
          minTotalSpendUsdCents: 5000,
        },
        sort: { field: "TOTAL_SPEND_USD", direction: "DESC" },
        take: 50, // maximum 100
        includeTotalCount: true, // omitted by default to avoid the count query
      },
    },
    customers: {
      uid: true, // brand-scoped customer UID
      name: true,
      email: true,
      phoneNumber: true,
      emailConsentStatus: true,
      smsConsentStatus: true,
      communicationsOptedOut: true,
      isSuppressed: true,
      totalSpendUsdCents: true,
      numMoments: true,
      updatedAt: true,
    },
    totalCount: true,
    hasNextPage: true,
    nextCursor: true,
  },
});

Filters are combined with AND; values inside one list are combined with OR. from timestamps are inclusive and before timestamps are exclusive. Audience pages and optional exact counts can be up to 30 seconds old. Consent and suppression fields reflect ITM's current brand-scoped records but do not replace your legal-purpose and communications-compliance checks.

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",
        capacity: 250,
        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,
    capacity: 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, startDate, endDate, timezone, type

Moment types:

  • IRL — in-person event (requires venue)
  • DIGITAL — online event (no venue needed; accepts onlineConfig)
  • HYBRID — venue plus online access. When creating ticket tiers for a hybrid moment, set each tier's attendanceMode to IN_PERSON or ONLINE.

For digital or hybrid moments, pass onlineConfig when you want ITM to store the meeting details:

await itm.mutation({
  createPartnerMoment: {
    __args: {
      input: {
        name: "Hybrid Show",
        description: "Attend in person or online",
        startDate: "2026-07-15T20:00:00.000Z",
        endDate: "2026-07-15T23:00:00.000Z",
        timezone: "America/New_York",
        type: "HYBRID",
        onlineConfig: {
          provider: "ZOOM", // GOOGLE_MEET | MS_TEAMS | OTHER | ZOOM
          meetingUrl: "https://zoom.us/j/123456789",
          meetingPasscode: "club",
        },
        venue: { name: "Main Room", city: "New York", country: "US" },
        ticketTiers: [
          {
            name: "Online Admission",
            price: 1500,
            supply: 300,
            maxPerUser: 4,
            currencyCode: "USD",
            attendanceMode: "ONLINE",
          },
        ],
      },
    },
    uid: true,
    type: true,
    ticketTiers: { uid: true, attendanceMode: true },
  },
});

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.

editPartnerTicketTiersForMoment

Bulk edit partner-owned ticket tiers for a moment. Provide either momentSlug or momentUid, then target specific tiers by ticketTierUid.

This endpoint intentionally supports only price and supply updates. Use getPartnerTicketTiersForMoment first if you need to discover tier UIDs.

const { editPartnerTicketTiersForMoment } = await itm.mutation({
  editPartnerTicketTiersForMoment: {
    __args: {
      input: {
        momentSlug: "my-event-slug",
        updates: [
          {
            ticketTierUid: "tier-uid",
            price: 3000, // cents
            supply: 120,
          },
        ],
      },
    },
    uid: true,
    name: true,
    price: true,
    supply: true,
    remainingSupply: true,
    fillRate: true,
    isActive: true,
  },
});

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