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/COMMERCIAL-LICENSE.md +73 -0
- package/LICENSE +687 -0
- package/README.md +323 -0
- package/dist/index.cjs +3516 -0
- package/dist/index.d.cts +10276 -0
- package/dist/index.d.ts +10276 -0
- package/dist/index.js +3344 -0
- package/package.json +78 -0
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).
|