@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.
@@ -527,7 +527,7 @@ interface ClientConfig {
527
527
  timeout: number;
528
528
  userAgent: string;
529
529
  }
530
- /** Per-request options for write operations. */
530
+ /** Per-request options. `headers` applies to every verb; the named options only matter on writes. */
531
531
  interface RequestOptions {
532
532
  /**
533
533
  * Idempotency key sent as the `Idempotency-Key` header. Retries with the
@@ -560,6 +560,22 @@ interface RequestOptions {
560
560
  * Ignored on routes that do not require a capability confirmation.
561
561
  */
562
562
  autoConfirm?: AutoConfirmOptions | false;
563
+ /**
564
+ * Set to `false` to send the request exactly once — no automatic retry on
565
+ * 429/5xx. Use it for writes whose FIRST attempt may have succeeded even
566
+ * though the response was lost: a one-time code that is burned on use, a
567
+ * logout or erasure that revokes the very credential a retry would present.
568
+ * A retry there does not repeat the operation, it misreports it as failed.
569
+ * Defaults to `true`.
570
+ */
571
+ retry?: boolean;
572
+ /**
573
+ * Extra request headers, e.g. `x-portal-session` for the customer portal.
574
+ * Applied first: the named options (`idempotencyKey`,
575
+ * `capabilityConfirmation`) win over a same-named entry here, so a bag can
576
+ * never smuggle in a key the SDK did not resolve.
577
+ */
578
+ headers?: Record<string, string>;
563
579
  }
564
580
  /**
565
581
  * Low-level HTTP client used by all resource classes.
@@ -570,9 +586,27 @@ declare class BaseClient {
570
586
  readonly config: ClientConfig;
571
587
  constructor(config: ClientConfig);
572
588
  /** Execute an authenticated GET request and return the parsed JSON body. */
573
- get<T>(path: string, params?: Record<string, string | undefined>): Promise<T>;
589
+ get<T>(path: string, params?: Record<string, string | undefined>, options?: Pick<RequestOptions, "headers">): Promise<T>;
574
590
  /** Execute an authenticated POST request with a JSON body. */
575
591
  post<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
592
+ /**
593
+ * Execute a POST that must never execute twice, guaranteeing an
594
+ * `Idempotency-Key`.
595
+ *
596
+ * {@link BaseClient.post} retries 429 and 5xx automatically, so a write
597
+ * whose transaction committed before the gateway failed would otherwise be
598
+ * submitted a second time — booking the same slot twice. A key turns that
599
+ * retry into a replay: the server keys on the key, the workspace, and the
600
+ * method+path, and answers a repeat with the stored response, or 409 while
601
+ * the first attempt is still in flight. Either way the write happens once.
602
+ *
603
+ * The key is minted ONCE here, outside the retry loop in `request`, so every
604
+ * attempt of the same logical call carries the same value — a key minted per
605
+ * attempt would deduplicate nothing. A caller-supplied key always wins, so
606
+ * callers keeping their own records stay in control. See
607
+ * {@link resolveIdempotencyKey} for what counts as supplied.
608
+ */
609
+ postOnce<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
576
610
  /** Execute an authenticated PATCH request with a JSON body. */
577
611
  patch<T>(path: string, body: unknown, options?: RequestOptions): Promise<T>;
578
612
  /** Execute an authenticated DELETE request. */
@@ -582,6 +616,425 @@ declare class BaseClient {
582
616
  private request;
583
617
  }
584
618
 
619
+ /**
620
+ * A timestamp on the way IN to the booking API: Unix milliseconds, or an
621
+ * ISO 8601 date-time string.
622
+ *
623
+ * Both are accepted so a caller can book by echoing back the `start_ts` of a
624
+ * slot it just fetched — responses render every timestamp as ISO, requests
625
+ * take either. The API normalises to milliseconds server-side.
626
+ */
627
+ type BookingTimestampInput = number | string;
628
+ /** Lifecycle state of a booking. */
629
+ type BookingStatus = "pending" | "confirmed" | "completed" | "cancelled" | "no_show";
630
+ /**
631
+ * Who cancelled a booking. `staff` is a cancel made through
632
+ * `bookings.cancel(id)`, `customer` one made through
633
+ * `bookings.manage.cancel(token)`, `system` an automated one.
634
+ */
635
+ type BookingCancelledBy = "customer" | "staff" | "system";
636
+ /** Payment state of a booking. */
637
+ type BookingPaymentStatus = "none" | "reserved" | "captured" | "refunded";
638
+ /** Surface a booking was created through. API-created bookings are `api`. */
639
+ type BookingCreatedVia = "web" | "dashboard" | "walk_in" | "api";
640
+ /** Kind of bookable resource a booking lands on. */
641
+ type BookingResourceType = "staff" | "room" | "equipment";
642
+ /**
643
+ * One appointment: a contact holding a service slot on a resource.
644
+ *
645
+ * Money is `amount_ore` — an INTEGER number of øre, never a float and never
646
+ * kroner. Timestamps are ISO 8601 strings.
647
+ */
648
+ interface Booking {
649
+ id: string;
650
+ contact_id: string | null;
651
+ service_id: string | null;
652
+ resource_id: string | null;
653
+ start_ts: string | null;
654
+ end_ts: string | null;
655
+ booked_for_name: string | null;
656
+ /** Birth year (not a birthdate) of whoever the appointment is for. */
657
+ booked_for_birth_year: number | null;
658
+ /** Shared by every booking created in the same party request. */
659
+ party_sequence_id: string | null;
660
+ status: BookingStatus;
661
+ cancelled_by: BookingCancelledBy | null;
662
+ cancel_reason: string | null;
663
+ /** Set on the booking a reschedule created, pointing at the one it replaced. */
664
+ rescheduled_from_id: string | null;
665
+ payment_status: BookingPaymentStatus;
666
+ /** Price in integer øre. */
667
+ amount_ore: number | null;
668
+ /** Customer-visible note. */
669
+ notes: string | null;
670
+ /** Staff-only note; never shown to the customer. */
671
+ internal_notes: string | null;
672
+ created_via: BookingCreatedVia | null;
673
+ created_at: string | null;
674
+ updated_at: string | null;
675
+ }
676
+ /** A bookable service in the workspace catalogue. */
677
+ interface BookingService {
678
+ id: string;
679
+ name: string | null;
680
+ description: string | null;
681
+ category: string | null;
682
+ duration_minutes: number | null;
683
+ buffer_before_minutes: number | null;
684
+ buffer_after_minutes: number | null;
685
+ /** Price in integer øre. */
686
+ price_ore: number | null;
687
+ weekend_surcharge_pct: number | null;
688
+ /** Resource types this service needs, e.g. `["staff"]`. */
689
+ resource_requirements: string[];
690
+ bookable_online: boolean;
691
+ max_per_booking: number | null;
692
+ color: string | null;
693
+ sort_order: number | null;
694
+ active: boolean;
695
+ created_at: string | null;
696
+ updated_at: string | null;
697
+ }
698
+ /** Who or what performs a service: a staff member, a room, or equipment. */
699
+ interface BookingResource {
700
+ id: string;
701
+ type: BookingResourceType | null;
702
+ name: string | null;
703
+ photo_url: string | null;
704
+ bio: string | null;
705
+ /** Services this resource can perform. */
706
+ service_ids: string[];
707
+ capacity: number | null;
708
+ sort_order: number | null;
709
+ active: boolean;
710
+ created_at: string | null;
711
+ updated_at: string | null;
712
+ }
713
+ /** One free slot returned by `bookings.availability(...)`. */
714
+ interface BookingSlot {
715
+ start_ts: string | null;
716
+ end_ts: string | null;
717
+ resource_id: string | null;
718
+ }
719
+ /**
720
+ * One date a service can be booked on, from `bookings.schedule(...)`.
721
+ *
722
+ * `date` is the workspace's own calendar date (`YYYY-MM-DD`), not a timestamp.
723
+ * `last_start_ts` is the last start this service could occupy on that date —
724
+ * not the closing time; a 30-minute service at a salon closing 17:00 has its
725
+ * last start at 16:30 — and is `null` for a date with posted hours that is
726
+ * shut outright (a public holiday). A date ABSENT from the list is closed.
727
+ */
728
+ interface BookingScheduleDay {
729
+ date: string | null;
730
+ opens_ts: string | null;
731
+ closes_ts: string | null;
732
+ last_start_ts: string | null;
733
+ }
734
+ /** One line of a party booking — a single service on a single slot. */
735
+ interface CreateBookingItemInput {
736
+ service_id: string;
737
+ /** Leave unset to let the engine pick a free resource. */
738
+ resource_id?: string;
739
+ start_ts: BookingTimestampInput;
740
+ booked_for_name?: string;
741
+ /** Birth year (not a birthdate) of whoever the appointment is for. */
742
+ booked_for_birth_year?: number;
743
+ }
744
+ /** The person the booking is made under. Phone is the CRM dedupe key. */
745
+ interface BookingContactInput {
746
+ phone: string;
747
+ email?: string;
748
+ name?: string;
749
+ }
750
+ /**
751
+ * Input for `bookings.create(...)`. `items` is a PARTY — one request books a
752
+ * whole family in one all-or-nothing transaction (max 50 items).
753
+ */
754
+ /** Provenance an API caller may claim. `dashboard` and `walk_in` are staff-only and rejected. */
755
+ type BookingClaimableCreatedVia = "web" | "api";
756
+ interface CreateBookingInput {
757
+ items: CreateBookingItemInput[];
758
+ contact: BookingContactInput;
759
+ notes?: string;
760
+ /**
761
+ * Defaults to `api`. A workspace's OWN website should send `web`, so "how
762
+ * many bookings did the site bring in" is answerable; integrations leave it
763
+ * unset. `dashboard` and `walk_in` are refused with 400 — a bearer token
764
+ * proves which workspace is calling, not that a member typed it in.
765
+ */
766
+ created_via?: BookingClaimableCreatedVia;
767
+ }
768
+ /** One entry of `BookingCreateResult.bookings`, in request order. */
769
+ interface CreatedBooking {
770
+ id: string;
771
+ /**
772
+ * Show-once secret for the customer's manage link.
773
+ *
774
+ * Only its SHA-256 hash is stored, so this response is the ONLY place it
775
+ * ever appears — persist it here or it is gone. It is **absent** (the key is
776
+ * dropped, not nulled) when the response is replayed from an
777
+ * `Idempotency-Key`, since tokens are redacted from replays.
778
+ *
779
+ * A lost token cannot be recovered: {@link Booking} carries no token field,
780
+ * so re-reading the booking will not produce it. Either reschedule the
781
+ * booking (which mints a fresh token) or have staff act on it by id.
782
+ */
783
+ manage_token?: string;
784
+ }
785
+ /** Result returned after creating a booking or party. */
786
+ interface BookingCreateResult {
787
+ bookings: CreatedBooking[];
788
+ contact_id: string;
789
+ }
790
+ /** Result of a cancel or no-show. */
791
+ interface BookingActionResult {
792
+ success: true;
793
+ }
794
+ /**
795
+ * Result of a reschedule. A reschedule cancels the old booking and inserts a
796
+ * new one, so `booking_id` is a NEW id — the one you passed in is now cancelled.
797
+ */
798
+ interface BookingRescheduleResult {
799
+ success: true;
800
+ booking_id: string;
801
+ /**
802
+ * Freshly minted manage token for the new booking. Same show-once rule as
803
+ * {@link CreatedBooking.manage_token}: absent on an idempotent replay.
804
+ */
805
+ manage_token?: string;
806
+ }
807
+ /**
808
+ * What the holder of a manage token may see and do — the payload behind a
809
+ * customer's "manage my booking" link.
810
+ *
811
+ * `can_cancel` / `can_reschedule` already account for the workspace's policy
812
+ * windows, so honour them rather than re-deriving from the window hours.
813
+ */
814
+ interface ManageSummary {
815
+ booking_id: string;
816
+ contact_id: string | null;
817
+ status: BookingStatus | null;
818
+ cancelled_by: BookingCancelledBy | null;
819
+ cancel_reason: string | null;
820
+ rescheduled_from_id: string | null;
821
+ start_ts: string | null;
822
+ end_ts: string | null;
823
+ service_id: string | null;
824
+ service_name: string | null;
825
+ resource_id: string | null;
826
+ resource_name: string | null;
827
+ booked_for_name: string | null;
828
+ party_sequence_id: string | null;
829
+ /** Price in integer øre. */
830
+ amount_ore: number | null;
831
+ payment_status: BookingPaymentStatus | null;
832
+ /** IANA zone the booking's local times should be rendered in. */
833
+ time_zone: string | null;
834
+ cancel_window_hours: number | null;
835
+ reschedule_window_hours: number | null;
836
+ can_cancel: boolean;
837
+ can_reschedule: boolean;
838
+ }
839
+ /** The annotation fields a booking accepts. */
840
+ interface BookingNoteFields {
841
+ /** Customer-visible note. Pass `""` to clear it. */
842
+ notes?: string;
843
+ /** Staff-only note; never shown to the customer. Pass `""` to clear it. */
844
+ internal_notes?: string;
845
+ }
846
+ /**
847
+ * Input for annotating a booking.
848
+ *
849
+ * At least one of `notes` or `internal_notes` must be present: the API's
850
+ * `updateBookingSchema` refuses a body carrying neither with a 400, so the
851
+ * union turns `update(id, {})` into a compile error rather than a wasted round
852
+ * trip. Note that `""` is a meaningful value — it clears the field — which is
853
+ * why the constraint is on presence, not on emptiness.
854
+ */
855
+ type UpdateBookingInput = (BookingNoteFields & {
856
+ notes: string;
857
+ }) | (BookingNoteFields & {
858
+ internal_notes: string;
859
+ });
860
+ /** Optional reason recorded against a cancellation. */
861
+ interface CancelBookingInput {
862
+ reason?: string;
863
+ }
864
+ /** Input for moving a booking to a new slot, and optionally a new resource. */
865
+ interface RescheduleBookingInput {
866
+ new_start_ts: BookingTimestampInput;
867
+ new_resource_id?: string;
868
+ }
869
+ /**
870
+ * Pagination for a bookings page.
871
+ *
872
+ * `truncated` is the extra statement this list carries: the underlying read is
873
+ * capped, and when the cap binds there are matching bookings that no cursor
874
+ * from this call reaches. Narrow `from_ts`/`to_ts` when you see it.
875
+ */
876
+ interface BookingsPagination {
877
+ has_more: boolean;
878
+ next_cursor: string | null;
879
+ truncated: boolean;
880
+ }
881
+ /** A page of bookings. Carries `truncated` on top of the usual pagination. */
882
+ interface BookingsPage {
883
+ data: Booking[];
884
+ pagination: BookingsPagination;
885
+ }
886
+ /** Options for listing bookings with pagination and filters. */
887
+ interface ListBookingsOptions extends PaginationOptions {
888
+ from_ts?: BookingTimestampInput;
889
+ to_ts?: BookingTimestampInput;
890
+ status?: BookingStatus;
891
+ resource_id?: string;
892
+ }
893
+ /** Options for listing the service catalogue. The endpoint is not paginated. */
894
+ interface ListBookingServicesOptions {
895
+ /** Include services with `active: false`. Defaults to active-only. */
896
+ include_inactive?: boolean;
897
+ }
898
+ /**
899
+ * Options for `bookings.schedule(...)`. Same shape as availability, and for
900
+ * the same reason: the last bookable start depends on the service's duration
901
+ * and buffers, so a 30-minute cut and a 90-minute colour run out at different
902
+ * hours of the same afternoon.
903
+ */
904
+ interface BookingScheduleOptions {
905
+ service_id: string;
906
+ from_ts: BookingTimestampInput;
907
+ /** Must be after `from_ts`. */
908
+ to_ts: BookingTimestampInput;
909
+ /** Restrict to one resource's hours. */
910
+ resource_id?: string;
911
+ }
912
+ /** Options for querying free slots. The window is required and half-open. */
913
+ interface BookingAvailabilityOptions {
914
+ service_id: string;
915
+ from_ts: BookingTimestampInput;
916
+ /** Must be after `from_ts`. */
917
+ to_ts: BookingTimestampInput;
918
+ /** Restrict slots to one resource. Defaults to every capable resource. */
919
+ resource_id?: string;
920
+ }
921
+
922
+ /**
923
+ * Customer-side booking management, addressed by the show-once manage token
924
+ * from `bookings.create(...)` rather than by booking id.
925
+ *
926
+ * These are NOT the staff routes with a different lookup key: possession of
927
+ * the token is the customer's own authorization, so the workspace's cancel and
928
+ * reschedule windows are ENFORCED here (they are bypassed on
929
+ * `bookings.cancel` / `bookings.reschedule`), and a cancel is attributed to
930
+ * the customer rather than to staff. Relay a customer's click on their
931
+ * confirmation-email link through these; act as the business through the
932
+ * id-addressed methods.
933
+ */
934
+ declare class BookingsManage {
935
+ private client;
936
+ constructor(client: BaseClient);
937
+ /**
938
+ * Read what the holder of a manage token may see and do. Honour
939
+ * `can_cancel` / `can_reschedule` — they already apply the policy windows.
940
+ */
941
+ get(token: string): Promise<ApiResponse<ManageSummary>>;
942
+ /** Cancel on the customer's behalf. Rejected outside the cancel window. */
943
+ cancel(token: string, input?: CancelBookingInput, options?: RequestOptions): Promise<ApiResponse<BookingActionResult>>;
944
+ /**
945
+ * Move the booking on the customer's behalf. Rejected outside the reschedule
946
+ * window. Returns a NEW booking id and a new manage token — the old token
947
+ * stops working, so relay the new one into whatever link you send next.
948
+ */
949
+ reschedule(token: string, input: RescheduleBookingInput, options?: RequestOptions): Promise<ApiResponse<BookingRescheduleResult>>;
950
+ }
951
+ /**
952
+ * Appointment bookings: the service catalogue, free slots, and the bookings
953
+ * themselves.
954
+ *
955
+ * Every method here acts as the BUSINESS — policy windows are bypassed and a
956
+ * cancel is recorded against staff. To relay a customer's own action on their
957
+ * confirmation-email link, use {@link Bookings.manage} instead.
958
+ *
959
+ * Money is always integer øre (`amount_ore`, `price_ore`). Timestamps come
960
+ * back as ISO 8601 strings; on the way in, either Unix milliseconds or an ISO
961
+ * string is accepted.
962
+ *
963
+ * @example
964
+ * ```ts
965
+ * const { data: slots } = await medal.bookings.availability({
966
+ * service_id: "svc_1",
967
+ * from_ts: Date.now(),
968
+ * to_ts: Date.now() + 7 * 86_400_000,
969
+ * });
970
+ * const { data } = await medal.bookings.create({
971
+ * items: [{ service_id: "svc_1", start_ts: slots[0].start_ts! }],
972
+ * contact: { phone: "+4790000000", name: "Ida" },
973
+ * });
974
+ * ```
975
+ */
976
+ declare class Bookings {
977
+ private client;
978
+ /** Customer-side actions addressed by manage token. */
979
+ readonly manage: BookingsManage;
980
+ constructor(client: BaseClient);
981
+ /** List the bookable service catalogue. Active-only unless asked otherwise. */
982
+ listServices(options?: ListBookingServicesOptions): Promise<ApiResponse<BookingService[]>>;
983
+ /** List the bookable resources — staff, rooms, and equipment. */
984
+ listResources(): Promise<ApiResponse<BookingResource[]>>;
985
+ /**
986
+ * List free slots for a service over a window. Slots reflect opening hours,
987
+ * time off, buffers, and existing bookings at the moment of the call — they
988
+ * are not held, so a slot can be taken before you book it.
989
+ */
990
+ availability(options: BookingAvailabilityOptions): Promise<ApiResponse<BookingSlot[]>>;
991
+ /**
992
+ * The dates a service can be booked on — the half `availability` cannot
993
+ * answer. Availability returns free slots and nothing else, so a closed day,
994
+ * an evening past closing and a fully booked day are all the same empty
995
+ * array. A date absent from this list is closed; on a listed date, compare
996
+ * `last_start_ts` against the clock to tell "too late today" from "full".
997
+ */
998
+ schedule(options: BookingScheduleOptions): Promise<ApiResponse<BookingScheduleDay[]>>;
999
+ /**
1000
+ * List bookings with cursor-based pagination and optional filters.
1001
+ *
1002
+ * Check `pagination.truncated`: when true the read window was clipped and
1003
+ * matching bookings exist that no cursor reaches — narrow `from_ts`/`to_ts`.
1004
+ */
1005
+ list(options?: ListBookingsOptions): Promise<BookingsPage>;
1006
+ /**
1007
+ * Book a party — every item succeeds or none do (max 50).
1008
+ *
1009
+ * Each created booking comes back with a `manage_token` exactly once; only
1010
+ * its hash is stored, so persist it if you need the customer's manage link.
1011
+ *
1012
+ * Automatically idempotent: the SDK mints an `Idempotency-Key` so its own
1013
+ * 5xx retries replay rather than book the slot twice. Supply
1014
+ * `options.idempotencyKey` to deduplicate across your OWN retries too — the
1015
+ * server keys on it for 24 hours, so re-sending the same key after a network
1016
+ * timeout returns the original bookings instead of a second set.
1017
+ */
1018
+ create(input: CreateBookingInput, options?: RequestOptions): Promise<ApiResponse<BookingCreateResult>>;
1019
+ /** Get a booking by ID. */
1020
+ get(id: string): Promise<ApiResponse<Booking>>;
1021
+ /**
1022
+ * Annotate a booking. At least one of `notes` (customer-visible) or
1023
+ * `internal_notes` (staff-only) is required; `""` clears a field.
1024
+ */
1025
+ update(id: string, input: UpdateBookingInput, options?: RequestOptions): Promise<ApiResponse<Booking>>;
1026
+ /** Cancel as the business — the cancel window is bypassed. */
1027
+ cancel(id: string, input?: CancelBookingInput, options?: RequestOptions): Promise<ApiResponse<BookingActionResult>>;
1028
+ /**
1029
+ * Move a booking as the business — the reschedule window is bypassed.
1030
+ * Returns a NEW booking id and a new manage token; the old booking is
1031
+ * cancelled and its token stops working.
1032
+ */
1033
+ reschedule(id: string, input: RescheduleBookingInput, options?: RequestOptions): Promise<ApiResponse<BookingRescheduleResult>>;
1034
+ /** Mark a booking as a no-show. */
1035
+ markNoShow(id: string, options?: RequestOptions): Promise<ApiResponse<BookingActionResult>>;
1036
+ }
1037
+
585
1038
  /**
586
1039
  * Mint short-lived capability confirmation tokens.
587
1040
  *
@@ -621,6 +1074,13 @@ declare class CapabilityConfirmations {
621
1074
  * Setting `user_approved: true` asserts that a human on your side approved
622
1075
  * this specific action. `preview_summary` is what they approved, and is
623
1076
  * retained for audit — write it for a human reader, not a log parser.
1077
+ *
1078
+ * Deliberately unkeyed, unlike the writes it authorizes. Minting is not the
1079
+ * state change the guarantee exists to protect: the write itself is already
1080
+ * bound to `idempotency_key`, so a retry that mints a second token cannot
1081
+ * produce a second write. Keying this call would instead park a credential
1082
+ * designed to expire in 15 minutes inside a replay cache that answers for 24
1083
+ * hours — a worse trade than the duplicate token it would avoid.
624
1084
  */
625
1085
  create(input: IssueCapabilityConfirmationInput): Promise<ApiResponse<CapabilityConfirmation>>;
626
1086
  }
@@ -665,6 +1125,13 @@ declare class ChannelConnectLinks {
665
1125
  *
666
1126
  * Requires the `channel.connect.manage` scope; OAuth callers additionally
667
1127
  * need the workspace `admin` role.
1128
+ *
1129
+ * Automatically idempotent: an unkeyed retry mints a SECOND live single-use
1130
+ * link for the same person, and only one of the two can ever be consumed —
1131
+ * the other stays outstanding until it is revoked or expires. The key the
1132
+ * confirmer chose is the key that goes out — a capability confirmation is
1133
+ * bound to its idempotency key, so minting a fresh one here would invalidate
1134
+ * the confirmation.
668
1135
  */
669
1136
  create(input: CreateConnectLinkInput, options?: RequestOptions): Promise<ApiResponse<ConnectLinkCreateResult>>;
670
1137
  /**
@@ -841,8 +1308,16 @@ declare class Contacts {
841
1308
  constructor(client: BaseClient);
842
1309
  /** List contacts with cursor-based pagination and optional filters. */
843
1310
  list(options?: ListContactsOptions): Promise<PaginatedResponse<Contact>>;
844
- /** Create a new contact. Email must be unique in the workspace. */
845
- create(input: CreateContactInput): Promise<ApiResponse<ContactCreateResult>>;
1311
+ /**
1312
+ * Create a new contact. Email must be unique in the workspace.
1313
+ *
1314
+ * Automatically idempotent: the SDK mints an `Idempotency-Key` so its own
1315
+ * 5xx retries replay rather than run the create a second time. Uniqueness
1316
+ * alone would not save you here — it turns the retry of a committed create
1317
+ * into a spurious conflict, which reads as "the contact was not created".
1318
+ * Supply `options.idempotencyKey` to deduplicate across your OWN retries too.
1319
+ */
1320
+ create(input: CreateContactInput, options?: RequestOptions): Promise<ApiResponse<ContactCreateResult>>;
846
1321
  /** Get a contact by ID. */
847
1322
  get(id: string): Promise<ApiResponse<Contact>>;
848
1323
  /** Update one or more fields on a contact. */
@@ -851,10 +1326,23 @@ declare class Contacts {
851
1326
  remove(id: string): Promise<ApiResponse<ContactRemoveResult>>;
852
1327
  /** Get the activity timeline for a contact. */
853
1328
  activities(id: string, options?: PaginationOptions): Promise<PaginatedResponse<Activity>>;
854
- /** Add a note to a contact's timeline. */
855
- addNote(id: string, input: AddNoteInput): Promise<ApiResponse<ContactNoteResult>>;
856
- /** Bulk import contacts (max 500). Duplicates are skipped. */
857
- import(contacts: ImportContactInput[]): Promise<ApiResponse<ImportContactsResult>>;
1329
+ /**
1330
+ * Add a note to a contact's timeline.
1331
+ *
1332
+ * Automatically idempotent: nothing about a note is unique, so an unkeyed
1333
+ * retry appends the same text to the timeline twice. Supply
1334
+ * `options.idempotencyKey` to deduplicate across your OWN retries too.
1335
+ */
1336
+ addNote(id: string, input: AddNoteInput, options?: RequestOptions): Promise<ApiResponse<ContactNoteResult>>;
1337
+ /**
1338
+ * Bulk import contacts (max 500). Duplicates are skipped.
1339
+ *
1340
+ * Automatically idempotent: the import is processed in chunks, so a retry
1341
+ * after a partial failure re-walks the whole batch and reports `added` /
1342
+ * `skipped` counts for a run that was not the first. Supply
1343
+ * `options.idempotencyKey` to deduplicate across your OWN retries too.
1344
+ */
1345
+ import(contacts: ImportContactInput[], options?: RequestOptions): Promise<ApiResponse<ImportContactsResult>>;
858
1346
  }
859
1347
 
860
1348
  /** A sponsorship or brand deal in the workspace. */
@@ -933,8 +1421,14 @@ declare class Deals {
933
1421
  constructor(client: BaseClient);
934
1422
  /** List deals with cursor-based pagination and optional filters. */
935
1423
  list(options?: ListDealsOptions): Promise<PaginatedResponse<Deal>>;
936
- /** Create a new deal. */
937
- create(input: CreateDealInput): Promise<ApiResponse<DealCreateResult>>;
1424
+ /**
1425
+ * Create a new deal.
1426
+ *
1427
+ * Automatically idempotent: nothing about a deal is unique, so an unkeyed
1428
+ * retry puts a second identical deal in the pipeline. Supply
1429
+ * `options.idempotencyKey` to deduplicate across your OWN retries too.
1430
+ */
1431
+ create(input: CreateDealInput, options?: RequestOptions): Promise<ApiResponse<DealCreateResult>>;
938
1432
  /** Get a deal by ID. */
939
1433
  get(id: string): Promise<ApiResponse<Deal>>;
940
1434
  /** Update one or more fields on a deal. Set contact_id to null to unlink. */
@@ -1076,15 +1570,29 @@ declare class Emails {
1076
1570
  /**
1077
1571
  * Send a transactional email using a template (HTTP 202). The returned `id`
1078
1572
  * is an email send id — poll `emails.get(id)` with it to track delivery.
1573
+ *
1574
+ * Automatically idempotent: the SDK mints an `Idempotency-Key` so its own
1575
+ * 5xx retries replay rather than queue a second copy into someone's inbox —
1576
+ * a send that already committed cannot be un-sent. Supply
1577
+ * `options.idempotencyKey` to deduplicate across your OWN retries too.
1578
+ *
1579
+ * `input.idempotency_key` is the older, body-level form of the same control
1580
+ * and still takes precedence server-side, so setting it keeps working
1581
+ * unchanged.
1079
1582
  */
1080
- send(input: SendEmailInput): Promise<ApiResponse<EmailSendResult>>;
1583
+ send(input: SendEmailInput, options?: RequestOptions): Promise<ApiResponse<EmailSendResult>>;
1081
1584
  /** Get the delivery status of a sent email. */
1082
1585
  get(id: string): Promise<ApiResponse<EmailSend>>;
1083
1586
  /**
1084
1587
  * Send the same template to multiple recipients (max 100, HTTP 202). Each
1085
1588
  * queued recipient gets its own send id in `results` for `emails.get(id)`.
1589
+ *
1590
+ * Automatically idempotent — and this is the call where it matters most: an
1591
+ * unkeyed retry of a batch that already committed sends up to 100 duplicate
1592
+ * emails. Supply `options.idempotencyKey` to deduplicate across your OWN
1593
+ * retries too.
1086
1594
  */
1087
- batch(input: BatchSendInput): Promise<ApiResponse<BatchSendSummary>>;
1595
+ batch(input: BatchSendInput, options?: RequestOptions): Promise<ApiResponse<BatchSendSummary>>;
1088
1596
  }
1089
1597
 
1090
1598
  /** A workspace data export request and its current status. */
@@ -1156,8 +1664,16 @@ interface CookieCategoryConsent {
1156
1664
  declare class Gdpr {
1157
1665
  private client;
1158
1666
  constructor(client: BaseClient);
1159
- /** Request a workspace data export. Runs asynchronously. */
1160
- requestExport(): Promise<ApiResponse<{
1667
+ /**
1668
+ * Request a workspace data export. Runs asynchronously.
1669
+ *
1670
+ * Automatically idempotent: the request is recorded and the export is
1671
+ * scheduled in one step with no de-duplication of its own, so an unkeyed
1672
+ * retry files a second subject-access request and runs a second full export
1673
+ * of the workspace. Supply `options.idempotencyKey` to deduplicate across
1674
+ * your OWN retries too.
1675
+ */
1676
+ requestExport(options?: RequestOptions): Promise<ApiResponse<{
1161
1677
  request_id: string;
1162
1678
  status: string;
1163
1679
  }>>;
@@ -1165,11 +1681,25 @@ declare class Gdpr {
1165
1681
  listExports(): Promise<ApiResponse<GdprExport[]>>;
1166
1682
  /** Get the status of a specific export. */
1167
1683
  getExport(id: string): Promise<ApiResponse<GdprExport>>;
1168
- /** Record a GDPR consent decision for a contact by email. */
1684
+ /**
1685
+ * Record a GDPR consent decision for a contact by email.
1686
+ *
1687
+ * Deliberately unkeyed: a decision is stored once per
1688
+ * (workspace, email, consent type) and overwritten in place, so re-sending
1689
+ * the same body reaches the same state and returns the same record id.
1690
+ */
1169
1691
  recordConsent(input: RecordConsentInput): Promise<ApiResponse<ConsentResult>>;
1170
1692
  /** Get all consent records for a contact by email. */
1171
1693
  getConsent(email: string): Promise<ApiResponse<ConsentRecord[]>>;
1172
- /** Record cookie consent from an external site (legacy endpoint). */
1694
+ /**
1695
+ * Record cookie consent from an external site (legacy endpoint).
1696
+ *
1697
+ * Deliberately unkeyed: this legacy route predates the versioned API and
1698
+ * does not run the `Idempotency-Key` machinery, so a key here would be a
1699
+ * header that changes nothing while implying a guarantee the endpoint cannot
1700
+ * make. Treat a failed call as "unknown" and re-send only if a missing
1701
+ * consent log matters more to you than a duplicate one.
1702
+ */
1173
1703
  cookieConsent(input: CookieConsentInput): Promise<{
1174
1704
  success: boolean;
1175
1705
  logId?: string;
@@ -1198,8 +1728,16 @@ declare class HelpdeskReplies {
1198
1728
  /**
1199
1729
  * Send an operator reply or internal note. Returns HTTP 201.
1200
1730
  *
1201
- * Pass an `idempotencyKey` so retried requests do not create duplicate
1202
- * messages it is REQUIRED for capability-scoped tokens.
1731
+ * Automatically idempotent: a reply is a message to a real person, and an
1732
+ * unkeyed retry sends it to them twice. The key the confirmer chose is the
1733
+ * key that goes out — a capability confirmation is bound to its idempotency
1734
+ * key, so minting a fresh one here would invalidate the confirmation.
1735
+ *
1736
+ * Pass `options.idempotencyKey` to deduplicate across your OWN retries too.
1737
+ * It is REQUIRED for capability-scoped tokens, which need it paired with a
1738
+ * `capabilityConfirmation` — a generated key satisfies the pairing's key
1739
+ * half only; the confirmation is still yours to supply (or to let
1740
+ * `autoConfirm` mint).
1203
1741
  */
1204
1742
  create(input: CreateReplyInput, options?: RequestOptions): Promise<ApiResponse<ReplyCreateResult>>;
1205
1743
  }
@@ -1210,6 +1748,210 @@ declare class Helpdesk {
1210
1748
  constructor(client: BaseClient, confirmer?: CapabilityConfirmer);
1211
1749
  }
1212
1750
 
1751
+ /** Input for starting an e-mail one-time-code login. */
1752
+ interface PortalLoginStartInput {
1753
+ /** The address the code is sent to. */
1754
+ email: string;
1755
+ /** Locale for the e-mail (e.g. `nb`, `en`); the workspace default when omitted. */
1756
+ locale?: string;
1757
+ }
1758
+ /**
1759
+ * Result of a login start. Always `"sent"`, whether or not the address is a
1760
+ * known contact — the route is enumeration-safe by design.
1761
+ */
1762
+ interface PortalLoginStartResult {
1763
+ status: "sent";
1764
+ }
1765
+ /** Input for exchanging an e-mailed code for a portal session. */
1766
+ interface PortalVerifyInput {
1767
+ /** The address the code was sent to. */
1768
+ email: string;
1769
+ /** The one-time code from the e-mail. */
1770
+ code: string;
1771
+ }
1772
+ /** The contact a portal session belongs to. */
1773
+ interface PortalContactSummary {
1774
+ contact_id: string;
1775
+ first_name: string | null;
1776
+ }
1777
+ /**
1778
+ * A portal session. `session_token` is a bearer credential for ONE contact —
1779
+ * keep it in an HttpOnly cookie on the site's server and never hand it to
1780
+ * the browser.
1781
+ */
1782
+ interface PortalSession {
1783
+ session_token: string;
1784
+ /** Unix timestamp in milliseconds. */
1785
+ expires_at: number;
1786
+ contact: PortalContactSummary;
1787
+ }
1788
+ /** A family member the contact books on behalf of. */
1789
+ interface PortalFamilyMember {
1790
+ name: string;
1791
+ birth_year: number;
1792
+ }
1793
+ /** The signed-in contact's own profile. */
1794
+ interface PortalProfile {
1795
+ contact_id: string;
1796
+ email: string;
1797
+ first_name: string | null;
1798
+ last_name: string | null;
1799
+ phone: string | null;
1800
+ family: PortalFamilyMember[];
1801
+ marketing_consent: boolean;
1802
+ /** Unix timestamp in milliseconds. */
1803
+ created_at: number;
1804
+ }
1805
+ /** Fields the signed-in contact may change on their own profile. */
1806
+ interface PortalProfilePatch {
1807
+ first_name?: string;
1808
+ last_name?: string;
1809
+ /** `null` clears the number. */
1810
+ phone?: string | null;
1811
+ /** Replaces the whole list. */
1812
+ family?: PortalFamilyMember[];
1813
+ /** Records a marketing_email consent change with source 'portal'. */
1814
+ marketing_consent?: boolean;
1815
+ }
1816
+ /** Lifecycle state of a booking as seen from the portal. */
1817
+ type PortalBookingStatus = BookingStatus;
1818
+ /** One of the signed-in contact's bookings. */
1819
+ interface PortalBooking {
1820
+ booking_id: string;
1821
+ status: PortalBookingStatus;
1822
+ /** Unix timestamp in milliseconds. */
1823
+ start_ts: number;
1824
+ /** Unix timestamp in milliseconds. */
1825
+ end_ts: number;
1826
+ service_id: string | null;
1827
+ service_name: string | null;
1828
+ resource_id: string | null;
1829
+ resource_name: string | null;
1830
+ booked_for_name: string | null;
1831
+ /** Integer øre, or `null` when the service has no price. */
1832
+ amount_ore: number | null;
1833
+ notes: string | null;
1834
+ /** Present only while the booking is upcoming and manageable; opens the site's manage page. */
1835
+ manage_token: string | null;
1836
+ can_manage: boolean;
1837
+ }
1838
+ /** The signed-in contact's bookings, split around now. */
1839
+ interface PortalBookings {
1840
+ upcoming: PortalBooking[];
1841
+ past: PortalBooking[];
1842
+ }
1843
+ /** A consent decision included in a portal data export. */
1844
+ interface PortalConsentRecord {
1845
+ consent_type: string;
1846
+ granted: boolean;
1847
+ /** Unix timestamp in milliseconds, or `null`. */
1848
+ granted_at: number | null;
1849
+ /** Unix timestamp in milliseconds, or `null`. */
1850
+ revoked_at: number | null;
1851
+ source: string;
1852
+ }
1853
+ /** Everything the workspace holds about the signed-in contact (GDPR Art. 15). */
1854
+ interface PortalExport {
1855
+ /** Unix timestamp in milliseconds. */
1856
+ exported_at: number;
1857
+ contact: PortalProfile;
1858
+ family: PortalFamilyMember[];
1859
+ consents: PortalConsentRecord[];
1860
+ bookings: PortalBooking[];
1861
+ }
1862
+
1863
+ /**
1864
+ * E-mail one-time-code login.
1865
+ *
1866
+ * Both routes are plain POSTs — deliberately not idempotency-keyed. A start
1867
+ * that is retried sends at most one more code, and a verify that is retried
1868
+ * meets a code that has already been burned and answers
1869
+ * `PORTAL_CODE_INVALID` — neither can duplicate anything.
1870
+ */
1871
+ declare class PortalLogin {
1872
+ private client;
1873
+ constructor(client: BaseClient);
1874
+ /**
1875
+ * E-mail a one-time code to the address.
1876
+ *
1877
+ * Always 202 `{ status: "sent" }` — enumeration-safe: `"sent"` does not
1878
+ * confirm that the address belongs to a contact. Rate-limited per address
1879
+ * and per caller (`429 RATE_LIMITED`).
1880
+ */
1881
+ start(input: PortalLoginStartInput): Promise<ApiResponse<PortalLoginStartResult>>;
1882
+ /**
1883
+ * Exchange the e-mailed code for a session.
1884
+ *
1885
+ * `session_token` is a bearer credential for ONE contact — keep it in an
1886
+ * HttpOnly cookie on the site's server. A wrong, burned or expired code all
1887
+ * answer `401 PORTAL_CODE_INVALID`; the three are not distinguished, so the
1888
+ * response is not an oracle for which codes exist.
1889
+ */
1890
+ verify(input: PortalVerifyInput): Promise<ApiResponse<PortalSession>>;
1891
+ }
1892
+ /**
1893
+ * Customer self-service portal. The session token is a bearer credential for a
1894
+ * single contact: the calling site server must keep it in an HttpOnly cookie
1895
+ * and never hand it to the browser. Session-bound methods send it as
1896
+ * `X-Portal-Session`; none of them carries an `Idempotency-Key`.
1897
+ *
1898
+ * The API key needs `read:portal` and `write:portal` (`403 FORBIDDEN`
1899
+ * otherwise). A missing header answers `401 PORTAL_SESSION_REQUIRED`; an
1900
+ * unknown, expired or revoked token answers `401 PORTAL_SESSION_INVALID` —
1901
+ * treat both as "sign in again".
1902
+ */
1903
+ declare class Portal {
1904
+ private client;
1905
+ /** E-mail one-time-code login: `start` sends the code, `verify` exchanges it. */
1906
+ readonly login: PortalLogin;
1907
+ constructor(client: BaseClient);
1908
+ /**
1909
+ * Revoke the session. Resolves to `undefined` (the route answers 204).
1910
+ *
1911
+ * Not keyed: revoking twice reaches the same state — the second call answers
1912
+ * `401 PORTAL_SESSION_INVALID`, which is the outcome you wanted anyway.
1913
+ */
1914
+ logout(session: string): Promise<void>;
1915
+ /** The signed-in contact's own profile. */
1916
+ me(session: string): Promise<ApiResponse<PortalProfile>>;
1917
+ /**
1918
+ * Update the signed-in contact's profile. Only the supplied fields change;
1919
+ * `phone: null` clears the number and `family` replaces the whole list.
1920
+ * `marketing_consent` records a `marketing_email` consent decision with
1921
+ * source `portal`. Returns the profile as it is after the change.
1922
+ */
1923
+ /**
1924
+ * A profile patch is not idempotency-keyed on the server and a `marketing_consent`
1925
+ * change records a dated consent event, so a retry after a committed-but-lost
1926
+ * response would repeat that event: sent exactly once, like the other writes.
1927
+ */
1928
+ updateMe(session: string, patch: PortalProfilePatch): Promise<ApiResponse<PortalProfile>>;
1929
+ /**
1930
+ * The contact's bookings split into `upcoming` and `past`. An upcoming
1931
+ * booking that is still inside the workspace's policy windows carries
1932
+ * `manage_token` and `can_manage: true`; use the token to open the site's
1933
+ * manage page (`medal.bookings.manage.*`).
1934
+ */
1935
+ myBookings(session: string): Promise<ApiResponse<PortalBookings>>;
1936
+ /**
1937
+ * Everything the workspace holds about the contact — profile, family,
1938
+ * consents and bookings — as one JSON document (GDPR Art. 15). Synchronous,
1939
+ * unlike `medal.gdpr.requestExport()`, which exports the whole workspace.
1940
+ *
1941
+ * Not keyed: a read-only snapshot, so a retried call costs nothing and
1942
+ * duplicates nothing.
1943
+ */
1944
+ exportMyData(session: string): Promise<ApiResponse<PortalExport>>;
1945
+ /**
1946
+ * Erase the contact (GDPR Art. 17). Resolves to `undefined` (the route
1947
+ * answers 204); the session is revoked as part of the deletion.
1948
+ *
1949
+ * Not keyed: deletion is terminal, so a retry meets a revoked session and
1950
+ * answers `401 PORTAL_SESSION_INVALID` rather than deleting anything else.
1951
+ */
1952
+ deleteMe(session: string): Promise<void>;
1953
+ }
1954
+
1213
1955
  /** A post in the workspace (list view). */
1214
1956
  interface Post {
1215
1957
  id: string;
@@ -1294,8 +2036,14 @@ declare class Posts {
1294
2036
  constructor(client: BaseClient);
1295
2037
  /** List posts with cursor-based pagination and optional filters. */
1296
2038
  list(options?: ListPostsOptions): Promise<PaginatedResponse<Post>>;
1297
- /** Create a new post with content and target channels. */
1298
- create(input: CreatePostInput): Promise<ApiResponse<{
2039
+ /**
2040
+ * Create a new post with content and target channels.
2041
+ *
2042
+ * Automatically idempotent: the SDK mints an `Idempotency-Key` so its own
2043
+ * 5xx retries replay rather than draft the post twice. Supply
2044
+ * `options.idempotencyKey` to deduplicate across your OWN retries too.
2045
+ */
2046
+ create(input: CreatePostInput, options?: RequestOptions): Promise<ApiResponse<{
1299
2047
  id: string;
1300
2048
  }>>;
1301
2049
  /** Get a post by ID, including its per-channel variants. */
@@ -1308,9 +2056,24 @@ declare class Posts {
1308
2056
  remove(id: string): Promise<ApiResponse<{
1309
2057
  success: boolean;
1310
2058
  }>>;
1311
- /** Schedule a post for future publication. */
2059
+ /**
2060
+ * Schedule a post for future publication.
2061
+ *
2062
+ * Deliberately unkeyed: re-sending the same `scheduled_at` for an
2063
+ * already-scheduled post returns the original `workflow_id` rather than
2064
+ * starting a second one, so a retried schedule cannot double-publish. A
2065
+ * *different* time is rejected — unschedule first.
2066
+ */
1312
2067
  schedule(id: string, input: SchedulePostInput): Promise<ApiResponse<ScheduleResult>>;
1313
- /** Publish a post immediately to all target channels. */
2068
+ /**
2069
+ * Publish a post immediately to all target channels.
2070
+ *
2071
+ * Deliberately unkeyed: publishing moves the post out of the set of statuses
2072
+ * that may be published, so the retry of a publish that already committed is
2073
+ * refused rather than posting a second time. It is refused with a 400 though
2074
+ * — treat an error here as "check the post's status", not as "nothing
2075
+ * happened".
2076
+ */
1314
2077
  publish(id: string): Promise<ApiResponse<PublishResult>>;
1315
2078
  /** List connected publishing channels for this workspace. */
1316
2079
  channels(): Promise<ApiResponse<Channel[]>>;
@@ -1457,10 +2220,15 @@ declare class Scan {
1457
2220
  * Queue a scan. Provide exactly one of `url`, `orgnr`, or `name`.
1458
2221
  * Runs asynchronously — poll with `get()` or use `waitForResult()`.
1459
2222
  *
2223
+ * Automatically idempotent: a scan job is queued the moment it is created,
2224
+ * so an unkeyed retry starts a second crawl of the same site and returns an
2225
+ * id for a job that duplicates one already running. Supply
2226
+ * `options.idempotencyKey` to deduplicate across your OWN retries too.
2227
+ *
1460
2228
  * @throws Error before any request when zero or several selectors are set —
1461
2229
  * the server would reject the body anyway; failing locally is clearer.
1462
2230
  */
1463
- create(input: ScanCreateInput): Promise<ApiResponse<ScanCreateResult>>;
2231
+ create(input: ScanCreateInput, options?: RequestOptions): Promise<ApiResponse<ScanCreateResult>>;
1464
2232
  /** Get a scan job's status and, once done, its findings payload. */
1465
2233
  get(id: string): Promise<ApiResponse<ScanJob>>;
1466
2234
  /** Search the Norwegian company registry by name (typeahead, top 5 hits). */
@@ -1491,6 +2259,13 @@ declare class Webhooks {
1491
2259
  * `secret` is typed optional because an idempotent replay (retrying with the
1492
2260
  * same `Idempotency-Key`, `X-Idempotent-Replayed: true`) returns the existing
1493
2261
  * endpoint WITHOUT the secret — handle that case (rotate if you lost it).
2262
+ *
2263
+ * Automatically idempotent: a duplicate endpoint is not a stray row, it is a
2264
+ * second copy of every future delivery to the same URL, forever. The key the
2265
+ * confirmer chose is the key that goes out — a capability confirmation is
2266
+ * bound to its idempotency key, so minting a fresh one here would invalidate
2267
+ * the confirmation. That the SDK now always sends a key is also what makes
2268
+ * the replay-without-secret case above reachable on a plain 5xx retry.
1494
2269
  */
1495
2270
  create(input: CreateWebhookInput, options?: RequestOptions): Promise<ApiResponse<WebhookEndpoint>>;
1496
2271
  /** Get a webhook endpoint by ID. */
@@ -1506,7 +2281,14 @@ declare class Webhooks {
1506
2281
  delete(id: string, options?: RequestOptions): Promise<ApiResponse<WebhookDeleteResult>>;
1507
2282
  /** List recent deliveries for an endpoint (most recent first). */
1508
2283
  deliveries(id: string, options?: ListDeliveriesOptions): Promise<ApiResponse<WebhookDelivery[]>>;
1509
- /** Queue a signed `test.ping` delivery to the endpoint. Returns HTTP 202. */
2284
+ /**
2285
+ * Queue a signed `test.ping` delivery to the endpoint. Returns HTTP 202.
2286
+ *
2287
+ * Deliberately unkeyed: a duplicate ping is the one duplicate that costs
2288
+ * nothing. Real deliveries are retried too, so any endpoint worth pointing at
2289
+ * already tolerates receiving the same event twice — that is what this call
2290
+ * exists to prove.
2291
+ */
1510
2292
  test(id: string): Promise<ApiResponse<WebhookTestResult>>;
1511
2293
  }
1512
2294
 
@@ -1813,6 +2595,19 @@ interface MedalOptions {
1813
2595
  * variables: { name: 'John' },
1814
2596
  * });
1815
2597
  *
2598
+ * // Bookings — free slots, then book a party (money is integer øre)
2599
+ * const { data: slots } = await medal.bookings.availability({
2600
+ * service_id: 'svc_1',
2601
+ * from_ts: Date.now(),
2602
+ * to_ts: Date.now() + 7 * 86_400_000,
2603
+ * });
2604
+ *
2605
+ * // Customer portal — e-mail code login, then session-bound self-service.
2606
+ * // Keep `session_token` in an HttpOnly cookie on your server.
2607
+ * await medal.portal.login.start({ email: 'ida@example.com' });
2608
+ * const { data: session } = await medal.portal.login.verify({ email: 'ida@example.com', code: '123456' });
2609
+ * const { data: mine } = await medal.portal.myBookings(session.session_token);
2610
+ *
1816
2611
  * // Contacts, Deals, GDPR, Workspaces
1817
2612
  * const contacts = await medal.contacts.list({ status: 'lead' });
1818
2613
  * const { data: deal } = await medal.deals.create({ title: 'Acme', value: 50000 });
@@ -1821,6 +2616,7 @@ interface MedalOptions {
1821
2616
  * ```
1822
2617
  */
1823
2618
  declare class Medal {
2619
+ readonly bookings: Bookings;
1824
2620
  readonly capabilityConfirmations: CapabilityConfirmations;
1825
2621
  readonly channels: Channels;
1826
2622
  readonly emails: Emails;
@@ -1828,6 +2624,7 @@ declare class Medal {
1828
2624
  readonly deals: Deals;
1829
2625
  readonly gdpr: Gdpr;
1830
2626
  readonly helpdesk: Helpdesk;
2627
+ readonly portal: Portal;
1831
2628
  readonly posts: Posts;
1832
2629
  readonly scan: Scan;
1833
2630
  readonly webhooks: Webhooks;
@@ -1838,4 +2635,4 @@ declare class Medal {
1838
2635
  /** Convenience factory — equivalent to `new Medal(apiKey, options)`. */
1839
2636
  declare function createMedalClient(apiKey: string, options?: MedalOptions): Medal;
1840
2637
 
1841
- export { type Activity, type AddNoteInput, type ApiResponse, type AutoConfirmContext, type AutoConfirmOptions, BaseClient, type BatchSendInput, type BatchSendResult, type BatchSendSummary, CAPABILITY_IDS, CAPABILITY_ROUTES, type CapabilityConfirmation, CapabilityConfirmations, CapabilityConfirmer, type CapabilityId, type CapabilityPathParamValue, type CapabilityRoute, type CapabilityWriteBodies, type CapabilityWriteRequest, type Channel, type ChannelConnectedEvent, type ChannelConnection, type ChannelConnectionDisconnectResult, type ChannelConnectionState, type ChannelDisconnectReason, type ChannelDisconnectedEvent, Channels, type ConnectLink, type ConnectLinkCreateResult, type ConnectLinkRevokeResult, type ConnectLinkStatus, type ConsentRecord, type ConsentResult, type ConsentType, type Contact, type ContactConsents, type ContactCreateResult, type ContactNoteResult, type ContactRemoveResult, type ContactStatus, type ContactUpdateResult, Contacts, type Conversation, type ConversationAssignedEvent, type ConversationCreatedEvent, type ConversationMessage, type ConversationStatus, type ConversationStatusChangedEvent, type ConversationUpdateResult, type CookieCategoryConsent, type CookieConsentInput, type CreateConnectLinkInput, type CreateContactInput, type CreateDealInput, type CreatePostInput, type CreateReplyInput, type CreateWebhookInput, DEFAULT_WEBHOOK_TOLERANCE_MS, type Deal, type DealCreateResult, type DealRemoveResult, type DealStatus, type DealUpdateResult, Deals, type EmailSend, type EmailSendResult, type EmailStatus, type EmailTemplate, type EmailTemplateDetail, Emails, Gdpr, type GdprExport, type GetTemplateOptions, Helpdesk, type HelpdeskMessageType, type ImportContactInput, type ImportContactsResult, type IssueCapabilityConfirmationInput, type ListConnectLinksOptions, type ListContactsOptions, type ListConversationsOptions, type ListDealsOptions, type ListDeliveriesOptions, type ListPostsOptions, Medal, MedalApiError, type MedalOptions, type MessageAuthorType, type MessageDeliveryStatus, type MessageDeliveryUpdatedEvent, type MessageReceivedEvent, type MessageSentEvent, type PaginatedResponse, type PaginationOptions, type Post, type PostDetail, type PostType, type PostVariant, Posts, type PublishResult, type RecordConsentInput, type ReplyCreateResult, type RequestOptions, Scan, type ScanCompany, type ScanCreateInput, type ScanCreateResult, type ScanJob, type ScanResultPayload, type ScanStatus, type ScanSubScores, type SchedulePostInput, type ScheduleResult, type SendEmailInput, type TestPingEvent, type UpdateContactInput, type UpdateConversationInput, type UpdateDealInput, type UpdatePostInput, type UpdateWebhookInput, type VerifyWebhookSignatureInput, type WaitForScanOptions, type WebhookChannelLifecycleData, type WebhookConversationSnapshot, type WebhookDeleteResult, type WebhookDelivery, type WebhookEndpoint, type WebhookEvent, type WebhookMessageSnapshot, type WebhookTestResult, WebhookVerificationError, type WebhookVerificationErrorCode, Webhooks, type Workspace, Workspaces, createMedalClient, Medal as default, verifyWebhookSignature };
2638
+ export { type Activity, type AddNoteInput, type ApiResponse, type AutoConfirmContext, type AutoConfirmOptions, BaseClient, type BatchSendInput, type BatchSendResult, type BatchSendSummary, type Booking, type BookingActionResult, type BookingAvailabilityOptions, type BookingCancelledBy, type BookingClaimableCreatedVia, type BookingContactInput, type BookingCreateResult, type BookingCreatedVia, type BookingPaymentStatus, type BookingRescheduleResult, type BookingResource, type BookingResourceType, type BookingScheduleDay, type BookingScheduleOptions, type BookingService, type BookingSlot, type BookingStatus, type BookingTimestampInput, Bookings, type BookingsPage, type BookingsPagination, CAPABILITY_IDS, CAPABILITY_ROUTES, type CancelBookingInput, type CapabilityConfirmation, CapabilityConfirmations, CapabilityConfirmer, type CapabilityId, type CapabilityPathParamValue, type CapabilityRoute, type CapabilityWriteBodies, type CapabilityWriteRequest, type Channel, type ChannelConnectedEvent, type ChannelConnection, type ChannelConnectionDisconnectResult, type ChannelConnectionState, type ChannelDisconnectReason, type ChannelDisconnectedEvent, Channels, type ConnectLink, type ConnectLinkCreateResult, type ConnectLinkRevokeResult, type ConnectLinkStatus, type ConsentRecord, type ConsentResult, type ConsentType, type Contact, type ContactConsents, type ContactCreateResult, type ContactNoteResult, type ContactRemoveResult, type ContactStatus, type ContactUpdateResult, Contacts, type Conversation, type ConversationAssignedEvent, type ConversationCreatedEvent, type ConversationMessage, type ConversationStatus, type ConversationStatusChangedEvent, type ConversationUpdateResult, type CookieCategoryConsent, type CookieConsentInput, type CreateBookingInput, type CreateBookingItemInput, type CreateConnectLinkInput, type CreateContactInput, type CreateDealInput, type CreatePostInput, type CreateReplyInput, type CreateWebhookInput, type CreatedBooking, DEFAULT_WEBHOOK_TOLERANCE_MS, type Deal, type DealCreateResult, type DealRemoveResult, type DealStatus, type DealUpdateResult, Deals, type EmailSend, type EmailSendResult, type EmailStatus, type EmailTemplate, type EmailTemplateDetail, Emails, Gdpr, type GdprExport, type GetTemplateOptions, Helpdesk, type HelpdeskMessageType, type ImportContactInput, type ImportContactsResult, type IssueCapabilityConfirmationInput, type ListBookingServicesOptions, type ListBookingsOptions, type ListConnectLinksOptions, type ListContactsOptions, type ListConversationsOptions, type ListDealsOptions, type ListDeliveriesOptions, type ListPostsOptions, type ManageSummary, Medal, MedalApiError, type MedalOptions, type MessageAuthorType, type MessageDeliveryStatus, type MessageDeliveryUpdatedEvent, type MessageReceivedEvent, type MessageSentEvent, type PaginatedResponse, type PaginationOptions, Portal, type PortalBooking, type PortalBookingStatus, type PortalBookings, type PortalConsentRecord, type PortalContactSummary, type PortalExport, type PortalFamilyMember, type PortalLoginStartInput, type PortalLoginStartResult, type PortalProfile, type PortalProfilePatch, type PortalSession, type PortalVerifyInput, type Post, type PostDetail, type PostType, type PostVariant, Posts, type PublishResult, type RecordConsentInput, type ReplyCreateResult, type RequestOptions, type RescheduleBookingInput, Scan, type ScanCompany, type ScanCreateInput, type ScanCreateResult, type ScanJob, type ScanResultPayload, type ScanStatus, type ScanSubScores, type SchedulePostInput, type ScheduleResult, type SendEmailInput, type TestPingEvent, type UpdateBookingInput, type UpdateContactInput, type UpdateConversationInput, type UpdateDealInput, type UpdatePostInput, type UpdateWebhookInput, type VerifyWebhookSignatureInput, type WaitForScanOptions, type WebhookChannelLifecycleData, type WebhookConversationSnapshot, type WebhookDeleteResult, type WebhookDelivery, type WebhookEndpoint, type WebhookEvent, type WebhookMessageSnapshot, type WebhookTestResult, WebhookVerificationError, type WebhookVerificationErrorCode, Webhooks, type Workspace, Workspaces, createMedalClient, Medal as default, verifyWebhookSignature };