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.
Modal auto-close
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.