@medalsocial/sdk 1.7.0 → 1.9.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 CHANGED
@@ -272,6 +272,69 @@ if (summary.can_reschedule) {
272
272
 
273
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
274
 
275
+ #### Persons, relations and events
276
+
277
+ `medal.bookings.persons` are the children, pets, or employees a contact books for — no login of their own. `medal.bookings.relations` links two contacts directionally (guardian, employer, partner, and so on). `medal.bookings.events` are arrangementer — scheduled group sessions bookings register against; registering a booking *to* an event ships in a later release, so this is a read/create surface today.
278
+
279
+ ```ts
280
+ // Persons a contact books for (active-only unless include_inactive)
281
+ const { data: persons } = await medal.bookings.persons.list(contact.id);
282
+
283
+ // Add a person under a contact
284
+ const { data: person } = await medal.bookings.persons.create({
285
+ contact_id: contact.id,
286
+ name: 'Ola',
287
+ birth_year: 2018,
288
+ relation_type: 'guardian',
289
+ });
290
+
291
+ // Events in a date range (yyyy-mm-dd, inclusive) — one month
292
+ const { data: events } = await medal.bookings.events.list({
293
+ from: '2026-09-01',
294
+ to: '2026-09-30',
295
+ });
296
+ ```
297
+
298
+ ### Customer portal
299
+
300
+ 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`.
301
+
302
+ 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`.
303
+
304
+ ```ts
305
+ // 1. Send the code. Always { status: 'sent' } — enumeration-safe, so "sent" does
306
+ // not confirm the address belongs to a contact.
307
+ await medal.portal.login.start({ email: 'ida@example.com', locale: 'nb' });
308
+
309
+ // 2. Exchange the code the customer typed. Wrong, burned and expired codes all
310
+ // answer 401 PORTAL_CODE_INVALID.
311
+ const { data: session } = await medal.portal.login.verify({ email: 'ida@example.com', code: '123456' });
312
+ // -> set an HttpOnly, Secure, SameSite cookie holding session.session_token,
313
+ // expiring at session.expires_at (Unix ms)
314
+
315
+ // 3. Session-bound calls, from your server, with the token read back from the cookie
316
+ const { data: me } = await medal.portal.me(session.session_token);
317
+ // me.persons (the contact's bookings.persons) and me.labels (the workspace's own
318
+ // words for the person concept, e.g. { person: 'Barn', persons: 'Barn' }) are new
319
+ const { data: bookings } = await medal.portal.myBookings(session.session_token);
320
+ // bookings.upcoming[i].manage_token is set while the booking is still manageable —
321
+ // it opens your site's manage page (medal.bookings.manage.*); past bookings carry null
322
+ await medal.portal.updateMe(session.session_token, {
323
+ phone: '+4790000000',
324
+ family: [{ name: 'Ola', birth_year: 2018 }], // replaces the whole list
325
+ marketing_consent: true, // recorded as a marketing_email consent, source 'portal'
326
+ });
327
+ const { data: exported } = await medal.portal.exportMyData(session.session_token); // GDPR Art. 15, synchronous
328
+ await medal.portal.logout(session.session_token); // 204 — revokes this session only
329
+ // …or, when the customer asks to be forgotten (terminal — the session is revoked, a later
330
+ // logout() would answer 401 PORTAL_SESSION_INVALID):
331
+ await medal.portal.deleteMe(session.session_token); // GDPR Art. 17 — 204
332
+ ```
333
+
334
+ `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`.
335
+
336
+ 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.
337
+
275
338
  ### GDPR
276
339
 
277
340
  ```ts