@proveanything/smartlinks 2.1.14 → 2.1.16

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 { ClaimSetScope, ClaimSetRange, CreateClaimSetRangeRequest, UpdateClaimSetRangeRequest, ClaimSetRangeTagsPage, ClaimSetRangeTag, ClaimSetRangeNextTag, ImportClaimSetRangeUidsRequest, ImportClaimSetRangeUidsResponse, AssignClaimSetRangeRequest, AssignClaimSetRangeResponse, ClaimSetRangeFile, UpdateClaimDataRequest, AssignClaimsRequest, CreateClaimSetTagRequest, CreateClaimSetTagResponse, CreateClaimSetRequest, ImportClaimSetTagsRequest, ImportClaimSetTagsResponse } from "../types/index.js";
1
+ import { ClaimSetScope, ClaimSetRange, CreateClaimSetRangeRequest, UpdateClaimSetRangeRequest, ClaimSetRangeTagsPage, ClaimSetRangeTag, ClaimSetRangeNextTag, ImportClaimSetRangeUidsRequest, ImportClaimSetRangeUidsResponse, AssignClaimSetRangeRequest, AddClaimSetRangeMemberRequest, AddClaimSetRangeMemberResponse, CloseClaimSetRangeResponse, AssignClaimSetRangeResponse, ClaimSetRangeFile, UpdateClaimDataRequest, AssignClaimsRequest, CreateClaimSetTagRequest, CreateClaimSetTagResponse, CreateClaimSetRequest, ImportClaimSetTagsRequest, ImportClaimSetTagsResponse } from "../types/index.js";
2
2
  export declare namespace claimSet {
3
3
  /**
4
4
  * Get all claim sets.
@@ -106,6 +106,14 @@ export declare namespace claimSet {
106
106
  function update(scope: ClaimSetScope, claimSetId: string, rangeId: string, params: UpdateClaimSetRangeRequest): Promise<ClaimSetRange>;
107
107
  /** Finish minting a range that was interrupted. */
108
108
  function resume(scope: ClaimSetScope, claimSetId: string, rangeId: string): Promise<ClaimSetRange>;
109
+ /** Close an open range: it becomes a range with the quantity it reached (removed if nothing was minted). */
110
+ function close(scope: ClaimSetScope, claimSetId: string, rangeId: string): Promise<CloseClaimSetRangeResponse>;
111
+ /** Put an existing tag of the set into a group (by code or chip UID), assigning it if a collection is given. */
112
+ function addMember(scope: ClaimSetScope, claimSetId: string, groupId: string, params: AddClaimSetRangeMemberRequest): Promise<AddClaimSetRangeMemberResponse>;
113
+ /** Take a tag out of a group. Its assignment is left as it is. */
114
+ function removeMember(scope: ClaimSetScope, claimSetId: string, groupId: string, code: string): Promise<{
115
+ range: ClaimSetRange;
116
+ }>;
109
117
  /** The range's tags in index order, a page at a time (keys: owners/admins only). */
110
118
  function tags(scope: ClaimSetScope, claimSetId: string, rangeId: string, options?: {
111
119
  from?: number;
@@ -190,6 +190,21 @@ export var claimSet;
190
190
  return post(`${rangesBase(scope, claimSetId)}/${encodeURIComponent(rangeId)}/resume`, {});
191
191
  }
192
192
  ranges.resume = resume;
193
+ /** Close an open range: it becomes a range with the quantity it reached (removed if nothing was minted). */
194
+ async function close(scope, claimSetId, rangeId) {
195
+ return post(`${rangesBase(scope, claimSetId)}/${encodeURIComponent(rangeId)}/close`, {});
196
+ }
197
+ ranges.close = close;
198
+ /** Put an existing tag of the set into a group (by code or chip UID), assigning it if a collection is given. */
199
+ async function addMember(scope, claimSetId, groupId, params) {
200
+ return post(`${rangesBase(scope, claimSetId)}/${encodeURIComponent(groupId)}/members`, params);
201
+ }
202
+ ranges.addMember = addMember;
203
+ /** Take a tag out of a group. Its assignment is left as it is. */
204
+ async function removeMember(scope, claimSetId, groupId, code) {
205
+ return del(`${rangesBase(scope, claimSetId)}/${encodeURIComponent(groupId)}/members/${encodeURIComponent(code)}`);
206
+ }
207
+ ranges.removeMember = removeMember;
193
208
  /** The range's tags in index order, a page at a time (keys: owners/admins only). */
194
209
  async function tags(scope, claimSetId, rangeId, options = {}) {
195
210
  const q = new URLSearchParams();
@@ -105,4 +105,32 @@ export declare namespace integrations {
105
105
  function runConnectorStep<T = unknown>(collectionId: string, appId: string, step: ConnectorConsoleStep, input?: Record<string, unknown> & {
106
106
  flowId?: string;
107
107
  }): Promise<T>;
108
+ /**
109
+ * Invoke a connector ACTION from the console (any visibility): redeem, enrol, adjust points, issue a
110
+ * coupon… The connector's function runs with its own credentials and is told who asked.
111
+ * POST /integrations/connectors/:appId/actions/:action { input }
112
+ */
113
+ function runConnectorAction<T = unknown>(collectionId: string, appId: string, action: string, input?: Record<string, unknown>): Promise<T>;
114
+ /**
115
+ * PUBLIC (the Hub, a site): the connector actions the caller may invoke — those declared `public`,
116
+ * plus `member` ones when signed in.
117
+ * GET /public/collection/:collectionId/connectors/:appId/actions
118
+ */
119
+ function listConnectorActions(collectionId: string, appId: string): Promise<{
120
+ actions: Array<{
121
+ name: string;
122
+ title: string;
123
+ description?: string;
124
+ visibility: 'public' | 'member' | 'admin';
125
+ input?: Record<string, unknown>;
126
+ output?: Record<string, unknown>;
127
+ }>;
128
+ }>;
129
+ /**
130
+ * PUBLIC (the Hub, a site): invoke a connector action — e.g. redeem a reward for the signed-in member.
131
+ * 401 SIGN_IN_REQUIRED for a `member` action without a session; 403 for an admin-only one;
132
+ * 400 with `missing[]` when a required input is absent.
133
+ * POST /public/collection/:collectionId/connectors/:appId/actions/:action { input }
134
+ */
135
+ function callConnectorAction<T = unknown>(collectionId: string, appId: string, action: string, input?: Record<string, unknown>): Promise<T>;
108
136
  }
@@ -200,4 +200,32 @@ export var integrations;
200
200
  return post(`${connectorsBase(collectionId)}/${enc(appId)}/steps/${enc(step)}`, input);
201
201
  }
202
202
  integrations.runConnectorStep = runConnectorStep;
203
+ /**
204
+ * Invoke a connector ACTION from the console (any visibility): redeem, enrol, adjust points, issue a
205
+ * coupon… The connector's function runs with its own credentials and is told who asked.
206
+ * POST /integrations/connectors/:appId/actions/:action { input }
207
+ */
208
+ async function runConnectorAction(collectionId, appId, action, input = {}) {
209
+ return post(`${connectorsBase(collectionId)}/${enc(appId)}/actions/${enc(action)}`, { input });
210
+ }
211
+ integrations.runConnectorAction = runConnectorAction;
212
+ /**
213
+ * PUBLIC (the Hub, a site): the connector actions the caller may invoke — those declared `public`,
214
+ * plus `member` ones when signed in.
215
+ * GET /public/collection/:collectionId/connectors/:appId/actions
216
+ */
217
+ async function listConnectorActions(collectionId, appId) {
218
+ return request(`/public/collection/${enc(collectionId)}/connectors/${enc(appId)}/actions`);
219
+ }
220
+ integrations.listConnectorActions = listConnectorActions;
221
+ /**
222
+ * PUBLIC (the Hub, a site): invoke a connector action — e.g. redeem a reward for the signed-in member.
223
+ * 401 SIGN_IN_REQUIRED for a `member` action without a session; 403 for an admin-only one;
224
+ * 400 with `missing[]` when a required input is absent.
225
+ * POST /public/collection/:collectionId/connectors/:appId/actions/:action { input }
226
+ */
227
+ async function callConnectorAction(collectionId, appId, action, input = {}) {
228
+ return post(`/public/collection/${enc(collectionId)}/connectors/${enc(appId)}/actions/${enc(action)}`, { input });
229
+ }
230
+ integrations.callConnectorAction = callConnectorAction;
203
231
  })(integrations || (integrations = {}));
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.1.14 | Generated: 2026-11-09T13:01:05.900Z
3
+ Version: 2.1.16 | Generated: 2026-11-10T17:19:48.505Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -65,7 +65,7 @@ For detailed guides on specific features:
65
65
  - **[Analytics Metadata Conventions](analytics-metadata-conventions.md)** - Standard recommended keys and conventions for analytics metadata fields
66
66
  - **[Loyalty: Points, Members & Earning Rules](loyalty.md)** - Loyalty schemes, automatic point earning via interaction rules, member balances, transaction history, and manual adjustments
67
67
  - **[Integrations](integrations.md)** - Inbound/outbound integration flows + the sealed-secret store; triggers (manual/event/schedule), event patterns, the event envelope (interaction/contact/event sources), identity policy, event mapping, signed deliveries, contact as a target, field mappings
68
- - **[Connectors](connectors.md)** - Connector apps (adapters for one external provider): a manifest `connector` block + server functions (testConnection/describeTarget/sample/fetch/shape/serialize/send/describeEvents/lookup); loyalty and CRM connectors; core does the mapping
68
+ - **[Connectors](connectors.md)** - Connector apps (adapters for one external provider): a manifest `connector` block + server functions (testConnection/describeTarget/sample/fetch/shape/serialize/send/describeEvents/lookup); loyalty and CRM connectors; actions (provider operations on demand); core does the mapping
69
69
  - **[Entitlements](entitlements.md)** - "Is this person gold or above?" answered on demand from signed claims, a connector lookup (cached), or the contact — never a mirrored member base; gates on interaction types and in server functions
70
70
  - **[AI Tools & Skills](ai-tools-and-skills.md)** - The AI capability catalog: skills apps invoke by name (e.g. research.brand), the tools the AI reaches for (web fetch/screenshot/brand assets/image gen), the agent loop, and how apps discover them
71
71
  - **[Deep Link Discovery](deep-link-discovery.md)** - Registering and discovering navigable app states for portal menus and AI orchestration
@@ -3112,6 +3112,19 @@ interface ConnectorPanel {
3112
3112
  }
3113
3113
  ```
3114
3114
 
3115
+ **ConnectorAction** (interface)
3116
+ ```typescript
3117
+ interface ConnectorAction {
3118
+ name: string
3119
+ title?: string
3120
+ description?: string
3121
+ function: string
3122
+ visibility?: 'admin' | 'member' | 'public'
3123
+ input?: Record<string, unknown>
3124
+ output?: Record<string, unknown>
3125
+ }
3126
+ ```
3127
+
3115
3128
  **AppConnectorDeclaration** (interface)
3116
3129
  ```typescript
3117
3130
  interface AppConnectorDeclaration {
@@ -3134,6 +3147,7 @@ interface AppConnectorDeclaration {
3134
3147
  lookup?: string
3135
3148
  }
3136
3149
  panels?: ConnectorPanel[]
3150
+ actions?: ConnectorAction[]
3137
3151
  aiGuide?: string
3138
3152
  recordId?: string
3139
3153
  limits?: { requestsPerSecond?: number; maxEvents?: number; maxAttributes?: number }
@@ -5761,10 +5775,11 @@ interface ClaimSetRange {
5761
5775
  id: string
5762
5776
  claimSetId: string
5763
5777
  namespace: string
5778
+ kind: ClaimSetRangeKind
5764
5779
  name: string
5765
5780
  notes: string | null
5766
- startIndex: number
5767
- endIndex: number
5781
+ startIndex: number | null
5782
+ endIndex: number | null
5768
5783
  quantity: number
5769
5784
  tagType: ClaimSetTagType
5770
5785
  fulfilment: 'printer' | 'sidekick'
@@ -5787,8 +5802,9 @@ interface ClaimSetRange {
5787
5802
  **CreateClaimSetRangeRequest** (interface)
5788
5803
  ```typescript
5789
5804
  interface CreateClaimSetRangeRequest {
5805
+ kind?: ClaimSetRangeKind
5790
5806
  name: string
5791
- quantity: number
5807
+ quantity?: number
5792
5808
  intendedCollectionId?: string
5793
5809
  intendedProductId?: string
5794
5810
  intendedBatchId?: string
@@ -5823,6 +5839,8 @@ interface ClaimSetRangeTag {
5823
5839
  productId: string | null
5824
5840
  batchId: string | null
5825
5841
  proofId: string | null
5842
+ rangeId: string | null
5843
+ groupId: string | null
5826
5844
  programmedAt: string | null
5827
5845
  scans: number
5828
5846
  }
@@ -5836,6 +5854,29 @@ interface ClaimSetRangeTagsPage {
5836
5854
  }
5837
5855
  ```
5838
5856
 
5857
+ **AddClaimSetRangeMemberRequest** (interface)
5858
+ ```typescript
5859
+ interface AddClaimSetRangeMemberRequest {
5860
+ code?: string
5861
+ uid?: string
5862
+ collectionId?: string
5863
+ productId?: string
5864
+ batchId?: string
5865
+ data?: Record<string, unknown>
5866
+ clear?: boolean
5867
+ }
5868
+ ```
5869
+
5870
+ **AddClaimSetRangeMemberResponse** (interface)
5871
+ ```typescript
5872
+ interface AddClaimSetRangeMemberResponse {
5873
+ tag: ClaimSetRangeTag
5874
+ range: ClaimSetRange
5875
+ assigned: { changed: number; skipped: Array<{ code: string; index: number | null; reason: 'other_range' | 'has_proof' | 'assigned_elsewhere'; collectionId?: string }> }
5876
+ movedFrom: string | null
5877
+ }
5878
+ ```
5879
+
5839
5880
  **ImportClaimSetRangeUidsRequest** (interface)
5840
5881
  ```typescript
5841
5882
  interface ImportClaimSetRangeUidsRequest {
@@ -5944,7 +5985,11 @@ type ClaimSetScope = { collectionId: string }
5944
5985
 
5945
5986
  **ClaimSetTagType** = `'ntag424' | 'ntag21x' | 'qr'`
5946
5987
 
5947
- **ClaimSetRangeStatus** = `'minting' | 'minted' | 'sent' | 'received' | 'assigned'`
5988
+ **ClaimSetRangeKind** = `'range' | 'open' | 'group'`
5989
+
5990
+ **ClaimSetRangeStatus** = `'minting' | 'minted' | 'sent' | 'received' | 'assigned' | 'open' | 'group'`
5991
+
5992
+ **CloseClaimSetRangeResponse** = `ClaimSetRange | { id: string`
5948
5993
 
5949
5994
  **ClaimSetImportResult** = ``
5950
5995
 
@@ -7980,6 +8025,7 @@ interface IntegrationFlowConfig {
7980
8025
  signing?: { credentialRef: string; previousCredentialRef?: string }
7981
8026
  identity?: { policy?: IdentityPolicy; hashKeyCredentialRef?: string }
7982
8027
  eventMapping?: EventMapping
8028
+ envelopeFormat?: 'cloudevents'
7983
8029
  connection?: FlowConnection
7984
8030
  fieldMappings?: FieldMapping[]
7985
8031
  connector?: FlowConnectorRef
@@ -8222,6 +8268,7 @@ interface ConnectorLookupResult {
8222
8268
  **EventEnvelope** (interface)
8223
8269
  ```typescript
8224
8270
  interface EventEnvelope {
8271
+ specVersion: number
8225
8272
  id: string
8226
8273
  type: string
8227
8274
  entityType: string | null
@@ -8234,6 +8281,8 @@ interface EventEnvelope {
8234
8281
  contactId: string | null
8235
8282
  userId: string | null
8236
8283
  externalIds: Record<string, string>
8284
+ anonymousId: string | null
8285
+ sessionId: string | null
8237
8286
  emailHash?: string | null
8238
8287
  phoneHash?: string | null
8239
8288
  hashAlgorithm?: 'hmac-sha256' | null
@@ -8243,6 +8292,7 @@ interface EventEnvelope {
8243
8292
  identityPolicy: IdentityPolicy
8244
8293
  product: { id: string; name?: string | null; sku?: string | null; gtin?: string | null; variantId?: string | null; batchId?: string | null } | null
8245
8294
  proof: { id: string } | null
8295
+ consent: { channels: Record<'email' | 'sms' | 'push' | 'whatsapp', 'opted_in' | 'opted_out' | 'suppressed' | 'unknown'>; topics: Record<string, boolean>; updatedAt: string | null } | null
8246
8296
  subject: Record<string, unknown>
8247
8297
  event?: { name: string; attributes: Record<string, unknown>; rule: string | null }
8248
8298
  attributes: Record<string, unknown>
@@ -12445,6 +12495,15 @@ Name, notes, order details, or a stage move (sent to the printer, received).
12445
12495
  **resume**(scope: ClaimSetScope, claimSetId: string, rangeId: string) → `Promise<ClaimSetRange>`
12446
12496
  Finish minting a range that was interrupted.
12447
12497
 
12498
+ **close**(scope: ClaimSetScope, claimSetId: string, rangeId: string) → `Promise<CloseClaimSetRangeResponse>`
12499
+ Close an open range: it becomes a range with the quantity it reached (removed if nothing was minted).
12500
+
12501
+ **addMember**(scope: ClaimSetScope, claimSetId: string, groupId: string, params: AddClaimSetRangeMemberRequest) → `Promise<AddClaimSetRangeMemberResponse>`
12502
+ Put an existing tag of the set into a group (by code or chip UID), assigning it if a collection is given.
12503
+
12504
+ **removeMember**(scope: ClaimSetScope, claimSetId: string, groupId: string, code: string) → `Promise<`
12505
+ Take a tag out of a group. Its assignment is left as it is.
12506
+
12448
12507
  **tags**(scope: ClaimSetScope, claimSetId: string, rangeId: string,
12449
12508
  options: { from?: number; limit?: number; keys?: boolean } = {}) → `Promise<ClaimSetRangeTagsPage>`
12450
12509
  The range's tags in index order, a page at a time (keys: owners/admins only).
@@ -12984,6 +13043,22 @@ Save a connector's non-secret Connect-form values and/or settings. PUT /integrat
12984
13043
  input: Record<string, unknown> & { flowId?: string } = {}) → `Promise<T>`
12985
13044
  Run a connector step from the console: `testConnection` → ConnectorTestResult, `describeTarget` → ConnectorTargetSchema, `sample` → ConnectorRecords, and dry runs of `shape` ({ records }) → ConnectorShapeResult and `serialize` ({ documents }) → ConnectorSerializeResult. The server adds the installation; pass `flowId` to layer that flow's settings. POST /integrations/connectors/:appId/steps/:step
12986
13045
 
13046
+ **runConnectorAction**(collectionId: string,
13047
+ appId: string,
13048
+ action: string,
13049
+ input: Record<string, unknown> = {}) → `Promise<T>`
13050
+ Invoke a connector ACTION from the console (any visibility): redeem, enrol, adjust points, issue a coupon… The connector's function runs with its own credentials and is told who asked. POST /integrations/connectors/:appId/actions/:action { input }
13051
+
13052
+ **listConnectorActions**(collectionId: string,
13053
+ appId: string) → `Promise<`
13054
+ PUBLIC (the Hub, a site): the connector actions the caller may invoke — those declared `public`, plus `member` ones when signed in. GET /public/collection/:collectionId/connectors/:appId/actions
13055
+
13056
+ **callConnectorAction**(collectionId: string,
13057
+ appId: string,
13058
+ action: string,
13059
+ input: Record<string, unknown> = {}) → `Promise<T>`
13060
+ PUBLIC (the Hub, a site): invoke a connector action — e.g. redeem a reward for the signed-in member. 401 SIGN_IN_REQUIRED for a `member` action without a session; 403 for an admin-only one; 400 with `missing[]` when a required input is absent. POST /public/collection/:collectionId/connectors/:appId/actions/:action { input }
13061
+
12987
13062
  ### interactions
12988
13063
 
12989
13064
  **query**(collectionId: string,
@@ -92,6 +92,7 @@ Types: `AppConnectorDeclaration` and friends in `@proveanything/smartlinks` (`sr
92
92
  | `aiGuide` | A guide the mapping assistant reads: the provider's quirks, required fields, good defaults. |
93
93
  | `recordId` | Path of the provider's own id in a fetched record (e.g. `gtin`). Inbound records are landed and matched by it, so a re-import updates the same product instead of creating another. Default `id`; a flow can override it (`config.upsert.externalIdPath`). |
94
94
  | `limits` | `{ requestsPerSecond }` toward the provider, enforced across every SmartLinks worker; `{ maxEvents, maxAttributes }` when the provider caps registered events — a flow's `eventMapping` is checked against them when saved. |
95
+ | `actions[]` | Provider operations invoked on demand (redeem, enrol, issue…): name, title, function, `visibility`, input schema. See [Actions](#actions-provider-operations-on-demand). |
95
96
 
96
97
  ---
97
98
 
@@ -219,6 +220,61 @@ A connector should ship an `eventMapping` **preset** with the provider's standar
219
220
 
220
221
  ---
221
222
 
223
+ ## Actions: provider operations on demand
224
+
225
+ Steps are called by the engine. **Actions** are called when someone asks: redeem a reward, enrol a
226
+ member, issue a coupon, raise a ticket, send a receipt, check a balance. A connector declares them in
227
+ the manifest and implements each as a server function; the platform decides who may call them and
228
+ runs them with the connector's credentials, telling the function who asked.
229
+
230
+ ```jsonc
231
+ "connector": {
232
+ "actions": [
233
+ { "name": "redeem", "title": "Redeem a reward", "function": "redeemReward", "visibility": "member",
234
+ "input": { "type": "object", "required": ["rewardId"], "properties": { "rewardId": { "type": "string" } } } },
235
+ { "name": "enrol", "title": "Join the programme", "function": "enrolMember", "visibility": "member" },
236
+ { "name": "coupon-status", "title": "Coupon status", "function": "couponStatus", "visibility": "public" },
237
+ { "name": "adjust-points", "title": "Adjust points", "function": "adjustPoints" } // admin only (the default)
238
+ ]
239
+ }
240
+ ```
241
+
242
+ | `visibility` | Who may invoke it |
243
+ |---|---|
244
+ | `admin` (default) | the console and admin API; any app function |
245
+ | `member` | also a signed-in person through the Hub / a site |
246
+ | `public` | also anyone |
247
+
248
+ The function receives `event.body = { input, installation, caller: { userId, contactId, role, surface } }`
249
+ and returns JSON (or a `Response`). `input.required` fields from the declared schema are checked before
250
+ the function runs (400 with `missing[]`); fuller validation is the connector's job.
251
+
252
+ ```ts
253
+ export async function redeemReward(ctx: any, event: any) {
254
+ const { input, caller } = event.body
255
+ if (!caller.contactId) return new Response(JSON.stringify({ error: 'no_contact' }), { status: 400 })
256
+ const memberId = (await ctx.sl.contacts.forCaller())?.customFields?.loyaltyMemberId // written by Profile
257
+ const res = await ctx.fetch(`${api}/members/${memberId}/redemptions`, { method: 'POST', headers: auth(await ctx.secrets.get('apiKey')), body: JSON.stringify({ reward: input.rewardId }) })
258
+ return res.json()
259
+ }
260
+ ```
261
+
262
+ Calling an action:
263
+
264
+ ```ts
265
+ // From the Hub or a site (visibility public / member):
266
+ const { actions } = await integrations.listConnectorActions(collectionId, 'acme-loyalty')
267
+ const out = await integrations.callConnectorAction(collectionId, 'acme-loyalty', 'redeem', { rewardId: 'R-5' })
268
+
269
+ // From another app's server function (any visibility; needs sl:connectors:write):
270
+ const out = await ctx.sl.connectors.call('acme-loyalty', 'redeem', { rewardId: 'R-5' })
271
+
272
+ // From the console (any visibility):
273
+ await integrations.runConnectorAction(collectionId, 'acme-loyalty', 'adjust-points', { memberId, delta: 100 })
274
+ ```
275
+
276
+ ---
277
+
222
278
  ## Where configuration lives
223
279
 
224
280
  | What | Where |
@@ -118,6 +118,7 @@ mapped with the flow's field mappings (or handed to a connector's `serialize`):
118
118
 
119
119
  ```jsonc
120
120
  {
121
+ "specVersion": 1, // bumped only when a consumer could break
121
122
  "id": "0b2b…", // the event's id — stable across retries; use it as the destination's idempotency / transaction id
122
123
  "type": "interaction.submitted:quiz-1",
123
124
  "entityType": "interaction", "entityId": "0b2b…",
@@ -126,12 +127,14 @@ mapped with the flow's field mappings (or handed to a connector's `serialize`):
126
127
  "identity": {
127
128
  "contactId": "…", "userId": "…",
128
129
  "externalIds": { "acme-loyalty": "M-9", "crm": "C-1" }, // ids other systems know this person by
130
+ "anonymousId": "v-77", "sessionId": "s-1", // the visitor / device and session, before anyone signed in (every policy)
129
131
  "emailHash": "…", "phoneHash": "…", "hashAlgorithm": "hmac-sha256" // policy 'hashed' (default)
130
132
  // "email", "phone": only under policy 'raw'
131
133
  },
132
134
  "identityPolicy": "hashed",
133
135
  "product": { "id": "P1", "name": "Widget", "sku": "W-1", "gtin": "0500…", "variantId": null, "batchId": null },
134
136
  "proof": { "id": "…" },
137
+ "consent": { "channels": { "email": "opted_in", "sms": "suppressed", "push": "unknown", "whatsapp": "unknown" }, "topics": { "offers": true }, "updatedAt": "…" }, // null when about nobody we hold
135
138
  "subject": { "interactionId": "quiz-1", "outcome": "completed", "metadata": { "score": 9 }, "source": "hub" },
136
139
  "event": { "name": "engagement_completed", "attributes": { "kind": "quiz", "score": 9 }, "rule": "quiz" }, // from eventMapping
137
140
  "attributes": {}
@@ -142,6 +145,11 @@ Field mappings read envelope paths (`identity.externalIds.acme-loyalty`, `subjec
142
145
  `event.name`, `product.sku`). With no mappings the envelope itself is sent. A failed event delivery
143
146
  keeps its envelope and is retried from it; a product is re-read instead.
144
147
 
148
+ **CloudEvents.** `config.envelopeFormat: 'cloudevents'` wraps each delivered body (mapped or not) as a
149
+ CloudEvents 1.0 structured event — `{ specversion: '1.0', id, type, source: 'urn:smartlinks:collection:<id>',
150
+ subject, time, datacontenttype, dataschema: 'urn:smartlinks:envelope:v1', data }` — the shape every event
151
+ bus, lake and serverless tool already consumes.
152
+
145
153
  ```ts
146
154
  const flow = await integrations.createFlow(collectionId, {
147
155
  appId: 'core-integrations', direction: 'outbound', name: 'Engagement → loyalty', status: 'active',
@@ -518,6 +518,7 @@ Prefer the platform primitives above over a dependency; when you do need one, pi
518
518
  | Put a visitor's details on a contact | `ctx.sl.contacts.upsert({ email, firstName, … })` → `{ contactId, created }` (fills empty fields only) | `sl:contacts:write` |
519
519
  | Record "they did X" | `ctx.sl.interactions.record({ interactionId, contactId, outcome, metadata })` | `sl:interactions:write` |
520
520
  | Land a loyalty / CRM provider's view of a person (member id, tier, points, tags, external ids) | `ctx.sl.members.apply({ identity, contact?, loyalty?, tags?, externalIds? }, { provider })` → `{ contactId, op }` — matches by id, external id, email or phone; profile fields fill-empty only; see [connectors.md](connectors.md#loyalty-and-crm-connectors) | `sl:members:write` |
521
+ | Invoke a connector's action — redeem, enrol, issue a coupon, raise a ticket — with the connector's credentials | `ctx.sl.connectors.call('acme-loyalty', 'redeem', { rewardId })` → the action's result; `ctx.sl.connectors.actions(appId)` lists them; see [connectors.md](connectors.md#actions-provider-operations-on-demand) | `sl:connectors:write` |
521
522
  | Ask "is this person gold or above?" without holding their member record | `ctx.sl.entitlements.check('tier>=gold')` → `{ allowed, basis, asOf, attributes }` for the signed-in caller (or `{ identity, token, fallback }`); see [entitlements.md](entitlements.md) | `sl:entitlements:read` |
522
523
  | Take a booking with your own checks (hours, queue token, payment) | `ctx.sl.bookings.create({ slotId \| resource + time, contactId, status, idempotencyKey })` — never oversold, never double-booked; also `confirm` / `cancel` / `move` / `mine` / `freebusy` / `message` (everyone booked, hydrated) / `slots.*` / `blocks.*` (see [bookings.md](bookings.md)) | `sl:bookings:write` (`sl:bookings:read` to read) |
523
524
 
@@ -55,6 +55,27 @@ tag without a chip, with its URL and key; write it, then `ranges.recordTag(scope
55
55
 
56
56
  To find where a chip or code went, `partner.lookupTags(partner.id, { uid })` or `{ code }` searches every set the partner owns and returns each hit's set, tag and range, never a key.
57
57
 
58
+ ## Open ranges and groups
59
+
60
+ A range's count is fixed when it is minted. Two phone-side jobs don't know their count in advance, so a range
61
+ has a `kind`:
62
+
63
+ - **`open`**: blank tags programmed on Sidekick one at a time. `ranges.create(scope, set.id, { kind: 'open', name })`
64
+ reserves only a start index; each `ranges.next()` hands back a free minted code or mints one more, so
65
+ `quantity`/`endIndex` grow as tags are written (`remaining` is null). One open range per set, and nothing can
66
+ be minted behind it (`409 RANGE_OPEN`) until `ranges.close(scope, set.id, range.id)` turns it into an ordinary
67
+ range (an open range that minted nothing is removed instead).
68
+ - **`group`**: tags that already exist, added by tapping, assigned by hand, and moved together later. Assigners may
69
+ make one: `ranges.create(scope, set.id, { kind: 'group', name, intendedCollectionId? })`. Each tap goes through
70
+ `ranges.addMember(scope, set.id, group.id, { code | uid, collectionId, productId? })`, which stamps the tag with
71
+ the group and assigns it with the single-tag rules; `assigned.skipped` says when a tag was left alone (claimed,
72
+ or in a collection you don't administer). `ranges.assign(scope, set.id, group.id, { collectionId })` moves every
73
+ member in one go; `ranges.removeMember` takes one out. Groups have no printer file, instructions, import or
74
+ `next` (`400 NOT_A_RANGE`). Its tags must be codes of the group's set; a chip that is a code elsewhere is
75
+ `409 OTHER_SET`.
76
+
77
+ Status reads `open` or `group` for these (and `assigned` once every member of a group is assigned).
78
+
58
79
  ## The printer file
59
80
 
60
81
  One row per tag, in index order. The manufacturer fills in `UID` and sends the same file back; rows are