OpheliaDocs
Search docs⌘K
DashboardCreate an account
Overview

Testing

Ophelia Test City has practice venues for every vertical, hotels and flights included. They run the real booking flow and book nothing real.

Search with "location": "Ophelia Test City" and only practice venues come back. Nothing else ever reaches them. Use them to test your integration end to end, as often as you like: no real venue, account or card is involved, and nobody is texted or emailed.

Practice venues are not mock responses. Restaurants, classes and tickets walk the same steps as the provider they imitate. Hotels and flights go further: they run Ophelia's production hotels and flights code against a pretend site, so the payment pause, the confirm step, the held checkout, the price ceiling and the unknown-outcome state all behave exactly as they do for a real booking.

Practice, test keys and live keys

What it booksBilledFree test bookings
Practice venues (any key)
Nothing. A pretend venue walks the real steps.
Never
Not used
Test key, anywhere else
Real reservations and purchases at real venues
No
Uses one of 3 per confirmed booking, then 402 free_tier_exhausted
Live key
Real reservations and purchases at real venues
Yes
—
A test key is not a sandbox. Outside Ophelia Test City, a test key drives the real site: a hotel confirm charges the card and a flight confirm buys a real ticket. Practise every flow here first.
Practice bookings are never billed and do not use your test key's free bookings. Your key still has to be usable: a live key needs billing set up, and a test key must not have used up its free allowance.

Practice venues

Every practice response carries "provider": "practice", and every practice venue has metadata.practice: true. Request and response shapes otherwise match the real provider.

VerticalVenueFlow
dining
Practice Bistro
Like OpenTable with no card: texted code, then confirmed. Can be cancelled.
dining
Practice Steakhouse
Like an OpenTable venue that holds a card: slots carry card_required: true. Texted code, then confirmed. Can be cancelled.
dining
Practice Omakase
Like Resy: texted code, then confirmed. Cannot be cancelled once confirmed.
fitness
Practice Flow, Practice Reformer, Practice Power Yoga
Like Mindbody: emailed code, then confirmed.
entertainment
Practice Arena
Like StubHub: pick a listing from metadata.sections and send its listing_id in metadata. Emailed code, then confirmed.
travel · hotels
Six hotels (below)
Like hotels.com: rates, then payment, card, confirm, confirmed. Or pay by link. No code.
travel · flights
Any route to or from Ophelia Test City
Like flights.com: itineraries, a held fchk_ checkout, card, confirm: true, booked. Or pay by link. No code.

The code

Every practice code is 246810. It never appears in an API response: as with a real provider, your agent has to ask the person for it. Any other code fails the booking with error_code: "otp_invalid", the way a wrong code does at the real provider. Hotels and flights send no code.

Test cards and failure cases

Practice venues never charge a card, so any card number that passes the Luhn check works, for example 4242424242424242. The values below make a hotel or flight fail on purpose, so you can test how your agent recovers. They follow the convention Stripe's test cards use.

SendHotelsFlights
4242424242424242
Confirmed
Booked: status: "succeeded" with a booking_reference
4000000000000002
Declined at the confirm press: failed, error_code: "not_confirmed"
Declined at the confirm press: 502, code: "payment_declined", not retryable
4000000000000119
Pressed, no answer: 202 processing, error_code: "outcome_unknown". The booking stays processing.
Pressed, no answer: reason: "verify_outcome", pressed: true, outcome_known: false
4000000000003220
Confirmed (hotels ask for no 3-D Secure)
The bank asks for 3-D Secure after the press: reason: "verify_outcome", pressed: true, outcome_known: false
A number that fails Luhn, e.g. 4242424242424241
Refused at the card step: failed, error_code: "card_fill_incomplete"
Refused at the card step: 502, code: "card_fill_incomplete"
max_total below the checkout total
Not enforced for hotels yet: the booking confirms. Check the total yourself.
Parks unpressed: reason: "over_max_total" with payable_total. Answer with a higher max_total and confirm: true.
A room or itinerary
Ophelia Budget Stay's "1 left at this price" rate sells out before booking: failed, error_code: "rate_not_available". Practice Lodge is sold out on every date: availability returns no slots.
The cheapest itinerary on every route re-prices at checkout by $12.40 a traveller, so price_drift is never 0 on it.
Practise the unknown outcome. When a confirm comes back outcome_unknown or verify_outcome, the pay button was pressed and the site never said what happened. Your agent must stop and tell the person. It must never press again: on a real site, a second press can buy a second ticket.

Timing

Practice answers can come back almost at once, while a real hotel search takes about 25 seconds and a flight checkout can take a minute. Set the Ophelia-Practice-Timing header to choose how long each practice step takes:

ValueEach step takes
live
As long as the real site takes. Use it to test your timeouts and your handling of 202 processing.
fast
A tenth of that. The default. Every step still takes a moment, so processing states stay visible.
instant
No time at all. For automated test suites.

Send it on every request. Any other value returns 400 invalid_practice_timing.

Hotels

Search hotels with "location": "Ophelia Test City"to get six hotels in hotels.com's shape: nightly and total prices with taxes, ratings and per-rate cancellation terms.

HotelAreaPer nightUse it for
Practice Inn
Test City Center
$159
The everyday case: a refundable and a non-refundable rate, plus a King Room
The Practice Grand
Harborfront
$412
A higher-priced hotel with a suite
Ophelia Budget Stay
Airport District
$89
A rate that sells out before booking (rate_not_available)
Practice Boutique Hotel
Old Town
$219
Two room types
Practice Resort & Spa
Lakeside
$329
Three rates, one of them a villa
Practice Lodge
North Hills
$129
Sold out on every date: availability returns no slots
Walkthrough: card, then confirm
curl
# 1. Search, then get rates for one hotel
curl -X POST https://api.opheliaos.com/v1/venues/search \
-H "Authorization: Bearer YOUR_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{
"vertical": "travel",
"term": "hotel",
"location": "Ophelia Test City",
"check_in": "2026-11-12",
"check_out": "2026-11-14",
"party_size": 2,
"rooms": 1
}'
 
curl -X POST https://api.opheliaos.com/v1/availability/search \
-H "Authorization: Bearer YOUR_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{ "venue_id": "ven_...", "check_in": "2026-11-12", "check_out": "2026-11-14", "party_size": 2, "rooms": 1 }'
# → slots[].offer: "Standard Queen Room", "$372", "Fully refundable before Nov 10", ...
 
# 2. Book a rate. No card yet.
curl -X POST https://api.opheliaos.com/v1/bookings \
-H "Authorization: Bearer YOUR_TEST_KEY" \
-H "Idempotency-Key: practice-hotel-001" \
-H "Content-Type: application/json" \
-d '{
"venue_id": "ven_...",
"availability_id": "avail_...",
"datetime": "2026-11-12T15:00:00-05:00",
"party_size": 2,
"customer": {
"name": "Jane Smith",
"phone_number": "+12124567890",
"email": "jane.smith.traveler@gmail.com"
},
"metadata": { "postal_code": "10014" }
}'
# → "status": "requires_action", next_action.type "payment"
 
# 3. Send the card. This fills it in and stops before booking.
curl -X POST https://api.opheliaos.com/v1/bookings/bkg_.../continue \
-H "Authorization: Bearer YOUR_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{
"card_number": "4242424242424242",
"exp_month": "12",
"exp_year": "2028",
"cvv": "123",
"postal_code": "10014",
"name_on_card": "Jane Smith"
}'
# → "status": "requires_action", next_action.type "confirm"
 
# 4. Confirm. On a real hotel this charges the card.
curl -X POST https://api.opheliaos.com/v1/bookings/bkg_.../continue \
-H "Authorization: Bearer YOUR_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{ "confirm": true }'
json
{
"id": "bkg_...",
"object": "booking",
"status": "confirmed",
"provider": "practice",
"provider_booking_id": "PRACTICE-HOTEL-...",
"confirmation": { "confirmation_code": "PRACTICE-HOTEL-..." },
"amount_cents": 37190,
"currency": "usd"
}

To practise a hosted card page instead, send "payment_collection": "link" on the booking. next_action.url then opens a practice pay page with no card fields: pressing its confirm button carries the booking on to confirmed.

Flights

Any route with Ophelia Test City at either end is a practice route, whatever providers says. Its airport is OTC. The other end can be JFK, BOS, ATL, MIA, ORD, DEN, LAS, LAX, SFO or SEA, or one of those cities by name. Each day has six departures on three practice airlines (Practice Air, Test City Airways, Ophelia Express), some nonstop and some with a stop at Practice Junction (PJX). All four cabins and three fares are offered, in the itinerary shape recorded from the real site.

Write the full name. Practice routes are matched on the words "Ophelia Test City". A search from "OTC" alone is not recognised as practice and goes to the real flights site.

As on a real route, the site's $14.10-a-traveller protection plan is bought unless you send "insurance": "decline", and it shows up in price_drift.

Walkthrough: held checkout, card, confirm
curl
# 1. Search the route, then list its itineraries
curl -X POST https://api.opheliaos.com/v1/venues/search \
-H "Authorization: Bearer YOUR_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{
"vertical": "travel",
"origin": "Ophelia Test City",
"destination": "Los Angeles",
"date": "2026-11-12",
"party_size": 1,
"cabin": "ECONOMY"
}'
 
curl -X POST https://api.opheliaos.com/v1/availability/search \
-H "Authorization: Bearer YOUR_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{ "venue_id": "ven_..." }'
# → slots[].itinerary.itinerary_key, e.g. "1_P1111.OTC20261112"
# "One-way · Ophelia Test City (OTC) → Los Angeles (LAX) · Thu, Nov 12 · ... · Nonstop · $174.34"
 
# 2. Start the checkout. It is held at the payment screen; nothing is created.
curl -X POST https://api.opheliaos.com/v1/bookings \
-H "Authorization: Bearer YOUR_TEST_KEY" \
-H "Idempotency-Key: practice-flight-001" \
-H "Content-Type: application/json" \
-d '{
"vertical": "travel",
"venue_id": "ven_...",
"party_size": 1,
"customer": { "name": "Jane Smith", "phone_number": "+12124567890", "email": "jane.smith.traveler@gmail.com" },
"travellers": [{
"first_name": "Jane", "last_name": "Smith", "gender": "female",
"birth_day": 14, "birth_month": 3, "birth_year": 1990,
"phone": "+12124567890", "email": "jane.smith.traveler@gmail.com"
}],
"metadata": {
"itinerary_key": "1_P0296_P0297.OTC20261112",
"insurance": "decline",
"max_total": 250,
"hold": true,
"billing": { "street": "1 Main St", "city": "New York", "state": "NY", "zipcode": "10014", "country": "US" }
}
}'
# → 202, "object": "flight_checkout", "id": "fchk_...", "status": "requires_action", "reason": "payment"
 
# 3. Send the card. It is filled in; the pay button is not pressed.
curl -X POST https://api.opheliaos.com/v1/bookings/fchk_.../continue \
-H "Authorization: Bearer YOUR_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{ "card": { "number": "4242424242424242", "expirationDate": "12/28", "cvc": "123", "name": "Jane Smith" } }'
# → 202, "reason": "confirm", "pay_control": "Complete booking", "checkout_total": { "total_text": "$184.48" }
 
# 4. Press it. On a real route this buys the ticket.
curl -X POST https://api.opheliaos.com/v1/bookings/fchk_.../continue \
-H "Authorization: Bearer YOUR_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{ "confirm": true }'
json
{
"object": "flight_checkout",
"status": "succeeded",
"provider": "practice",
"booking_id": "bkg_fchk_...",
"booking_reference": "PRACTICE-FLIGHT-...",
"checkout_total": { "flight": "$162.34", "taxes": "$22.14", "total_text": "$184.48" },
"quoted_price": 184.48,
"payable_total": 184.48,
"price_drift": 0
}

The pressed checkout is recorded as booking bkg_fchk_..., which GET /v1/bookings/{id} returns. To practise paying by link, leave out the card and send "payment_collection": "link". The booking comes back as bkg_... with next_action.type "payment" and a URL. After the practice pay page is confirmed, the booking goes through processing to requires_action with next_action.type "confirm". Send {"confirm": true} to /continue and it is confirmed.

Walkthrough: dining with a code

curl
# 1. Search
curl -X POST https://api.opheliaos.com/v1/venues/search \
-H "Authorization: Bearer YOUR_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{
"vertical": "dining",
"term": "dinner",
"location": "Ophelia Test City",
"datetime": "2026-11-14T19:00:00-05:00",
"party_size": 2
}'
 
# 2. Book a slot from Practice Bistro
curl -X POST https://api.opheliaos.com/v1/bookings \
-H "Authorization: Bearer YOUR_TEST_KEY" \
-H "Idempotency-Key: practice-001" \
-H "Content-Type: application/json" \
-d '{
"venue_id": "ven_...",
"availability_id": "avail_...",
"datetime": "2026-11-14T19:00:00-05:00",
"party_size": 2,
"customer": {
"name": "Jane Smith",
"phone_number": "+12124567890",
"email": "jane.smith.diner@gmail.com"
}
}'
# → "status": "requires_action", next_action.type "otp"
 
# 3. Send the code
curl -X POST https://api.opheliaos.com/v1/bookings/bkg_.../continue \
-H "Authorization: Bearer YOUR_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{ "otp_code": "246810" }'
json
{
"id": "bkg_...",
"object": "booking",
"status": "confirmed",
"provider": "practice",
"confirmation": { "confirmation_code": "PRACTICE-BISTRO-..." }
}

The usual request rules still apply, so a practice run also tests your input. The customer needs a first and last name. Placeholder guests are refused with placeholder_customer: for example an example.com email, or a phone number in the 555 exchange such as +12125551234.