@itm-studio/partner-sdk 0.2.10 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,6 +32,8 @@ const { getPartnerMomentsForBrand } = await itm.query({
32
32
  slug: true,
33
33
  startDate: true,
34
34
  endDate: true,
35
+ category: true,
36
+ subcategory: true,
35
37
  status: true,
36
38
  timezone: true,
37
39
  externalUrl: true,
@@ -103,6 +105,71 @@ export function createITMClientForRequest(request: Request) {
103
105
 
104
106
  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.
105
107
 
108
+ ### Brand Agent session authentication (first-party only)
109
+
110
+ First-party ITM agents authenticate with a short-lived Brand Agent session
111
+ instead of a partner API token. This mode is not available to external
112
+ integrators: it requires workload identity (e.g. a Vercel OIDC token) trusted
113
+ by the ITM backend and a backend-registered agent session, and only the
114
+ capability-scoped operations granted to the session (`CREATE_MOMENT`,
115
+ `SEND_EMAIL`, `READ_MOMENT_ANALYTICS`) will authorize. Everything else stays
116
+ partner-token-only, and a request carrying both a partner API key and an
117
+ agent session is rejected as ambiguous.
118
+
119
+ ```typescript
120
+ import { createITMPartnerClient } from "@itm-studio/partner-sdk";
121
+
122
+ const itm = createITMPartnerClient({
123
+ agentSession: {
124
+ // Minted fresh for every request; never cached or persisted.
125
+ workloadToken: () => getWorkloadOidcToken(),
126
+ sessionUid,
127
+ bootstrapToken, // optional one-time session bootstrap
128
+ },
129
+ });
130
+ ```
131
+
132
+ The brand is always derived server-side from the session — this mode never
133
+ carries a brand identifier or a partner API key. Agent-session mutations must
134
+ supply `toolCallId` on their inputs, plus `idempotencyKey` where it is not
135
+ already required (`CreatePartnerMomentInput`; `SendPartnerAgentEmailInput`
136
+ requires `idempotencyKey` for every caller). Partner-token callers must omit
137
+ the agent-only fields (`toolCallId`, and `idempotencyKey` on
138
+ `CreatePartnerMomentInput`).
139
+
140
+ ### Brand Agent access-token authentication (first-party only)
141
+
142
+ A third mode exists for the one moment in the session lifecycle when no agent
143
+ session exists yet: fetching a conversation thread's seed context while a new
144
+ session is being created. It authenticates with workload identity plus the
145
+ short-lived agent access token the caller presented, and it authorizes only
146
+ the operations guarded for it (currently `getAgentThreadSeed`, which takes no
147
+ arguments — the admin, brand, and thread scope all come from the token's
148
+ verified claims).
149
+
150
+ ```typescript
151
+ const itm = createITMPartnerClient({
152
+ agentAccess: {
153
+ // Minted fresh for every request; never cached or persisted.
154
+ workloadToken: () => getWorkloadOidcToken(),
155
+ // The raw bearer from the inbound session-create request.
156
+ accessToken,
157
+ },
158
+ });
159
+
160
+ const { getAgentThreadSeed } = await itm.query({
161
+ getAgentThreadSeed: {
162
+ seed: true,
163
+ includedMessages: true,
164
+ totalMessages: true,
165
+ truncated: true,
166
+ },
167
+ });
168
+ ```
169
+
170
+ Exactly one of `token`, `agentSession`, or `agentAccess` must be provided;
171
+ combining modes is rejected.
172
+
106
173
  ## Money Amounts
107
174
 
108
175
  Dynamic-pricing price values use the currency's integer minor unit: cents for
@@ -329,7 +396,13 @@ const { getPartnerMomentsForBrand } = await itm.query({
329
396
  },
330
397
  sortOrder: "ASC", // ASC | DESC by start date (default: DESC)
331
398
  },
332
- moments: { uid: true, name: true, startDate: true },
399
+ moments: {
400
+ uid: true,
401
+ name: true,
402
+ startDate: true,
403
+ category: true,
404
+ subcategory: true,
405
+ },
333
406
  totalCount: true,
334
407
  hasNextPage: true,
335
408
  nextCursor: true,
@@ -344,6 +417,49 @@ setting `includePrivate` to `false`, preserves the public-only behavior.
344
417
  `filters.search` matches moment names and slugs case-insensitively, is limited
345
418
  to 100 characters, and composes with visibility, status, sorting, and cursor
346
419
  pagination.
420
+ Select `category` and `subcategory` on each moment when you need to filter or
421
+ group SDK results by the event taxonomy. Both values are set when the moment
422
+ is created; a film screening created with `category: "SCREENING"` and
423
+ `subcategory: "FILM_SCREENING"` is returned with exactly those values.
424
+
425
+ Moment tags can provide campaign-specific grouping without changing the event
426
+ taxonomy. `momentTags` is available on every `Moment` the SDK returns, including
427
+ `getPartnerMoment`. Only tags marked partner-visible are returned; private tags
428
+ are never available through the SDK. Each assignment includes whether it was
429
+ added manually or automatically. `pagination.take` must be a positive integer;
430
+ it defaults to 50 and is capped at 100. Pass `nextCursor` back as
431
+ `pagination.cursor` to fetch the next page:
432
+
433
+ ```typescript
434
+ const { getPartnerMomentsForBrand } = await itm.query({
435
+ getPartnerMomentsForBrand: {
436
+ __args: { take: 20 },
437
+ moments: {
438
+ uid: true,
439
+ name: true,
440
+ momentTags: {
441
+ __args: {
442
+ sources: ["MANUAL"], // omit to include MANUAL and AUTO
443
+ pagination: { take: 50 },
444
+ },
445
+ assignments: {
446
+ tag: { uid: true, name: true },
447
+ source: true,
448
+ isPublic: true,
449
+ confidence: true,
450
+ },
451
+ totalCount: true,
452
+ hasNextPage: true,
453
+ nextCursor: true,
454
+ },
455
+ },
456
+ totalCount: true,
457
+ },
458
+ });
459
+ ```
460
+
461
+ Use the returned Moment UIDs with audience filters when you need users who
462
+ interacted with Moments from a particular film campaign.
347
463
 
348
464
  ### `getPartnerDrops`
349
465
 
@@ -418,6 +534,11 @@ const { getPartnerMoment } = await itm.query({
418
534
  url: true,
419
535
  mimeType: true,
420
536
  },
537
+ venue: {
538
+ name: true,
539
+ city: true,
540
+ postalCode: true,
541
+ },
421
542
  ticketTiers: {
422
543
  name: true,
423
544
  price: true,
@@ -676,6 +797,55 @@ const { getPartnerCustomersForMoment } = await itm.query({
676
797
  });
677
798
  ```
678
799
 
800
+ ### `getPartnerAudience`
801
+
802
+ Get the authenticated brand's customer audience with explicit filters, one
803
+ deterministic sort, and opaque cursor pagination. Call this only from a trusted
804
+ Node.js server: the partner token and returned customer data must never be
805
+ embedded in a browser/mobile bundle or written to application logs.
806
+
807
+ ```typescript
808
+ const { getPartnerAudience } = await itm.query({
809
+ getPartnerAudience: {
810
+ __args: {
811
+ input: {
812
+ filters: {
813
+ search: "john@example.com",
814
+ emailConsentStatuses: ["SUBSCRIBED"],
815
+ momentUids: ["moment-uid"],
816
+ minTotalSpendUsdCents: 5000,
817
+ },
818
+ sort: { field: "TOTAL_SPEND_USD", direction: "DESC" },
819
+ take: 50, // maximum 100
820
+ includeTotalCount: true, // omitted by default to avoid the count query
821
+ },
822
+ },
823
+ customers: {
824
+ uid: true, // brand-scoped customer UID
825
+ name: true,
826
+ email: true,
827
+ phoneNumber: true,
828
+ emailConsentStatus: true,
829
+ smsConsentStatus: true,
830
+ communicationsOptedOut: true,
831
+ isSuppressed: true,
832
+ totalSpendUsdCents: true,
833
+ numMoments: true,
834
+ updatedAt: true,
835
+ },
836
+ totalCount: true,
837
+ hasNextPage: true,
838
+ nextCursor: true,
839
+ },
840
+ });
841
+ ```
842
+
843
+ Filters are combined with AND; values inside one list are combined with OR.
844
+ `from` timestamps are inclusive and `before` timestamps are exclusive. Audience
845
+ pages and optional exact counts can be up to 30 seconds old. Consent and
846
+ suppression fields reflect ITM's current brand-scoped records but do not replace
847
+ your legal-purpose and communications-compliance checks.
848
+
679
849
  ### `getPartnerMomentCollection`
680
850
 
681
851
  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.
@@ -857,8 +1027,43 @@ Use OTP subscribe when you want the user to confirm the phone number before the
857
1027
  - `listNames`: optional brand list names. Missing lists are created automatically and the subscribed user is attached to them.
858
1028
  - `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.
859
1029
  - `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.
1030
+ - `consentText`: optional but strongly recommended. Send the verbatim opt-in disclosure the subscriber accepted on your signup surface, up to 5000 characters. See [Marketing Consent](#marketing-consent).
860
1031
  - `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](#post-subscription-welcome-message).
861
1032
 
1033
+ ### Marketing Consent
1034
+
1035
+ A successful subscription **is** the marketing opt-in. Once `createPartnerBrandSubscription` (or `verifyPartnerBrandSubscription` for OTP flows) returns, the subscriber is consented for the brand's marketing SMS, and for email once an address is on file. Consent flows through to the brand's connected email/SMS destinations automatically.
1036
+
1037
+ Existing opt-outs always win, per channel. A prior STOP keyword or SMS unsubscribe for that brand blocks the SMS consent grant, and a prior email unsubscribe blocks the email consent grant — the subscription still succeeds, but consent is not granted on an opted-out channel. Users the brand has blacklisted cannot subscribe at all: the mutation fails with an authorization error.
1038
+
1039
+ **Because the subscribe call grants consent, collect the opt-in properly on your surface:**
1040
+
1041
+ - Show an explicit marketing consent disclosure at signup.
1042
+ - Send that exact disclosure back as `consentText`. It is stored with the subscription's compliance record as evidence of what the subscriber agreed to.
1043
+ - Send `compliance` metadata (at minimum `pageUrl` and `submissionMethod`) alongside it, and forward `x-forwarded-for` and `user-agent` so IP and user agent are captured.
1044
+
1045
+ ```typescript
1046
+ await client.mutation({
1047
+ createPartnerBrandSubscription: {
1048
+ __args: {
1049
+ input: {
1050
+ phoneNumber: "+14155551234",
1051
+ consentText:
1052
+ "By joining, you agree to receive recurring marketing texts from Brand. Msg & data rates may apply. Reply STOP to opt out.",
1053
+ compliance: {
1054
+ pageUrl: "https://example.com/signup",
1055
+ submissionMethod: "partner_sdk",
1056
+ },
1057
+ },
1058
+ },
1059
+ subscription: { uid: true },
1060
+ verificationRequired: true,
1061
+ },
1062
+ });
1063
+ ```
1064
+
1065
+ `consentText` is trimmed before it is stored, and a blank value is treated as omitted. Values longer than 5000 characters are rejected with a validation error, not truncated. In OTP mode the text is captured on the first call and re-used automatically when the flow is verified — send it on the first call only.
1066
+
862
1067
  ### OTP Flow Step By Step
863
1068
 
864
1069
  1. Call `createPartnerBrandSubscription` with `requireOtpVerification: true`.