Bitvoya Edit
Curating the next chapter
Layering imagery, place, and detail before the page opens.
Bitvoya Edit
Layering imagery, place, and detail before the page opens.
Bitvoya B2B API · v1
Search our curated hotels, quote live rates and book prepaid stays from your own platform. Every rate has one price, in CNY, with taxes and our service fee already in it.
Quickstart
Six calls take a booking from start to finish. Each step comes with its request and response.
Every request sends your API key as a Bearer token. GET /me tells you which tenant the key belongs to, its scopes and your per-minute limit.
curl https://app.bitvoya.com/api/b2b/v1/me \
-H "Authorization: Bearer $BITVOYA_API_KEY"{
"data": {
"tenant": { "code": "acme-travel", "name": "Acme Travel" },
"api_key": {
"name": "Production server",
"prefix": "bvk_live_AbCd1234",
"scopes": [
"catalog:read",
"rates:read",
"bookings:read",
"bookings:write"
],
"expires_at": null
},
"pricing": {
"currency": "CNY",
"price_basis": "tax_inclusive_total_with_service_fee"
},
"rate_limit": { "requests_per_minute": 120 }
}
}Page through hotels by destination or brand with cursors. Catalog responses carry an ETag; when nothing changed you get a 304 and download nothing again.
curl "https://app.bitvoya.com/api/b2b/v1/hotels?destination_id=5736&limit=1" \
-H "Authorization: Bearer $BITVOYA_API_KEY"{
"data": [
{
"id": "644",
"name": "Sofitel Hangzhou Westlake",
"address": "333 Xihu Avenue, Shangcheng District",
"destination": { "id": "5736", "name": "Hangzhou" },
"brand": { "id": "1862", "name": "Sofitel Hotels" },
"location": { "latitude": 30.2537, "longitude": 120.1652 },
"image_url": "https://images.example/hotels/644/cover.jpg"
}
],
"next_cursor": "eyJrIjoiaG90ZWwiLCJhIjoiNjQ0In0"
}Ask for a stay: the dates and 1–4 adults in one room. Every rate has one price, its meal plan and the cancellation terms in the hotel's local time.
curl "https://app.bitvoya.com/api/b2b/v1/hotels/644/rates?checkin=2026-12-05&checkout=2026-12-07&adults=2" \
-H "Authorization: Bearer $BITVOYA_API_KEY"{
"data": {
"hotel_id": "644",
"checkin": "2026-12-05",
"checkout": "2026-12-07",
"nights": 2,
"adults": 2,
"rooms": [
{
"room_id": "38211",
"name": "Luxury Room, Lake View",
"image_url": "https://images.example/hotels/644/rooms/38211.jpg",
"rates": [
{
"rate_key": "eyJoIjoiNjQ0IiwiZCI6IjkwMTc3NDAyIiwiY2kiOiIyMDI2LTEyLTA1IiwiY28iOiIyMDI2LTEyLTA3IiwiYSI6Mn0",
"name": "Breakfast for two, free cancellation",
"meal_plan": "breakfast",
"executive_benefit": false,
"price": { "amount": "1709.65", "currency": "CNY" },
"cancellation": {
"type": "free_cancellation",
"deadline": "2026-12-04T18:00:00+08:00",
"confirmed": true
},
"pay_at_hotel": null
}
]
}
]
}
}Send the rate_key you picked. We check it live with the hotel's supplier and hold the price for 15 minutes.
curl -X POST https://app.bitvoya.com/api/b2b/v1/quotes \
-H "Authorization: Bearer $BITVOYA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"rate_key": "eyJoIjoiNjQ0IiwiZCI6IjkwMTc3NDAyIiwiY2kiOiIyMDI2LTEyLTA1IiwiY28iOiIyMDI2LTEyLTA3IiwiYSI6Mn0"
}'{
"data": {
"quote_id": "qt_7Hk2Qx9PmW4rT8vN3bLc",
"expires_at": "2026-11-20T09:15:00Z",
"hotel_id": "644",
"room": { "room_id": "38211", "name": "Luxury Room, Lake View" },
"rate": {
"name": "Breakfast for two, free cancellation",
"meal_plan": "breakfast",
"executive_benefit": false
},
"checkin": "2026-12-05",
"checkout": "2026-12-07",
"nights": 2,
"adults": 2,
"price": { "amount": "1709.65", "currency": "CNY" },
"cancellation": {
"type": "free_cancellation",
"deadline": "2026-12-04T18:00:00+08:00",
"confirmed": true
},
"pay_at_hotel": null
}
}Create the booking with the quote and the guest names. Send an Idempotency-Key, so a retry never books twice. You get a Stripe payment page for the full price.
curl -X POST https://app.bitvoya.com/api/b2b/v1/bookings \
-H "Authorization: Bearer $BITVOYA_API_KEY" \
-H "Idempotency-Key: 6f1c2a9e-4b7d-4c1e-9a53-2d8e0f7b1c44" \
-H "Content-Type: application/json" \
-d '{
"quote_id": "qt_7Hk2Qx9PmW4rT8vN3bLc",
"client_reference": "ACME-10293",
"guests": [
{ "first_name": "Mei", "last_name": "Lin" },
{ "first_name": "James", "last_name": "Lin" }
],
"contact": { "email": "ops@acme.example", "phone": "+86 21 5555 0100" },
"arrival_time": "18:00",
"special_requests": ["king_bed"],
"success_url": "https://acme.example/trips/ACME-10293/paid",
"cancel_url": "https://acme.example/trips/ACME-10293"
}'{
"data": {
"booking_id": "BV20261120090112",
"client_reference": "ACME-10293",
"status": "awaiting_payment",
"hotel": { "hotel_id": "644", "name": "Sofitel Hangzhou Westlake" },
"room": { "room_id": "38211", "name": "Luxury Room, Lake View" },
"rate": {
"name": "Breakfast for two, free cancellation",
"meal_plan": "breakfast",
"executive_benefit": false
},
"checkin": "2026-12-05",
"checkout": "2026-12-07",
"nights": 2,
"adults": 2,
"guests": [
{ "first_name": "Mei", "last_name": "Lin" },
{ "first_name": "James", "last_name": "Lin" }
],
"price": { "amount": "1709.65", "currency": "CNY" },
"cancellation": {
"type": "free_cancellation",
"deadline": "2026-12-04T18:00:00+08:00",
"confirmed": true
},
"pay_at_hotel": null,
"payment": {
"status": "pending",
"url": "https://checkout.stripe.com/c/pay/cs_live_a1B2c3D4e5F6g7H8",
"expires_at": "2026-11-20T09:31:12Z",
"paid_at": null
},
"hotel_confirmation_number": null,
"created_at": "2026-11-20T09:01:12Z",
"updated_at": "2026-11-20T09:01:12Z"
}
}After payment we confirm with the hotel, which can take a few hours. Poll the booking until it is confirmed and carries the hotel's confirmation number.
curl https://app.bitvoya.com/api/b2b/v1/bookings/BV20261120090112 \
-H "Authorization: Bearer $BITVOYA_API_KEY"{
"data": {
"booking_id": "BV20261120090112",
"client_reference": "ACME-10293",
"status": "confirmed",
"hotel": { "hotel_id": "644", "name": "Sofitel Hangzhou Westlake" },
"room": { "room_id": "38211", "name": "Luxury Room, Lake View" },
"rate": {
"name": "Breakfast for two, free cancellation",
"meal_plan": "breakfast",
"executive_benefit": false
},
"checkin": "2026-12-05",
"checkout": "2026-12-07",
"nights": 2,
"adults": 2,
"guests": [
{ "first_name": "Mei", "last_name": "Lin" },
{ "first_name": "James", "last_name": "Lin" }
],
"price": { "amount": "1709.65", "currency": "CNY" },
"cancellation": {
"type": "free_cancellation",
"deadline": "2026-12-04T18:00:00+08:00",
"confirmed": true
},
"pay_at_hotel": null,
"payment": {
"status": "paid",
"url": null,
"expires_at": null,
"paid_at": "2026-11-20T09:04:51Z"
},
"hotel_confirmation_number": "48213977",
"created_at": "2026-11-20T09:01:12Z",
"updated_at": "2026-11-20T11:42:05Z"
}
}Pricing
Every rate comes with a single price: the tax-inclusive total for the whole stay, with your Bitvoya service fee already in it. No supplier prices, commissions or fees to add up.
Example rate · 2 nights · 2 adults
rate.priceprice
What you prepay through Stripe
¥1,709.65
"price": { "amount": "1709.65", "currency": "CNY" }pay_at_hotel · Paid at the hotel, not in price
Booking lifecycle
A booking moves along one main path. Every other status says what happened and what to do next.
Main path
Waiting for payment at payment.url. The payment page stays open for 30 minutes.
Paid. We are confirming with the hotel; this can take a few hours.
The hotel confirmed. hotel_confirmation_number is set.
Other outcomes
The payment page expired or the payment failed. Book again.
Fromawaiting_payment
Cancelled. payment.status shows the refund.
Fromawaiting_paymentpending_confirmationconfirmed
A payment problem we are checking by hand.
API reference
Paths are relative to the base URL. Open an endpoint for its parameters and an example.
The tenant, key and scopes behind this request, and your rate limit.
Parameters
No parameters.
Example
Cities with hotels. Search by name in any language or by city code, or page through all of them.
Parameters
queryquerystring · 1–100
cursorquerystring · next_cursor
limitqueryinteger · 1–100 · default 50
Example
Hotels in ID order, filtered by destination or brand. Without filters, a full sync.
Parameters
destination_idquerystring · digits
brand_idquerystring · digits
cursorquerystring · next_cursor
limitqueryinteger · 1–100 · default 50
Example
One hotel: address, location, star rating, check-in times, description, amenities and images.
Parameters
hotel_idpathrequiredstring · digits
Example
Hotel brands and how many of our hotels each has.
Parameters
No parameters.
Example
Room types and bookable rates for one stay, cheapest first.
Parameters
hotel_idpathrequiredstring · digits
checkinqueryrequireddate · YYYY-MM-DD
checkoutqueryrequireddate · YYYY-MM-DD
adultsqueryinteger · 1–4 · default 2
Example
Check a rate live and hold its price for 15 minutes.
Parameters
rate_keybodyrequiredstring
Example
Book a quote. Returns the booking and its Stripe payment page.
Parameters
Idempotency-Keyheaderrequiredstring · ≤255
quote_idbodyrequiredstring · qt_…
guests[]bodyrequired1–2 × { first_name, last_name }
contact.emailbodyrequiredcontact.phonebodystring · +86 …
client_referencebodystring · ≤64
arrival_timebodyHH:MM
special_requests[]bodyking_bed | twin_beds | baby_cot | extra_bed | birthday
remarksbodystring · ≤200
success_urlbodyhttps URL
cancel_urlbodyhttps URL
Example
Your bookings, newest first. Find one by your client_reference.
Parameters
client_referencequerystring · ≤64
cursorquerystring · next_cursor
limitqueryinteger · 1–100 · default 50
Example
One booking with its status, payment and confirmation number.
Parameters
booking_idpathrequiredstring · BV + 14 digits
Example
Cancel a booking. Before the deadline, paid bookings are refunded in full.
Parameters
booking_idpathrequiredstring · BV + 14 digits
reasonbodystring · ≤200
Example
The page payment returns to when you send no success_url.
Parameters
No parameters.
Example
The OpenAPI 3.1 contract for this API.
Parameters
No parameters.
Example
Conventions
Familiar HTTP conventions throughout. No proprietary SDK required.
Send your key in Authorization: Bearer. Keys carry scopes and can be rotated, with up to 10 active keys per tenant. We store only a hash; a key is shown once.
Authorization: Bearer bvk_live_…Errors are RFC 9457 problem details. Branch on the stable code field; detail is for people, and request_id is what to send us.
Content-Type: application/problem+jsonEach tenant has a per-minute budget shared by its keys. The IETF RateLimit headers show what is left, and a 429 carries Retry-After.
RateLimit: "tenant";r=118;t=41POST /bookings needs an Idempotency-Key. The same key and body return the original booking; the same key with another body is rejected.
Idempotency-Key: 6f1c2a9e-4b7d…Lists use cursors, not page numbers. Pass next_cursor back as cursor until it is null. limit goes up to 100.
?limit=100&cursor=eyJrIjoiaG90ZWwi…Send Accept-Language with en, zh-CN, zh-TW, ja or ko. Missing translations fall back sensibly, and Content-Language says what you got.
Accept-Language: jaCatalog responses can be cached for 5 minutes and carry a weak ETag. Rates, quotes and bookings are never cached.
If-None-Match: W/"5c1-9f3a7e21"The major version is in the path. Within v1 we only add; a breaking change would go to v2, announced ahead with Deprecation and Sunset headers.
/api/b2b/v1Error codes
Every error has an HTTP status and a stable code.
{
"type": "about:blank",
"title": "Conflict",
"status": 409,
"code": "quote_changed",
"detail": "The price or the cancellation terms changed. Check current_quote and book with its quote_id.",
"request_id": "req-3k9x1",
"current_quote": {
"quote_id": "qt_Vb8nR2kL5tY7wQ3mZ9xP",
"price": { "amount": "1788.40", "currency": "CNY" }
}
}| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_requestA parameter or field is invalid; see errors. | A parameter or field is invalid; see errors. |
| 400 | invalid_cursorThe cursor is malformed or belongs to another list. | The cursor is malformed or belongs to another list. |
| 400 | invalid_rate_keyThe rate_key cannot be read. Get the rates again. | The rate_key cannot be read. Get the rates again. |
| 400 | idempotency_key_requiredPOST /bookings needs an Idempotency-Key header. | POST /bookings needs an Idempotency-Key header. |
| 401 | missing_api_keyNo API key was sent. | No API key was sent. |
| 401 | invalid_api_keyThe key is not recognised. | The key is not recognised. |
| 401 | api_key_revokedThe key was revoked. Use another key. | The key was revoked. Use another key. |
| 401 | api_key_expiredThe key has passed its expiry date. | The key has passed its expiry date. |
| 403 | tenant_suspendedYour tenant is suspended. Contact us. | Your tenant is suspended. Contact us. |
| 403 | insufficient_scopeThe key lacks a scope; see required_scopes. | The key lacks a scope; see required_scopes. |
| 404 | hotel_not_foundNo such hotel, or it is no longer offered. | No such hotel, or it is no longer offered. |
| 404 | quote_not_foundYour tenant has no quote with this id. | Your tenant has no quote with this id. |
| 404 | booking_not_foundYour tenant has no booking with this id. | Your tenant has no booking with this id. |
| 409 | rate_unavailableThe rate can no longer be booked. | The rate can no longer be booked. |
| 409 | quote_expiredThe quote is older than 15 minutes. Quote again. | The quote is older than 15 minutes. Quote again. |
| 409 | quote_changedThe price or terms changed; book current_quote instead. | The price or terms changed; book current_quote instead. |
| 409 | already_cancelledThe booking is already cancelled. | The booking is already cancelled. |
| 409 | not_cancellablePast the free-cancellation deadline, non-refundable, or under review. | Past the free-cancellation deadline, non-refundable, or under review. |
| 409 | conflictAnother request changed this booking. Get it again and retry. | Another request changed this booking. Get it again and retry. |
| 422 | idempotency_key_reusedThat Idempotency-Key was used with a different body. | That Idempotency-Key was used with a different body. |
| 429 | rate_limitedOver your per-minute limit. Retry after Retry-After. | Over your per-minute limit. Retry after Retry-After. |
| 500 | internal_errorOur error. Quote request_id when you contact us. | Our error. Quote request_id when you contact us. |
| 502 | payment_unavailableThe booking exists but its payment page failed. Retry with the same Idempotency-Key. | The booking exists but its payment page failed. Retry with the same Idempotency-Key. |
| 502 | supplier_errorThe hotel's supplier returned an error. | The hotel's supplier returned an error. |
| 503 | supplier_unavailableThe hotel's supplier did not answer. Retry shortly. | The hotel's supplier did not answer. Retry shortly. |
Get access
Tell us about your business. We open your tenant, agree on your service fee and issue your API keys from our console.