WidgetsBooking widgetCallbacks

onSuccess

Fires once per completed booking, with the appointment id, the queue token, and how the patient paid.

onSuccess fires when the widget reaches its confirmation screen — the success screen for a scheduled appointment, or the token screen for a queue booking. It is your hook for redirects, analytics events, CRM syncing, and confirmation messages beyond what the widget shows.

Signature

interface MedosBookingConfig {
  onSuccess?: (result: BookingResult) => void;
}

Payload

One shape for every booking, with the fields that don't apply left unset:

interface BookingResult {
  /** Appointment row id. Set for scheduled bookings, and for queue bookings
   *  that carry one. */
  appointmentId?: number | null;

  /** Reference returned when the booking went through online payment. */
  appointmentRef?: string | null;

  /** Present only for QMS bookings. */
  queueToken?: QueueToken | null;

  /** Online-payment lifecycle at the moment of confirmation. */
  paymentStatus: "idle" | "verifying" | "success" | "failed" | "timeout";

  /** What the patient chose on the summary step. */
  paymentMethod:
    | { kind: "AT_CLINIC" }
    | { kind: "ONLINE" }
    | { kind: "MANUAL"; methodId: number };
}
interface QueueToken {
  tokenNumber: string;
  queuePosition: number;
  estimatedWaitTime: number; // minutes
  patientName: string;
  appointmentDate: string; // YYYY-MM-DD
  appointmentTime?: string; // ISO datetime of the shift start
  dateDisplay: string; // pre-formatted, e.g. "Friday, Jul 17"
  timeDisplay: string; // pre-formatted, e.g. "9:00 AM – 1:00 PM"
  doctorName?: string;
  locationName?: string;
  bookingType: "TODAY" | "FUTURE";
}

appointmentId is the same whether the patient paid for a new appointment or redeemed an active session pack — the pack redemption is recorded server-side against the appointment.

An idle paymentStatus is normal

It only leaves idle when the booking went through the hosted online checkout. A pay-at-clinic or manual-payment booking confirms with paymentStatus: "idle", which is not an error — check paymentMethod to see how the patient chose to pay. See Online payment.

A MANUAL payment method does not mean paid

A manual payment claim is submitted, not settled — the clinic still has to confirm the money arrived. Don't treat MANUAL as a completed payment in your own records.

Firing rules

  • Once per booking. The callback is guarded by the booking's own identity (appointment id, token number, or payment reference), so re-rendering the confirmation screen doesn't fire it again.
  • Again for a second booking. A patient who books twice without reloading gets two calls, because the identity changed.
  • Never without an identity. If the confirmation screen renders with no appointment id, token, or payment reference to name the booking, nothing fires.

In modal mode the widget closes itself 3 seconds after onSuccess fires — long enough to read the confirmation — and that close fires onClose exactly as a manual dismissal would.

Inline mode never auto-closes; the confirmation stays on screen.

Redirecting cancels the confirmation screen

If you navigate away inside onSuccess, the patient never sees the appointment id or token number the widget was about to show them. Either give them a few seconds first, or carry the details into the page you send them to.

Common patterns

Redirect on success

MedosBooking.init({
  apiKey: "mk_your_publishable_key",
  onSuccess: (result) => {
    setTimeout(() => {
      window.location.href = `/booked?id=${result.appointmentId}`;
    }, 3000);
  },
});

Track a conversion event

MedosBooking.init({
  apiKey: "mk_your_publishable_key",
  onSuccess: (result) => {
    window.dataLayer?.push({
      event: "medos_booking_success",
      appointmentId: result.appointmentId ?? null,
      tokenNumber: result.queueToken?.tokenNumber ?? null,
      paymentMethod: result.paymentMethod?.kind,
    });
  },
});

See Analytics recipes for GA4, GTM, and Mixpanel patterns.

Handle both scheduled and QMS

MedosBooking.init({
  apiKey: "mk_your_publishable_key",
  onSuccess: (result) => {
    if (result.queueToken) {
      alert(`Your token: ${result.queueToken.tokenNumber}`);
    } else {
      alert(`Appointment booked: #${result.appointmentId}`);
    }
  },
});

Branch on `queueToken`, not on `tokenNumber`

The token number is nested inside queueToken — a top-level result.tokenNumber is always undefined. If you wrote against an earlier version of these docs that described a flat payload, this is the line to change.

On this page