@dossier-wiki/os-sdk 0.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/REFERENCE.md CHANGED
@@ -16,6 +16,12 @@ new DossiersOSAPIService({ baseUrl, apiKey, fetch? })
16
16
 
17
17
  ## Resource map
18
18
 
19
+ ### `sessions` and `channelEndpoints`
20
+
21
+ Both expose `list()`, `get(id)`, and `update(id, input)`. Creation is owned by
22
+ transaction ingestion resolution. Timestamp updates widen, rather than narrow,
23
+ the recorded event-time bounds.
24
+
19
25
  ### `auth`
20
26
 
21
27
  | Method | Endpoint |
@@ -34,11 +40,24 @@ new DossiersOSAPIService({ baseUrl, apiKey, fetch? })
34
40
  | `get(customerId, query?)` | `GET /v1/customers/individuals/{customerId}` |
35
41
  | `update(customerId, input)` | `PATCH /v1/customers/individuals/{customerId}` |
36
42
 
43
+ `create(input)` requires `externalId` and `fullName`. Optional create fields are
44
+ `status`, `preferredName`, `aliases`, `dateOfBirth`, `nationality[]`,
45
+ `countryOfResidence[]`, `taxId`, `emails[]`, `phones[]`,
46
+ `identificationDocuments[]`, `faceImageFileIds[]`, `addresses[]`, `tags[]`,
47
+ `assignedToUserIds[]`, and `assignedToTeamIds[]`. Contact entries are
48
+ `{ value, primary }`.
49
+
50
+ Legacy field names from the pre-1.0 contract are still accepted on create and
51
+ map onto the canonical ones — `email` / `phone` scalars fold into `emails[]` /
52
+ `phones[]`, an identification document's `issuingCountry` folds into `country`,
53
+ and `createdAt` backfills the creation timestamp. The canonical field wins
54
+ whenever both are supplied. Prefer the canonical names in new integrations.
55
+
37
56
  `individuals.identificationDocuments`
38
57
 
39
58
  | Method | Endpoint |
40
59
  | --- | --- |
41
- | `add(customerId, input)` | `POST .../identification-documents` |
60
+ | `create(customerId, input)` | `POST .../identification-documents` |
42
61
  | `update(customerId, documentId, input)` | `PATCH .../identification-documents/{documentId}` |
43
62
  | `delete(customerId, documentId)` | `DELETE .../identification-documents/{documentId}` |
44
63
 
@@ -59,6 +78,20 @@ new DossiersOSAPIService({ baseUrl, apiKey, fetch? })
59
78
  | `get(customerId, query?)` | `GET /v1/customers/entities/{customerId}` |
60
79
  | `update(customerId, input)` | `PATCH /v1/customers/entities/{customerId}` |
61
80
 
81
+ `create(input)` requires `externalId` and `legalName`. Optional create fields
82
+ are `status`, `tradingNames[]`, `registrationNumber`, `taxId`,
83
+ `countryOfIncorporation`, `dateOfIncorporation`, `entityType`, `websites[]`,
84
+ `stakeholders[]`, `locations[]`, `tags[]`, `assignedToUserIds[]`, and
85
+ `assignedToTeamIds[]`. Each stakeholder has required `name` and uppercase
86
+ `role` (`SHAREHOLDER`, `UBO`, or `DIRECTOR`). Entity locations use the same
87
+ `AddressInputDto` shape as individual addresses.
88
+
89
+ Migration: prefer individual `fullName`, contact-entry arrays, and country-code
90
+ arrays; prefer entity `tradingNames` and `locations`. The older create-time
91
+ `createdAt`, scalar `email` / `phone`, entity `addresses`, and entity
92
+ `parentEntityId` remain accepted as deprecated aliases so existing integrations
93
+ keep working — the canonical field wins whenever both are supplied.
94
+
62
95
  `entities.stakeholders`
63
96
 
64
97
  | Method | Endpoint |
@@ -87,11 +120,33 @@ new DossiersOSAPIService({ baseUrl, apiKey, fetch? })
87
120
  | Method | Endpoint |
88
121
  | --- | --- |
89
122
  | `trigger(customerId)` | `POST .../screen` |
123
+ | `listHistory(customerId, { limit? })` | `GET .../screening-history` |
124
+ | `getLast(customerId)` | `GET .../screening-history/latest` |
125
+ | `listResults(customerId, { disposition? })` | `GET .../screening-results` |
90
126
  | `listConfirmedMatches(customerId)` | `GET .../confirmed-matches` |
91
127
 
128
+ `listResults` returns every match for the customer (`ScreeningMatchResultDto[]`) with its current disposition — true positives (`CONFIRMED`), false positives (`FALSE_POSITIVE`) and unresolved (`PENDING`). Pass `disposition` to filter. `listConfirmedMatches` is the `CONFIRMED`-only alias.
129
+
130
+ Every result carries `tagline` as a string: the provider's short role/bio line for `HIGH_RISK` (PEP) matches, and `""` for every other match type or where no tagline exists. It is never omitted or `null`.
131
+
132
+ **Not every `FALSE_POSITIVE` was set by a human.** Where the provider supports it, every match is compared against the customer's date of birth, geography, role timeline and entity kind; one that is positively contradicted is dispositioned `FALSE_POSITIVE` automatically, and an alert whose every match is eliminated this way closes itself. The verdict is on `disambiguation` (`MatchDisambiguationDto`):
133
+
134
+ ```ts
135
+ const matches = await os.customers.screening.listResults(customerId);
136
+
137
+ for (const m of matches.filter((m) => m.disposition === 'FALSE_POSITIVE')) {
138
+ if (m.disambiguation?.decision === 'discard') {
139
+ // Cleared by the provider, not by one of your users.
140
+ console.log(m.matchedName, m.disambiguation.reasonCodes, m.disambiguation.rationale);
141
+ }
142
+ }
143
+ ```
144
+
145
+ The assessment only ever eliminates on a contradiction — never on a missing attribute, never on geography alone, and never on a sanctions listing unless the contradiction is hard. A `retain` is not a confirmation, only the absence of grounds to eliminate, and an absent `disambiguation` means the match was never assessed. `resolvedBy` distinguishes the automation from your users.
146
+
92
147
  `customers.riskRating` — `update(customerId, input)` → `PUT .../risk-rating`
93
148
 
94
- `customers.riskQuestionnaires` — `list`, `create` on `.../risk-questionnaires`
149
+ `customers.riskAssessments` — `list`, `create` on `.../risk-assessments`
95
150
 
96
151
  `customers.expectedActivity` — `get`, `update` on `.../expected-activity`
97
152
 
@@ -103,6 +158,87 @@ new DossiersOSAPIService({ baseUrl, apiKey, fetch? })
103
158
  | `register(customerId, input)` | `POST .../devices` |
104
159
  | `updateTrust(customerId, deviceId, input)` | `PUT .../devices/{deviceId}/trust` |
105
160
 
161
+ Registration and transaction ingestion share one canonical identity path. Send
162
+ one or more raw identities; the server converts each to a tenant-bound protected
163
+ key in memory, links the resulting canonical Device, and never persists or
164
+ returns the raw values:
165
+
166
+ ```ts
167
+ const device = await os.customers.devices.register(customerId, {
168
+ fingerprints: [
169
+ { value: browserVisitorId },
170
+ { value: mobileInstallationId, provider: 'MOBILE_SDK', type: 'INSTALLATION_ID' },
171
+ ],
172
+ browserDetails: { browserName: 'Chrome', os: 'ANDROID', device: 'MOBILE' },
173
+ });
174
+ ```
175
+
176
+ `list`, `register`, `updateTrust`, and `os.devices.get` return the same safe
177
+ `DeviceResponseDto`. It excludes raw fingerprints, protected hashes, and raw IP
178
+ addresses; temporal network and location evidence is exposed through dedicated
179
+ observation contracts.
180
+
181
+ `customers.assignment`
182
+
183
+ | Method | Endpoint |
184
+ | --- | --- |
185
+ | `assign(customerId, targets)` | `POST /v1/customers/{customerId}/assignment` |
186
+ | `unassign(customerId, targets)` | `DELETE /v1/customers/{customerId}/assignment` |
187
+ | `assignMany({ customerIds, ...targets })` | `POST /v1/customers/assignment` |
188
+
189
+ Standing assignment can also be stamped at create via `assignedToUserIds` / `assignedToTeamIds` on `individuals.create` / `entities.create`.
190
+
191
+ `individuals.create(input, { screening: 'sync' })` / `entities.create(input, { screening: 'sync' })` run the INITIAL screening inline and return it under `screening` (`InitialScreeningResultDto`: run status, `alertId`, and the `matches` array in the `listResults` shape). Without the option the call queues the screening as before and the response has no `screening` field.
192
+
193
+ `targets` must contain at least one non-empty `userIds` or `teamIds` array.
194
+ Bulk assignment requires at least one `customerId` and validates the complete
195
+ request before writing: an unknown or archived customer, or an invalid target,
196
+ rejects the whole request instead of returning partial success. Assignment
197
+ writes require `customer:update`; archived customers return 422.
198
+
199
+ ### `identificationDocuments` (document-scoped)
200
+
201
+ Top-level resource for operating on a single identification document by its id
202
+ — the review workflow's surface. Customer-scoped create/update/delete also
203
+ exist on `individuals.identificationDocuments` / `entities.identificationDocuments`.
204
+
205
+ | Method | Endpoint |
206
+ | --- | --- |
207
+ | `get(id)` | `GET /v1/identification-documents/{id}` |
208
+ | `update(id, input)` | `PATCH /v1/identification-documents/{id}` |
209
+ | `transition(id, input)` | `POST /v1/identification-documents/{id}/transitions` |
210
+ | `archive(id, input)` | `POST /v1/identification-documents/{id}/archive` |
211
+ | `restore(id)` | `POST /v1/identification-documents/{id}/restore` |
212
+ | `delete(id)` | `DELETE /v1/identification-documents/{id}` |
213
+
214
+ `transition(id, input)` moves the review verdict (`PENDING → APPROVED /
215
+ REJECTED`); `rejectionNarrative` is required when `status = REJECTED`, and
216
+ `rejectionTags` optionally classify the rejection. `update(id, input)` corrects
217
+ captured document fields (name, number, DOB, addresses, …) — allowed while the
218
+ document is `PENDING`; captured fields on an `APPROVED` document are frozen
219
+ until it is transitioned back to `PENDING` (notes/tags/attachments stay
220
+ editable). Every update is activity-logged with a per-field `{field, from, to}`
221
+ diff. `comments` proxy: `identificationDocuments.comments`.
222
+
223
+ ### `teams`
224
+
225
+ | Method | Endpoint |
226
+ | --- | --- |
227
+ | `list({ search?, tags? })` | `GET /v1/teams` |
228
+ | `get(id)` | `GET /v1/teams/{id}` |
229
+ | `create(input)` | `POST /v1/teams` |
230
+ | `update(id, input)` | `PATCH /v1/teams/{id}` |
231
+ | `archive(id)` | `POST /v1/teams/{id}/archive` |
232
+ | `delete(id)` | `DELETE /v1/teams/{id}` |
233
+ | `addMembers(id, { userIds })` | `POST /v1/teams/{id}/members` |
234
+ | `removeMembers(id, { userIds })` | `DELETE /v1/teams/{id}/members` |
235
+
236
+ `list` / `get` are open to any authenticated caller. Every other method needs the `team:create` / `team:update` / `team:delete` org permissions, which only `owner` and `admin` hold. An API key inherits the role of the user who created it, so a key created by a `member` gets a 403 — make the key under an owner or admin account if it has to manage teams.
237
+
238
+ `archive(id)` keeps the team resolvable for history but takes it out of `list` and out of notification fan-out. Prefer it to `delete(id)`, which leaves historical assignment ids dangling as tombstones.
239
+
240
+ Teams carry catalogue-governed tags under the `team.tags` field-key. `create` and `update` accept `tags: TagValueInput[]`, and every read returns `tags: TagValue[]`. `update` replaces the whole set — send every tag the team should end up with, omit the field to leave the tags alone, or pass `[]` to clear them. `list({ tags: ['aml-review'] })` is an any-of filter: a team matches when it carries at least one of the given values.
241
+
106
242
  ### `accounts`
107
243
 
108
244
  | Method | Endpoint |
@@ -134,6 +270,26 @@ new DossiersOSAPIService({ baseUrl, apiKey, fetch? })
134
270
  | Method | Endpoint |
135
271
  | --- | --- |
136
272
  | `get(deviceId)` | `GET /v1/devices/{deviceId}` |
273
+ | `merge(targetDeviceId, input)` | `POST /v1/devices/{targetDeviceId}/merge` |
274
+
275
+ `merge` makes the target canonical without deleting source history. Its required
276
+ `reason` and actor are retained in the immutable merge audit record.
277
+
278
+ ### `paymentInstruments`
279
+
280
+ | Method | Endpoint |
281
+ | --- | --- |
282
+ | `list()` | `GET /v1/payment-instruments` |
283
+ | `get(id)` | `GET /v1/payment-instruments/{id}` |
284
+ | `create(input)` | `POST /v1/payment-instruments` |
285
+ | `update(id, input)` | `PATCH /v1/payment-instruments/{id}` |
286
+ | `linkAccount(id, input)` | `POST /v1/payment-instruments/{id}/accounts` |
287
+ | `unlinkAccount(id, relationshipId, validTo?)` | `POST /v1/payment-instruments/{id}/accounts/{relationshipId}/unlink` |
288
+ | `linkCustomer(id, input)` | `POST /v1/payment-instruments/{id}/customers` |
289
+ | `unlinkCustomer(id, relationshipId, validTo?)` | `POST /v1/payment-instruments/{id}/customers/{relationshipId}/unlink` |
290
+
291
+ Account and Customer links are temporal relationships. Unlinking closes the
292
+ relationship at `validTo` (or the server's current time); it does not delete it.
137
293
 
138
294
  ### `transactions`
139
295
 
@@ -141,6 +297,89 @@ new DossiersOSAPIService({ baseUrl, apiKey, fetch? })
141
297
  | --- | --- |
142
298
  | `ingest(input)` | `POST /v1/transactions` |
143
299
  | `patchStatus(input)` | `PATCH /v1/transactions` |
300
+ | `get(reference, { by? })` | `GET /v1/transactions/{transactionId}` — returns `TransactionResponseDto`; `by: 'id'` looks up the internal UUID |
301
+
302
+ `ingest` accepts canonical `paymentInstrument` identity by internal ID,
303
+ integration `externalId`, or typed fingerprint. Transaction resolution creates
304
+ a Payment Instrument on a deterministic miss. `transaction.channel` may include
305
+ an endpoint `identifier`, provider namespace, display name, physical location,
306
+ and bounded scalar `context`.
307
+
308
+ ### `transactionAttempts`
309
+
310
+ | Method | Endpoint |
311
+ | --- | --- |
312
+ | `ingest(input)` | `POST /v1/transaction-attempts` |
313
+ | `list(input?)` | `GET /v1/transaction-attempts` |
314
+ | `get(id)` | `GET /v1/transaction-attempts/{id}` |
315
+
316
+ `attemptId` is a tenant-scoped idempotency key. Reusing it with identical
317
+ content returns the existing record; reusing it with changed content returns
318
+ `409 ATTEMPT_IDEMPOTENCY_CONFLICT`. Attempts are behavioural history and do not
319
+ trigger rule evaluation or contribute to transaction financial analytics.
320
+ Its `paymentInstrument` resolution is lookup-only: a matching ID, external ID,
321
+ or protected typed fingerprint is linked to the attempt, while a miss remains
322
+ unresolved and never creates a Payment Instrument.
323
+
324
+ `list` supports opaque cursor pagination and safe filters for outcomes,
325
+ operation, observation time, resolved subjects, IIN, device, session, and
326
+ channel endpoint. Read projections exclude request hashes and protected
327
+ payment-instrument/IP lookup hashes.
328
+
329
+ ### `behaviorSettings`
330
+
331
+ | Method | Endpoint |
332
+ | --- | --- |
333
+ | `get()` | `GET /v1/config/behavior-settings` |
334
+ | `update(input)` | `PATCH /v1/config/behavior-settings` |
335
+
336
+ The partial update controls attempt retention (1–3650 days) and attempt coverage
337
+ (`ALL` or `SELECTIVE`). Existing attempt expiry timestamps are unchanged; the
338
+ new retention applies to later ingestion and cleanup remains explicit.
339
+
340
+ `alerts.attemptEvidence(alertId)` returns the immutable, bounded Transaction
341
+ Attempt facts captured when attempt history contributed to an alert. Evidence
342
+ retention is independent of source-attempt cleanup.
343
+
344
+ ### `alerts`
345
+
346
+ | Method | Endpoint |
347
+ | --- | --- |
348
+ | `create(input)` | `POST /v1/alerts` |
349
+ | `get(alertId)` | `GET /v1/alerts/{alertId}` |
350
+ | `updateNotes(alertId, { notes })` | `PATCH /v1/alerts/{alertId}/notes` |
351
+ | `resources.list(alertId)` | `GET /v1/alerts/{alertId}/resources` |
352
+ | `resources.add(alertId, { resourceType, resourceId })` | `POST /v1/alerts/{alertId}/resources` |
353
+ | `resources.remove(alertId, alertResourceId)` | `DELETE /v1/alerts/{alertId}/resources/{alertResourceId}` |
354
+ | `attemptEvidence(alertId)` | `GET /v1/alerts/{alertId}/attempt-evidence` |
355
+ | `comments.*` | `/v1/platform/v1/comments` (resourceType `ALERT`) |
356
+
357
+ `create` raises a **custom alert** by hand. `alertType` is optional: name one of
358
+ the organisation's Custom Alert Types (`config.customAlertTypes`) by its machine
359
+ name and `fields` are Tier A typed fields validated against its schema (unknown
360
+ keys rejected, required keys enforced, at creation only); omit it for a
361
+ free-form alert whose `fields` are self-declaring (Tier B). `riskLevel`
362
+ defaults to the type's `defaultRiskLevel`, or MEDIUM without a type. `assignedToUserIds` / `assignedToTeamIds` set at
363
+ creation notify the assignees. On tenants using Defined workflows the alert is
364
+ enrolled into the "Custom alerts" workflow (Initial review → Secondary review →
365
+ Resolved by default); otherwise it follows the simple Open/Closed process.
366
+
367
+ `externalRequestId` is an idempotency key scoped per **organization**, not per
368
+ API key: two integrations replaying the same upstream event converge on one
369
+ alert, and the replay returns the existing alert with `201`.
370
+
371
+ `resources` accept any registered resource type (`customer`, `account`,
372
+ `transaction`, `counterparty`, `alert`, `rule`, …); the key is case-insensitive
373
+ and the target must exist in the organisation. Links are idempotent on
374
+ `(resourceType, resourceId)`. `remove` takes the **link id** returned by
375
+ `list` / `add`.
376
+
377
+ `notes` is a single markdown narrative on any alert type (screening and
378
+ rule-trigger alerts included). Writes obey the same owner-only editing rule as
379
+ the rest of the alert on workflow alerts that restrict editing to owners.
380
+
381
+ These endpoints need the `alerts:read` / `alerts:write` API-key permissions
382
+ (`Alerts Read` / `Alerts Write` in Settings → Developer).
144
383
 
145
384
  ### `events`
146
385
 
@@ -148,6 +387,100 @@ new DossiersOSAPIService({ baseUrl, apiKey, fetch? })
148
387
  | --- | --- |
149
388
  | `ingest(input)` | `POST /v1/events` |
150
389
 
390
+ Every Event is about exactly one resource, named as
391
+ `resource: { type, id }`. `type` is one of `CUSTOMER`, `ACCOUNT`,
392
+ `COUNTERPARTY`, `EMPLOYEE`, `BRANCH`, `PAYMENT_INSTRUMENT` (`EventResourceType`
393
+ — there is no `INSTRUMENT` spelling). The resource must already exist in the
394
+ organization.
395
+
396
+ A bare top-level `customerId` is the deprecated legacy spelling of
397
+ `resource: { type: 'CUSTOMER', id }` and is still accepted; `resource` wins when
398
+ both are sent, and alongside a non-customer `resource` the `customerId` is
399
+ context read by customer-scoped event aggregates. An event naming neither is
400
+ rejected with 400. `deviceId` / `sessionId` qualify an event but never
401
+ substitute for its primary resource.
402
+
403
+ `IngestEventResponse` carries `eventId`, `id`, `type`, the resolved `resource`,
404
+ and `riskSignalIds` — the Risk Signals emitted by the event definition's
405
+ `riskSignalEffects`. Event and signals are written in one transaction, and the
406
+ event's idempotency covers them: a replayed `eventId` echoes the original ids
407
+ (`cached: true`) instead of emitting a second set.
408
+
409
+ ### `riskSignals`
410
+
411
+ | Method | Endpoint |
412
+ | --- | --- |
413
+ | `create(input)` | `POST /v1/risk-signals` |
414
+ | `list(query)` | `GET /v1/risk-signals` |
415
+
416
+ A Risk Signal is time-decaying evidence attached to one resource — evidence,
417
+ **not** an Alert. Creating one raises no alert, notification, assignment or
418
+ task, and does not change the resource's Risk Rating.
419
+
420
+ `create(input)` (`CreateRiskSignalInput`) requires `type`, `resourceType`
421
+ (`RiskSignalResourceType`, the same six values), `resourceId`, `weight` (> 0)
422
+ and `halfLife` (a positive ISO 8601 duration; calendar units Y/M are rejected).
423
+ Optional: `externalId` (idempotency key — a resubmission returns the original
424
+ signal), `occurredAt` (defaults to now), `note`. The server always sets
425
+ `sourceType: 'MANUAL'` and captures the authenticated actor; a caller cannot
426
+ claim a signal came from an Event or a Rule.
427
+
428
+ `list(query)` (`ListRiskSignalsQuery`) requires `resourceType` + `resourceId`
429
+ and accepts `asOf`, `type[]`, `sourceType[]`, `limit` (1–200, default 50) and
430
+ `offset`. It returns `RiskSignalListResponse`: `edges` / `pageInfo` /
431
+ `totalCount` plus `asOf`, `aggregateEffectiveWeight`, `contributingCount`,
432
+ `latestAt` and `uniqueTypeCount`.
433
+
434
+ Decay is `weight * 2 ^ (-ageSeconds / halfLifeSeconds)`; `weight`, `halfLife`
435
+ and `halfLifeSeconds` on a stored signal are an immutable snapshot, while
436
+ `effectiveWeight` is what they have decayed to at `asOf`. The aggregate is
437
+ computed on read, never cached on the resource, so a historical `asOf` returns
438
+ what the resource looked like then. `type` / `sourceType` choose which records
439
+ contribute; they never change how far a contributing signal has decayed.
440
+
441
+ ### `employees`
442
+
443
+ | Method | Endpoint |
444
+ | --- | --- |
445
+ | `list(query?)` | `GET /v1/employees` |
446
+ | `get(id)` | `GET /v1/employees/{id}` |
447
+ | `create(input)` | `POST /v1/employees` |
448
+ | `update(id, input)` | `PATCH /v1/employees/{id}` |
449
+
450
+ People acting inside the financial institution's ecosystem, and an attachable
451
+ resource for Events and Risk Signals. Deliberately distinct from a Dossiers
452
+ organization member: an Employee need not have a Dossiers login, and an
453
+ Employee id is never an org-member id — `orgMemberId` is an optional link, not
454
+ an identity.
455
+
456
+ `create(input)` requires `fullName`; optional `externalEmployeeId` (unique per
457
+ organization, rejected with 400 on a duplicate), `email`, `jobTitle`,
458
+ `department`, `branchId` (must be in the same organization), `status`
459
+ (`ACTIVE` | `INACTIVE` | `SUSPENDED` | `TERMINATED`, default `ACTIVE`) and
460
+ `orgMemberId`. `update` additionally accepts `metadata`.
461
+
462
+ `list` returns `EmployeeListResponse` — `data` plus the connection shape
463
+ (`edges`, `pageInfo`, `meta`). The endpoint declares `cursor` / `limit`, but
464
+ the server currently resolves its own paging defaults (first page, 50 records).
465
+
466
+ ### `branches`
467
+
468
+ | Method | Endpoint |
469
+ | --- | --- |
470
+ | `list(query?)` | `GET /v1/branches` |
471
+ | `get(id)` | `GET /v1/branches/{id}` |
472
+ | `create(input)` | `POST /v1/branches` |
473
+ | `update(id, input)` | `PATCH /v1/branches/{id}` |
474
+
475
+ The institution's own physical or organizational units, through which Employees
476
+ act; also an attachable resource for Events and Risk Signals.
477
+
478
+ `create(input)` requires `name`; optional `externalBranchId` (unique per
479
+ organization), `code`, `country`, `city`, `institutionId` and `status`
480
+ (`ACTIVE` | `INACTIVE` | `CLOSED`, default `ACTIVE`). `update` additionally
481
+ accepts `metadata`. `list` returns `BranchListResponse` with the same shape and
482
+ the same paging caveat as `employees`.
483
+
151
484
  ### `exchangeRates`
152
485
 
153
486
  | Method | Endpoint |
@@ -162,6 +495,42 @@ new DossiersOSAPIService({ baseUrl, apiKey, fetch? })
162
495
 
163
496
  `config.rules`, `config.constants`, `config.products`, `config.eventDefinitions`, `config.webhooks` — each exposes `list(query?)`, `create(input)`, `getBySyncKey(syncKey)`, `get(id)`, `update(id, input)`, `delete(id)` on `/v1/config/{resource}`.
164
497
 
498
+ `config.eventDefinitions` create/update accept `riskSignalEffects:
499
+ RiskSignalEffect[]` — the Risk Signals emitted on every valid occurrence of the
500
+ event. Each effect is `{ type, weight, halfLife, target? }`, where `target`
501
+ defaults to `EVENT_RESOURCE` (the event's own primary resource). Effects are
502
+ tenant configuration: an integration pushing an event cannot add, change, or
503
+ suppress one. `weight` and `halfLife` are snapshotted onto each emitted signal,
504
+ so editing a definition never rewrites evidence already recorded.
505
+
506
+ `config.rules` actions accept `ADD_RISK_SIGNAL` with `signalType`, `weight`
507
+ (> 0), `halfLife` (positive ISO 8601 duration) and
508
+ `target: { resource: 'customer' | 'account' | 'counterparty' | 'paymentInstrument' }`.
509
+ `target.resource` is a closed selector over the evaluation, never a field
510
+ expression. `employee` and `branch` are **not** valid — no evaluation envelope
511
+ resolves either slot yet, so such an action could never fire, and both are
512
+ rejected at save time with a 400 saying so. Unlike other action types,
513
+ `ADD_RISK_SIGNAL` may appear several times on one rule; what is rejected is the
514
+ same `signalType` on the same `target.resource` twice, which would double-count
515
+ in every aggregate.
516
+
517
+ `config.customAlertTypes` — org-defined alert types for `alerts.create`.
518
+
519
+ | Method | Endpoint |
520
+ | --- | --- |
521
+ | `list({ lifecycle? })` | `GET /v1/config/custom-alert-types` |
522
+ | `create(input)` | `POST /v1/config/custom-alert-types` |
523
+ | `getByName(name)` | `GET /v1/config/custom-alert-types/by-name/{name}` |
524
+ | `get(id)` | `GET /v1/config/custom-alert-types/{id}` |
525
+ | `update(id, input)` | `PATCH /v1/config/custom-alert-types/{id}` |
526
+ | `archive(id)` / `unarchive(id)` | `POST /v1/config/custom-alert-types/{id}/archive` / `/unarchive` |
527
+ | `delete(id)` | `DELETE /v1/config/custom-alert-types/{id}` (tombstone; archive first) |
528
+
529
+ `name` is snake_case and immutable; `fields` is a `TypedFieldDefinition[]`
530
+ schema. Editing `fields` never re-validates existing alerts — new required
531
+ fields apply to alerts raised afterwards. Archive is refused (409) while any
532
+ alert of the type is OPEN.
533
+
165
534
  `config.currencies`
166
535
 
167
536
  | Method | Endpoint |
@@ -183,7 +552,123 @@ new DossiersOSAPIService({ baseUrl, apiKey, fetch? })
183
552
 
184
553
  | Method | Endpoint |
185
554
  | --- | --- |
186
- | `getSyncExport()` | `GET /v1/settings/sync-export` |
555
+ | `listSyncSnapshots()` | `GET /v1/settings/sync-snapshots` |
556
+ | `getSyncDrift()` | `GET /v1/settings/sync-snapshots/drift` |
557
+ | `createSyncSnapshot(input?)` | `POST /v1/settings/sync-snapshots` |
558
+ | `downloadSyncSnapshot(id)` | `GET /v1/settings/sync-snapshots/{id}/download` |
559
+
560
+ For a typed live configuration export, use `config.sync.export()`.
561
+ Its `SyncConfig.behaviorSettings` singleton carries attempt retention and attempt
562
+ coverage (`ALL` or `SELECTIVE`). The section is optional/unmanaged when absent;
563
+ properties are partial.
564
+
565
+ ### `customLists`
566
+
567
+ Custom screening lists — tenant-managed lists checked by every screening run
568
+ alongside external providers. Archived lists are read-only and never screened;
569
+ disabled lists are skipped but stay editable. List tags (catalogue-governed)
570
+ are shown against every match the list produces.
571
+
572
+ | Method | Endpoint |
573
+ | --- | --- |
574
+ | `list(params?)` | `GET /v1/screening/custom-lists` |
575
+ | `create(input)` | `POST /v1/screening/custom-lists` |
576
+ | `get(listId)` | `GET /v1/screening/custom-lists/{listId}` |
577
+ | `update(listId, input)` | `PATCH /v1/screening/custom-lists/{listId}` |
578
+ | `archive(listId)` | `POST /v1/screening/custom-lists/{listId}/archive` |
579
+ | `unarchive(listId)` | `POST /v1/screening/custom-lists/{listId}/unarchive` |
580
+ | `listEntries(listId, params?)` | `GET /v1/screening/custom-lists/{listId}/entries` |
581
+ | `createEntry(listId, input)` | `POST /v1/screening/custom-lists/{listId}/entries` |
582
+ | `bulkCreateEntries(listId, rows)` | `POST /v1/screening/custom-lists/{listId}/entries/bulk` (≤500 rows/request; per-row rejection; `externalRef` upsert) |
583
+ | `updateEntry(listId, entryId, input)` | `PATCH /v1/screening/custom-lists/{listId}/entries/{entryId}` |
584
+ | `deleteEntry(listId, entryId)` | `DELETE /v1/screening/custom-lists/{listId}/entries/{entryId}` |
585
+
586
+ Entry fields: `name` (required), `entryType` (`INDIVIDUAL` \| `ENTITY`, default
587
+ `INDIVIDUAL` — entries only match subjects of the same type), `aliases`,
588
+ `dateOfBirth` (`YYYY-MM-DD`), `country` (ISO 3166-1 **alpha-3**), `idNumber`,
589
+ `notes`, `externalRef`.
590
+
591
+ ### `reports`
592
+
593
+ On-demand PDF reports. Every method resolves to the raw PDF bytes as a `Uint8Array`; write them to disk (`fs.writeFileSync`) or stream them to your own download response. Gated on `report:read`. Each call records a `report.generated` activity on the subject record.
594
+
595
+ | Method | Endpoint |
596
+ | --- | --- |
597
+ | `reports.alert(alertId)` | `GET /v1/reports/alerts/{alertId}/pdf` |
598
+ | `reports.customerProfile(customerId)` | `GET /v1/reports/customers/{customerId}/pdf` |
599
+ | `reports.counterpartyProfile(counterpartyId)` | `GET /v1/reports/counterparties/{counterpartyId}/pdf` |
600
+ | `reports.resourceProfile(resourceType, resourceId)` | `GET /v1/reports/resources/{resourceType}/{resourceId}/pdf` — `ACCOUNT`, `DEVICE`, `INSTITUTION`, `PRODUCT`, `TASK`, … (`ALERT` / `CUSTOMER` / `COUNTERPARTY` → 400, use the dedicated methods) |
601
+
602
+ ## Typed Fields types
603
+
604
+ Available since `v0.2.0`. Spec: [`docs/specs/typed-fields/README.md`](../../../docs/specs/typed-fields/README.md).
605
+
606
+ ```ts
607
+ type TypedFieldType =
608
+ | 'TEXT' | 'NUMBER' | 'BOOLEAN' | 'DATE'
609
+ | 'LIST' | 'SINGLE_SELECT' | 'MULTI_SELECT'
610
+ | 'CURRENCY' | 'MONETARY'
611
+ | 'SET'; // deprecated
612
+
613
+ interface TypedFieldDefinition {
614
+ key: string;
615
+ displayName: string;
616
+ type: TypedFieldType;
617
+ options?: string[] | null; // required for select types
618
+ required: boolean;
619
+ defaultValue?: unknown;
620
+ description?: string | null;
621
+ }
622
+
623
+ // Write input — `{key, type, value}`. `type` tells the validator how to
624
+ // interpret `value`:
625
+ // TEXT / DATE / CURRENCY → string
626
+ // NUMBER → finite JS number
627
+ // BOOLEAN → boolean
628
+ // LIST / MULTI_SELECT / legacy SET → string[]
629
+ // SINGLE_SELECT → string from the definition's options
630
+ // MONETARY → denominated literal `"<amount>@<TYPE:CODE>"`
631
+ interface TypedFieldValueInput {
632
+ key: string;
633
+ type: TypedFieldType;
634
+ value: unknown;
635
+ }
636
+
637
+ interface UntypedKeyValueInput {
638
+ key: string;
639
+ value: unknown;
640
+ }
641
+
642
+ // Read shape — discriminated by `type`.
643
+ type TypedFieldStored =
644
+ | { key: string; type: 'text'; value: string }
645
+ | { key: string; type: 'number'; value: number }
646
+ | { key: string; type: 'boolean'; value: boolean }
647
+ | { key: string; type: 'date'; value: string }
648
+ | { key: string; type: 'list'; value: string[] }
649
+ | { key: string; type: 'single_select'; value: string }
650
+ | { key: string; type: 'multi_select'; value: string[] }
651
+ | { key: string; type: 'set'; value: string[] }
652
+ | { key: string; type: 'currency'; value: string }
653
+ | { key: string; type: 'monetary'; amountMinor: number; currency: string; precision: number };
654
+
655
+ interface UntypedKeyValueDto { key: string; value: unknown }
656
+ ```
657
+
658
+ DTOs that carry these:
659
+
660
+ | DTO | Field | Tier |
661
+ | --- | --- | --- |
662
+ | `CreateAccountDto` / `AccountResponseDto` | `fields`, `otherProperties` | A/B + C |
663
+ | `CreateProductDto` / `UpdateProductDto` | `accountFieldConfig` (`typedAccountFieldConfig` deprecated alias), `fields`, `otherProperties` | schema + B + C |
664
+ | `CreateEventDefinitionDto` / `UpdateEventDefinitionDto` | `properties` (`TypedFieldDefinition[]`) | schema |
665
+ | `CreateCustomAlertTypeDto` / `UpdateCustomAlertTypeDto` | `fields` (`TypedFieldDefinition[]`) | schema |
666
+ | `CreateCustomAlertDto` / `AlertResponseDto` | `fields` | A |
667
+ | `CounterpartyResponseDto` | `otherProperties` | C |
668
+ | Event ingestion (`POST /v1/events`) | `fields` | B |
669
+
670
+ Update semantics on `fields` / `otherProperties` is **merge** — each supplied
671
+ key overwrites; absent keys preserved.
187
672
 
188
673
  ## Errors
189
674
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dossier-wiki/os-sdk",
3
- "version": "0.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "TypeScript SDK for the Dossiers OS API — customers, accounts, transactions, rules, screening, and configuration.",
5
5
  "license": "MIT",
6
6
  "type": "module",