@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 +206 -1
- package/dist/index.d.mts +316 -6
- package/dist/index.d.ts +316 -6
- package/dist/index.js +1056 -659
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1056 -660
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/schema/partner.graphql +252 -4
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: {
|
|
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`.
|