> For the complete documentation index, see [llms.txt](https://docs.okup.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.okup.ai/okupai-api/connect-your-website.md).

# Connect your website

Take direct bookings on your own website — availability, booking creation, and guest payment in three API calls.

## 🌐 Connect your website

Build a booking flow on your own website powered by OkupAI. Your site shows live availability and prices, creates the booking, and (optionally) lets the guest pay by card — while OkupAI handles calendar sync, invoicing, guest messaging, and overbooking protection behind the scenes.

The whole integration is **three calls with a single `X-API-Key` header**. No Stripe SDK, no Stripe keys, and no webhooks are needed on your side.

***

### 🧭 Flow overview

| Step | Call                                             | Purpose                                          |
| ---- | ------------------------------------------------ | ------------------------------------------------ |
| 1    | `GET /data/pricing`                              | Show availability & prices, validate stay length |
| 2    | `POST /data/bookings`                            | Create the booking                               |
| 3    | Redirect to `checkout_url`                       | Guest pays via Stripe (card payments only)       |
| 4    | `GET /data/bookings/{booking_id}/payment-status` | Confirm payment on your success page             |

For **Bank Transfer**, **Cash**, or **Paypal** bookings, steps 3 and 4 are not needed — the booking is final after step 2.

{% hint style="info" %}
Authentication works exactly like the rest of the API — see Authentication. Your key only sees the properties assigned to your account. Always identify properties by `okupai_property_id` (UUID) — never by name, which can change.
{% endhint %}

***

### 1️⃣ Get prices and availability

Use the Pricing endpoint to render your calendar:

```bash
curl -sS \
  -H "X-API-Key: $OKUPAI_API_KEY" \
  "https://api.okup.ai/data/pricing?from=2026-08-01&to=2026-08-05&okupai_property_id=0017a629-2f7f-4798-af77-98660c19d3a1"
```

```json
{
  "total": 4,
  "limit": 1000,
  "offset": 0,
  "currency": "EUR",
  "items": [
    {
      "okupai_property_id": "0017a629-2f7f-4798-af77-98660c19d3a1",
      "friendly_name": "Apartment A1",
      "date": "2026-08-01",
      "price": 200,
      "available": true,
      "min_nights": 2,
      "max_nights": null
    }
  ]
}
```

**Only offer dates where:**

* every night of the stay has `available: true`, and
* the stay length satisfies the `min_nights` of the **arrival date**

The API enforces both rules on booking creation — closed nights are rejected with `409`, a too-short stay with `400`. Validating them upfront in your UI avoids failed bookings.

***

### 2️⃣ Create the booking

```
POST https://api.okup.ai/data/bookings
```

Returns **`201 Created`** on success.

{% hint style="info" %}
For `payment_method: "Stripe"`, a successful `201` response always has `status: "checkout_created"`, `checkout_session_id`, and `checkout_url`. Treat a Stripe response missing either checkout field as an error — do not confirm the booking as payment-ready.
{% endhint %}

#### Minimal request (non-Stripe)

```json
{
  "platform": "Website",
  "okupai_property_id": "0017a629-2f7f-4798-af77-98660c19d3a1",
  "reservation_start": "2026-08-01",
  "reservation_end": "2026-08-05",
  "guest_name": "Jane Doe",
  "email": "jane@example.com",
  "phone": "+431234567",
  "adults": 2,
  "children": 0,
  "language": "de",
  "nights_price": "800.00",
  "cleaning": "50.00",
  "payment_method": "Bank Transfer"
}
```

#### Field reference

| Field                | Required    | Notes                                                                             |
| -------------------- | ----------- | --------------------------------------------------------------------------------- |
| `platform`           | ✅           | Must be exactly `Website`                                                         |
| `okupai_property_id` | ✅           | Property UUID from the Pricing endpoint                                           |
| `reservation_start`  | ✅           | Check-in date, `YYYY-MM-DD`                                                       |
| `reservation_end`    | ✅           | **Checkout date, exclusive** — must be after check-in; maximum stay **90 nights** |
| `guest_name`         | ✅           | Full guest name (whitespace is normalized)                                        |
| `adults`             | ✅           | Integer ≥ 1                                                                       |
| `nights_price`       | ✅           | Total for the nights, as a string decimal (`"800.00"`); `"0.00"` is allowed       |
| `payment_method`     | ✅           | One of `Stripe`, `Bank Transfer`, `Cash`, `Paypal`                                |
| `children`           | optional    | Default `0`, maximum `10`                                                         |
| `email`              | optional    | Basic format check                                                                |
| `phone`              | optional    | E.164 format, e.g. `+431234567` (whitespace is stripped)                          |
| `language`           | optional    | ISO language code (2–3 letters), default `en` — controls guest messaging language |
| `cleaning`           | optional    | Cleaning fee, string decimal, default `"0.00"`                                    |
| `currency`           | optional    | If provided, must be `EUR`                                                        |
| `extension`          | optional    | Default `false` — marks the booking as extending an existing stay                 |
| `invoice`            | optional    | Billing block (see below)                                                         |
| `success_url`        | Stripe only | Your confirmation page, must be `https://` (required for Stripe)                  |
| `cancel_url`         | Stripe only | Your cancel page, must be `https://`; defaults to `success_url`                   |

**Important rules:**

* Do **not** send a `booking_id` — OkupAI generates it and returns it in the response (`400` if you send one)
* `city_tax_platform` is not accepted for website bookings
* `payment_method` values like `Platform`, `Airbnb`, or `Block` are rejected with `400`
* Monetary values are parsed as decimals and rounded to 2 places — send them as **strings**

#### Safe retries with an idempotency key

Send a unique `Idempotency-Key` header with every new booking request (8–128 URL-safe characters). Keep the same key when retrying the **same JSON payload** after a timeout or `503`; generate a new key for a genuinely new booking.

```bash
curl -sS -X POST "https://api.okup.ai/data/bookings" \
  -H "X-API-Key: $OKUPAI_API_KEY" \
  -H "Idempotency-Key: 85f63a8b-6692-4c9f-9255-e19067bca378" \
  -H "Content-Type: application/json" \
  --data @booking.json
```

A replay returns the original booking and, for Stripe, the same checkout session and URL. Reusing a key with a different payload returns `409`. The API echoes `Idempotency-Key` and `X-Booking-Operation-Id` in response headers; log those headers with the status code and response body when contacting support.

#### Invoice block (optional)

To have the guest invoice made out to a company:

```json
"invoice": {
  "company_name": "ACME GmbH",
  "company_id": null,
  "street": "Hauptstrasse",
  "street_number": "1",
  "city": "Wien",
  "zip_code": "1010",
  "country": "AT",
  "tax_id": "ATU12345678",
  "email": "billing@acme.example"
}
```

All fields are optional nullable strings; `email` must be a valid address if provided.

#### Response — Bank Transfer / Cash / Paypal

```json
{
  "status": "created",
  "booking_id": "0d8ff6b5-0e6e-4fcb-99fd-dae4ff385b38",
  "internal_booking_id": "4d7e6f19-495c-4fc8-a373-d0169c61ccdf",
  "platform": "Website",
  "payment_method": "Bank Transfer"
}
```

Done — the booking is live in the host's calendar, availability syncs to all platforms, and the guest receives automatic messages.

#### Response — Stripe

Send `payment_method: "Stripe"` plus `success_url` (required, `https://`) and optionally `cancel_url`, and you get a ready-to-use checkout link:

```json
{
  "status": "checkout_created",
  "booking_id": "0d8ff6b5-0e6e-4fcb-99fd-dae4ff385b38",
  "internal_booking_id": "4d7e6f19-495c-4fc8-a373-d0169c61ccdf",
  "platform": "Website",
  "payment_method": "Stripe",
  "payment_status": "checkout_created",
  "checkout_session_id": "cs_live_...",
  "checkout_url": "https://checkout.stripe.com/c/pay/cs_live_..."
}
```

**Redirect the guest to `checkout_url`.** The checkout charges **`nights_price + cleaning`** on the property's connected Stripe account.

{% hint style="info" %}
When Stripe redirects the guest back, your `success_url` automatically receives the booking as a query parameter: `https://your-site.com/success?booking_id=...` — read it there to start polling the payment status. If no `cancel_url` is provided, cancellations also return to `success_url`.
{% endhint %}

{% hint style="warning" %}
Stripe bookings require the property to have Stripe configured in OkupAI — otherwise the API returns `400`. Ask the property manager to complete their Stripe setup first.
{% endhint %}

{% hint style="warning" %}
If you ever receive `payment_method: "Stripe"` without `checkout_url`, do not silently continue as a non-card booking. Record the HTTP status, full response body, `Idempotency-Key`, and `X-Booking-Operation-Id`, then retry the same request with the same idempotency key. A conforming successful response includes the checkout URL.
{% endhint %}

***

### 3️⃣ Confirm the payment (Stripe only)

The redirect back to your `success_url` is only a UX signal — the **authoritative confirmation** comes from Stripe to OkupAI directly. On your success page, poll:

```
GET https://api.okup.ai/data/bookings/{booking_id}/payment-status
```

```json
{
  "booking_id": "0d8ff6b5-0e6e-4fcb-99fd-dae4ff385b38",
  "payment_status": "paid",
  "checkout_session_id": "cs_live_...",
  "checkout_url": "https://checkout.stripe.com/c/pay/cs_live_..."
}
```

| `payment_status`   | Meaning                                               |
| ------------------ | ----------------------------------------------------- |
| `checkout_created` | Checkout link exists, payment not completed yet       |
| `paid`             | Stripe confirmed the payment — show your confirmation |
| `expired`          | Checkout session expired unpaid                       |
| `none`             | No Stripe payment exists for this booking             |

Unknown or out-of-scope booking IDs return **`404`**.

**Recommended polling:** every 2–3 seconds, for up to \~30 seconds. Show the confirmation only on `paid`.

***

### ⚡ Complete JavaScript example

```js
const apiKey = "<customer-api-key>"; // keep this server-side!

async function createWebsiteBooking(payload, idempotencyKey) {
  const response = await fetch("https://api.okup.ai/data/bookings", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": apiKey,
      "Idempotency-Key": idempotencyKey
    },
    body: JSON.stringify(payload)
  });

  const body = await response.json();
  if (!response.ok) {
    throw new Error(body.error || "Booking failed");
  }

  if (payload.payment_method === "Stripe") {
    if (!body.checkout_session_id || !body.checkout_url) {
      throw new Error("Stripe booking response is missing its checkout URL");
    }
    window.location.href = body.checkout_url;
  }
  return body;
}

async function waitForPayment(bookingId) {
  for (let attempt = 0; attempt < 15; attempt++) {
    const response = await fetch(
      `https://api.okup.ai/data/bookings/${bookingId}/payment-status`,
      { headers: { "X-API-Key": apiKey } }
    );
    const body = await response.json();
    if (body.payment_status === "paid") return body;
    await new Promise(resolve => setTimeout(resolve, 2000));
  }
  throw new Error("Payment confirmation timed out");
}
```

***

### ❌ Error handling

| Status | Meaning                                                                                                              | What your site should do                                                        |
| ------ | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `400`  | Invalid payload — missing field, bad dates, stay too short for `min_nights`, unsupported payment method, invalid URL | Show the human-readable `error` message                                         |
| `401`  | Missing API key                                                                                                      | Check your server configuration                                                 |
| `403`  | Invalid key, or property outside your allowed scope / inactive                                                       | Check key & property assignment                                                 |
| `404`  | Unknown booking on the payment-status endpoint                                                                       | Verify the `booking_id`                                                         |
| `409`  | Dates no longer available — overlap with an existing booking, host-closed nights, or duplicate submission            | **Refresh `GET /data/pricing` and ask the guest to pick new dates**             |
| `500`  | Unexpected OkupAI-side error                                                                                         | Record the response and operation headers, then contact support if it persists  |
| `503`  | Temporary database or booking-state uncertainty                                                                      | Retry the identical request with the same `Idempotency-Key` after `Retry-After` |

{% hint style="info" %}
**Retry with the same idempotency key.** After a timeout or `503`, resend the identical payload with the same `Idempotency-Key`. If the booking committed, the API replays the created booking and its Stripe checkout URL. If the outcome is still being reconciled, the API returns `503` with `Retry-After`; do not create a second request with a new key while that outcome is unknown.
{% endhint %}

***

### 🏆 Connecting optimally — best practices

* 🔑 **Keep the API key server-side.** Never ship it in browser JavaScript — proxy the calls through your backend
* 🆔 **Use `okupai_property_id` everywhere.** Property names (`friendly_name`) can change; the UUID never does
* 📆 **Cache pricing sensibly.** Refresh the calendar with the Pricing endpoint's `changed_since` parameter instead of re-downloading everything — see Pricing
* ✅ **Validate `min_nights` and availability in your UI** before submitting, so guests never hit an error they could have avoided
* 🔁 **Always re-check pricing after a `409`** — availability changes in real time as bookings arrive from all platforms
* 🌍 **Send the guest's `language`.** OkupAI's automatic guest messages (confirmation, check-in instructions) are sent in that language
* 🧾 **Collect the invoice block for business guests** at checkout — the invoice data flows straight into OkupAI's automatic invoicing
* 📩 **Let OkupAI handle post-booking communication.** Website guests automatically receive the same message automations as platform guests — no confirmation emails needed on your side

***

### 🧭 Related

* Authentication — API keys and scoping
* Pricing — availability, rates, and incremental sync
* Bookings — reading bookings back
