> ## Documentation Index
> Fetch the complete documentation index at: https://docs.onflay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Appointment checkout

> Book an APPOINTMENT listing via the public API — either pick a slot in your app, or create an open session and let the buyer pick on Onflay hosted checkout.

Appointment listings need a **time slot before payment can complete**. You can choose
who owns the calendar UI:

| Flow                               | Who picks the time                                               | Best for                     |
| ---------------------------------- | ---------------------------------------------------------------- | ---------------------------- |
| **A — Your app owns the calendar** | Your UI → pass `appointmentSlot` at create                       | Full control over booking UX |
| **B — Onflay owns the calendar**   | Omit slots → buyer picks on hosted page or overlay (incl. packs) | Fastest integrate            |

Both sit on [Tier 1](/developers/integration-ladder) with the same Tier 2 receipt
confirmation path.

## Prerequisites

The listing must be ready to book:

| Requirement               | Notes                                                                                        |
| ------------------------- | -------------------------------------------------------------------------------------------- |
| `type`                    | `APPOINTMENT`                                                                                |
| `status`                  | `ACTIVE`                                                                                     |
| Variant `durationMinutes` | At least `5`                                                                                 |
| Creator availability      | Weekly hours configured in the dashboard                                                     |
| Meeting / delivery config | Virtual meeting link provider and/or in-person address, matching the listing delivery method |

Configure availability and meeting settings in the Onflay dashboard under
**Scheduling**. Without availability, the public slots endpoints return empty days.

***

## Flow B — Onflay calendar (simplest)

Create a checkout session **without** `appointmentSlot`. Onflay shows the
availability calendar, then payment — either full-page or overlay.

### Full-page hosted

```http theme={"dark"}
POST /v1/checkout-sessions
Authorization: Bearer sk_test_...
Content-Type: application/json
Idempotency-Key: checkout:user_123:appt:open

{
  "listingId": "listing_...",
  "variantId": "variant_...",
  "email": "buyer@example.com",
  "successUrl": "https://app.example.com/booking/success",
  "cancelUrl": "https://app.example.com/booking"
}
```

Redirect to `checkoutUrl`.

### Overlay (`onflay.js`)

Same create call with `uiMode: "embedded"`, then open `embedUrl` in the overlay
(see [Overlay checkout](/developers/overlay-checkout)):

```http theme={"dark"}
POST /v1/checkout-sessions
Authorization: Bearer sk_test_...
Content-Type: application/json

{
  "listingId": "listing_...",
  "variantId": "variant_...",
  "email": "buyer@example.com",
  "successUrl": "https://app.example.com/booking/success",
  "cancelUrl": "https://app.example.com/booking",
  "uiMode": "embedded"
}
```

```js theme={"dark"}
OnflayCheckout.open({ url: session.embedUrl, onComplete: async ({ checkoutId }) => { /* confirm receipt */ } });
```

Notes:

* Works for **single-session** and **session packs**. Packs collect all N times
  inside hosted/overlay checkout before payment.
* No Stripe PaymentIntent is created until the buyer confirms their time(s).
* Hybrid listings show a Virtual / In person choice before times.
* Sessions still expire after **30 minutes** if unpaid.

***

## Flow A — Your app owns the calendar

### Step 1 — Fetch availability

Public (no API key) endpoints:

```http theme={"dark"}
GET /v1/listings/{listingId}/available-dates?from=2026-08-01&to=2026-08-31&variantId={variantId}
```

```http theme={"dark"}
GET /v1/listings/{listingId}/slots?date=2026-08-20&variantId={variantId}
```

Optional query params:

* `variantId` — defaults to the listing's default variant when omitted
* `durationMinutes` — override session length (defaults to the variant duration)

Use `available-dates` to highlight days on a month calendar, then `slots` for the
chosen day's open start times. Each slot includes an ISO `startsAt` you can pass
straight into checkout.

There is also a range helper when you want several days at once:

```http theme={"dark"}
GET /v1/listings/{listingId}/slots/range?from=2026-08-20&to=2026-08-27&variantId={variantId}
```

### Step 2 — Buyer selects a slot

Collect in your UI:

| Field          | Required  | Notes                                                                                                              |
| -------------- | --------- | ------------------------------------------------------------------------------------------------------------------ |
| `startsAt`     | Yes       | ISO 8601 from the slots response                                                                                   |
| `timezone`     | Yes       | IANA timezone for the buyer's view (e.g. `America/Santo_Domingo`)                                                  |
| `deliveryMode` | Sometimes | `virtual` or `in_person`. Required for hybrid listings; inferred when the listing is online-only or in-person-only |

For pack variants (`sessionCount > 1`), the buyer must pick **exactly N** distinct,
non-overlapping times.

### Step 3 — Create a checkout session with the slot

```http theme={"dark"}
POST /v1/checkout-sessions
Authorization: Bearer sk_test_...
Content-Type: application/json
Idempotency-Key: checkout:user_123:appt:2026-08-20T15:00:00Z

{
  "listingId": "listing_...",
  "variantId": "variant_...",
  "email": "buyer@example.com",
  "successUrl": "https://app.example.com/booking/success",
  "cancelUrl": "https://app.example.com/booking",
  "appointmentSlot": {
    "startsAt": "2026-08-20T19:00:00.000Z",
    "timezone": "America/Santo_Domingo",
    "deliveryMode": "virtual"
  }
}
```

For packs, send `appointmentSlots` (array) instead of `appointmentSlot`, with
exactly `sessionCount` entries.

Onflay validates that each slot is still free, stores the selection on the
session, and returns a hosted checkout URL (`checkoutUrl`). Appointment sessions
expire after **30 minutes** if unpaid.

Always set `cancelUrl` so the buyer can return to your booking UI if the slot
becomes unavailable while they are on the payment page.

### Step 4 — Redirect to hosted checkout

Send the buyer to `checkoutUrl`. They enter payment details; the pending page
shows the selected session time(s).

If the slot was taken between create and page load, hosted checkout shows a
`SLOT_UNAVAILABLE` state. Prefer linking back via your `cancelUrl`.

***

## Confirm payment (both flows)

Do **not** grant access from `successUrl` alone. Use Tier 2 confirmation:

```http theme={"dark"}
GET /v1/checkout-sessions/{checkoutId}/receipt?returnToken=...
Authorization: Bearer sk_test_...
```

Or headless: `?headless=true` with the secret key (no return token). Poll until
`paid` | `failed` | `expired`.

After payment succeeds, Onflay creates the appointment row (meeting link /
location as configured).

## Error contract

| Situation                                 | HTTP  | Notes                                                               |
| ----------------------------------------- | ----- | ------------------------------------------------------------------- |
| Pack with wrong slot count / overlap      | `400` | On create (Flow A) or attach (Flow B)                               |
| Wrong pack length / overlapping slots     | `400` | See pack helper messages                                            |
| Slot in the past                          | `400` | Choose a future time                                                |
| Slot no longer free at create / attach    | `400` | Re-fetch slots and pick again                                       |
| Slot taken while pending checkout is open | `409` | `code: "SLOT_UNAVAILABLE"`; recovery via `listingUrl` / `cancelUrl` |
| Session expired                           | `410` | Create a new checkout session                                       |

## Related

* [Integration ladder](/developers/integration-ladder)
* [Quickstart](/developers/quickstart)
* [Customer access](/developers/customer-access) (Tier 2 / 3 provisioning)
* [OpenAPI reference](/api-reference/openapi)
