@proveanything/smartlinks 2.0.58 → 2.0.62

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: 2.0.58 | Generated: 2026-10-13T14:08:01.791Z
3
+ Version: 2.0.62 | Generated: 2026-10-13T20:05:22.753Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -135,6 +135,7 @@ The Smartlinks SDK is organized into the following namespaces:
135
135
  - **serialNumber** - Assign, lookup, and manage serial numbers across scopes.
136
136
 
137
137
  — Other —
138
+ - **adminCards** - Functions for adminCards operations
138
139
  - **analytics** - Functions for analytics operations
139
140
  - **async** - Functions for async operations
140
141
  - **attestation** - Functions for attestation operations
@@ -474,6 +475,112 @@ Examples include:
474
475
 
475
476
  ## Types
476
477
 
478
+ ### adminCards
479
+
480
+ **AdminCardRefresh** (interface)
481
+ ```typescript
482
+ interface AdminCardRefresh {
483
+ on?: AdminCardRefreshOn[]
484
+ maxAge?: number | string
485
+ }
486
+ ```
487
+
488
+ **AdminCardDeclaration** (interface)
489
+ ```typescript
490
+ interface AdminCardDeclaration {
491
+ id: string
492
+ title: string
493
+ description?: string
494
+ kind?: 'data' | 'component'
495
+ function?: string
496
+ component?: string
497
+ sizes?: AdminCardSize[]
498
+ priority?: number
499
+ levels?: Array<'collection'>
500
+ refresh?: AdminCardRefresh
501
+ }
502
+ ```
503
+
504
+ **AppManifestAdminCards** (interface)
505
+ ```typescript
506
+ interface AppManifestAdminCards {
507
+ cards: AdminCardDeclaration[]
508
+ files?: {
509
+ js: { umd: string; esm?: string }
510
+ css?: string
511
+ }
512
+ }
513
+ ```
514
+
515
+ **AdminCardAction** (interface)
516
+ ```typescript
517
+ interface AdminCardAction {
518
+ label: string
519
+ path?: string
520
+ url?: string
521
+ }
522
+ ```
523
+
524
+ **AdminCardPayload** (interface)
525
+ ```typescript
526
+ interface AdminCardPayload {
527
+ title?: string
528
+ value?: number | string
529
+ label?: string
530
+ unit?: string
531
+ trend?: { direction: 'up' | 'down' | 'flat'; label?: string; good?: boolean }
532
+ status?: { tone: 'ok' | 'attention' | 'neutral' | 'error'; text: string }
533
+ progress?: { done: number; total: number; label?: string }
534
+ items?: Array<{ title: string; subtitle?: string; meta?: string; path?: string }>
535
+ todo?: Array<{ text: string; action?: AdminCardAction }>
536
+ actions?: AdminCardAction[]
537
+ empty?: { text: string; action?: AdminCardAction }
538
+ }
539
+ ```
540
+
541
+ **AdminCard** (interface)
542
+ ```typescript
543
+ interface AdminCard {
544
+ key: string
545
+ appId: string
546
+ cardId: string
547
+ title: string
548
+ description?: string
549
+ kind: 'data' | 'component'
550
+ sizes: AdminCardSize[]
551
+ size: AdminCardSize
552
+ priority: number
553
+ state: AdminCardState
554
+ payload: AdminCardPayload | null
555
+ computedAt: number | null
556
+ error?: string
557
+ component?: { name: string; files: NonNullable<AppManifestAdminCards['files']> }
558
+ }
559
+ ```
560
+
561
+ **StudioHomeLayout** (interface)
562
+ ```typescript
563
+ interface StudioHomeLayout {
564
+ version: 1
565
+ cards: Array<{ key: string; size?: AdminCardSize; hidden?: boolean }>
566
+ }
567
+ ```
568
+
569
+ **AdminCardsResponse** (interface)
570
+ ```typescript
571
+ interface AdminCardsResponse {
572
+ cards: AdminCard[]
573
+ hidden: AdminCard[]
574
+ layout: StudioHomeLayout | null
575
+ }
576
+ ```
577
+
578
+ **AdminCardSize** = `'small' | 'medium' | 'wide' | 'full'`
579
+
580
+ **AdminCardRefreshOn** = ``
581
+
582
+ **AdminCardState** = ``
583
+
477
584
  ### ai
478
585
 
479
586
  **ContentPart** (interface)
@@ -2852,6 +2959,7 @@ interface AppManifest {
2852
2959
  executor?: AppManifestExecutor;
2853
2960
  functions?: AppManifestFunctions;
2854
2961
  realtime?: { channels: RealtimeChannelDeclaration[] };
2962
+ adminCards?: AppManifestAdminCards;
2855
2963
  data?: AppDataDeclaration;
2856
2964
  headless?: AppHeadlessDeclaration;
2857
2965
  connector?: AppConnectorDeclaration;
@@ -2915,6 +3023,8 @@ interface AppConnectorDeclaration {
2915
3023
  }
2916
3024
  panels?: ConnectorPanel[]
2917
3025
  aiGuide?: string
3026
+ recordId?: string
3027
+ limits?: { requestsPerSecond?: number }
2918
3028
  }
2919
3029
  ```
2920
3030
 
@@ -3008,7 +3118,15 @@ interface ConnectorSendInput {
3008
3118
  **ConnectorSendResult** (interface)
3009
3119
  ```typescript
3010
3120
  interface ConnectorSendResult {
3011
- results: Array<{ index: number; ok: boolean; status?: number; error?: string; externalId?: string }>
3121
+ results: Array<{
3122
+ index: number
3123
+ ok: boolean
3124
+ status?: number
3125
+ error?: string
3126
+ externalId?: string
3127
+ retryable?: boolean
3128
+ retryAfterSeconds?: number
3129
+ }>
3012
3130
  }
3013
3131
  ```
3014
3132
 
@@ -7108,6 +7226,102 @@ interface IntegrationFlow {
7108
7226
  lastPollAt?: string | null
7109
7227
  lastCursor?: string | null
7110
7228
  totalSynced?: number | null
7229
+ circuitOpenedAt?: string | null
7230
+ circuitProbeAt?: string | null
7231
+ circuitReason?: string | null
7232
+ }
7233
+ ```
7234
+
7235
+ **FailedRecord** (interface)
7236
+ ```typescript
7237
+ interface FailedRecord {
7238
+ id: string
7239
+ entityId?: string | null
7240
+ externalId?: string
7241
+ status: 'failed' | 'dead'
7242
+ attempts: number
7243
+ nextAttemptAt?: string | null
7244
+ lastError?: string | null
7245
+ updatedAt?: string
7246
+ }
7247
+ ```
7248
+
7249
+ **FlowHealth** (interface)
7250
+ ```typescript
7251
+ interface FlowHealth {
7252
+ flowId: string
7253
+ name: string
7254
+ direction: FlowDirection
7255
+ status: FlowStatus
7256
+ paused: { since: string; nextCheckAt: string | null; reason: string | null } | null
7257
+ running: { runId: string; startedAt: string; heartbeatAt: string | null; trigger: string; progress: Record<string, number | string> } | null
7258
+ lastRun: { at: string | null; status: string | null; error: string | null }
7259
+ last24h: {
7260
+ runs: number
7261
+ byStatus: Record<string, number>
7262
+ records: number
7263
+ delivered: number
7264
+ failed: number
7265
+ failureRate: number
7266
+ avgRunMs: number | null
7267
+ recordsPerSecond: number | null
7268
+ }
7269
+ backlog: {
7270
+ landed?: number
7271
+ waiting?: number
7272
+ oldestWaitingSince?: string | null
7273
+ delivered?: number
7274
+ retrying: number
7275
+ retriesDue: number
7276
+ gaveUp: number
7277
+ }
7278
+ }
7279
+ ```
7280
+
7281
+ **IntegrationHealth** (interface)
7282
+ ```typescript
7283
+ interface IntegrationHealth {
7284
+ flows: FlowHealth[]
7285
+ summary: { flows: number; active: number; paused: number; running: number; failedLast24h: number; gaveUp: number }
7286
+ }
7287
+ ```
7288
+
7289
+ **ProductLineage** (interface)
7290
+ ```typescript
7291
+ interface ProductLineage {
7292
+ productId: string
7293
+ sources: Array<{
7294
+ source: string
7295
+ externalId: string
7296
+ flow: { id: string; name: string }
7297
+ fetchedAt: string
7298
+ processedAt: string | null
7299
+ processedRunId: string | null
7300
+ status: string
7301
+ lastError: string | null
7302
+ currentMapping: boolean
7303
+ mapping: FieldMapping[]
7304
+ payload?: unknown
7305
+ }>
7306
+ deliveries: Array<{
7307
+ flow: { id: string; name: string }
7308
+ status: 'sent' | 'failed' | 'dead'
7309
+ lastSentAt: string | null
7310
+ attempts: number
7311
+ nextAttemptAt: string | null
7312
+ lastError: string | null
7313
+ lastRunId: string | null
7314
+ }>
7315
+ recentRuns: FlowRun[]
7316
+ }
7317
+ ```
7318
+
7319
+ **FailedRecordList** (interface)
7320
+ ```typescript
7321
+ interface FailedRecordList {
7322
+ direction: FlowDirection
7323
+ items: FailedRecord[]
7324
+ nextCursor: string | null
7111
7325
  }
7112
7326
  ```
7113
7327
 
@@ -7161,6 +7375,7 @@ interface RunFlowSummary {
7161
7375
  created?: number
7162
7376
  updated?: number
7163
7377
  skipped?: number
7378
+ held?: number
7164
7379
  status: RunStatus
7165
7380
  runId?: string
7166
7381
  }
@@ -10283,6 +10498,17 @@ interface Gs1DigitalLinkParams {
10283
10498
 
10284
10499
  ## API Functions
10285
10500
 
10501
+ ### adminCards
10502
+
10503
+ **list**(collectionId: string) → `Promise<AdminCardsResponse>`
10504
+ The collection's dashboard: every card, in display order, with its cached payload and how fresh it is (`state`). Never waits on slow cards — those come back `stale` / `pending` and arrive later as a realtime `cards` change.
10505
+
10506
+ **refresh**(collectionId: string, appId: string, cardId: string) → `Promise<AdminCard>`
10507
+ Recompute one card now (the card's "refresh" button). Rate-limited per card.
10508
+
10509
+ **saveLayout**(collectionId: string, layout: StudioHomeLayout) → `Promise<StudioHomeLayout>`
10510
+ Save the collection's dashboard layout (order, sizes, hidden cards) — every admin sees it.
10511
+
10286
10512
  ### agent
10287
10513
 
10288
10514
  **run**(collectionId: string, body: AgentRunRequest) → `Promise<AgentRunResult>`
@@ -11704,6 +11930,26 @@ Is enhanced logging active for a connection? GET /integrations/logging/:connecti
11704
11930
  **setLogging**(collectionId: string, connectionId: string, input: SetLoggingInput = {}) → `Promise<`
11705
11931
  Enable enhanced logging. POST /integrations/logging/:connectionId (omit ttlMinutes = until cancelled)
11706
11932
 
11933
+ **listFailed**(collectionId: string,
11934
+ flowId: string,
11935
+ query: { status?: 'failed' | 'dead'; limit?: number; cursor?: string } = {}) → `Promise<FailedRecordList>`
11936
+ A flow's failed / dead records. GET /integrations/flows/:id/failed
11937
+
11938
+ **retryFailed**(collectionId: string, flowId: string, ids?: string[]) → `Promise<`
11939
+ Retry failed / dead records now (all of them when `ids` is omitted). POST /integrations/flows/:id/failed/retry
11940
+
11941
+ **getHealth**(collectionId: string) → `Promise<IntegrationHealth>`
11942
+ Every flow at a glance: paused?, running?, last 24h, backlog. GET /integrations/health
11943
+
11944
+ **getFlowHealth**(collectionId: string, flowId: string) → `Promise<FlowHealth>`
11945
+ One flow's health. GET /integrations/flows/:id/health
11946
+
11947
+ **getProductLineage**(collectionId: string, productId: string, opts: { payload?: boolean } = {}) → `Promise<ProductLineage>`
11948
+ Where a product's data came from (the provider record, when it was fetched and imported, by which run, with the current mapping or an older one) and where it went (each outbound flow's last delivery). `payload: true` includes the provider records as received. GET /integrations/lineage/product/:productId
11949
+
11950
+ **resumeFlow**(collectionId: string, flowId: string) → `Promise<`
11951
+ Resume a flow that paused itself (circuit open): held records go out now. POST /integrations/flows/:id/resume
11952
+
11707
11953
  **cancelLogging**(collectionId: string, connectionId: string) → `Promise<`
11708
11954
  Cancel enhanced logging now. DELETE /integrations/logging/:connectionId
11709
11955
 
@@ -0,0 +1,167 @@
1
+ # Admin cards
2
+
3
+ Cards your app adds to a collection's **dashboard** in SmartLinks Studio — the first thing an admin
4
+ sees. "3 new enquiries", "2 questions waiting for an answer", "Your site is live", with a button
5
+ straight into your app's admin.
6
+
7
+ - **Data cards** (the default, any app): you declare the card and an **admin server function** that
8
+ returns a small JSON payload. Studio draws it — a consistent look, no app code in Studio.
9
+ - **Component cards** (first-party and approved publishers): a React component from your app's
10
+ `adminCards` bundle, for things a data card can't show (charts, inline edits). It gets the same
11
+ cached payload as its `data` prop.
12
+
13
+ ## Fast by construction
14
+
15
+ Dashboards that load 20 widgets × 50 calls on every visit are the thing to avoid. So:
16
+
17
+ 1. **One request per dashboard.** Studio calls `adminCards.list(collectionId)` once and gets every
18
+ card back — never one request per card.
19
+ 2. **Served from a cache.** A card's last payload is kept and shown immediately. On a warm dashboard
20
+ **no card function runs at all.**
21
+ 3. **Recomputed only when its data changed.** You declare what the card depends on in
22
+ `refresh.on`; when one of those changes (any write through SmartLinks), the card is marked stale and
23
+ recomputed **in the background** the next time someone looks. The new payload arrives over
24
+ SmartLinks Realtime as a `cards` change. Your FAQ count is recounted when an FAQ changes, not on
25
+ every login.
26
+ 4. **Budgets.** A card function gets about **2 seconds** and **8KB** of output. A slow or failing card
27
+ shows its last payload marked as updating / couldn't refresh — it never holds up the page.
28
+
29
+ Keep card functions to one cheap query. If a number is expensive (a big aggregate), keep it up to date
30
+ yourself as data changes and have the card function read it.
31
+
32
+ ## Declare a card
33
+
34
+ In `app.manifest.json`:
35
+
36
+ ```json
37
+ "adminCards": {
38
+ "cards": [
39
+ {
40
+ "id": "unanswered",
41
+ "title": "FAQ",
42
+ "description": "Questions waiting for an answer",
43
+ "function": "cardUnanswered",
44
+ "sizes": ["small", "medium"],
45
+ "priority": 40,
46
+ "refresh": { "on": ["records:question"], "maxAge": "1d" }
47
+ }
48
+ ]
49
+ },
50
+ "functions": {
51
+ "files": { "js": { "umd": "functions.umd.js", "esm": "functions.es.js" } },
52
+ "definitions": [
53
+ { "name": "cardUnanswered", "trigger": { "type": "http" }, "visibility": "admin",
54
+ "authority": "collection", "capabilities": ["sl:records:read"] }
55
+ ]
56
+ }
57
+ ```
58
+
59
+ | Field | |
60
+ |---|---|
61
+ | `id` | Unique in the app (letters, digits, `-`, `_`; up to 40). |
62
+ | `title` | The card's heading (the payload can override it). |
63
+ | `description` | One line for the "add a card" picker. |
64
+ | `kind` | `data` (default) or `component`. |
65
+ | `function` | The **admin** http server function returning the payload. Required for data cards. |
66
+ | `component` | Component cards: the export name in the `adminCards` bundle. |
67
+ | `sizes` | `small` (3/12), `medium` (6/12), `wide` (8/12), `full` (12/12). First = default. Default `["medium"]`. |
68
+ | `priority` | Lower first, until the collection arranges its dashboard. Default 100. |
69
+ | `refresh.on` | What the data depends on — see below. |
70
+ | `refresh.maxAge` | Recompute at least this often: seconds, or `"30m"`, `"6h"`, `"2d"`. Default 6h, minimum 1m. |
71
+
72
+ ### `refresh.on`
73
+
74
+ | Value | Changes when |
75
+ |---|---|
76
+ | `records` | Any of **this app's** records is created, updated or deleted |
77
+ | `records:<type>` | This app's records of that `recordType` |
78
+ | `products` | A product in the collection |
79
+ | `proofs` | A proof |
80
+ | `assets` | An asset |
81
+ | `apps` | The collection's installed apps |
82
+ | `collection` | The collection itself |
83
+ | `settings` / `settings:<group>` | Any settings group / that one |
84
+
85
+ Data that changes without an event (visit counts, "due this week") — use `maxAge` only.
86
+
87
+ ## Return the payload
88
+
89
+ The function returns an `AdminCardPayload` — every field optional; return what the card needs:
90
+
91
+ ```ts
92
+ import type { AdminCardPayload } from '@proveanything/smartlinks'
93
+
94
+ export async function cardUnanswered(ctx): Promise<AdminCardPayload> {
95
+ // One query: the newest 3 open questions, plus the total.
96
+ const open = await ctx.sl.appRecords.query({ recordType: 'question', status: 'open', limit: 3, sort: '-createdAt' })
97
+ const total = open.pagination.total ?? open.data.length
98
+ if (!total) return { value: 0, label: 'questions waiting', status: { tone: 'ok', text: 'All answered' } }
99
+ return {
100
+ value: total,
101
+ label: total === 1 ? 'question waiting' : 'questions waiting',
102
+ status: { tone: 'attention', text: 'Answer them so they show on your site' },
103
+ items: open.data.map((q) => ({ title: q.data.question, meta: q.createdAt, path: `/questions/${q.id}` })),
104
+ todo: [{ text: `Answer ${total} FAQ question${total === 1 ? '' : 's'}`, action: { label: 'Answer', path: '/questions?status=open' } }],
105
+ actions: [{ label: 'Open FAQ', path: '/' }],
106
+ }
107
+ }
108
+ ```
109
+
110
+ | Field | Shown as |
111
+ |---|---|
112
+ | `title` | Heading (overrides the declared title) |
113
+ | `value`, `label`, `unit` | The big number/value and its caption |
114
+ | `trend` | `{ direction: 'up'\|'down'\|'flat', label?, good? }` — `good` picks the colour |
115
+ | `status` | `{ tone: 'ok'\|'attention'\|'neutral'\|'error', text }` — `attention`/`error` also go to the To do strip |
116
+ | `progress` | `{ done, total, label? }` — a progress bar |
117
+ | `items` | Up to 5 rows `{ title, subtitle?, meta?, path? }` |
118
+ | `todo` | `[{ text, action? }]` — collected into the dashboard's To do strip |
119
+ | `actions` | Up to 2 buttons `{ label, path? \| url? }` — `path` opens your app's admin at that path |
120
+ | `empty` | `{ text, action? }` — shown instead of the body when there's nothing yet |
121
+
122
+ No HTML, no styling: Studio draws every card the same way, in the collection's theme.
123
+
124
+ Use **`authority: "collection"`** with read capabilities: a card's payload is cached and shown to every
125
+ admin of the collection, so it shouldn't depend on who happened to load it.
126
+
127
+ ## The dashboard and its layout
128
+
129
+ The layout belongs to the **collection** — every admin sees the same dashboard, set up once (by the
130
+ sign-up wizard, or an admin arranging it). It's the `studioHome` settings group:
131
+
132
+ ```json
133
+ { "version": 1, "cards": [ { "key": "faq:unanswered", "size": "small" }, { "key": "platform:products", "hidden": true } ] }
134
+ ```
135
+
136
+ Cards not in the layout follow by `priority`, so a newly installed app's cards appear on their own;
137
+ an uninstalled app's cards drop out. Save it with `adminCards.saveLayout`.
138
+
139
+ ## Reading the dashboard (hosts)
140
+
141
+ ```ts
142
+ import { adminCards, realtime } from '@proveanything/smartlinks'
143
+
144
+ const { cards, hidden, layout } = await adminCards.list(collectionId) // display order
145
+ realtime.onChange({ collectionId }, { type: 'cards' }, () => reload()) // a card was recomputed
146
+ await adminCards.refresh(collectionId, 'faq', 'unanswered') // the card's refresh button
147
+ ```
148
+
149
+ Each card has `state`: `fresh`, `stale` (last payload, a newer one is coming), `pending` (first
150
+ computation still running) or `error` (last attempt failed; `payload` is the last good one).
151
+
152
+ ## Component cards
153
+
154
+ For first-party and approved publishers. Ship an `adminCards` bundle (built like widgets) and declare
155
+ it:
156
+
157
+ ```json
158
+ "adminCards": {
159
+ "files": { "js": { "umd": "admin-cards.umd.js", "esm": "admin-cards.esm.js" }, "css": "admin-cards.css" },
160
+ "cards": [ { "id": "visits", "title": "Visitors", "kind": "component", "component": "VisitsCard",
161
+ "function": "cardVisits", "sizes": ["wide", "full"], "refresh": { "maxAge": "1h" } } ]
162
+ }
163
+ ```
164
+
165
+ The component gets `{ collectionId, appId, size, data, SL, openApp(path?), lang }`. Draw from `data`
166
+ (the cached payload of its `function`) — don't fetch on mount; that's what keeps the dashboard fast.
167
+ `smartlinks-doctor` checks your cards.
@@ -90,6 +90,8 @@ Types: `AppConnectorDeclaration` and friends in `@proveanything/smartlinks` (`sr
90
90
  | `functions` | Which of this app's server functions implement each step (below). Fill in only what applies. |
91
91
  | `panels[]` | Provider-specific screens the console mounts (an API explorer, a status page). |
92
92
  | `aiGuide` | A guide the mapping assistant reads: the provider's quirks, required fields, good defaults. |
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
+ | `limits` | `{ requestsPerSecond }` toward the provider, enforced across every SmartLinks worker. |
93
95
 
94
96
  ---
95
97
 
@@ -112,9 +114,12 @@ declare the provider's hosts (`network:<host>`) and the secrets it reads (`secre
112
114
  | `send` | Every outbound run | `ConnectorSendInput` `{ installation, payloads }` | `ConnectorSendResult` `{ results: [{ index, ok, status, error, externalId }] }` |
113
115
 
114
116
  **How a run flows:**
115
- - **Inbound:** `fetch` (paged) → `shape` → core maps the documents with the flow's mapping → upsert.
117
+ - **Inbound:** `fetch` (paged; every record is landed as received) → `shape` → core maps the documents
118
+ with the flow's mapping → upsert, matched by `recordId`. Unchanged records aren't processed again; a
119
+ changed flow configuration replays the landed records without fetching.
116
120
  - **Outbound:** gather the SmartLinks records → core maps them to the provider's fields → `serialize`
117
- → `send` (or a generic transport).
121
+ → `send` (or a generic transport). A record identical to its last successful delivery isn't sent again.
122
+ - Large runs are processed in short slices that resume after an interruption; there's no record cap.
118
123
 
119
124
  **What happens when a step is left out:**
120
125
  - **No `shape`:** core maps the provider records as they come.
@@ -19,6 +19,25 @@ Provider-specific logic (a provider's payload format, its API, its connect form)
19
19
  **connector app**, not in the engine: see **[Connectors](connectors.md)**. A flow uses one with
20
20
  `config.connector = { appId }`.
21
21
 
22
+ **Reliability.** A record that fails is retried on its own with exponential backoff (honouring the
23
+ destination's `Retry-After`); a permanent failure (most 4xx) or one out of attempts is **dead** and
24
+ waits in `integrations.listFailed` for `integrations.retryFailed`. A destination that keeps failing
25
+ **pauses the flow automatically** (`flow.circuitOpenedAt`): edits are held, small probes check for
26
+ recovery, and it resumes on its own (or with `integrations.resumeFlow`).
27
+
28
+ **Observability.** `integrations.getHealth` shows every flow at a glance (paused?, running?, last 24h,
29
+ backlog); `getFlowHealth` one flow; `getProductLineage` where a product's data came from and where it
30
+ went. Every run and every record is kept in BigQuery (`analytics.integration_runs`,
31
+ `analytics.integration_run_items`); the API's run history is a 7-day window. Outbound sends carry
32
+ `X-SmartLinks-Run` (the run) and an `Idempotency-Key` (same record, same content → same key).
33
+
34
+ **Change capture.** Every product and record write is captured where it's saved, whatever made it (the
35
+ admin, an import, a server function). A product change *wakes* each subscribed outbound flow, which then
36
+ sweeps every product changed since its last sweep (deletions included). A burst of edits, such as a big
37
+ import, is one sweep, not one send per product. A reconciliation sweep every 15 minutes catches anything
38
+ a wake missed. Events that need one call each (app functions, flows on record events) are recorded
39
+ durably and retried until they're delivered.
40
+
22
41
  Credentials are **never** stored on the flow. The connection holds an opaque
23
42
  `credentialRef` into the **sealed-secret store** (`secrets` namespace); the value is sealed
24
43
  at rest and resolved server-side only, at execution.
package/docs/overview.md CHANGED
@@ -69,6 +69,7 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
69
69
  | **Portal Request Action** | `docs/portal-request-action.md` | Triggering portal built-in actions (__qrScanner, __share, __logout, etc.) from sub-apps |
70
70
  | **Interactions** | `docs/interactions.md` | Business events, outcomes, voting, competitions, and journey triggers |
71
71
  | **App Config Files** | `docs/app-manifest.md` | Full field-by-field reference for both JSON config files |
72
+ | **Admin cards** | `docs/admin-cards.md` | Cards on the collection's dashboard in Studio: declare a card + an admin function returning its data; cached, refreshed only when its data changes |
72
73
  | **Realtime** | `docs/realtime.md` | Live updates: change events (refetch when something changes) and declared app channels (a draw, a status) — no provider library or keys |
73
74
  | **Liquid Templates** | `docs/liquid-templates.md` | Dynamic content rendering with LiquidJS |
74
75
  | **Iframe Responder** | `docs/iframe-responder.md` | Iframe communication and proxy mode internals |
@@ -359,6 +360,7 @@ The SmartLinks SDK is organised into namespaces. See `docs/API_SUMMARY.md` for t
359
360
  - `contact` — Customer contact management
360
361
  - `comms` / `broadcasts` — Notifications and campaigns
361
362
  - `asset` — File/image uploads
363
+ - `adminCards` — the collection's dashboard: `list`, `refresh`, `saveLayout` (see `docs/admin-cards.md`)
362
364
  - `realtime` — SmartLinks Realtime: `onChange`, `channel`, `onMine` (see `docs/realtime.md`)
363
365
 
364
366
  ---