salesdock 0.1.1

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/README.md ADDED
@@ -0,0 +1,323 @@
1
+ # salesdock
2
+
3
+ A universal, fully-typed **TypeScript SDK for the [Salesdock](https://app.salesdock.nl) API**.
4
+
5
+ - **Every endpoint** of the Salesdock API, as typed resource methods (sales, leads, forms, products, relations, commissions, tasks, dialer, users/organisations, webhooks).
6
+ - **Runs everywhere** — built on the Web-standard `fetch`, with **zero Node.js built-ins**. Works on **Cloudflare Workers**, Deno, Bun, the browser and Node.js 18+.
7
+ - **Runtime validation with [Zod](https://zod.dev)** — request inputs are validated _before_ they are sent; responses are validated on the way in (and tolerant of new fields).
8
+ - **Dual ESM + CommonJS** with bundled type declarations.
9
+ - Built-in **pagination** (offset + cursor), **automatic retries** with backoff for `429`/`5xx`, **timeouts**, and **typed errors**.
10
+ - One runtime dependency (`zod`).
11
+
12
+ ```bash
13
+ npm install salesdock
14
+ ```
15
+
16
+ ---
17
+
18
+ ## Quick start
19
+
20
+ ```ts
21
+ import { Salesdock } from "salesdock";
22
+
23
+ const sd = new Salesdock({
24
+ domain: "testomgeving", // your Salesdock account sub-domain
25
+ token: process.env.SALESDOCK_TOKEN!, // Bearer API token from your account admin
26
+ });
27
+
28
+ // Fetch products
29
+ const products = await sd.products.list({ business: 1 });
30
+
31
+ // Read a sale
32
+ const sale = await sd.sales.get(440);
33
+
34
+ // Create a sale on a flow
35
+ const { sale_id } = await sd.sales.createDefault("my-flow-type", "my-flow-id", {
36
+ transaction_type: "offer",
37
+ product_id: "1",
38
+ firstname: "Jane",
39
+ lastname: "Doe",
40
+ email: "jane@example.com",
41
+ questionData: { betaalwijze: "Automatische incasso" },
42
+ agreements: { "privacy-statement": "1" },
43
+ });
44
+
45
+ // Paginate leads (auto-follows next_page_url)
46
+ for await (const lead of await sd.leads.list()) {
47
+ console.log(lead.id);
48
+ }
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Configuration
54
+
55
+ ```ts
56
+ const sd = new Salesdock({
57
+ domain: "testomgeving",
58
+ token: "…",
59
+
60
+ // Optional:
61
+ scope: "user", // "user" | "account" — default scope for scope-flexible endpoints
62
+ version: "v1", // API version segment (default "v1")
63
+ baseUrl: Salesdock.STAGING_BASE_URL, // default Salesdock.PRODUCTION_BASE_URL
64
+ fetch: customFetch, // custom fetch implementation (default: global fetch)
65
+ validateRequests: true, // validate inputs before sending (default true)
66
+ validateResponses: true, // validate responses (default true)
67
+ maxRetries: 2, // retries for 429/5xx/network errors (default 2)
68
+ retryDelayMs: 500, // base backoff in ms (default 500)
69
+ timeout: 60000, // per-request timeout in ms, 0 disables (default 60000)
70
+ headers: { "X-My-Header": "1" }, // extra headers on every request
71
+ onRequest: (req) => {}, // hook before each request
72
+ onResponse: (res) => {}, // hook after each response
73
+ });
74
+ ```
75
+
76
+ ### The API path & scope
77
+
78
+ Every Salesdock URL has the shape:
79
+
80
+ ```
81
+ https://app.salesdock.nl/api/{domain}/{version}/{scope}/{resource}
82
+ ```
83
+
84
+ - **`domain`** — your account sub-domain (e.g. `testomgeving`), set once in the config.
85
+ - **`scope`** — `user` (your own data) or `account` (account-wide). Many endpoints accept either; the SDK uses your configured `scope`, overridable per-call:
86
+
87
+ ```ts
88
+ await sd.products.list({}, { scope: "account" });
89
+ ```
90
+
91
+ Endpoints that are fixed to one scope (e.g. webhooks are always `account`) ignore this and are handled automatically.
92
+
93
+ - **Environments** — production is the default. For staging:
94
+
95
+ ```ts
96
+ new Salesdock({ domain, token, baseUrl: Salesdock.STAGING_BASE_URL });
97
+ ```
98
+
99
+ ### Authentication
100
+
101
+ The SDK sends `Authorization: Bearer <token>` on every request. Tokens are created by your account admin and may be IP-restricted.
102
+
103
+ ---
104
+
105
+ ## What you can do
106
+
107
+ Each Salesdock module is a property on the client.
108
+
109
+ | Property | Module | Highlights |
110
+ | ----------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
111
+ | `sd.sales` | Sales | `list`, `get`, `getContract`, `getAnswers`, `listByExportTemplate`, `createDefault`, `createDefaultMultiple`, `updateDefaultProposal`, `updateDefaultFinalized`, `listStatuses`, `updateStatus`, `updateStatusByIncoming`, `updateFields`, `updateFieldsByIncoming`, `cancel`, `getOfLead`, `getProducts`, `getOfFormInstance`, `getDocuments`, `getHistory`, `createOptin`, `listFlows`, `getFlow`, `listProductQuestions`, `getProductQuestion`, `listAgreements`, `getAgreement`, `createSale` (raw) |
112
+ | `sd.sales.energy` | Energy | `nl.create`, `nl.listProducts`, `nl.getProduct`, `nl.updateProduct`, `be.create`, `be.listProducts`, `listSales`, `estimateUsage` |
113
+ | `sd.sales.telecom`| Telecom | `create`, `updateProposal`, `updateFinalized`, `compare` |
114
+ | `sd.sales.hostedVoice` | Hosted Voice | `create` |
115
+ | `sd.sales.solar` | Solar Panels | `create`, `estimatePanels`, `compare` |
116
+ | `sd.sales.heatPumps` | Heat Pumps | `create`, `updateProposal`, `updateFinalized` |
117
+ | `sd.dialer` | Dialer | `createConceptTransaction`, `createConceptTransactionWithOptin`, `getConceptTransactionFields`, `getConceptTransactionFieldsWithOptin`, `createConceptLead`, `getConceptLeadFields`, `getLastSale`, `getSales`, `getSaleViewUrl` |
118
+ | `sd.users` | Users | `list`, `invite`, `users.organisations.*` |
119
+ | `sd.users.organisations` | Organisations | `list`, `get`, `create`, `update`, `delete`, `listProducts` |
120
+ | `sd.commissions` | Commissions | `outgoing.list`, `outgoing.forSale`, `incoming.list`, `incoming.forSale` |
121
+ | `sd.leads` | Leads | `list`, `get`, `listActivities`, `listSources`, `createAsAdmin`, `updateAsAdmin`, `createAsReseller`, `updateAsReseller`, `getFormPdf`, `getHistory`, `getResults`, `listResults`, `updateStatus`, `listForms`, `getFormElements`, `createFormInstance`, `listStatuses`, `townships.list` |
122
+ | `sd.forms` | Forms | `list`, `getElements`, `listInstances`, `getInstance`, `fillInstance`, `listInstancesBySale`, `getInstancePdf`, `listStatuses` |
123
+ | `sd.relations` | Relations | `list`, `get`, `create`, `update` |
124
+ | `sd.products` | Products | `list`, `get`, `create`, `update`, `delete`, `connectOrganisationToProducts`, `connectProductToOrganisations`, `suppliers.*` |
125
+ | `sd.products.suppliers` | Suppliers | `list`, `get`, `create`, `update`, `delete` |
126
+ | `sd.tasks` | Tasks | `list`, `get`, `create`, `update`, `delete` |
127
+ | `sd.webhooks` | Webhooks | `listEvents`, `subscribe`, `listSubscriptions`, `unsubscribe` |
128
+
129
+ Every method returns the unwrapped `data` of the Salesdock envelope, typed. Every method accepts a trailing `options` argument (`{ scope?, signal?, headers?, timeout? }`).
130
+
131
+ ---
132
+
133
+ ## Pagination
134
+
135
+ List endpoints that paginate return a `Page` object you can iterate or step through.
136
+
137
+ ```ts
138
+ // Iterate every item across all pages (auto-follows next_page_url)
139
+ for await (const relation of await sd.relations.list({ q: "jansen" })) {
140
+ console.log(relation.id);
141
+ }
142
+
143
+ // Or work page by page
144
+ const page = await sd.relations.list();
145
+ console.log(page.items, page.total, page.currentPage, page.hasNextPage);
146
+ const next = await page.nextPage(); // OffsetPage | null
147
+
148
+ // Collect everything into one array
149
+ const all = await (await sd.relations.list()).all();
150
+ ```
151
+
152
+ Cursor-paginated endpoints (e.g. `sd.leads.list()`, `sd.tasks.list()`) return a `CursorPage` with the same `items` / `nextPage()` / `all()` / async-iteration API (no total count).
153
+
154
+ Non-paginated list endpoints (e.g. `sd.products.list()`) simply return an array.
155
+
156
+ ---
157
+
158
+ ## Errors
159
+
160
+ All failures throw a subclass of `SalesdockError`:
161
+
162
+ | Class | When |
163
+ | ---------------------------------- | ------------------------------------------ |
164
+ | `SalesdockValidationError` | HTTP `400` / `422` (with `.errors` map) |
165
+ | `SalesdockAuthenticationError` | HTTP `401` |
166
+ | `SalesdockForbiddenError` | HTTP `403` |
167
+ | `SalesdockNotFoundError` | HTTP `404` |
168
+ | `SalesdockMethodNotAllowedError` | HTTP `405` |
169
+ | `SalesdockRateLimitError` | HTTP `429` (throttling) |
170
+ | `SalesdockServerError` | HTTP `5xx` |
171
+ | `SalesdockConnectionError` | Network failure |
172
+ | `SalesdockTimeoutError` | Request timed out |
173
+ | `SalesdockInvalidRequestError` | Your input failed local validation |
174
+ | `SalesdockResponseValidationError` | Response did not match the expected schema |
175
+
176
+ ```ts
177
+ import { SalesdockValidationError, SalesdockRateLimitError } from "salesdock";
178
+
179
+ try {
180
+ await sd.leads.createAsAdmin({ firstname: "Jane" /* … */ });
181
+ } catch (err) {
182
+ if (err instanceof SalesdockValidationError) {
183
+ console.error(err.errors); // { email: ["Het veld email is verplicht."], … }
184
+ } else if (err instanceof SalesdockRateLimitError) {
185
+ console.error("Throttled, retry after", err.retryAfterMs, "ms");
186
+ }
187
+ }
188
+ ```
189
+
190
+ > Local **input** validation (`SalesdockInvalidRequestError`) is thrown **synchronously** as you call the method (fail-fast on bad arguments). Using `try { await sd.x() } catch` handles both this and async errors.
191
+
192
+ ---
193
+
194
+ ## Retries & rate limiting
195
+
196
+ Salesdock throttles at **120 requests/minute** per IP+user. The SDK retries with exponential backoff (honoring the `Retry-After` header), but **never silently duplicates a write**:
197
+
198
+ - **HTTP 429 (throttling)** is retried for **any** method — the request was rejected before processing, so it's always safe.
199
+ - **5xx and network errors** are retried only for **idempotent** requests (`GET` by default). A failed `POST`/`PUT`/`PATCH`/`DELETE` is surfaced immediately rather than re-sent, so a transient 503 after the server already committed a write can't create a duplicate sale.
200
+ - **Timeouts and caller aborts** are never retried (a timeout is your deadline; an abort takes effect at once).
201
+
202
+ To opt a specific mutating call into retries (e.g. an idempotent endpoint, or one you guard with your own dedupe key), pass `idempotent: true`:
203
+
204
+ ```ts
205
+ await sd.sales.createDefault(flowType, flowId, body, { idempotent: true });
206
+ ```
207
+
208
+ Tune globally via `maxRetries` / `retryDelayMs`, or disable with `maxRetries: 0`.
209
+
210
+ ---
211
+
212
+ ## Validation
213
+
214
+ `salesdock` validates with Zod on both sides:
215
+
216
+ - **Requests** — your inputs are checked before sending, surfacing mistakes locally instead of as a server `422`.
217
+ - **Responses** — validated as they arrive, but with **passthrough** semantics: unknown or newly-added fields flow through untouched, so the SDK doesn't break when Salesdock adds fields.
218
+
219
+ Disable either independently:
220
+
221
+ ```ts
222
+ new Salesdock({ domain, token, validateResponses: false }); // skip response checks
223
+ new Salesdock({ domain, token, validateRequests: false }); // skip input checks
224
+ ```
225
+
226
+ All schemas are exported (e.g. `RelationSchema`, `SaleSchema`, `EnergyNlSaleInputSchema`) if you want to validate elsewhere.
227
+
228
+ ---
229
+
230
+ ## Cloudflare Workers
231
+
232
+ No configuration needed — the global `fetch` is used automatically.
233
+
234
+ ```ts
235
+ import { Salesdock } from "salesdock";
236
+
237
+ export interface Env {
238
+ SALESDOCK_DOMAIN: string;
239
+ SALESDOCK_TOKEN: string;
240
+ }
241
+
242
+ export default {
243
+ async fetch(request: Request, env: Env): Promise<Response> {
244
+ const sd = new Salesdock({
245
+ domain: env.SALESDOCK_DOMAIN,
246
+ token: env.SALESDOCK_TOKEN,
247
+ });
248
+
249
+ const sales = await sd.sales.list({ period: "last_30_days" });
250
+ return Response.json(sales.items);
251
+ },
252
+ } satisfies ExportedHandler<Env>;
253
+ ```
254
+
255
+ Deno, Bun and modern browsers work the same way. On a runtime without a global `fetch`, pass one explicitly via `fetch`.
256
+
257
+ ---
258
+
259
+ ## Working with PDFs
260
+
261
+ Endpoints that return documents (sale contract, lead/form PDFs) deliver the file as a **base64 string**. Decode it to bytes with the included Web-standard helper:
262
+
263
+ ```ts
264
+ import { decodeBase64 } from "salesdock";
265
+
266
+ const { contract } = await sd.sales.getContract(saleId);
267
+ if (contract?.content) {
268
+ const bytes = decodeBase64(contract.content);
269
+ // e.g. in a Worker:
270
+ return new Response(bytes, { headers: { "content-type": "application/pdf" } });
271
+ }
272
+ ```
273
+
274
+ ---
275
+
276
+ ## Escape hatch
277
+
278
+ For anything not covered by a typed method (or a custom query/body), use the raw request method. `segments` are the path parts after `/api/{domain}/{version}/` and include the scope yourself:
279
+
280
+ ```ts
281
+ const data = await sd.request<MyType>("GET", ["account", "commissions", "outgoing"], {
282
+ query: { page: 2 },
283
+ });
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Webhooks
289
+
290
+ Manage subscriptions via the API; Salesdock then POSTs events to your `target_url`.
291
+
292
+ ```ts
293
+ const events = await sd.webhooks.listEvents(); // ["offer.accepted", "lead.created", …]
294
+ await sd.webhooks.subscribe({ event: "offer.accepted", target_url: "https://example.com/hook" });
295
+ const subs = await sd.webhooks.listSubscriptions();
296
+ await sd.webhooks.unsubscribe(subs[0].uuid);
297
+ ```
298
+
299
+ ---
300
+
301
+ ## Development
302
+
303
+ ```bash
304
+ npm install
305
+ npm run typecheck # type-check the source
306
+ npm run typecheck:test # type-check source + tests
307
+ npm test # run the vitest suite
308
+ npm run build # build dual ESM + CJS + d.ts into dist/
309
+ npm run format # prettier
310
+ ```
311
+
312
+ ## License
313
+
314
+ `salesdock` is **dual-licensed**:
315
+
316
+ - **Open source — [GNU AGPL-3.0](LICENSE).** Free to use, modify and distribute, provided you meet the AGPL's terms. Note §13: if you run a modified version as a network service, you must offer its complete source to your users. Best for open-source projects and evaluation.
317
+ - **Commercial.** For use in **proprietary / closed-source** software or hosted services **without** the AGPL's source-disclosure obligations, a commercial license is available. See **[COMMERCIAL-LICENSE.md](COMMERCIAL-LICENSE.md)** or contact [Risker](https://risker.nl).
318
+
319
+ `salesdock` is an independent, unofficial client library and is not affiliated with or endorsed by Salesdock.
320
+
321
+ ---
322
+
323
+ Built and maintained by [Risker](https://risker.nl).