@medalsocial/sdk 1.6.0 → 1.8.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/README.md +106 -0
- package/dist/openapi/medal-social.openapi.json +4783 -2489
- package/dist/src/index.d.mts +822 -25
- package/dist/src/index.d.ts +822 -25
- package/dist/src/index.js +607 -156
- package/dist/src/index.js.map +1 -1
- package/dist/src/index.mjs +605 -156
- package/dist/src/index.mjs.map +1 -1
- package/dist/src/openapi.generated.d.mts +1892 -559
- package/dist/src/openapi.generated.d.ts +1892 -559
- package/dist/src/openapi.generated.js.map +1 -1
- package/openapi/medal-social.openapi.yaml +1402 -12
- package/package.json +18 -17
- package/skills/client/SKILL.md +4 -2
- package/skills/resources/SKILL.md +152 -3
package/README.md
CHANGED
|
@@ -204,6 +204,112 @@ const { data: removed } = await medal.deals.remove(deal.id);
|
|
|
204
204
|
console.log(updated.success, unlinked.success, removed.success);
|
|
205
205
|
```
|
|
206
206
|
|
|
207
|
+
### Bookings
|
|
208
|
+
|
|
209
|
+
Money is always **integer øre** (`amount_ore`, `price_ore`) — never a float, never kroner. Timestamps come back as ISO 8601 strings; on the way in, either Unix milliseconds or an ISO string is accepted.
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
// Catalogue + free slots
|
|
213
|
+
const { data: services } = await medal.bookings.listServices();
|
|
214
|
+
const { data: resources } = await medal.bookings.listResources();
|
|
215
|
+
const { data: slots } = await medal.bookings.availability({
|
|
216
|
+
service_id: services[0].id,
|
|
217
|
+
from_ts: Date.now(),
|
|
218
|
+
to_ts: Date.now() + 7 * 86_400_000,
|
|
219
|
+
resource_id: resources[0].id, // optional — defaults to every capable resource
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Book a party — all items succeed or none do (max 50)
|
|
223
|
+
const { data: created } = await medal.bookings.create(
|
|
224
|
+
{
|
|
225
|
+
items: [
|
|
226
|
+
{ service_id: services[0].id, start_ts: slots[0].start_ts! },
|
|
227
|
+
{ service_id: services[0].id, start_ts: slots[1].start_ts!, booked_for_name: 'Ida', booked_for_birth_year: 2018 },
|
|
228
|
+
],
|
|
229
|
+
contact: { phone: '+4790000000', email: 'ida@example.com', name: 'Ida Hansen' },
|
|
230
|
+
notes: 'Bursdag',
|
|
231
|
+
},
|
|
232
|
+
{ idempotencyKey: crypto.randomUUID() }, // optional — see the note below
|
|
233
|
+
);
|
|
234
|
+
// created.bookings[i].manage_token is returned EXACTLY ONCE (only its hash is
|
|
235
|
+
// stored, and an idempotent replay omits it) — persist it for the manage link.
|
|
236
|
+
// It is UNRECOVERABLE if lost: `Booking` has no token field, so re-reading the
|
|
237
|
+
// booking gives you nothing. Reschedule to mint a fresh one, or act by id.
|
|
238
|
+
|
|
239
|
+
// Staff actions — policy windows are bypassed, cancels attributed to staff
|
|
240
|
+
const { data: booking } = await medal.bookings.get(created.bookings[0].id);
|
|
241
|
+
await medal.bookings.update(booking.id, { internal_notes: 'Allergisk mot parfyme' });
|
|
242
|
+
await medal.bookings.cancel(booking.id, { reason: 'Sykdom' });
|
|
243
|
+
const { data: moved } = await medal.bookings.reschedule(booking.id, {
|
|
244
|
+
new_start_ts: '2026-09-02T09:00:00.000Z',
|
|
245
|
+
new_resource_id: resources[0].id,
|
|
246
|
+
});
|
|
247
|
+
// moved.booking_id is a NEW id with a NEW manage_token — the old booking is cancelled
|
|
248
|
+
await medal.bookings.markNoShow(moved.booking_id);
|
|
249
|
+
|
|
250
|
+
// Listing — check `truncated`: when true, matching bookings exist that no
|
|
251
|
+
// cursor reaches, so narrow the from_ts/to_ts window
|
|
252
|
+
const page = await medal.bookings.list({ status: 'confirmed', from_ts: Date.now(), limit: 50 });
|
|
253
|
+
console.log(page.pagination.has_more, page.pagination.next_cursor, page.pagination.truncated);
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Customer actions go through `medal.bookings.manage`**, keyed by the manage token instead of the booking id. This is not the same route with a different lookup key: the workspace's cancel/reschedule windows are **enforced**, and the cancel is attributed to the customer. Use it to relay a customer's own click on the link in their confirmation email.
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
const { data: summary } = await medal.bookings.manage.get(manageToken);
|
|
260
|
+
if (summary.can_cancel) await medal.bookings.manage.cancel(manageToken, { reason: 'Endret plan' });
|
|
261
|
+
if (summary.can_reschedule) {
|
|
262
|
+
const { data } = await medal.bookings.manage.reschedule(manageToken, {
|
|
263
|
+
new_start_ts: '2026-09-02T09:00:00.000Z',
|
|
264
|
+
});
|
|
265
|
+
console.log(data.booking_id, data.manage_token); // old token stops working
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
`can_cancel` / `can_reschedule` already apply the policy windows — honour them rather than re-deriving from `cancel_window_hours`.
|
|
270
|
+
|
|
271
|
+
**Booking writes are idempotent by default.** The SDK retries 429/5xx automatically, so every booking `POST` (`create`, `cancel`, `reschedule`, `markNoShow`, and both `manage` writes) carries a generated `Idempotency-Key` — a retry after a gateway failure replays the original result instead of booking the slot twice. Supply your own `options.idempotencyKey` to extend that guarantee across *your* retries too: the server remembers a key for 24 hours, keyed by `(key, workspace, method + path)`.
|
|
272
|
+
|
|
273
|
+
`update(id, input)` requires at least one of `notes` / `internal_notes`; `update(id, {})` is a compile error, matching the API's own 400.
|
|
274
|
+
|
|
275
|
+
### Customer portal
|
|
276
|
+
|
|
277
|
+
Self-service for the workspace's own customers: they sign in with an e-mailed one-time code, then see and change their profile, list their bookings, export their data, or erase their account. The API key needs `read:portal` and `write:portal`.
|
|
278
|
+
|
|
279
|
+
The session token is a **bearer credential for one contact**. Your site's server exchanges the code for it and keeps it in an **HttpOnly cookie on the site's own domain** — never hand it to the browser, and never let the browser call Medal directly. Session-bound methods take the token as their first argument and send it as `X-Portal-Session`.
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
// 1. Send the code. Always { status: 'sent' } — enumeration-safe, so "sent" does
|
|
283
|
+
// not confirm the address belongs to a contact.
|
|
284
|
+
await medal.portal.login.start({ email: 'ida@example.com', locale: 'nb' });
|
|
285
|
+
|
|
286
|
+
// 2. Exchange the code the customer typed. Wrong, burned and expired codes all
|
|
287
|
+
// answer 401 PORTAL_CODE_INVALID.
|
|
288
|
+
const { data: session } = await medal.portal.login.verify({ email: 'ida@example.com', code: '123456' });
|
|
289
|
+
// -> set an HttpOnly, Secure, SameSite cookie holding session.session_token,
|
|
290
|
+
// expiring at session.expires_at (Unix ms)
|
|
291
|
+
|
|
292
|
+
// 3. Session-bound calls, from your server, with the token read back from the cookie
|
|
293
|
+
const { data: me } = await medal.portal.me(session.session_token);
|
|
294
|
+
const { data: bookings } = await medal.portal.myBookings(session.session_token);
|
|
295
|
+
// bookings.upcoming[i].manage_token is set while the booking is still manageable —
|
|
296
|
+
// it opens your site's manage page (medal.bookings.manage.*); past bookings carry null
|
|
297
|
+
await medal.portal.updateMe(session.session_token, {
|
|
298
|
+
phone: '+4790000000',
|
|
299
|
+
family: [{ name: 'Ola', birth_year: 2018 }], // replaces the whole list
|
|
300
|
+
marketing_consent: true, // recorded as a marketing_email consent, source 'portal'
|
|
301
|
+
});
|
|
302
|
+
const { data: exported } = await medal.portal.exportMyData(session.session_token); // GDPR Art. 15, synchronous
|
|
303
|
+
await medal.portal.logout(session.session_token); // 204 — revokes this session only
|
|
304
|
+
// …or, when the customer asks to be forgotten (terminal — the session is revoked, a later
|
|
305
|
+
// logout() would answer 401 PORTAL_SESSION_INVALID):
|
|
306
|
+
await medal.portal.deleteMe(session.session_token); // GDPR Art. 17 — 204
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
`401 PORTAL_SESSION_REQUIRED` (header missing) and `401 PORTAL_SESSION_INVALID` (unknown, expired or revoked) both mean "sign in again" — clear the cookie and send the customer back to step 1. `403 FORBIDDEN` means the key lacks the portal scopes; `429 RATE_LIMITED` applies per address and per caller on `login.start`.
|
|
310
|
+
|
|
311
|
+
None of the portal calls carries an `Idempotency-Key`: the two login routes cannot duplicate anything, and the session routes are either reads or terminal.
|
|
312
|
+
|
|
207
313
|
### GDPR
|
|
208
314
|
|
|
209
315
|
```ts
|