@proveanything/smartlinks 1.16.3 → 1.16.5

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,4 +1,4 @@
1
- import { ProofResponse, ProofCreateRequest, ProofUpdateRequest, ProofValuesUpdateRequest, ProofClaimRequest, ProofGrant, CreateGrantOptions, RedeemGrantOptions, RedeemGrantResult, ProofTransfer, TransferProofOptions, TransferProofResult } from "../types/proof";
1
+ import { ProofResponse, ProofCreateRequest, ProofUpdateRequest, ProofValuesUpdateRequest, ProofClaimRequest, ProofGrant, CreateGrantOptions, RedeemGrantOptions, RedeemGrantResult, ProofTransfer, TransferProofOptions, TransferProofResult, AcceptTransferOptions, CancelTransferOptions } from "../types/proof";
2
2
  export declare namespace proof {
3
3
  /**
4
4
  * Retrieves a single Proof by Collection ID, Product ID, and Proof ID.
@@ -177,6 +177,23 @@ export declare namespace proof {
177
177
  * ```ts
178
178
  * await proof.transfer(collectionId, productId, proofId, { release: true })
179
179
  * ```
180
+ * Send comms by naming templates per role (server decides who receives each):
181
+ * ```ts
182
+ * await proof.transfer(collectionId, productId, proofId, {
183
+ * toEmail: 'buyer@example.com',
184
+ * comms: {
185
+ * recipient: { templateId: 'transfer-incoming', props: { note: 'Enjoy!' } },
186
+ * sender: { templateId: 'transfer-sent' },
187
+ * },
188
+ * })
189
+ * ```
190
+ * Direct to an SMS recipient by phone (E.164) and notify them by text:
191
+ * ```ts
192
+ * await proof.transfer(collectionId, productId, proofId, {
193
+ * toPhone: '+14155551234',
194
+ * comms: { recipient: { templateId: 'transfer-incoming', channel: 'sms' } },
195
+ * })
196
+ * ```
180
197
  */
181
198
  function transfer(collectionId: string, productId: string, proofId: string, options: TransferProofOptions): Promise<TransferProofResult>;
182
199
  /**
@@ -184,12 +201,12 @@ export declare namespace proof {
184
201
  * move — the proof's `userId` becomes the caller and the previous owner's private
185
202
  * data and share grants are cleared/voided.
186
203
  */
187
- function acceptTransfer(collectionId: string, productId: string, proofId: string): Promise<{
204
+ function acceptTransfer(collectionId: string, productId: string, proofId: string, options?: AcceptTransferOptions): Promise<{
188
205
  ok: boolean;
189
206
  proof: ProofResponse;
190
207
  }>;
191
208
  /** Cancel a pending push transfer (current owner / collection admin only). */
192
- function cancelTransfer(collectionId: string, productId: string, proofId: string): Promise<{
209
+ function cancelTransfer(collectionId: string, productId: string, proofId: string, options?: CancelTransferOptions): Promise<{
193
210
  ok: boolean;
194
211
  }>;
195
212
  /**
package/dist/api/proof.js CHANGED
@@ -270,6 +270,23 @@ export var proof;
270
270
  * ```ts
271
271
  * await proof.transfer(collectionId, productId, proofId, { release: true })
272
272
  * ```
273
+ * Send comms by naming templates per role (server decides who receives each):
274
+ * ```ts
275
+ * await proof.transfer(collectionId, productId, proofId, {
276
+ * toEmail: 'buyer@example.com',
277
+ * comms: {
278
+ * recipient: { templateId: 'transfer-incoming', props: { note: 'Enjoy!' } },
279
+ * sender: { templateId: 'transfer-sent' },
280
+ * },
281
+ * })
282
+ * ```
283
+ * Direct to an SMS recipient by phone (E.164) and notify them by text:
284
+ * ```ts
285
+ * await proof.transfer(collectionId, productId, proofId, {
286
+ * toPhone: '+14155551234',
287
+ * comms: { recipient: { templateId: 'transfer-incoming', channel: 'sms' } },
288
+ * })
289
+ * ```
273
290
  */
274
291
  async function transfer(collectionId, productId, proofId, options) {
275
292
  return post(transferBase(collectionId, productId, proofId), Object.assign({}, options));
@@ -280,13 +297,13 @@ export var proof;
280
297
  * move — the proof's `userId` becomes the caller and the previous owner's private
281
298
  * data and share grants are cleared/voided.
282
299
  */
283
- async function acceptTransfer(collectionId, productId, proofId) {
284
- return post(`${transferBase(collectionId, productId, proofId)}/accept`, {});
300
+ async function acceptTransfer(collectionId, productId, proofId, options = {}) {
301
+ return post(`${transferBase(collectionId, productId, proofId)}/accept`, Object.assign({}, options));
285
302
  }
286
303
  proof.acceptTransfer = acceptTransfer;
287
304
  /** Cancel a pending push transfer (current owner / collection admin only). */
288
- async function cancelTransfer(collectionId, productId, proofId) {
289
- return post(`${transferBase(collectionId, productId, proofId)}/cancel`, {});
305
+ async function cancelTransfer(collectionId, productId, proofId, options = {}) {
306
+ return post(`${transferBase(collectionId, productId, proofId)}/cancel`, Object.assign({}, options));
290
307
  }
291
308
  proof.cancelTransfer = cancelTransfer;
292
309
  /**
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.16.3 | Generated: 2026-09-05T10:57:07.719Z
3
+ Version: 1.16.5 | Generated: 2026-09-05T15:46:49.017Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -35,6 +35,7 @@ For detailed guides on specific features:
35
35
  - **[Proof Claiming Methods](proof-claiming-methods.md)** - All methods for claiming/registering product ownership (NFC tags, serial numbers, auto-generated claims)
36
36
  - **[Proof Share Grants](proof-share-grants.md)** - Delegated, scoped, revocable bearer access to a single proof (read/comment/verify-owner links)
37
37
  - **[Proof Ownership Transfer](proof-ownership-transfer.md)** - Moving a proof to a new owner: directed transfer, open release, accept/cancel, and the state machine
38
+ - **[Comms Triggers](proof-comms-triggers.md)** - Sending transactional comms from proof actions (claim/transfer/accept/cancel): the role→template map, per-action roles, and the trust boundary
38
39
  - **[Lots](lots.md)** - Collection-scoped production groupings spanning many SKUs; facet/product selectors, member resolution, and GS1 AI(10) batch-then-lot resolution
39
40
  - **[Item Context](item-context.md)** - The `itemContext` container prop derived from a serial-proof URL or NFC tap (what item the URL points at)
40
41
  - **[Product Facets SDK](PRODUCT_FACETS_SDK.md)** - Admin and public product facet endpoints and TypeScript interfaces
@@ -7508,6 +7509,15 @@ interface ProofValuesUpdateRequest {
7508
7509
  }
7509
7510
  ```
7510
7511
 
7512
+ **ProofClaimRequest** (interface)
7513
+ ```typescript
7514
+ interface ProofClaimRequest {
7515
+ data?: Record<string, any>
7516
+ comms?: CommsTriggerMap
7517
+ [key: string]: any
7518
+ }
7519
+ ```
7520
+
7511
7521
  **ProofFieldsConfig** (interface)
7512
7522
  ```typescript
7513
7523
  interface ProofFieldsConfig {
@@ -7580,13 +7590,34 @@ interface ProofTransfer {
7580
7590
  }
7581
7591
  ```
7582
7592
 
7593
+ **CommsTrigger** (interface)
7594
+ ```typescript
7595
+ interface CommsTrigger {
7596
+ templateId: string
7597
+ channel?: 'preferred' | 'email' | 'sms' | 'push' | 'wallet' | 'whatsapp'
7598
+ props?: Record<string, any>
7599
+ notify?: boolean
7600
+ appId?: string
7601
+ }
7602
+ ```
7603
+
7583
7604
  **TransferProofOptions** (interface)
7584
7605
  ```typescript
7585
7606
  interface TransferProofOptions {
7607
+ * Comms to send. Roles: `recipient` (the named new owner) and `sender` (the
7608
+ * initiator) for a directed transfer; `owner` for an open release. For an
7609
+ * SMS recipient, set `comms.recipient.channel = 'sms'`.
7610
+ comms?: CommsTriggerMap
7586
7611
  toEmail?: string
7612
+ * Directed: recipient phone in E.164 (e.g. `+14155551234`). Resolves to the same
7613
+ * user they log in as via SMS OTP (created if needed). Pair with an SMS comms
7614
+ * trigger to notify them: `comms.recipient.channel = 'sms'`.
7615
+ toPhone?: string
7587
7616
  toUserId?: string
7588
7617
  toName?: string
7589
7618
  release?: boolean
7619
+ * @deprecated The legacy fixed transfer email is gone. Put a note in a comms
7620
+ * trigger's props instead, e.g. `comms.recipient.props.note`.
7590
7621
  message?: string
7591
7622
  notify?: boolean
7592
7623
  }
@@ -7601,12 +7632,26 @@ interface TransferProofResult {
7601
7632
  }
7602
7633
  ```
7603
7634
 
7635
+ **AcceptTransferOptions** (interface)
7636
+ ```typescript
7637
+ interface AcceptTransferOptions {
7638
+ comms?: CommsTriggerMap
7639
+ notify?: boolean
7640
+ }
7641
+ ```
7642
+
7643
+ **CancelTransferOptions** (interface)
7644
+ ```typescript
7645
+ interface CancelTransferOptions {
7646
+ comms?: CommsTriggerMap
7647
+ notify?: boolean
7648
+ }
7649
+ ```
7650
+
7604
7651
  **ProofResponse** = `Proof`
7605
7652
 
7606
7653
  **ProofUpdateRequest** = `Partial<ProofWrite> & { proof?: ProofWrite }`
7607
7654
 
7608
- **ProofClaimRequest** = `Record<string, any>`
7609
-
7610
7655
  **ProofFieldScope** = `'public' | 'owner' | 'personal' | 'admin'`
7611
7656
 
7612
7657
  **ProofFieldDef** = `ScopedFieldDef & { scope?: ProofFieldScope }`
@@ -7619,6 +7664,8 @@ interface TransferProofResult {
7619
7664
 
7620
7665
  **ProofTransferState** = ``
7621
7666
 
7667
+ **CommsTriggerMap** = `Record<string, CommsTrigger>`
7668
+
7622
7669
  ### qr
7623
7670
 
7624
7671
  **QrShortCodeLookupResponse** (interface)
@@ -10428,16 +10475,18 @@ Redeem a grant token (anonymous or signed-in). Records the redemption and return
10428
10475
  productId: string,
10429
10476
  proofId: string,
10430
10477
  options: TransferProofOptions) → `Promise<TransferProofResult>`
10431
- Start a push transfer of a proof (current owner / collection admin only). Directed — hand it to a named recipient who then calls {@link acceptTransfer}: ```ts await proof.transfer(collectionId, productId, proofId, { toEmail: 'buyer@example.com' }) ``` Open release — make the proof claimable by anyone: ```ts await proof.transfer(collectionId, productId, proofId, { release: true }) ```
10478
+ Start a push transfer of a proof (current owner / collection admin only). Directed — hand it to a named recipient who then calls {@link acceptTransfer}: ```ts await proof.transfer(collectionId, productId, proofId, { toEmail: 'buyer@example.com' }) ``` Open release — make the proof claimable by anyone: ```ts await proof.transfer(collectionId, productId, proofId, { release: true }) ``` Send comms by naming templates per role (server decides who receives each): ```ts await proof.transfer(collectionId, productId, proofId, { toEmail: 'buyer@example.com', comms: { recipient: { templateId: 'transfer-incoming', props: { note: 'Enjoy!' } }, sender: { templateId: 'transfer-sent' }, }, }) ``` Direct to an SMS recipient by phone (E.164) and notify them by text: ```ts await proof.transfer(collectionId, productId, proofId, { toPhone: '+14155551234', comms: { recipient: { templateId: 'transfer-incoming', channel: 'sms' } }, }) ```
10432
10479
 
10433
10480
  **acceptTransfer**(collectionId: string,
10434
10481
  productId: string,
10435
- proofId: string) → `Promise<`
10482
+ proofId: string,
10483
+ options: AcceptTransferOptions = {}) → `Promise<`
10436
10484
  Accept a directed transfer (the named recipient only). Completes the ownership move — the proof's `userId` becomes the caller and the previous owner's private data and share grants are cleared/voided.
10437
10485
 
10438
10486
  **cancelTransfer**(collectionId: string,
10439
10487
  productId: string,
10440
- proofId: string) → `Promise<`
10488
+ proofId: string,
10489
+ options: CancelTransferOptions = {}) → `Promise<`
10441
10490
  Cancel a pending push transfer (current owner / collection admin only).
10442
10491
 
10443
10492
  **getTransfer**(collectionId: string,
@@ -289,6 +289,22 @@ try {
289
289
 
290
290
  ---
291
291
 
292
+ ## Sending a claim confirmation
293
+
294
+ Claims can send a transactional confirmation once the proof is committed to the
295
+ ledger. Pass a `comms` map (role `claimer`) alongside `data` — you author the
296
+ template, the server hydrates `{{ proof }}` / `{{ product }}` / `{{ contact }}`:
297
+
298
+ ```ts
299
+ await proof.claim(collectionId, productId, proofId, {
300
+ data: { store: 'Regent St' },
301
+ comms: { claimer: { templateId: 'welcome-owner', channel: 'preferred' } },
302
+ })
303
+ ```
304
+
305
+ Omit `comms` and nothing is sent (opt-in). Full detail:
306
+ [Comms Triggers](./proof-comms-triggers.md).
307
+
292
308
  ## API Reference
293
309
 
294
310
  ### `proof.claim(collectionId, productId, proofId, data?)`
@@ -0,0 +1,142 @@
1
+ # Comms Triggers
2
+
3
+ The standard way to send a **transactional message** as a side-effect of a proof
4
+ action — a claim confirmation, a "you've received a transfer" email, a
5
+ cancellation notice. You pass a **comms trigger** into the action; the server
6
+ sends it once the action's write is durable.
7
+
8
+ This replaces the old fixed SendGrid proof emails (`createProof` / `transferProof`).
9
+ Instead of a hardcoded template baked into the backend, **you author the template**
10
+ (in the template editor) and **name it per action**. The copy, branding, channel,
11
+ and merge fields are yours.
12
+
13
+ ---
14
+
15
+ ## The shape
16
+
17
+ An action takes a `comms` object: a **role → `CommsTrigger`** map.
18
+
19
+ ```ts
20
+ interface CommsTrigger {
21
+ templateId: string // the template to render + send (you author it)
22
+ channel?: 'preferred' | 'email' | 'sms' | 'push' | 'wallet' | 'whatsapp'
23
+ props?: Record<string, any> // merge data: a note, a message, custom fields
24
+ notify?: boolean // false → do the action, send nothing for this role
25
+ appId?: string // your app id, recorded in comms history
26
+ }
27
+
28
+ // role → trigger
29
+ type CommsTriggerMap = Record<string, CommsTrigger>
30
+ ```
31
+
32
+ Each action documents the **roles** it exposes (who can be messaged). You supply a
33
+ trigger only for the roles you want to notify.
34
+
35
+ ---
36
+
37
+ ## What you control vs what the server owns
38
+
39
+ You name a template and supply soft data. The server owns everything that decides
40
+ **who** receives it and **what context** the template can see. A caller can never
41
+ redirect a message to a contact of its choosing.
42
+
43
+ | You supply (per role) | The server injects |
44
+ |---|---|
45
+ | `templateId` | which contact the role resolves to (recipient / sender / claimer / owner) |
46
+ | `props` (note, message, custom) | `{{ proof }}`, `{{ product }}`, `{{ contact }}` hydration |
47
+ | `channel`, `notify`, `appId` | the proof/product ids, the portal URL (`{{ proofUrl }}`) |
48
+
49
+ Templates are rendered with **Liquid**, so a template body can read
50
+ `{{ proof.values.owner.name }}`, `{{ product.title }}`, `{{ contact.firstName }}`,
51
+ `{{ proofUrl }}`, and any `props` you passed (`{{ note }}`). See
52
+ [Liquid Templates](./liquid-templates.md) and [Comms](./comms.md) for the template
53
+ model and channel resolution.
54
+
55
+ > This is a **1:1 transactional** send. It goes straight through the transactional
56
+ > comms path — it does **not** touch the interactions → segments → broadcasts
57
+ > (marketing/audience) pipeline. Recording a CRM interaction or awarding loyalty is
58
+ > a separate, independent concern; don't route those through the comms trigger.
59
+
60
+ ---
61
+
62
+ ## Roles by action
63
+
64
+ | Action | Roles | Fired when |
65
+ |--------|-------|-----------|
66
+ | `claim` / `claimProduct` | `claimer` | after the proof is committed to the ledger |
67
+ | `transfer` (directed) | `recipient`, `sender` | on initiation |
68
+ | `transfer` (`release: true`) | `owner` | on initiation |
69
+ | `acceptTransfer` | `recipient` (new owner), `sender` (previous owner) | after the ownership move completes |
70
+ | `cancelTransfer` | `owner` (canceller), `recipient` (earmarked, directed only) | after the transfer is cancelled |
71
+
72
+ Timing is deliberate: comms fire **only after the durable write** (the ledger
73
+ commit for a claim, the ownership move for an accept). A failed action sends
74
+ nothing; a comms failure never breaks the action (best-effort).
75
+
76
+ ---
77
+
78
+ ## Examples
79
+
80
+ Directed transfer — notify the buyer, confirm to the seller:
81
+
82
+ ```ts
83
+ await proof.transfer(collectionId, productId, proofId, {
84
+ toEmail: 'buyer@example.com',
85
+ comms: {
86
+ recipient: { templateId: 'transfer-incoming', props: { note: 'Enjoy the watch!' } },
87
+ sender: { templateId: 'transfer-sent' },
88
+ },
89
+ })
90
+ ```
91
+
92
+ Claim — confirm to the claimer:
93
+
94
+ ```ts
95
+ await proof.claim(collectionId, productId, proofId, {
96
+ data: { purchaseStore: 'Regent St' },
97
+ comms: {
98
+ claimer: { templateId: 'welcome-owner', channel: 'preferred' },
99
+ },
100
+ })
101
+ ```
102
+
103
+ Accept — completion confirmation to both parties:
104
+
105
+ ```ts
106
+ await proof.acceptTransfer(collectionId, productId, proofId, {
107
+ comms: {
108
+ recipient: { templateId: 'transfer-complete-owner' },
109
+ sender: { templateId: 'transfer-complete-previous' },
110
+ },
111
+ })
112
+ ```
113
+
114
+ Suppress comms for one role while still messaging another:
115
+
116
+ ```ts
117
+ comms: {
118
+ recipient: { templateId: 'transfer-incoming' },
119
+ sender: { notify: false }, // do the transfer, don't email the seller
120
+ }
121
+ ```
122
+
123
+ Omit `comms` entirely and nothing is sent — comms are **opt-in**.
124
+
125
+ ---
126
+
127
+ ## Notes
128
+
129
+ - **Email-only recipients work.** A directed transfer to a `toEmail` that isn't a
130
+ contact yet resolves/creates a contact before sending.
131
+ - **SMS recipients work the same way.** A directed transfer to a `toPhone` (E.164)
132
+ resolves the recipient by phone and gives their contact a phone identity, so an
133
+ SMS-channel trigger (`channel: 'sms'`) reaches them. Because the phone maps to a
134
+ stable Firebase uid, they accept by logging in via SMS OTP — the accept is still
135
+ auth-gated exactly as with email.
136
+ - **Consent + suppression apply.** Transactional sends still respect the contact's
137
+ channel consent and suppression list; a template's `topic` governs this.
138
+ - **Delivery is logged** to comms history (with your `appId` and a `ref`), so sends
139
+ are auditable per contact.
140
+
141
+ See also [Proof Ownership Transfer](./proof-ownership-transfer.md),
142
+ [Proof Claiming Methods](./proof-claiming-methods.md), and [Comms](./comms.md).
@@ -15,9 +15,20 @@ reserves states for them.
15
15
 
16
16
  | Mode | How | Who completes it |
17
17
  |------|-----|------------------|
18
- | **Directed** | Owner names a recipient (`toEmail` / `toUserId`). The proof is earmarked for them (`claimUserId`) and they're emailed a link. | The named recipient calls `acceptTransfer`. |
18
+ | **Directed** | Owner names a recipient (`toEmail` / `toPhone` / `toUserId`). The proof is earmarked for them (`claimUserId`); you notify them with a comms trigger. | The named recipient calls `acceptTransfer`. |
19
19
  | **Open release** | Owner marks the proof `claimable`. | Anyone claims it via the normal claim flow. |
20
20
 
21
+ A `toPhone` recipient (E.164) resolves to the **same user they log in as via SMS
22
+ OTP** — Firebase keys the uid by phone number, so the earmark matches when they
23
+ sign in. Pair it with an SMS comms trigger to notify them by text:
24
+
25
+ ```ts
26
+ await proof.transfer(collectionId, productId, proofId, {
27
+ toPhone: '+14155551234',
28
+ comms: { recipient: { templateId: 'transfer-incoming', channel: 'sms' } },
29
+ })
30
+ ```
31
+
21
32
  Only the current owner (or a collection admin) can start a transfer. A proof can
22
33
  have **one active transfer at a time**.
23
34
 
@@ -41,10 +52,14 @@ authoritative and atomic:
41
52
  ```ts
42
53
  import { proof } from '@proveanything/smartlinks'
43
54
 
44
- // Directed — hand it to a named buyer (they must accept)
55
+ // Directed — hand it to a named buyer (they must accept). Notify each party with a
56
+ // comms trigger naming a template you authored (see Comms Triggers below).
45
57
  const { transfer } = await proof.transfer(collectionId, productId, proofId, {
46
58
  toEmail: 'buyer@example.com',
47
- message: 'Enjoy the watch!',
59
+ comms: {
60
+ recipient: { templateId: 'transfer-incoming', props: { note: 'Enjoy the watch!' } },
61
+ sender: { templateId: 'transfer-sent' },
62
+ },
48
63
  })
49
64
 
50
65
  // …or an open release — anyone can now claim it
@@ -60,12 +75,18 @@ await proof.cancelTransfer(collectionId, productId, proofId)
60
75
  ## Recipient flow (directed)
61
76
 
62
77
  ```ts
63
- // Only the named recipient can accept — a third party is rejected.
64
- const { proof: mine } = await proof.acceptTransfer(collectionId, productId, proofId)
78
+ // Only the named recipient can accept — a third party is rejected. Optionally
79
+ // send completion comms to both parties.
80
+ const { proof: mine } = await proof.acceptTransfer(collectionId, productId, proofId, {
81
+ comms: {
82
+ recipient: { templateId: 'transfer-complete-owner' }, // the new owner
83
+ sender: { templateId: 'transfer-complete-previous' }, // the previous owner
84
+ },
85
+ })
65
86
  ```
66
87
 
67
88
  The recipient sees the pending transfer on their account (a pending claim) and via
68
- the email link. Until they accept, the proof still belongs to the seller.
89
+ the notification you sent. Until they accept, the proof still belongs to the seller.
69
90
 
70
91
  ---
71
92
 
@@ -101,14 +122,34 @@ the email link. Until they accept, the proof still belongs to the seller.
101
122
  added, are dispute-protected and never auto-transfer by default).
102
123
  - **Every state change is audited** as an append-only attestation on the proof.
103
124
 
125
+ ## Comms
126
+
127
+ Every transfer action takes an optional `comms` map (role → comms trigger) that
128
+ sends a transactional message once the action's write is durable. Roles:
129
+
130
+ | Action | Roles |
131
+ |--------|-------|
132
+ | `transfer` (directed) | `recipient`, `sender` |
133
+ | `transfer` (`release: true`) | `owner` |
134
+ | `acceptTransfer` | `recipient` (new owner), `sender` (previous owner) |
135
+ | `cancelTransfer` | `owner`, `recipient` (directed only) |
136
+
137
+ You author each template; the server owns who each role resolves to and hydrates
138
+ `{{ proof }}` / `{{ product }}` / `{{ contact }}` / `{{ proofUrl }}`. Pass
139
+ `notify: false` on a role (or omit `comms`) to send nothing. Full detail:
140
+ [Comms Triggers](./proof-comms-triggers.md).
141
+
104
142
  ## API
105
143
 
106
144
  | Method | Endpoint |
107
145
  |--------|----------|
108
146
  | `proof.transfer(c, p, id, opts)` | `POST …/proof/:id/transfer` |
109
- | `proof.acceptTransfer(c, p, id)` | `POST …/proof/:id/transfer/accept` |
110
- | `proof.cancelTransfer(c, p, id)` | `POST …/proof/:id/transfer/cancel` |
147
+ | `proof.acceptTransfer(c, p, id, opts?)` | `POST …/proof/:id/transfer/accept` |
148
+ | `proof.cancelTransfer(c, p, id, opts?)` | `POST …/proof/:id/transfer/cancel` |
111
149
  | `proof.getTransfer(c, p, id)` | `GET …/proof/:id/transfer` |
112
150
 
113
- See also [Proof Share Grants](./proof-share-grants.md) and
151
+ `opts` on transfer/accept/cancel carries the `comms` map (and `notify`).
152
+
153
+ See also [Comms Triggers](./proof-comms-triggers.md),
154
+ [Proof Share Grants](./proof-share-grants.md) and
114
155
  [Proof Claiming Methods](./proof-claiming-methods.md).
package/dist/openapi.yaml CHANGED
@@ -25717,6 +25717,14 @@ components:
25717
25717
  type: object
25718
25718
  additionalProperties:
25719
25719
  $ref: "#/components/schemas/JsonValue"
25720
+ ProofClaimRequest:
25721
+ type: object
25722
+ properties:
25723
+ data:
25724
+ type: object
25725
+ additionalProperties: true
25726
+ comms:
25727
+ $ref: "#/components/schemas/CommsTriggerMap"
25720
25728
  ProofFieldsConfig:
25721
25729
  type: object
25722
25730
  properties:
@@ -25844,11 +25852,38 @@ components:
25844
25852
  - state
25845
25853
  - createdAt
25846
25854
  - updatedAt
25855
+ CommsTrigger:
25856
+ type: object
25857
+ properties:
25858
+ templateId:
25859
+ type: string
25860
+ channel:
25861
+ type: string
25862
+ enum:
25863
+ - preferred
25864
+ - email
25865
+ - sms
25866
+ - push
25867
+ - wallet
25868
+ - whatsapp
25869
+ props:
25870
+ type: object
25871
+ additionalProperties: true
25872
+ notify:
25873
+ type: boolean
25874
+ appId:
25875
+ type: string
25876
+ required:
25877
+ - templateId
25847
25878
  TransferProofOptions:
25848
25879
  type: object
25849
25880
  properties:
25881
+ comms:
25882
+ $ref: "#/components/schemas/CommsTriggerMap"
25850
25883
  toEmail:
25851
25884
  type: string
25885
+ toPhone:
25886
+ type: string
25852
25887
  toUserId:
25853
25888
  type: string
25854
25889
  toName:
@@ -25875,6 +25910,20 @@ components:
25875
25910
  - ok
25876
25911
  - mode
25877
25912
  - transfer
25913
+ AcceptTransferOptions:
25914
+ type: object
25915
+ properties:
25916
+ comms:
25917
+ $ref: "#/components/schemas/CommsTriggerMap"
25918
+ notify:
25919
+ type: boolean
25920
+ CancelTransferOptions:
25921
+ type: object
25922
+ properties:
25923
+ comms:
25924
+ $ref: "#/components/schemas/CommsTriggerMap"
25925
+ notify:
25926
+ type: boolean
25878
25927
  ProofTransferType:
25879
25928
  type: string
25880
25929
  enum:
@@ -136,7 +136,20 @@ export interface ProofValuesUpdateRequest {
136
136
  /** The caller's own private slot, merged into `proof.values.personal[callerUid]`. */
137
137
  personal?: Record<string, JsonValue>;
138
138
  }
139
- export type ProofClaimRequest = Record<string, any>;
139
+ /**
140
+ * Claim/mint payload. Remains open (server-side rules vary), but two keys are
141
+ * recognised across the claim endpoints:
142
+ * - `data` — claim values written to the proof
143
+ * - `comms` — comms to send once the proof is committed to the ledger. Role:
144
+ * `claimer` (the claiming user). See {@link CommsTrigger}.
145
+ */
146
+ export interface ProofClaimRequest {
147
+ /** Claim values written to the proof. */
148
+ data?: Record<string, any>;
149
+ /** Comms to send on claim; role `claimer`. Fired only after the ledger write. */
150
+ comms?: CommsTriggerMap;
151
+ [key: string]: any;
152
+ }
140
153
  /**
141
154
  * `'public'` (default, omitted) reads/writes `proof.values[key]`.
142
155
  * `'owner'` reads/writes `proof.values.owner[key]`.
@@ -238,23 +251,65 @@ export interface ProofTransfer {
238
251
  createdAt: string;
239
252
  updatedAt: string;
240
253
  }
254
+ /**
255
+ * A comms trigger — the standard way to have an action send a transactional
256
+ * message as a side-effect of doing its work. You name a template (authored in
257
+ * the template editor) and optionally supply merge props; the server owns who
258
+ * receives it and injects the authoritative context ({{ proof }}, {{ product }},
259
+ * the target contact). This is a 1:1 transactional send — it does not touch the
260
+ * interactions → segments → broadcasts (marketing) pipeline.
261
+ */
262
+ export interface CommsTrigger {
263
+ /** The comms template to render and send. */
264
+ templateId: string;
265
+ /** Delivery channel; defaults to the contact's preferred channel. */
266
+ channel?: 'preferred' | 'email' | 'sms' | 'push' | 'wallet' | 'whatsapp';
267
+ /** Freeform merge data for the template (note, message, custom fields). */
268
+ props?: Record<string, any>;
269
+ /** Set false to skip this message while still performing the action. */
270
+ notify?: boolean;
271
+ /** Owning app id, recorded in comms history. */
272
+ appId?: string;
273
+ }
274
+ /**
275
+ * A role → {@link CommsTrigger} map passed into an action. Each action documents
276
+ * the roles it exposes (e.g. a transfer exposes `recipient` and `sender`); only
277
+ * roles the action recognises are sent. The server decides which contact each
278
+ * role resolves to — the caller only names templates, never recipients.
279
+ */
280
+ export type CommsTriggerMap = Record<string, CommsTrigger>;
241
281
  /**
242
282
  * Start a push transfer. Provide **one** of:
243
- * - `toEmail` / `toUserId` — a directed transfer to a named recipient (they accept).
283
+ * - `toEmail` / `toPhone` / `toUserId` — a directed transfer to a named recipient (they accept).
244
284
  * - `release: true` — an open release (the proof becomes claimable by anyone).
245
285
  */
246
286
  export interface TransferProofOptions {
287
+ /**
288
+ * Comms to send. Roles: `recipient` (the named new owner) and `sender` (the
289
+ * initiator) for a directed transfer; `owner` for an open release. For an
290
+ * SMS recipient, set `comms.recipient.channel = 'sms'`.
291
+ */
292
+ comms?: CommsTriggerMap;
247
293
  /** Directed: recipient email (created/looked up if needed). */
248
294
  toEmail?: string;
295
+ /**
296
+ * Directed: recipient phone in E.164 (e.g. `+14155551234`). Resolves to the same
297
+ * user they log in as via SMS OTP (created if needed). Pair with an SMS comms
298
+ * trigger to notify them: `comms.recipient.channel = 'sms'`.
299
+ */
300
+ toPhone?: string;
249
301
  /** Directed: recipient user id, if already known. */
250
302
  toUserId?: string;
251
303
  /** Directed: display name for a newly-created recipient. */
252
304
  toName?: string;
253
305
  /** Open release: mark the proof claimable instead of directing it. */
254
306
  release?: boolean;
255
- /** Optional note included in the recipient email. */
307
+ /**
308
+ * @deprecated The legacy fixed transfer email is gone. Put a note in a comms
309
+ * trigger's props instead, e.g. `comms.recipient.props.note`.
310
+ */
256
311
  message?: string;
257
- /** Set `false` to skip the recipient notification email (directed only). */
312
+ /** Set `false` to skip ALL comms for this action (per-role: `comms.<role>.notify`). */
258
313
  notify?: boolean;
259
314
  }
260
315
  /** Result of initiating a push transfer. */
@@ -263,3 +318,22 @@ export interface TransferProofResult {
263
318
  mode: 'directed' | 'open_release';
264
319
  transfer: ProofTransfer;
265
320
  }
321
+ /**
322
+ * Options for accepting a directed transfer. Comms roles: `recipient` (the
323
+ * accepting new owner) and `sender` (the previous owner) — the completion
324
+ * confirmation for each party.
325
+ */
326
+ export interface AcceptTransferOptions {
327
+ comms?: CommsTriggerMap;
328
+ /** Set false to skip all completion comms. */
329
+ notify?: boolean;
330
+ }
331
+ /**
332
+ * Options for cancelling a pending transfer. Comms roles: `owner` (the canceller)
333
+ * and, for a directed transfer, `recipient` (whose earmarked transfer is withdrawn).
334
+ */
335
+ export interface CancelTransferOptions {
336
+ comms?: CommsTriggerMap;
337
+ /** Set false to skip all cancellation comms. */
338
+ notify?: boolean;
339
+ }
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.16.3 | Generated: 2026-09-05T10:57:07.719Z
3
+ Version: 1.16.5 | Generated: 2026-09-05T15:46:49.017Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -35,6 +35,7 @@ For detailed guides on specific features:
35
35
  - **[Proof Claiming Methods](proof-claiming-methods.md)** - All methods for claiming/registering product ownership (NFC tags, serial numbers, auto-generated claims)
36
36
  - **[Proof Share Grants](proof-share-grants.md)** - Delegated, scoped, revocable bearer access to a single proof (read/comment/verify-owner links)
37
37
  - **[Proof Ownership Transfer](proof-ownership-transfer.md)** - Moving a proof to a new owner: directed transfer, open release, accept/cancel, and the state machine
38
+ - **[Comms Triggers](proof-comms-triggers.md)** - Sending transactional comms from proof actions (claim/transfer/accept/cancel): the role→template map, per-action roles, and the trust boundary
38
39
  - **[Lots](lots.md)** - Collection-scoped production groupings spanning many SKUs; facet/product selectors, member resolution, and GS1 AI(10) batch-then-lot resolution
39
40
  - **[Item Context](item-context.md)** - The `itemContext` container prop derived from a serial-proof URL or NFC tap (what item the URL points at)
40
41
  - **[Product Facets SDK](PRODUCT_FACETS_SDK.md)** - Admin and public product facet endpoints and TypeScript interfaces
@@ -7508,6 +7509,15 @@ interface ProofValuesUpdateRequest {
7508
7509
  }
7509
7510
  ```
7510
7511
 
7512
+ **ProofClaimRequest** (interface)
7513
+ ```typescript
7514
+ interface ProofClaimRequest {
7515
+ data?: Record<string, any>
7516
+ comms?: CommsTriggerMap
7517
+ [key: string]: any
7518
+ }
7519
+ ```
7520
+
7511
7521
  **ProofFieldsConfig** (interface)
7512
7522
  ```typescript
7513
7523
  interface ProofFieldsConfig {
@@ -7580,13 +7590,34 @@ interface ProofTransfer {
7580
7590
  }
7581
7591
  ```
7582
7592
 
7593
+ **CommsTrigger** (interface)
7594
+ ```typescript
7595
+ interface CommsTrigger {
7596
+ templateId: string
7597
+ channel?: 'preferred' | 'email' | 'sms' | 'push' | 'wallet' | 'whatsapp'
7598
+ props?: Record<string, any>
7599
+ notify?: boolean
7600
+ appId?: string
7601
+ }
7602
+ ```
7603
+
7583
7604
  **TransferProofOptions** (interface)
7584
7605
  ```typescript
7585
7606
  interface TransferProofOptions {
7607
+ * Comms to send. Roles: `recipient` (the named new owner) and `sender` (the
7608
+ * initiator) for a directed transfer; `owner` for an open release. For an
7609
+ * SMS recipient, set `comms.recipient.channel = 'sms'`.
7610
+ comms?: CommsTriggerMap
7586
7611
  toEmail?: string
7612
+ * Directed: recipient phone in E.164 (e.g. `+14155551234`). Resolves to the same
7613
+ * user they log in as via SMS OTP (created if needed). Pair with an SMS comms
7614
+ * trigger to notify them: `comms.recipient.channel = 'sms'`.
7615
+ toPhone?: string
7587
7616
  toUserId?: string
7588
7617
  toName?: string
7589
7618
  release?: boolean
7619
+ * @deprecated The legacy fixed transfer email is gone. Put a note in a comms
7620
+ * trigger's props instead, e.g. `comms.recipient.props.note`.
7590
7621
  message?: string
7591
7622
  notify?: boolean
7592
7623
  }
@@ -7601,12 +7632,26 @@ interface TransferProofResult {
7601
7632
  }
7602
7633
  ```
7603
7634
 
7635
+ **AcceptTransferOptions** (interface)
7636
+ ```typescript
7637
+ interface AcceptTransferOptions {
7638
+ comms?: CommsTriggerMap
7639
+ notify?: boolean
7640
+ }
7641
+ ```
7642
+
7643
+ **CancelTransferOptions** (interface)
7644
+ ```typescript
7645
+ interface CancelTransferOptions {
7646
+ comms?: CommsTriggerMap
7647
+ notify?: boolean
7648
+ }
7649
+ ```
7650
+
7604
7651
  **ProofResponse** = `Proof`
7605
7652
 
7606
7653
  **ProofUpdateRequest** = `Partial<ProofWrite> & { proof?: ProofWrite }`
7607
7654
 
7608
- **ProofClaimRequest** = `Record<string, any>`
7609
-
7610
7655
  **ProofFieldScope** = `'public' | 'owner' | 'personal' | 'admin'`
7611
7656
 
7612
7657
  **ProofFieldDef** = `ScopedFieldDef & { scope?: ProofFieldScope }`
@@ -7619,6 +7664,8 @@ interface TransferProofResult {
7619
7664
 
7620
7665
  **ProofTransferState** = ``
7621
7666
 
7667
+ **CommsTriggerMap** = `Record<string, CommsTrigger>`
7668
+
7622
7669
  ### qr
7623
7670
 
7624
7671
  **QrShortCodeLookupResponse** (interface)
@@ -10428,16 +10475,18 @@ Redeem a grant token (anonymous or signed-in). Records the redemption and return
10428
10475
  productId: string,
10429
10476
  proofId: string,
10430
10477
  options: TransferProofOptions) → `Promise<TransferProofResult>`
10431
- Start a push transfer of a proof (current owner / collection admin only). Directed — hand it to a named recipient who then calls {@link acceptTransfer}: ```ts await proof.transfer(collectionId, productId, proofId, { toEmail: 'buyer@example.com' }) ``` Open release — make the proof claimable by anyone: ```ts await proof.transfer(collectionId, productId, proofId, { release: true }) ```
10478
+ Start a push transfer of a proof (current owner / collection admin only). Directed — hand it to a named recipient who then calls {@link acceptTransfer}: ```ts await proof.transfer(collectionId, productId, proofId, { toEmail: 'buyer@example.com' }) ``` Open release — make the proof claimable by anyone: ```ts await proof.transfer(collectionId, productId, proofId, { release: true }) ``` Send comms by naming templates per role (server decides who receives each): ```ts await proof.transfer(collectionId, productId, proofId, { toEmail: 'buyer@example.com', comms: { recipient: { templateId: 'transfer-incoming', props: { note: 'Enjoy!' } }, sender: { templateId: 'transfer-sent' }, }, }) ``` Direct to an SMS recipient by phone (E.164) and notify them by text: ```ts await proof.transfer(collectionId, productId, proofId, { toPhone: '+14155551234', comms: { recipient: { templateId: 'transfer-incoming', channel: 'sms' } }, }) ```
10432
10479
 
10433
10480
  **acceptTransfer**(collectionId: string,
10434
10481
  productId: string,
10435
- proofId: string) → `Promise<`
10482
+ proofId: string,
10483
+ options: AcceptTransferOptions = {}) → `Promise<`
10436
10484
  Accept a directed transfer (the named recipient only). Completes the ownership move — the proof's `userId` becomes the caller and the previous owner's private data and share grants are cleared/voided.
10437
10485
 
10438
10486
  **cancelTransfer**(collectionId: string,
10439
10487
  productId: string,
10440
- proofId: string) → `Promise<`
10488
+ proofId: string,
10489
+ options: CancelTransferOptions = {}) → `Promise<`
10441
10490
  Cancel a pending push transfer (current owner / collection admin only).
10442
10491
 
10443
10492
  **getTransfer**(collectionId: string,
@@ -289,6 +289,22 @@ try {
289
289
 
290
290
  ---
291
291
 
292
+ ## Sending a claim confirmation
293
+
294
+ Claims can send a transactional confirmation once the proof is committed to the
295
+ ledger. Pass a `comms` map (role `claimer`) alongside `data` — you author the
296
+ template, the server hydrates `{{ proof }}` / `{{ product }}` / `{{ contact }}`:
297
+
298
+ ```ts
299
+ await proof.claim(collectionId, productId, proofId, {
300
+ data: { store: 'Regent St' },
301
+ comms: { claimer: { templateId: 'welcome-owner', channel: 'preferred' } },
302
+ })
303
+ ```
304
+
305
+ Omit `comms` and nothing is sent (opt-in). Full detail:
306
+ [Comms Triggers](./proof-comms-triggers.md).
307
+
292
308
  ## API Reference
293
309
 
294
310
  ### `proof.claim(collectionId, productId, proofId, data?)`
@@ -0,0 +1,142 @@
1
+ # Comms Triggers
2
+
3
+ The standard way to send a **transactional message** as a side-effect of a proof
4
+ action — a claim confirmation, a "you've received a transfer" email, a
5
+ cancellation notice. You pass a **comms trigger** into the action; the server
6
+ sends it once the action's write is durable.
7
+
8
+ This replaces the old fixed SendGrid proof emails (`createProof` / `transferProof`).
9
+ Instead of a hardcoded template baked into the backend, **you author the template**
10
+ (in the template editor) and **name it per action**. The copy, branding, channel,
11
+ and merge fields are yours.
12
+
13
+ ---
14
+
15
+ ## The shape
16
+
17
+ An action takes a `comms` object: a **role → `CommsTrigger`** map.
18
+
19
+ ```ts
20
+ interface CommsTrigger {
21
+ templateId: string // the template to render + send (you author it)
22
+ channel?: 'preferred' | 'email' | 'sms' | 'push' | 'wallet' | 'whatsapp'
23
+ props?: Record<string, any> // merge data: a note, a message, custom fields
24
+ notify?: boolean // false → do the action, send nothing for this role
25
+ appId?: string // your app id, recorded in comms history
26
+ }
27
+
28
+ // role → trigger
29
+ type CommsTriggerMap = Record<string, CommsTrigger>
30
+ ```
31
+
32
+ Each action documents the **roles** it exposes (who can be messaged). You supply a
33
+ trigger only for the roles you want to notify.
34
+
35
+ ---
36
+
37
+ ## What you control vs what the server owns
38
+
39
+ You name a template and supply soft data. The server owns everything that decides
40
+ **who** receives it and **what context** the template can see. A caller can never
41
+ redirect a message to a contact of its choosing.
42
+
43
+ | You supply (per role) | The server injects |
44
+ |---|---|
45
+ | `templateId` | which contact the role resolves to (recipient / sender / claimer / owner) |
46
+ | `props` (note, message, custom) | `{{ proof }}`, `{{ product }}`, `{{ contact }}` hydration |
47
+ | `channel`, `notify`, `appId` | the proof/product ids, the portal URL (`{{ proofUrl }}`) |
48
+
49
+ Templates are rendered with **Liquid**, so a template body can read
50
+ `{{ proof.values.owner.name }}`, `{{ product.title }}`, `{{ contact.firstName }}`,
51
+ `{{ proofUrl }}`, and any `props` you passed (`{{ note }}`). See
52
+ [Liquid Templates](./liquid-templates.md) and [Comms](./comms.md) for the template
53
+ model and channel resolution.
54
+
55
+ > This is a **1:1 transactional** send. It goes straight through the transactional
56
+ > comms path — it does **not** touch the interactions → segments → broadcasts
57
+ > (marketing/audience) pipeline. Recording a CRM interaction or awarding loyalty is
58
+ > a separate, independent concern; don't route those through the comms trigger.
59
+
60
+ ---
61
+
62
+ ## Roles by action
63
+
64
+ | Action | Roles | Fired when |
65
+ |--------|-------|-----------|
66
+ | `claim` / `claimProduct` | `claimer` | after the proof is committed to the ledger |
67
+ | `transfer` (directed) | `recipient`, `sender` | on initiation |
68
+ | `transfer` (`release: true`) | `owner` | on initiation |
69
+ | `acceptTransfer` | `recipient` (new owner), `sender` (previous owner) | after the ownership move completes |
70
+ | `cancelTransfer` | `owner` (canceller), `recipient` (earmarked, directed only) | after the transfer is cancelled |
71
+
72
+ Timing is deliberate: comms fire **only after the durable write** (the ledger
73
+ commit for a claim, the ownership move for an accept). A failed action sends
74
+ nothing; a comms failure never breaks the action (best-effort).
75
+
76
+ ---
77
+
78
+ ## Examples
79
+
80
+ Directed transfer — notify the buyer, confirm to the seller:
81
+
82
+ ```ts
83
+ await proof.transfer(collectionId, productId, proofId, {
84
+ toEmail: 'buyer@example.com',
85
+ comms: {
86
+ recipient: { templateId: 'transfer-incoming', props: { note: 'Enjoy the watch!' } },
87
+ sender: { templateId: 'transfer-sent' },
88
+ },
89
+ })
90
+ ```
91
+
92
+ Claim — confirm to the claimer:
93
+
94
+ ```ts
95
+ await proof.claim(collectionId, productId, proofId, {
96
+ data: { purchaseStore: 'Regent St' },
97
+ comms: {
98
+ claimer: { templateId: 'welcome-owner', channel: 'preferred' },
99
+ },
100
+ })
101
+ ```
102
+
103
+ Accept — completion confirmation to both parties:
104
+
105
+ ```ts
106
+ await proof.acceptTransfer(collectionId, productId, proofId, {
107
+ comms: {
108
+ recipient: { templateId: 'transfer-complete-owner' },
109
+ sender: { templateId: 'transfer-complete-previous' },
110
+ },
111
+ })
112
+ ```
113
+
114
+ Suppress comms for one role while still messaging another:
115
+
116
+ ```ts
117
+ comms: {
118
+ recipient: { templateId: 'transfer-incoming' },
119
+ sender: { notify: false }, // do the transfer, don't email the seller
120
+ }
121
+ ```
122
+
123
+ Omit `comms` entirely and nothing is sent — comms are **opt-in**.
124
+
125
+ ---
126
+
127
+ ## Notes
128
+
129
+ - **Email-only recipients work.** A directed transfer to a `toEmail` that isn't a
130
+ contact yet resolves/creates a contact before sending.
131
+ - **SMS recipients work the same way.** A directed transfer to a `toPhone` (E.164)
132
+ resolves the recipient by phone and gives their contact a phone identity, so an
133
+ SMS-channel trigger (`channel: 'sms'`) reaches them. Because the phone maps to a
134
+ stable Firebase uid, they accept by logging in via SMS OTP — the accept is still
135
+ auth-gated exactly as with email.
136
+ - **Consent + suppression apply.** Transactional sends still respect the contact's
137
+ channel consent and suppression list; a template's `topic` governs this.
138
+ - **Delivery is logged** to comms history (with your `appId` and a `ref`), so sends
139
+ are auditable per contact.
140
+
141
+ See also [Proof Ownership Transfer](./proof-ownership-transfer.md),
142
+ [Proof Claiming Methods](./proof-claiming-methods.md), and [Comms](./comms.md).
@@ -15,9 +15,20 @@ reserves states for them.
15
15
 
16
16
  | Mode | How | Who completes it |
17
17
  |------|-----|------------------|
18
- | **Directed** | Owner names a recipient (`toEmail` / `toUserId`). The proof is earmarked for them (`claimUserId`) and they're emailed a link. | The named recipient calls `acceptTransfer`. |
18
+ | **Directed** | Owner names a recipient (`toEmail` / `toPhone` / `toUserId`). The proof is earmarked for them (`claimUserId`); you notify them with a comms trigger. | The named recipient calls `acceptTransfer`. |
19
19
  | **Open release** | Owner marks the proof `claimable`. | Anyone claims it via the normal claim flow. |
20
20
 
21
+ A `toPhone` recipient (E.164) resolves to the **same user they log in as via SMS
22
+ OTP** — Firebase keys the uid by phone number, so the earmark matches when they
23
+ sign in. Pair it with an SMS comms trigger to notify them by text:
24
+
25
+ ```ts
26
+ await proof.transfer(collectionId, productId, proofId, {
27
+ toPhone: '+14155551234',
28
+ comms: { recipient: { templateId: 'transfer-incoming', channel: 'sms' } },
29
+ })
30
+ ```
31
+
21
32
  Only the current owner (or a collection admin) can start a transfer. A proof can
22
33
  have **one active transfer at a time**.
23
34
 
@@ -41,10 +52,14 @@ authoritative and atomic:
41
52
  ```ts
42
53
  import { proof } from '@proveanything/smartlinks'
43
54
 
44
- // Directed — hand it to a named buyer (they must accept)
55
+ // Directed — hand it to a named buyer (they must accept). Notify each party with a
56
+ // comms trigger naming a template you authored (see Comms Triggers below).
45
57
  const { transfer } = await proof.transfer(collectionId, productId, proofId, {
46
58
  toEmail: 'buyer@example.com',
47
- message: 'Enjoy the watch!',
59
+ comms: {
60
+ recipient: { templateId: 'transfer-incoming', props: { note: 'Enjoy the watch!' } },
61
+ sender: { templateId: 'transfer-sent' },
62
+ },
48
63
  })
49
64
 
50
65
  // …or an open release — anyone can now claim it
@@ -60,12 +75,18 @@ await proof.cancelTransfer(collectionId, productId, proofId)
60
75
  ## Recipient flow (directed)
61
76
 
62
77
  ```ts
63
- // Only the named recipient can accept — a third party is rejected.
64
- const { proof: mine } = await proof.acceptTransfer(collectionId, productId, proofId)
78
+ // Only the named recipient can accept — a third party is rejected. Optionally
79
+ // send completion comms to both parties.
80
+ const { proof: mine } = await proof.acceptTransfer(collectionId, productId, proofId, {
81
+ comms: {
82
+ recipient: { templateId: 'transfer-complete-owner' }, // the new owner
83
+ sender: { templateId: 'transfer-complete-previous' }, // the previous owner
84
+ },
85
+ })
65
86
  ```
66
87
 
67
88
  The recipient sees the pending transfer on their account (a pending claim) and via
68
- the email link. Until they accept, the proof still belongs to the seller.
89
+ the notification you sent. Until they accept, the proof still belongs to the seller.
69
90
 
70
91
  ---
71
92
 
@@ -101,14 +122,34 @@ the email link. Until they accept, the proof still belongs to the seller.
101
122
  added, are dispute-protected and never auto-transfer by default).
102
123
  - **Every state change is audited** as an append-only attestation on the proof.
103
124
 
125
+ ## Comms
126
+
127
+ Every transfer action takes an optional `comms` map (role → comms trigger) that
128
+ sends a transactional message once the action's write is durable. Roles:
129
+
130
+ | Action | Roles |
131
+ |--------|-------|
132
+ | `transfer` (directed) | `recipient`, `sender` |
133
+ | `transfer` (`release: true`) | `owner` |
134
+ | `acceptTransfer` | `recipient` (new owner), `sender` (previous owner) |
135
+ | `cancelTransfer` | `owner`, `recipient` (directed only) |
136
+
137
+ You author each template; the server owns who each role resolves to and hydrates
138
+ `{{ proof }}` / `{{ product }}` / `{{ contact }}` / `{{ proofUrl }}`. Pass
139
+ `notify: false` on a role (or omit `comms`) to send nothing. Full detail:
140
+ [Comms Triggers](./proof-comms-triggers.md).
141
+
104
142
  ## API
105
143
 
106
144
  | Method | Endpoint |
107
145
  |--------|----------|
108
146
  | `proof.transfer(c, p, id, opts)` | `POST …/proof/:id/transfer` |
109
- | `proof.acceptTransfer(c, p, id)` | `POST …/proof/:id/transfer/accept` |
110
- | `proof.cancelTransfer(c, p, id)` | `POST …/proof/:id/transfer/cancel` |
147
+ | `proof.acceptTransfer(c, p, id, opts?)` | `POST …/proof/:id/transfer/accept` |
148
+ | `proof.cancelTransfer(c, p, id, opts?)` | `POST …/proof/:id/transfer/cancel` |
111
149
  | `proof.getTransfer(c, p, id)` | `GET …/proof/:id/transfer` |
112
150
 
113
- See also [Proof Share Grants](./proof-share-grants.md) and
151
+ `opts` on transfer/accept/cancel carries the `comms` map (and `notify`).
152
+
153
+ See also [Comms Triggers](./proof-comms-triggers.md),
154
+ [Proof Share Grants](./proof-share-grants.md) and
114
155
  [Proof Claiming Methods](./proof-claiming-methods.md).
package/openapi.yaml CHANGED
@@ -25717,6 +25717,14 @@ components:
25717
25717
  type: object
25718
25718
  additionalProperties:
25719
25719
  $ref: "#/components/schemas/JsonValue"
25720
+ ProofClaimRequest:
25721
+ type: object
25722
+ properties:
25723
+ data:
25724
+ type: object
25725
+ additionalProperties: true
25726
+ comms:
25727
+ $ref: "#/components/schemas/CommsTriggerMap"
25720
25728
  ProofFieldsConfig:
25721
25729
  type: object
25722
25730
  properties:
@@ -25844,11 +25852,38 @@ components:
25844
25852
  - state
25845
25853
  - createdAt
25846
25854
  - updatedAt
25855
+ CommsTrigger:
25856
+ type: object
25857
+ properties:
25858
+ templateId:
25859
+ type: string
25860
+ channel:
25861
+ type: string
25862
+ enum:
25863
+ - preferred
25864
+ - email
25865
+ - sms
25866
+ - push
25867
+ - wallet
25868
+ - whatsapp
25869
+ props:
25870
+ type: object
25871
+ additionalProperties: true
25872
+ notify:
25873
+ type: boolean
25874
+ appId:
25875
+ type: string
25876
+ required:
25877
+ - templateId
25847
25878
  TransferProofOptions:
25848
25879
  type: object
25849
25880
  properties:
25881
+ comms:
25882
+ $ref: "#/components/schemas/CommsTriggerMap"
25850
25883
  toEmail:
25851
25884
  type: string
25885
+ toPhone:
25886
+ type: string
25852
25887
  toUserId:
25853
25888
  type: string
25854
25889
  toName:
@@ -25875,6 +25910,20 @@ components:
25875
25910
  - ok
25876
25911
  - mode
25877
25912
  - transfer
25913
+ AcceptTransferOptions:
25914
+ type: object
25915
+ properties:
25916
+ comms:
25917
+ $ref: "#/components/schemas/CommsTriggerMap"
25918
+ notify:
25919
+ type: boolean
25920
+ CancelTransferOptions:
25921
+ type: object
25922
+ properties:
25923
+ comms:
25924
+ $ref: "#/components/schemas/CommsTriggerMap"
25925
+ notify:
25926
+ type: boolean
25878
25927
  ProofTransferType:
25879
25928
  type: string
25880
25929
  enum:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.16.3",
3
+ "version": "1.16.5",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",