@proveanything/smartlinks 1.13.8 → 1.13.9

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.
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.13.8 | Generated: 2026-05-11T12:03:30.435Z
3
+ Version: 1.13.9 | Generated: 2026-05-12T11:17:48.753Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -5457,8 +5457,9 @@ interface InteractionTypeRecord {
5457
5457
  permissions?: InteractionPermissions
5458
5458
  data?: {
5459
5459
  display?: InteractionDisplay
5460
- scopes?: Record<string, InteractionDisplay>;
5460
+ scopes?: Record<string, InteractionDisplay>
5461
5461
  interactionType?: string
5462
+ effects?: InteractionEffect[]
5462
5463
  [key: string]: unknown
5463
5464
  }
5464
5465
  createdAt: string
@@ -5526,6 +5527,113 @@ interface SubmitInteractionError {
5526
5527
  }
5527
5528
  ```
5528
5529
 
5530
+ **InteractionEffect** (interface)
5531
+ ```typescript
5532
+ interface InteractionEffect {
5533
+ type: EffectType
5534
+ config?: EffectConfig
5535
+ }
5536
+ ```
5537
+
5538
+ **LoyaltyEffectConfig** (interface)
5539
+ ```typescript
5540
+ interface LoyaltyEffectConfig {
5541
+ [key: string]: never
5542
+ }
5543
+ ```
5544
+
5545
+ **TransactionalEffectConfig** (interface)
5546
+ ```typescript
5547
+ interface TransactionalEffectConfig {
5548
+ templateId: string
5549
+ * Channel to use.
5550
+ * Default: 'preferred' — auto-selects the contact's best available channel.
5551
+ channel?: 'email' | 'sms' | 'push' | 'whatsapp' | 'wallet' | 'preferred'
5552
+ props?: Record<string, unknown>
5553
+ include?: {
5554
+ productId?: string
5555
+ proofId?: string
5556
+ user?: boolean
5557
+ appCase?: string
5558
+ appThread?: string
5559
+ appRecord?: string
5560
+ }
5561
+ appId?: string
5562
+ }
5563
+ ```
5564
+
5565
+ **WebhookEffectConfig** (interface)
5566
+ ```typescript
5567
+ interface WebhookEffectConfig {
5568
+ url: string
5569
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
5570
+ headers?: Record<string, string>
5571
+ body?: Record<string, unknown>
5572
+ timeout?: number
5573
+ }
5574
+ ```
5575
+
5576
+ **TagEffectConfig** (interface)
5577
+ ```typescript
5578
+ interface TagEffectConfig {
5579
+ tags: string[]
5580
+ action?: 'add' | 'remove'
5581
+ }
5582
+ ```
5583
+
5584
+ **AppRecordEffectConfig** (interface)
5585
+ ```typescript
5586
+ interface AppRecordEffectConfig {
5587
+ appId?: string
5588
+ recordType?: string
5589
+ * Singleton cardinality key. At most one record per recordType+singletonPer will
5590
+ * exist per scope. Common values: 'contact', 'product', 'proof', 'global'
5591
+ singletonPer?: string
5592
+ data?: Record<string, unknown>
5593
+ anchors?: {
5594
+ productId?: string
5595
+ proofId?: string
5596
+ variantId?: string
5597
+ batchId?: string
5598
+ }
5599
+ }
5600
+ ```
5601
+
5602
+ **SegmentEffectConfig** (interface)
5603
+ ```typescript
5604
+ interface SegmentEffectConfig {
5605
+ segmentId: string
5606
+ action?: 'add' | 'remove'
5607
+ }
5608
+ ```
5609
+
5610
+ **InteractionEventContext** (interface)
5611
+ ```typescript
5612
+ interface InteractionEventContext {
5613
+ eventId: string | null
5614
+ collectionId: string
5615
+ appId: string | null
5616
+ interactionId: string | null
5617
+ contactId: string | null
5618
+ userId: string | null
5619
+ productId: string | null
5620
+ proofId: string | null
5621
+ variantId: string | null
5622
+ batchId: string | null
5623
+ outcome: string | null
5624
+ eventType: string | null
5625
+ source: string | null
5626
+ timestamp: string | null
5627
+ metadata: Record<string, unknown>
5628
+ broadcastId: string | null
5629
+ journeyId: string | null
5630
+ }
5631
+ ```
5632
+
5633
+ **EffectType** = ``
5634
+
5635
+ **EffectConfig** = ``
5636
+
5529
5637
  ### jobs
5530
5638
 
5531
5639
  **Job** (interface)
@@ -280,6 +280,195 @@ When defining a journey trigger, reference the `interactionId` that should fire
280
280
 
281
281
  ---
282
282
 
283
+ ## Interaction Effects
284
+
285
+ **Interaction effects** are automatic side-effects that fire immediately after an interaction event is recorded. They are defined directly on the interaction definition (`data.effects`) and run inline, fire-and-forget — they never block or fail the interaction response.
286
+
287
+ Common use cases: send a confirmation email, post a webhook to a CRM, tag the contact, award loyalty points, create an app record, or add the contact to a segment.
288
+
289
+ ### How It Works
290
+
291
+ 1. Event is logged to BigQuery
292
+ 2. Interaction definition is loaded
293
+ 3. Each configured effect runs **in order**, fire-and-forget
294
+ 4. Response is returned to the caller immediately
295
+
296
+ A failure in one effect is logged and swallowed; subsequent effects still run.
297
+
298
+ > **Note:** `transactional` and `webhook` effects currently run inline. They are planned to move to background jobs for retry-on-failure support once the jobs infrastructure is fully operational.
299
+
300
+ ### Configuring Effects
301
+
302
+ Pass `data.effects` when creating or updating an interaction type:
303
+
304
+ ```typescript
305
+ await SL.interactions.create(collectionId, {
306
+ id: 'donation',
307
+ appId: 'my-app',
308
+ permissions: { allowPublicSubmit: true },
309
+ data: {
310
+ effects: [
311
+ {
312
+ type: 'transactional',
313
+ config: {
314
+ templateId: 'tmpl_donation_receipt',
315
+ channel: 'email',
316
+ props: {
317
+ donationAmount: '{{metadata.amount}}',
318
+ campaignName: '{{metadata.campaign}}',
319
+ },
320
+ },
321
+ },
322
+ {
323
+ type: 'tag',
324
+ config: { tags: ['donor'] },
325
+ },
326
+ ],
327
+ },
328
+ });
329
+ ```
330
+
331
+ ### Token Interpolation
332
+
333
+ All effect `config` values support `{{token}}` interpolation resolved from the event context at runtime. Nested paths using dot notation are supported (e.g. `{{metadata.amount}}`).
334
+
335
+ | Token | Description |
336
+ |---|---|
337
+ | `{{eventId}}` | BigQuery event UUID |
338
+ | `{{collectionId}}` | Collection / brand ID |
339
+ | `{{appId}}` | App ID that submitted the event |
340
+ | `{{interactionId}}` | Interaction definition ID |
341
+ | `{{contactId}}` | Contact UUID (if resolved) |
342
+ | `{{userId}}` | Firebase UID (if authenticated) |
343
+ | `{{productId}}` | Product ID (if provided with event) |
344
+ | `{{proofId}}` | Proof ID (if provided with event) |
345
+ | `{{variantId}}` | Variant ID (if provided with event) |
346
+ | `{{batchId}}` | Batch ID (if provided with event) |
347
+ | `{{outcome}}` | Event outcome (e.g. `"submitted"`) |
348
+ | `{{eventType}}` | Event type string |
349
+ | `{{source}}` | Event source string |
350
+ | `{{timestamp}}` | ISO 8601 event timestamp |
351
+ | `{{metadata.*}}` | Any key from the event `metadata` object |
352
+
353
+ Tokens that resolve to `null` or `undefined` become an empty string.
354
+
355
+ ### Effect Types
356
+
357
+ #### `loyalty`
358
+
359
+ Awards loyalty points by evaluating `LoyaltyEarningRule` records configured against this interaction. Earning rules are managed separately via the Loyalty API. If no rules are configured, this effect is a no-op.
360
+
361
+ ```json
362
+ { "type": "loyalty", "config": {} }
363
+ ```
364
+
365
+ #### `transactional`
366
+
367
+ Sends a message to the contact using a comms template. Supports all channels with full Liquid template hydration.
368
+
369
+ ```json
370
+ {
371
+ "type": "transactional",
372
+ "config": {
373
+ "templateId": "tmpl_donation_receipt",
374
+ "channel": "email",
375
+ "props": {
376
+ "donationAmount": "{{metadata.amount}}"
377
+ }
378
+ }
379
+ }
380
+ ```
381
+
382
+ `channel` defaults to `"preferred"` — auto-selects the contact's best available channel respecting consent, suppression, and template availability. Optionally pass `include` to hydrate product/proof/user context, and `props` for additional Liquid variables.
383
+
384
+ #### `webhook`
385
+
386
+ Posts a JSON payload to an HTTP endpoint. All config values support `{{token}}` interpolation.
387
+
388
+ ```json
389
+ {
390
+ "type": "webhook",
391
+ "config": {
392
+ "url": "https://crm.example.com/api/events",
393
+ "headers": { "Authorization": "Bearer sk_live_abc123" },
394
+ "body": {
395
+ "contactId": "{{contactId}}",
396
+ "amount": "{{metadata.amount}}"
397
+ },
398
+ "timeout": 8000
399
+ }
400
+ }
401
+ ```
402
+
403
+ When `body` is omitted, a standard payload is sent containing `eventId`, `collectionId`, `appId`, `interactionId`, `contactId`, `userId`, `productId`, `proofId`, `outcome`, `eventType`, `metadata`, and `timestamp`.
404
+
405
+ #### `tag`
406
+
407
+ Adds or removes string tags on the contact record. Skipped if there is no `contactId` on the event.
408
+
409
+ ```json
410
+ {
411
+ "type": "tag",
412
+ "config": {
413
+ "tags": ["donor", "tier-{{metadata.tier}}"],
414
+ "action": "add"
415
+ }
416
+ }
417
+ ```
418
+
419
+ `action` defaults to `"add"`. Duplicate adds are no-ops; removing a tag not present is a no-op.
420
+
421
+ #### `appRecord`
422
+
423
+ Creates or upserts an app record scoped to the event context. Use `singletonPer` for cardinality control — if a matching singleton already exists it is updated rather than duplicated.
424
+
425
+ ```json
426
+ {
427
+ "type": "appRecord",
428
+ "config": {
429
+ "recordType": "participation",
430
+ "singletonPer": "contact",
431
+ "data": {
432
+ "participatedAt": "{{timestamp}}",
433
+ "outcome": "{{outcome}}"
434
+ }
435
+ }
436
+ }
437
+ ```
438
+
439
+ Common `singletonPer` values: `"contact"`, `"product"`, `"proof"`, `"global"`.
440
+
441
+ #### `segment`
442
+
443
+ Adds or removes the contact from a **static** segment. Skipped if there is no `contactId` on the event.
444
+
445
+ ```json
446
+ {
447
+ "type": "segment",
448
+ "config": {
449
+ "segmentId": "seg_donors_2026",
450
+ "action": "add"
451
+ }
452
+ }
453
+ ```
454
+
455
+ The segment must already exist and be of `filterType: 'static'`.
456
+
457
+ ### Effects Error Handling
458
+
459
+ | Scenario | Behaviour |
460
+ |---|---|
461
+ | Effect throws (e.g. template not found, webhook 500) | Error logged server-side; next effect continues |
462
+ | Contact has no email, `transactional` channel = `email` | Effect throws, logged, skipped |
463
+ | `contactId` absent on anonymous event | `tag`, `appRecord`, `segment` log a warning and skip |
464
+ | Unknown `type` value | Warning logged, effect skipped |
465
+ | `loyalty` — no earning rules configured | Silent no-op |
466
+ | Interaction has no `data.effects` | No effects run, no overhead |
467
+
468
+ Effects do **not** propagate errors back to the API caller.
469
+
470
+ ---
471
+
283
472
  ## TypeScript Types
284
473
 
285
474
  ```typescript
@@ -299,6 +488,17 @@ import type {
299
488
  AdminInteractionsCountsByOutcomeRequest,
300
489
  PublicInteractionsCountsByOutcomeRequest,
301
490
  PublicInteractionsByUserRequest,
491
+ // Effects
492
+ InteractionEffect, // Single effect entry { type, config? }
493
+ EffectType, // 'loyalty' | 'transactional' | 'webhook' | 'tag' | 'appRecord' | 'segment'
494
+ EffectConfig, // Union of all effect config shapes
495
+ LoyaltyEffectConfig,
496
+ TransactionalEffectConfig,
497
+ WebhookEffectConfig,
498
+ TagEffectConfig,
499
+ AppRecordEffectConfig,
500
+ SegmentEffectConfig,
501
+ InteractionEventContext, // Token interpolation context shape
302
502
  } from '@proveanything/smartlinks';
303
503
  ```
304
504
 
package/dist/openapi.yaml CHANGED
@@ -21121,6 +21121,10 @@ components:
21121
21121
  $ref: "#/components/schemas/InteractionDisplay"
21122
21122
  interactionType:
21123
21123
  type: string
21124
+ effects:
21125
+ type: array
21126
+ items:
21127
+ $ref: "#/components/schemas/InteractionEffect"
21124
21128
  createdAt:
21125
21129
  type: string
21126
21130
  required:
@@ -21196,6 +21200,183 @@ components:
21196
21200
  - FORBIDDEN
21197
21201
  required:
21198
21202
  - error
21203
+ InteractionEffect:
21204
+ type: object
21205
+ properties:
21206
+ type:
21207
+ $ref: "#/components/schemas/EffectType"
21208
+ config:
21209
+ $ref: "#/components/schemas/EffectConfig"
21210
+ required:
21211
+ - type
21212
+ LoyaltyEffectConfig:
21213
+ type: object
21214
+ properties: {}
21215
+ TransactionalEffectConfig:
21216
+ type: object
21217
+ properties:
21218
+ templateId:
21219
+ type: string
21220
+ channel:
21221
+ type: string
21222
+ enum:
21223
+ - email
21224
+ - sms
21225
+ - push
21226
+ - whatsapp
21227
+ - wallet
21228
+ - preferred
21229
+ props:
21230
+ type: object
21231
+ additionalProperties: true
21232
+ include:
21233
+ type: object
21234
+ additionalProperties: true
21235
+ productId:
21236
+ type: string
21237
+ proofId:
21238
+ type: string
21239
+ user:
21240
+ type: boolean
21241
+ appCase:
21242
+ type: string
21243
+ appThread:
21244
+ type: string
21245
+ appRecord:
21246
+ type: string
21247
+ appId:
21248
+ type: string
21249
+ required:
21250
+ - templateId
21251
+ WebhookEffectConfig:
21252
+ type: object
21253
+ properties:
21254
+ url:
21255
+ type: string
21256
+ method:
21257
+ type: string
21258
+ enum:
21259
+ - GET
21260
+ - POST
21261
+ - PUT
21262
+ - PATCH
21263
+ - DELETE
21264
+ headers:
21265
+ type: object
21266
+ additionalProperties:
21267
+ type: string
21268
+ body:
21269
+ type: object
21270
+ additionalProperties: true
21271
+ timeout:
21272
+ type: number
21273
+ required:
21274
+ - url
21275
+ TagEffectConfig:
21276
+ type: object
21277
+ properties:
21278
+ tags:
21279
+ type: array
21280
+ items:
21281
+ type: string
21282
+ action:
21283
+ type: string
21284
+ enum:
21285
+ - add
21286
+ - remove
21287
+ required:
21288
+ - tags
21289
+ AppRecordEffectConfig:
21290
+ type: object
21291
+ properties:
21292
+ appId:
21293
+ type: string
21294
+ recordType:
21295
+ type: string
21296
+ singletonPer:
21297
+ type: string
21298
+ data:
21299
+ type: object
21300
+ additionalProperties: true
21301
+ anchors:
21302
+ type: object
21303
+ additionalProperties: true
21304
+ productId:
21305
+ type: string
21306
+ proofId:
21307
+ type: string
21308
+ variantId:
21309
+ type: string
21310
+ batchId:
21311
+ type: string
21312
+ SegmentEffectConfig:
21313
+ type: object
21314
+ properties:
21315
+ segmentId:
21316
+ type: string
21317
+ action:
21318
+ type: string
21319
+ enum:
21320
+ - add
21321
+ - remove
21322
+ required:
21323
+ - segmentId
21324
+ InteractionEventContext:
21325
+ type: object
21326
+ properties:
21327
+ eventId:
21328
+ type: string
21329
+ collectionId:
21330
+ type: string
21331
+ appId:
21332
+ type: string
21333
+ interactionId:
21334
+ type: string
21335
+ contactId:
21336
+ type: string
21337
+ userId:
21338
+ type: string
21339
+ productId:
21340
+ type: string
21341
+ proofId:
21342
+ type: string
21343
+ variantId:
21344
+ type: string
21345
+ batchId:
21346
+ type: string
21347
+ outcome:
21348
+ type: string
21349
+ eventType:
21350
+ type: string
21351
+ source:
21352
+ type: string
21353
+ timestamp:
21354
+ type: string
21355
+ metadata:
21356
+ type: object
21357
+ additionalProperties: true
21358
+ broadcastId:
21359
+ type: string
21360
+ journeyId:
21361
+ type: string
21362
+ required:
21363
+ - eventId
21364
+ - collectionId
21365
+ - appId
21366
+ - interactionId
21367
+ - contactId
21368
+ - userId
21369
+ - productId
21370
+ - proofId
21371
+ - variantId
21372
+ - batchId
21373
+ - outcome
21374
+ - eventType
21375
+ - source
21376
+ - timestamp
21377
+ - metadata
21378
+ - broadcastId
21379
+ - journeyId
21199
21380
  Job:
21200
21381
  type: object
21201
21382
  properties:
@@ -185,6 +185,7 @@ export interface InteractionTypeRecord {
185
185
  display?: InteractionDisplay;
186
186
  scopes?: Record<string, InteractionDisplay>;
187
187
  interactionType?: string;
188
+ effects?: InteractionEffect[];
188
189
  [key: string]: unknown;
189
190
  };
190
191
  createdAt: string;
@@ -218,3 +219,99 @@ export interface SubmitInteractionError {
218
219
  error: 'FORBIDDEN';
219
220
  reason: 'not_public' | 'auth_required' | 'duplicate' | 'duplicate_anon' | 'disabled' | 'before_start' | 'after_end' | 'origin_forbidden';
220
221
  }
222
+ export type EffectType = 'loyalty' | 'transactional' | 'webhook' | 'tag' | 'appRecord' | 'segment';
223
+ export interface InteractionEffect {
224
+ type: EffectType;
225
+ config?: EffectConfig;
226
+ }
227
+ export type EffectConfig = LoyaltyEffectConfig | TransactionalEffectConfig | WebhookEffectConfig | TagEffectConfig | AppRecordEffectConfig | SegmentEffectConfig;
228
+ /** No config required — earning rules are driven by the interactionId */
229
+ export interface LoyaltyEffectConfig {
230
+ [key: string]: never;
231
+ }
232
+ export interface TransactionalEffectConfig {
233
+ /** Required. Firestore template ID */
234
+ templateId: string;
235
+ /**
236
+ * Channel to use.
237
+ * Default: 'preferred' — auto-selects the contact's best available channel.
238
+ */
239
+ channel?: 'email' | 'sms' | 'push' | 'whatsapp' | 'wallet' | 'preferred';
240
+ /** Additional Liquid variables. Supports {{token}} interpolation. */
241
+ props?: Record<string, unknown>;
242
+ /** Hydration directives. productId/proofId default to the event's own values. */
243
+ include?: {
244
+ productId?: string;
245
+ proofId?: string;
246
+ user?: boolean;
247
+ appCase?: string;
248
+ appThread?: string;
249
+ appRecord?: string;
250
+ };
251
+ /** Override the appId logged on the comms event row. */
252
+ appId?: string;
253
+ }
254
+ export interface WebhookEffectConfig {
255
+ /** Required. Target URL. Supports {{token}} interpolation. */
256
+ url: string;
257
+ /** HTTP verb. Default: 'POST' */
258
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
259
+ /** Additional HTTP headers. Supports {{token}} interpolation on values. */
260
+ headers?: Record<string, string>;
261
+ /** JSON body template. Supports {{token}} interpolation on string values. */
262
+ body?: Record<string, unknown>;
263
+ /** Request timeout in milliseconds. Default: 10000 */
264
+ timeout?: number;
265
+ }
266
+ export interface TagEffectConfig {
267
+ /** One or more tag strings to add or remove. Supports {{token}} interpolation. */
268
+ tags: string[];
269
+ /** Default: 'add' */
270
+ action?: 'add' | 'remove';
271
+ }
272
+ export interface AppRecordEffectConfig {
273
+ /** Override the appId for the created record. */
274
+ appId?: string;
275
+ /** Record type identifier. Default: 'default' */
276
+ recordType?: string;
277
+ /**
278
+ * Singleton cardinality key. At most one record per recordType+singletonPer will
279
+ * exist per scope. Common values: 'contact', 'product', 'proof', 'global'
280
+ */
281
+ singletonPer?: string;
282
+ /** Data object stored on the record. Supports {{token}} interpolation. */
283
+ data?: Record<string, unknown>;
284
+ /** Override scope anchors. Defaults to the event's own ids. */
285
+ anchors?: {
286
+ productId?: string;
287
+ proofId?: string;
288
+ variantId?: string;
289
+ batchId?: string;
290
+ };
291
+ }
292
+ export interface SegmentEffectConfig {
293
+ /** Required. UUID of the static segment to modify */
294
+ segmentId: string;
295
+ /** Default: 'add' */
296
+ action?: 'add' | 'remove';
297
+ }
298
+ /** Shape of the event context used for {{token}} interpolation in effect configs */
299
+ export interface InteractionEventContext {
300
+ eventId: string | null;
301
+ collectionId: string;
302
+ appId: string | null;
303
+ interactionId: string | null;
304
+ contactId: string | null;
305
+ userId: string | null;
306
+ productId: string | null;
307
+ proofId: string | null;
308
+ variantId: string | null;
309
+ batchId: string | null;
310
+ outcome: string | null;
311
+ eventType: string | null;
312
+ source: string | null;
313
+ timestamp: string | null;
314
+ metadata: Record<string, unknown>;
315
+ broadcastId: string | null;
316
+ journeyId: string | null;
317
+ }
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.13.8 | Generated: 2026-05-11T12:03:30.435Z
3
+ Version: 1.13.9 | Generated: 2026-05-12T11:17:48.753Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -5457,8 +5457,9 @@ interface InteractionTypeRecord {
5457
5457
  permissions?: InteractionPermissions
5458
5458
  data?: {
5459
5459
  display?: InteractionDisplay
5460
- scopes?: Record<string, InteractionDisplay>;
5460
+ scopes?: Record<string, InteractionDisplay>
5461
5461
  interactionType?: string
5462
+ effects?: InteractionEffect[]
5462
5463
  [key: string]: unknown
5463
5464
  }
5464
5465
  createdAt: string
@@ -5526,6 +5527,113 @@ interface SubmitInteractionError {
5526
5527
  }
5527
5528
  ```
5528
5529
 
5530
+ **InteractionEffect** (interface)
5531
+ ```typescript
5532
+ interface InteractionEffect {
5533
+ type: EffectType
5534
+ config?: EffectConfig
5535
+ }
5536
+ ```
5537
+
5538
+ **LoyaltyEffectConfig** (interface)
5539
+ ```typescript
5540
+ interface LoyaltyEffectConfig {
5541
+ [key: string]: never
5542
+ }
5543
+ ```
5544
+
5545
+ **TransactionalEffectConfig** (interface)
5546
+ ```typescript
5547
+ interface TransactionalEffectConfig {
5548
+ templateId: string
5549
+ * Channel to use.
5550
+ * Default: 'preferred' — auto-selects the contact's best available channel.
5551
+ channel?: 'email' | 'sms' | 'push' | 'whatsapp' | 'wallet' | 'preferred'
5552
+ props?: Record<string, unknown>
5553
+ include?: {
5554
+ productId?: string
5555
+ proofId?: string
5556
+ user?: boolean
5557
+ appCase?: string
5558
+ appThread?: string
5559
+ appRecord?: string
5560
+ }
5561
+ appId?: string
5562
+ }
5563
+ ```
5564
+
5565
+ **WebhookEffectConfig** (interface)
5566
+ ```typescript
5567
+ interface WebhookEffectConfig {
5568
+ url: string
5569
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
5570
+ headers?: Record<string, string>
5571
+ body?: Record<string, unknown>
5572
+ timeout?: number
5573
+ }
5574
+ ```
5575
+
5576
+ **TagEffectConfig** (interface)
5577
+ ```typescript
5578
+ interface TagEffectConfig {
5579
+ tags: string[]
5580
+ action?: 'add' | 'remove'
5581
+ }
5582
+ ```
5583
+
5584
+ **AppRecordEffectConfig** (interface)
5585
+ ```typescript
5586
+ interface AppRecordEffectConfig {
5587
+ appId?: string
5588
+ recordType?: string
5589
+ * Singleton cardinality key. At most one record per recordType+singletonPer will
5590
+ * exist per scope. Common values: 'contact', 'product', 'proof', 'global'
5591
+ singletonPer?: string
5592
+ data?: Record<string, unknown>
5593
+ anchors?: {
5594
+ productId?: string
5595
+ proofId?: string
5596
+ variantId?: string
5597
+ batchId?: string
5598
+ }
5599
+ }
5600
+ ```
5601
+
5602
+ **SegmentEffectConfig** (interface)
5603
+ ```typescript
5604
+ interface SegmentEffectConfig {
5605
+ segmentId: string
5606
+ action?: 'add' | 'remove'
5607
+ }
5608
+ ```
5609
+
5610
+ **InteractionEventContext** (interface)
5611
+ ```typescript
5612
+ interface InteractionEventContext {
5613
+ eventId: string | null
5614
+ collectionId: string
5615
+ appId: string | null
5616
+ interactionId: string | null
5617
+ contactId: string | null
5618
+ userId: string | null
5619
+ productId: string | null
5620
+ proofId: string | null
5621
+ variantId: string | null
5622
+ batchId: string | null
5623
+ outcome: string | null
5624
+ eventType: string | null
5625
+ source: string | null
5626
+ timestamp: string | null
5627
+ metadata: Record<string, unknown>
5628
+ broadcastId: string | null
5629
+ journeyId: string | null
5630
+ }
5631
+ ```
5632
+
5633
+ **EffectType** = ``
5634
+
5635
+ **EffectConfig** = ``
5636
+
5529
5637
  ### jobs
5530
5638
 
5531
5639
  **Job** (interface)
@@ -280,6 +280,195 @@ When defining a journey trigger, reference the `interactionId` that should fire
280
280
 
281
281
  ---
282
282
 
283
+ ## Interaction Effects
284
+
285
+ **Interaction effects** are automatic side-effects that fire immediately after an interaction event is recorded. They are defined directly on the interaction definition (`data.effects`) and run inline, fire-and-forget — they never block or fail the interaction response.
286
+
287
+ Common use cases: send a confirmation email, post a webhook to a CRM, tag the contact, award loyalty points, create an app record, or add the contact to a segment.
288
+
289
+ ### How It Works
290
+
291
+ 1. Event is logged to BigQuery
292
+ 2. Interaction definition is loaded
293
+ 3. Each configured effect runs **in order**, fire-and-forget
294
+ 4. Response is returned to the caller immediately
295
+
296
+ A failure in one effect is logged and swallowed; subsequent effects still run.
297
+
298
+ > **Note:** `transactional` and `webhook` effects currently run inline. They are planned to move to background jobs for retry-on-failure support once the jobs infrastructure is fully operational.
299
+
300
+ ### Configuring Effects
301
+
302
+ Pass `data.effects` when creating or updating an interaction type:
303
+
304
+ ```typescript
305
+ await SL.interactions.create(collectionId, {
306
+ id: 'donation',
307
+ appId: 'my-app',
308
+ permissions: { allowPublicSubmit: true },
309
+ data: {
310
+ effects: [
311
+ {
312
+ type: 'transactional',
313
+ config: {
314
+ templateId: 'tmpl_donation_receipt',
315
+ channel: 'email',
316
+ props: {
317
+ donationAmount: '{{metadata.amount}}',
318
+ campaignName: '{{metadata.campaign}}',
319
+ },
320
+ },
321
+ },
322
+ {
323
+ type: 'tag',
324
+ config: { tags: ['donor'] },
325
+ },
326
+ ],
327
+ },
328
+ });
329
+ ```
330
+
331
+ ### Token Interpolation
332
+
333
+ All effect `config` values support `{{token}}` interpolation resolved from the event context at runtime. Nested paths using dot notation are supported (e.g. `{{metadata.amount}}`).
334
+
335
+ | Token | Description |
336
+ |---|---|
337
+ | `{{eventId}}` | BigQuery event UUID |
338
+ | `{{collectionId}}` | Collection / brand ID |
339
+ | `{{appId}}` | App ID that submitted the event |
340
+ | `{{interactionId}}` | Interaction definition ID |
341
+ | `{{contactId}}` | Contact UUID (if resolved) |
342
+ | `{{userId}}` | Firebase UID (if authenticated) |
343
+ | `{{productId}}` | Product ID (if provided with event) |
344
+ | `{{proofId}}` | Proof ID (if provided with event) |
345
+ | `{{variantId}}` | Variant ID (if provided with event) |
346
+ | `{{batchId}}` | Batch ID (if provided with event) |
347
+ | `{{outcome}}` | Event outcome (e.g. `"submitted"`) |
348
+ | `{{eventType}}` | Event type string |
349
+ | `{{source}}` | Event source string |
350
+ | `{{timestamp}}` | ISO 8601 event timestamp |
351
+ | `{{metadata.*}}` | Any key from the event `metadata` object |
352
+
353
+ Tokens that resolve to `null` or `undefined` become an empty string.
354
+
355
+ ### Effect Types
356
+
357
+ #### `loyalty`
358
+
359
+ Awards loyalty points by evaluating `LoyaltyEarningRule` records configured against this interaction. Earning rules are managed separately via the Loyalty API. If no rules are configured, this effect is a no-op.
360
+
361
+ ```json
362
+ { "type": "loyalty", "config": {} }
363
+ ```
364
+
365
+ #### `transactional`
366
+
367
+ Sends a message to the contact using a comms template. Supports all channels with full Liquid template hydration.
368
+
369
+ ```json
370
+ {
371
+ "type": "transactional",
372
+ "config": {
373
+ "templateId": "tmpl_donation_receipt",
374
+ "channel": "email",
375
+ "props": {
376
+ "donationAmount": "{{metadata.amount}}"
377
+ }
378
+ }
379
+ }
380
+ ```
381
+
382
+ `channel` defaults to `"preferred"` — auto-selects the contact's best available channel respecting consent, suppression, and template availability. Optionally pass `include` to hydrate product/proof/user context, and `props` for additional Liquid variables.
383
+
384
+ #### `webhook`
385
+
386
+ Posts a JSON payload to an HTTP endpoint. All config values support `{{token}}` interpolation.
387
+
388
+ ```json
389
+ {
390
+ "type": "webhook",
391
+ "config": {
392
+ "url": "https://crm.example.com/api/events",
393
+ "headers": { "Authorization": "Bearer sk_live_abc123" },
394
+ "body": {
395
+ "contactId": "{{contactId}}",
396
+ "amount": "{{metadata.amount}}"
397
+ },
398
+ "timeout": 8000
399
+ }
400
+ }
401
+ ```
402
+
403
+ When `body` is omitted, a standard payload is sent containing `eventId`, `collectionId`, `appId`, `interactionId`, `contactId`, `userId`, `productId`, `proofId`, `outcome`, `eventType`, `metadata`, and `timestamp`.
404
+
405
+ #### `tag`
406
+
407
+ Adds or removes string tags on the contact record. Skipped if there is no `contactId` on the event.
408
+
409
+ ```json
410
+ {
411
+ "type": "tag",
412
+ "config": {
413
+ "tags": ["donor", "tier-{{metadata.tier}}"],
414
+ "action": "add"
415
+ }
416
+ }
417
+ ```
418
+
419
+ `action` defaults to `"add"`. Duplicate adds are no-ops; removing a tag not present is a no-op.
420
+
421
+ #### `appRecord`
422
+
423
+ Creates or upserts an app record scoped to the event context. Use `singletonPer` for cardinality control — if a matching singleton already exists it is updated rather than duplicated.
424
+
425
+ ```json
426
+ {
427
+ "type": "appRecord",
428
+ "config": {
429
+ "recordType": "participation",
430
+ "singletonPer": "contact",
431
+ "data": {
432
+ "participatedAt": "{{timestamp}}",
433
+ "outcome": "{{outcome}}"
434
+ }
435
+ }
436
+ }
437
+ ```
438
+
439
+ Common `singletonPer` values: `"contact"`, `"product"`, `"proof"`, `"global"`.
440
+
441
+ #### `segment`
442
+
443
+ Adds or removes the contact from a **static** segment. Skipped if there is no `contactId` on the event.
444
+
445
+ ```json
446
+ {
447
+ "type": "segment",
448
+ "config": {
449
+ "segmentId": "seg_donors_2026",
450
+ "action": "add"
451
+ }
452
+ }
453
+ ```
454
+
455
+ The segment must already exist and be of `filterType: 'static'`.
456
+
457
+ ### Effects Error Handling
458
+
459
+ | Scenario | Behaviour |
460
+ |---|---|
461
+ | Effect throws (e.g. template not found, webhook 500) | Error logged server-side; next effect continues |
462
+ | Contact has no email, `transactional` channel = `email` | Effect throws, logged, skipped |
463
+ | `contactId` absent on anonymous event | `tag`, `appRecord`, `segment` log a warning and skip |
464
+ | Unknown `type` value | Warning logged, effect skipped |
465
+ | `loyalty` — no earning rules configured | Silent no-op |
466
+ | Interaction has no `data.effects` | No effects run, no overhead |
467
+
468
+ Effects do **not** propagate errors back to the API caller.
469
+
470
+ ---
471
+
283
472
  ## TypeScript Types
284
473
 
285
474
  ```typescript
@@ -299,6 +488,17 @@ import type {
299
488
  AdminInteractionsCountsByOutcomeRequest,
300
489
  PublicInteractionsCountsByOutcomeRequest,
301
490
  PublicInteractionsByUserRequest,
491
+ // Effects
492
+ InteractionEffect, // Single effect entry { type, config? }
493
+ EffectType, // 'loyalty' | 'transactional' | 'webhook' | 'tag' | 'appRecord' | 'segment'
494
+ EffectConfig, // Union of all effect config shapes
495
+ LoyaltyEffectConfig,
496
+ TransactionalEffectConfig,
497
+ WebhookEffectConfig,
498
+ TagEffectConfig,
499
+ AppRecordEffectConfig,
500
+ SegmentEffectConfig,
501
+ InteractionEventContext, // Token interpolation context shape
302
502
  } from '@proveanything/smartlinks';
303
503
  ```
304
504
 
package/openapi.yaml CHANGED
@@ -21121,6 +21121,10 @@ components:
21121
21121
  $ref: "#/components/schemas/InteractionDisplay"
21122
21122
  interactionType:
21123
21123
  type: string
21124
+ effects:
21125
+ type: array
21126
+ items:
21127
+ $ref: "#/components/schemas/InteractionEffect"
21124
21128
  createdAt:
21125
21129
  type: string
21126
21130
  required:
@@ -21196,6 +21200,183 @@ components:
21196
21200
  - FORBIDDEN
21197
21201
  required:
21198
21202
  - error
21203
+ InteractionEffect:
21204
+ type: object
21205
+ properties:
21206
+ type:
21207
+ $ref: "#/components/schemas/EffectType"
21208
+ config:
21209
+ $ref: "#/components/schemas/EffectConfig"
21210
+ required:
21211
+ - type
21212
+ LoyaltyEffectConfig:
21213
+ type: object
21214
+ properties: {}
21215
+ TransactionalEffectConfig:
21216
+ type: object
21217
+ properties:
21218
+ templateId:
21219
+ type: string
21220
+ channel:
21221
+ type: string
21222
+ enum:
21223
+ - email
21224
+ - sms
21225
+ - push
21226
+ - whatsapp
21227
+ - wallet
21228
+ - preferred
21229
+ props:
21230
+ type: object
21231
+ additionalProperties: true
21232
+ include:
21233
+ type: object
21234
+ additionalProperties: true
21235
+ productId:
21236
+ type: string
21237
+ proofId:
21238
+ type: string
21239
+ user:
21240
+ type: boolean
21241
+ appCase:
21242
+ type: string
21243
+ appThread:
21244
+ type: string
21245
+ appRecord:
21246
+ type: string
21247
+ appId:
21248
+ type: string
21249
+ required:
21250
+ - templateId
21251
+ WebhookEffectConfig:
21252
+ type: object
21253
+ properties:
21254
+ url:
21255
+ type: string
21256
+ method:
21257
+ type: string
21258
+ enum:
21259
+ - GET
21260
+ - POST
21261
+ - PUT
21262
+ - PATCH
21263
+ - DELETE
21264
+ headers:
21265
+ type: object
21266
+ additionalProperties:
21267
+ type: string
21268
+ body:
21269
+ type: object
21270
+ additionalProperties: true
21271
+ timeout:
21272
+ type: number
21273
+ required:
21274
+ - url
21275
+ TagEffectConfig:
21276
+ type: object
21277
+ properties:
21278
+ tags:
21279
+ type: array
21280
+ items:
21281
+ type: string
21282
+ action:
21283
+ type: string
21284
+ enum:
21285
+ - add
21286
+ - remove
21287
+ required:
21288
+ - tags
21289
+ AppRecordEffectConfig:
21290
+ type: object
21291
+ properties:
21292
+ appId:
21293
+ type: string
21294
+ recordType:
21295
+ type: string
21296
+ singletonPer:
21297
+ type: string
21298
+ data:
21299
+ type: object
21300
+ additionalProperties: true
21301
+ anchors:
21302
+ type: object
21303
+ additionalProperties: true
21304
+ productId:
21305
+ type: string
21306
+ proofId:
21307
+ type: string
21308
+ variantId:
21309
+ type: string
21310
+ batchId:
21311
+ type: string
21312
+ SegmentEffectConfig:
21313
+ type: object
21314
+ properties:
21315
+ segmentId:
21316
+ type: string
21317
+ action:
21318
+ type: string
21319
+ enum:
21320
+ - add
21321
+ - remove
21322
+ required:
21323
+ - segmentId
21324
+ InteractionEventContext:
21325
+ type: object
21326
+ properties:
21327
+ eventId:
21328
+ type: string
21329
+ collectionId:
21330
+ type: string
21331
+ appId:
21332
+ type: string
21333
+ interactionId:
21334
+ type: string
21335
+ contactId:
21336
+ type: string
21337
+ userId:
21338
+ type: string
21339
+ productId:
21340
+ type: string
21341
+ proofId:
21342
+ type: string
21343
+ variantId:
21344
+ type: string
21345
+ batchId:
21346
+ type: string
21347
+ outcome:
21348
+ type: string
21349
+ eventType:
21350
+ type: string
21351
+ source:
21352
+ type: string
21353
+ timestamp:
21354
+ type: string
21355
+ metadata:
21356
+ type: object
21357
+ additionalProperties: true
21358
+ broadcastId:
21359
+ type: string
21360
+ journeyId:
21361
+ type: string
21362
+ required:
21363
+ - eventId
21364
+ - collectionId
21365
+ - appId
21366
+ - interactionId
21367
+ - contactId
21368
+ - userId
21369
+ - productId
21370
+ - proofId
21371
+ - variantId
21372
+ - batchId
21373
+ - outcome
21374
+ - eventType
21375
+ - source
21376
+ - timestamp
21377
+ - metadata
21378
+ - broadcastId
21379
+ - journeyId
21199
21380
  Job:
21200
21381
  type: object
21201
21382
  properties:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.13.8",
3
+ "version": "1.13.9",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",