B2B GraphQL API Documentation
Direct GraphQL integration for B2B travel partners with credit line accounts. Search 2.5M+ hotels, book at wholesale rates, manage the full booking lifecycle.
Building an AI agent? The MCP server exposes the same product via REST / JSON-RPC / SSE with a simpler ISO date format. See the MCP docs →
Authentication
All B2B calls require a Bearer JWT. Your account must have B2B status with an active credit line — without it, confirmB2bBooking returns "User is not b2b user".
Account requirements
{
"isB2B": true,
"hasCL": true
}Getting a Bearer token
curl -X POST https://users.locktrip.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "your-password"}'
# Response:
{
"token": "eyJhbGciOiJIUzUxMiJ9...",
"expiresIn": 86400
}Using the token
curl -X POST https://locktrip.com/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9..." \
-d '{"query": "...", "variables": {...}}'B2B onboarding (credit line setup, pricing tier) is provisioned by LockTrip — contact your account manager or fill the business inquiry form.
Endpoint
| Environment | URL |
|---|---|
| Production | POST https://locktrip.com/graphql |
All requests use POST with a JSON body containing query and variables.
Complete Booking Flow
The full flow from destination lookup to charged booking is 8 operations. Session lifetime is ~30 minutes — restart from hotelSearch if it expires.
1. locationSearch("bali, indonesia") → regionId
2. hotelSearch(regionId, dates) → searchKey (DD/MM/YYYY)
3. WAIT 2 seconds (mandatory — first polls will be empty)
4. hotelSearchResults(searchKey) → hotels[] (poll until isResultCompleted=true)
5. getHotelRooms(hotelId) → quoteId (hotelId must be NUMBER)
6. hotelCancellationPolicies(packageIds) → refund terms (packageId = quoteId.split('_')[0])
7. hotelBookingPrepare(guests) → preparedBookingId
8. confirmB2bBooking(bookingInternalId) → CHARGES credit line
── post-booking ──
getUserBookings({ type: "UPCOMING" }) → list
getBookingDetails(bookingId) → full info
cancelBookingRequest(bookingId, confirmed) → cancel (confirmed=false is dry-run)Critical gotchas
- Dates are
DD/MM/YYYY(e.g."23/08/2027") - Page numbers are 1-based, not 0-indexed
hotelIdis a NUMBER forgetHotelRooms— useparseInt()- Guest count in booking must exactly match search (2 adults searched → 2 adults booked)
- Extract packageId from quoteId:
quoteId.split('_')[0]
Error Handling
| Error | Cause | Solution |
|---|---|---|
| 0 results after 30 polls | Polling started too fast | Wait 2+ seconds before first poll |
| Guest count mismatch | Adults count differs from search | Match exactly: 2 adults searched = 2 in booking |
| User is not b2b user | Account lacks B2B status | Contact LockTrip to upgrade the account |
| 400 on cancellation policy | Passed full quoteId | Extract packageId: quoteId.split('_')[0] |
| hotelId must be a number | String hotelId passed | Use parseInt(hotelId, 10) for getHotelRooms |
| Session expired | Flow took longer than ~30 min | Restart from hotelSearch |
| Invalid date format | Used ISO or US format | Use DD/MM/YYYY (e.g. "23/08/2027") |
Operation Reference
Every GraphQL query and mutation with sample variables and real responses.
locationSearch
Query · PublicFind the regionId for a destination. Never hardcode regionId — IDs can change.
query LocationSearch($query: String!) {
locationSearch(query: $query) {
locationData
}
}Variables
| Field | Type | Required | Description |
|---|---|---|---|
| query | String! | Yes | City, region, or hotel name |
Sample response (real)
{
"data": {
"locationSearch": {
"locationData": [
{
"id": "645f64dace586e4a12d943ed",
"externalId": "ChIJoQ8Q6NNB0S0RkOYkS7EPkSQ",
"type": "administrative_area_level_1",
"query": "Bali, Indonesia"
},
{
"id": "6089739c76290b7c193c17b6",
"type": "locality",
"query": "Kuta, Badung Regency, Bali, Indonesia"
},
{
"id": "645f64dace586e4a12d943ed_5879110",
"displayName": "The Bali Bill Villa",
"type": "Apartment",
"score": 28.49
}
]
}
}
}Location types: administrative_area_level_1 (region), locality (city), Hotel / Apartment (specific property). Use the first region/locality id as regionId.
hotelSearch
Mutation · PublicInitiate an async search. Returns immediately with a searchKey to poll with hotelSearchResults.
mutation HotelSearch(
$searchHotelsInput: searchHotelsInput!
$isAsyncSearch: Boolean
) {
hotelSearch(
searchHotelsInput: $searchHotelsInput
isAsyncSearch: $isAsyncSearch
) {
searchKey
sessionId
}
}searchHotelsInput
| Field | Type | Required | Description |
|---|---|---|---|
| regionId | String | No | From locationSearch. Required unless lat/lng provided. |
| latitude | Float | No | Latitude (-90 to 90). Use with longitude instead of regionId. |
| longitude | Float | No | Longitude (-180 to 180). |
| radiusInMeters | Int | No | Search radius in meters (default: 30000, max: 100000). Only with lat/lng. |
| startDate | String! | Yes | Check-in date — DD/MM/YYYY (e.g. "23/08/2027"). MUST be in the future; a past date is rejected. Example dates are illustrative — substitute your own future window. |
| endDate | String! | Yes | Check-out date — DD/MM/YYYY |
| currency | String! | Yes | REQUIRED — currency code (e.g. "EUR", "USD"). Omitting it fails validation with BAD_USER_INPUT. |
| rooms | [RoomSearchInput!]! | Yes | Array of room configurations |
| rooms[].adults | Int! | Yes | Number of adults (1–4) |
| rooms[].children | [ChildrenInput!] | No | Child ages as OBJECTS, not bare integers: [{"age":5},{"age":8}]. Passing [5,8] fails with BAD_USER_INPUT ("Expected type ChildrenInput to be an object"). NOTE: this is ChildrenInput{age} at SEARCH time; hotelBookingPrepare takes ChildInput{firstName,lastName,age} at BOOKING time. |
| uuid | String! | Yes | Unique ID per search (client-generated, for dedup) |
| nat | String | No | Nationality — 2-letter ISO country code (e.g. US, GB). Optional in the schema, but supply it: nationality affects supplier pricing and availability. |
Example
curl -X POST https://locktrip.com/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ..." \
-d '{
"query": "mutation HotelSearch($searchHotelsInput: searchHotelsInput!, $isAsyncSearch: Boolean) { hotelSearch(searchHotelsInput: $searchHotelsInput, isAsyncSearch: $isAsyncSearch) { searchKey sessionId } }",
"variables": {
"searchHotelsInput": {
"regionId": "645f64dace586e4a12d943ed",
"startDate": "23/08/2027",
"endDate": "25/08/2027",
"currency": "EUR",
"rooms": [{ "adults": 2, "children": [] }],
"uuid": "search-abc123",
"nat": "US"
},
"isAsyncSearch": true
}
}'
# Response:
{
"data": {
"hotelSearch": {
"searchKey": "fa0060c7a52e5a8f81ab63f0a7808f39",
"sessionId": "sess_abc123"
}
}
}hotelSearchResults
Query · PublicPoll for results. Wait 2+ seconds before the first poll — empty results for the first 5–10 polls is normal. Poll until isResultCompleted === true.
query HotelSearchResults($input: SearchResultsInput!) {
hotelSearchResults(input: $input) {
results {
externalId
name
star
address
latitude
longitude
price
lastBestPrice
discountScore
distance
boardType
payment
quality
reviewsScore
hasFreeCancellationOption
refundability
refundableUntil
availableBoards
reviews { scoreSummary reviewsCount }
hotelPhoto { url }
features
}
totalResults
page
size
hasNextPage
isResultCompleted
isSearchFinished
}
}input
| Field | Type | Required | Description |
|---|---|---|---|
| searchKey | String! | Yes | From hotelSearch |
| page | Int | No | Page number — 1-based (default: 1) |
| size | Int | No | Results per page (default: 100, max: 5000) |
| filters | Object | No | Optional filters (price range, stars, amenities, boards) |
| sortParams | [String!] | No | Sort field + direction (e.g. ["price", "asc"]) |
| singleHotelId | Int | No | Restrict to a single hotel by externalId |
Sample response (completed, real data)
{
"data": {
"hotelSearchResults": {
"results": [
{
"externalId": 5948584,
"name": "The Kayon Jungle Resort",
"star": 5,
"address": "Br. Bresela, Payangan, Gianyar",
"latitude": -8.4231,
"longitude": 115.2134,
"price": 127.50,
"lastBestPrice": 195.00,
"discountScore": 35,
"distance": 12.4,
"boardType": "Breakfast",
"payment": "Cash",
"quality": 2.4,
"reviewsScore": 9.2,
"hasFreeCancellationOption": true,
"refundability": true,
"refundableUntil": "2027-08-20T00:00:00Z",
"availableBoards": ["Room Only", "Breakfast", "Half Board"],
"reviews": { "scoreSummary": 9.2, "reviewsCount": 1247 },
"hotelPhoto": { "url": "https://imagecontent.net/images/full/hotel-5948584.jpeg" },
"features": ["Pool", "Spa", "WiFi", "Restaurant", "Gym"]
}
],
"totalResults": 2089,
"page": 1,
"size": 100,
"hasNextPage": true,
"isResultCompleted": true,
"isSearchFinished": true
}
}
}Pricing fields
price is LockTrip's offer (wholesale rate after B2B markup).lastBestPrice is the best competing price our ML model found (Booking.com, Expedia, etc.). When equal to price, no discount was detected.discountScore — higher means bigger savings vs. competitors. It is a relative ML output with no fixed upper bound: it is not clamped, so do not normalise it against a constant maximum. Across millions of live results we observed a range of roughly 0 to 60, with about one result in ten above 10. Rank offers against each other instead.quality is our internal ranking scalar and the default sort key — derived from discountScore, distance and review score. It is likewise unbounded (observed roughly 0 to 23 over the same data, with under a third above 1) and carries no absolute meaning, so use it to order results and never render it as a rating. For a user-facing rating use reviewsScore (0–10).
Polling pattern
await sleep(2000); // MANDATORY 2s wait
for (let attempt = 0; attempt < 30; attempt++) {
const result = await graphql(QUERY, {
input: { searchKey, page: 1, size: 100 }
});
if (result.hotelSearchResults.isResultCompleted) {
return result.hotelSearchResults.results;
}
await sleep(1000);
}getHotelRooms
Query · PublicGet available room packages for a specific hotel. Returns quoteIds used for booking.
query GetHotelRooms($input: GetRoomsInput) {
getHotelRooms(input: $input) {
searchKey
hotelRoomsResponse {
quoteId
refundable
finalPrice
mealType
roomType
originalName
payment
roomCount
roomContent {
amenities
images
descriptions
}
}
}
}input
| Field | Type | Required | Description |
|---|---|---|---|
| searchKey | String! | Yes | From hotelSearch |
| hotelId | Float! | Yes | externalId as NUMBER — use parseInt(). The schema declares it Float; send a number, not a string. |
| startDate | String! | Yes | DD/MM/YYYY. MUST be in the future; a past date is rejected. Example dates are illustrative — substitute your own future window. |
| endDate | String! | Yes | DD/MM/YYYY |
| regionId | String! | Yes | From locationSearch |
| rooms | [RoomSearchInput!]! | Yes | Same shape as hotelSearch |
| currency | String! | Yes | Currency code |
| nat | String! | Yes | 2-letter nationality code |
quoteId format: {packageId}_{hotelId}. Extract packageId with quoteId.split('_')[0] for cancellation policy lookup.
Sample response (real)
{
"data": {
"getHotelRooms": {
"searchKey": "fdd291c01f8a77809f176ccc279e2a0a",
"hotelRoomsResponse": [
{
"quoteId": "cde06235-53aa-489c-a383-62d79019e4e4_17347136",
"refundable": true,
"finalPrice": 2.98,
"mealType": "Room Only",
"roomType": "Deluxe Room, Double Or Twin Beds",
"originalName": "Double Deluxe",
"payment": "Cash",
"roomCount": 1,
"roomContent": {
"amenities": ["Non-Smoking", "Air conditioning", "Free WiFi", "Flat-panel TV"],
"images": [
"https://imagecontent.net/images/fullrm/a15e00fa-32ab.jpeg",
"https://imagecontent.net/images/fullrm/40629a60-0e00.jpeg"
],
"descriptions": []
}
}
]
}
}
}hotelCancellationPolicies
Query · PublicGet cancellation fee schedule before booking. Pass packageIds (not full quoteIds).
query HotelCancellationPolicies(
$searchKey: String!
$hotelId: String!
$packageIds: [String!]!
) {
hotelCancellationPolicies(
searchKey: $searchKey
hotelId: $hotelId
packageIds: $packageIds
) {
packageId
fallbackPolicy
cancellations {
nonRefundable
boardType
roomType
originalName
canxFees {
amount { amt }
from
}
}
}
}Variables
| Field | Type | Required | Description |
|---|---|---|---|
| searchKey | String! | Yes | From hotelSearch |
| hotelId | String! | Yes | externalId as STRING (note: this is string here, not number) |
| packageIds | [String!]! | Yes | Array of packageIds — quoteId.split('_')[0] |
Sample response
{
"data": {
"hotelCancellationPolicies": [
{
"packageId": "73513d6a-b452-449b-ae29-9797790ed569",
"fallbackPolicy": null,
"cancellations": [{
"nonRefundable": false,
"boardType": "Breakfast",
"roomType": "Deluxe Room",
"originalName": "Deluxe Room with Garden View - Breakfast Included",
"canxFees": [
{ "amount": { "amt": 0 }, "from": 1724284800000 },
{ "amount": { "amt": 127.50 }, "from": 1724457600000 },
{ "amount": { "amt": 255.00 }, "from": 1724544000000 }
]
}]
}
]
}
}Interpreting canxFees: from is a Unix millisecond timestamp. First entry with amt: 0 = free cancellation window. Subsequent entries = penalty after that date. Last entry typically equals full cost (no refund).
hotelBookingPrepare
Mutation · Auth requiredCreate a booking with guest details. Does NOT charge — this just prepares the booking record.
mutation HotelBookingPrepare($bookingCreateInput: BookingCreateInput!) {
hotelBookingPrepare(bookingCreateInput: $bookingCreateInput) {
preparedBookingId
fiatPrice
currency
isUpfrontPaid
essentialInformation
payment
discount { amount currency }
taxes { feeTitle value currency isIncludedInPrice }
}
}bookingCreateInput
| Field | Type | Required | Description |
|---|---|---|---|
| quoteId | String | No | Full quoteId from getHotelRooms — with the _hotelId suffix, unlike check_cancellation_policy. Nullable in the schema, but the booking is meaningless without it: always send it. |
| rooms | [RoomBookingInput!] | No | One entry per room in search. Each entry is { adults: [RoomAdultsInput!]!, children: [ChildInput!]! }. Nullable in the schema, but always send it — a booking with no rooms is meaningless. |
| rooms[].adults | [RoomAdultsInput!]! | No | Adult guests. NON-NULL with a default of [] — identical declaration to rooms[].children, so the same rule applies: you may OMIT it, but never send null. Always send it in practice: one entry per adult, and the count must match the search exactly. |
| rooms[].adults[].title | String | No | "Mr", "Mrs", or "Ms". Nullable in the schema, but send it — suppliers reject some bookings without a title. |
| rooms[].adults[].firstName | String | No | First name. Nullable in the schema; send it — the supplier needs a real guest name. |
| rooms[].adults[].lastName | String | No | Last name. Nullable in the schema; send it — the supplier needs a real guest name. |
| rooms[].children | [ChildInput!]! | No | Child guests. NON-NULL with a default of []: you may OMIT it entirely, but never send null — an explicit null is rejected with BAD_USER_INPUT ('Expected non-nullable type [ChildInput!]! not to be null'), because a schema default applies only to an omitted field. Same rule for rooms[].adults. |
| contactPerson | ContactPersonInput | No | Lead contact for the booking. Nullable in the schema; always send it — the supplier and the confirmation email both need a reachable contact. |
| contactPerson.email | String | No | Contact email. Nullable in the schema; send it — it is where the booking confirmation goes. |
| contactPerson.phone | String | No | Phone with country code. Nullable in the schema; send it — suppliers use it to reach the guest. |
Example
# variables
{
"bookingCreateInput": {
"quoteId": "73513d6a-b452-449b-ae29-9797790ed569_5948584",
"rooms": [{
"adults": [
{ "title": "Mr", "firstName": "John", "lastName": "Smith" },
{ "title": "Mrs", "firstName": "Jane", "lastName": "Smith" }
],
"children": []
}],
"contactPerson": {
"title": "Mr",
"firstName": "John",
"lastName": "Smith",
"email": "[email protected]",
"phone": "+14155551234"
}
}
}
# Response
{
"data": {
"hotelBookingPrepare": {
"preparedBookingId": "698341601f3796feeed6a790",
"fiatPrice": 255.00,
"currency": "EUR",
"isUpfrontPaid": false,
"essentialInformation": ["Check-in: 14:00", "Photo ID required"],
"payment": "Cash",
"discount": null,
"taxes": [{
"feeTitle": "City Tax",
"value": "2.50",
"currency": "EUR",
"isIncludedInPrice": false
}]
}
}
}confirmB2bBooking
Mutation · Auth requiredConfirm the booking and charge the credit line. This is the billing step. Booking IDs for B2B bookings are suffixed with -CL-LT.
mutation ConfirmB2bBooking($bookingConfirmInput: BookingConfirmInput!) {
confirmB2bBooking(bookingConfirmInput: $bookingConfirmInput) {
accepted
message
serviceUnavailable # see the note below before adding this line
}
}serviceUnavailable is a recent addition. GraphQL rejects an entire query that names a field the server does not know, so if your request comes back GRAPHQL_VALIDATION_FAILED mentioning that field, drop the line and retry with accepted and message alone — those two have always been present.
A failure here is HTTP 200 — read accepted, not the status code
Every outcome, including a refusal, returns HTTP 200 with accepted telling you which it was. On accepted: false, branch on serviceUnavailable:
- absent / false — the credit line service answered and refused.
messagecarries its verdict verbatim:Insufficient credit line.,Credit line not active.,No credit line available.,User not found., orUser is not b2b user. This will not change on retry — settle the balance (see settlement) or contact us. - true — the credit line service was unreachable. This is our fault, not a decision about your credit. But do not retry blindly: the reservation may already have been applied upstream. Re-read the booking with
getBookingDetailsbefore confirming again.
accepted: true is not yet a supplier confirmation
It means the credit line was charged and the booking accepted. Placement with the supplier completes asynchronously afterwards, so a supplier-side failure does not appear in this response. Poll getBookingDetails until bookingStatus is DONE before treating the booking as placed.
bookingConfirmInput
| Field | Type | Required | Description |
|---|---|---|---|
| bookingInternalId | String! | Yes | preparedBookingId from hotelBookingPrepare |
| quoteId | String! | Yes | Same quoteId used in prepare |
| paymentMethod | String! | Yes | REQUIRED — always "CREDIT_LINE" for B2B. There is no server-side default: omitting it fails validation with BAD_USER_INPUT ("Field paymentMethod of required type String! was not provided"). The MCP tool defaults it for you; raw GraphQL does not. |
| ipAddress | String | No | Originating client IP, for fraud scoring. Optional but recommended when you are booking on behalf of an end customer. |
| lockTripPlatform | String | No | Free-text source label for your integration. |
Sample request
{
"bookingConfirmInput": {
"bookingInternalId": "69a2b7540a93415a4b4dbf13",
"quoteId": "3e242771-1c15-4356-9a3b-1cd4bfc6d9fc_4384127",
"paymentMethod": "CREDIT_LINE"
}
}Sample responses
# Success
{
"data": {
"confirmB2bBooking": {
"accepted": true,
"message": null
}
}
}
# Failure — account not B2B
{
"data": {
"confirmB2bBooking": {
"accepted": false,
"message": "User is not b2b user"
}
}
}
# Failure — credit line exhausted (a VERDICT: will not change on retry)
{
"data": {
"confirmB2bBooking": {
"accepted": false,
"message": "Insufficient credit line.",
"serviceUnavailable": null
}
}
}
# Failure — our credit line service was unreachable (NOT a credit decision)
{
"data": {
"confirmB2bBooking": {
"accepted": false,
"message": "The credit line service is temporarily unavailable, ...",
"serviceUnavailable": true
}
}
}getUserBookings
Query · Auth requiredList bookings by lifecycle state.
query GetUserBookings($input: BookingListingInput!) {
getUserBookings(input: $input) {
bookings {
id
booking_id
hotel_name
hotel_id
arrival_date
nights
status
rooms_count
created_on
has_details
isB2B
hotel_photo
}
}
}input
| Field | Type | Required | Description |
|---|---|---|---|
| type | String! | Yes | "UPCOMING", "COMPLETED", "CANCELLED", or "PENDING" |
Sample response
{
"data": {
"getUserBookings": {
"bookings": [{
"id": "698341601f3796feeed6a790",
"booking_id": "LT-2026-ABC123",
"hotel_name": "The Kayon Jungle Resort",
"hotel_id": "5948584",
"arrival_date": "2027-08-23",
"nights": 2,
"status": "CONFIRMED",
"rooms_count": 1,
"created_on": "2026-02-04T10:30:00Z",
"has_details": true,
"isB2B": true,
"hotel_photo": "https://imagecontent.net/images/full/hotel-5948584.jpeg"
}]
}
}
}getBookingDetails
Query · Auth requiredFull booking record — hotel, guests, price breakdown, cancellation policy.
query GetBookingDetails($bookingId: String!) {
getBookingDetails(bookingId: $bookingId) {
success
data {
bookingId
bookingReferenceId
providerReference
status
hotel { id name address city country phone email starRating }
checkIn
checkOut
rooms {
roomName
mealType
guests { firstName lastName }
price
}
contactPerson { firstName lastName email phone }
totalPrice
currency
paymentStatus
cancellationPolicy {
isRefundable
freeCancellationUntil
fees { fromDate amount currency }
}
specialRequests
createdAt
confirmedAt
}
}
}Variables
| Field | Type | Required | Description |
|---|---|---|---|
| bookingId | String! | Yes | Booking ID from getUserBookings or preparedBookingId |
cancelBookingRequest
Mutation · Auth requiredRequest cancellation. Use confirmed: false as a dry-run first to verify penalty terms, then confirmed: true to execute. Refund (if any) is transferred back to the credit line automatically.
mutation CancelBookingRequest($cancelBookingInput: CancelBookingInput!) {
cancelBookingRequest(cancelBookingInput: $cancelBookingInput) {
isCancellationRequested
}
}cancelBookingInput
| Field | Type | Required | Description |
|---|---|---|---|
| bookingId | String! | Yes | Booking ID to cancel |
| confirmed | Boolean! | Yes | false = dry-run (preview), true = execute cancellation |
Sample response
{
"data": {
"cancelBookingRequest": {
"isCancellationRequested": true
}
}
}Credit line: statement & settlement
REST · Auth requiredRead your own statement and settle your balance by bank wire. These are REST endpoints, not GraphQL, and they live on a different host from the booking API. They take no account identifier — the credit line is resolved server-side from your bearer token, so you can only ever read and pay your own. Authenticate exactly as for the GraphQL API (see Authentication); the token must be 2FA-complete.
Every error carries a stable code alongside the message — branch on the code, never on the message text. Codes: NO_TOKEN, INVALID_TOKEN, NO_CREDIT_LINE, INVALID_REQUEST, INVALID_AMOUNT, NOT_CANCELLABLE, BANK_DETAILS_UNAVAILABLE, STATEMENT_UNAVAILABLE, SETTLEMENT_UNAVAILABLE, AUTH_UNAVAILABLE, LOOKUP_UNAVAILABLE. A transient outage answers 500 or 503 — never 401, so never treat an outage as a session expiry and never sign the user out on one. Specifically: AUTH_UNAVAILABLE, LOOKUP_UNAVAILABLE and BANK_DETAILS_UNAVAILABLE answer 503; STATEMENT_UNAVAILABLE and SETTLEMENT_UNAVAILABLE answer 500. Retry on both — keying a retry to 503 alone skips the two most likely outages.
GET /api/b2b/credit-line/statement
Your balance, bookings and payments. Optional from / to (ISO dates) filter the booking rows by due date (cancellation deadline, or arrival date when a booking has none); omit both for everything.
curl "https://users.locktrip.com/api/b2b/credit-line/statement?from=2026-07-01&to=2026-07-31" \
-H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9..."
# Response (200 OK) — abridged:
{
"success": true,
"account": { "name": "...", "currency": "EUR", "creditLimit": 10600, "periodType": "..." },
"balance": { "due": 498.82, "pending": 8204.97, "availableCredit": 1896.21,
"creditRemaining": 0, "evaluatedAt": "2026-10-01T12:00:00.000Z", "source": "fifo-realtime" },
"balanceAsRecorded": { "currentDebt": 498.82, "pendingDebt": 8204.97, "availableCredit": 1896.21 }, // deprecated alias
"period": { "from": "2026-07-01", "to": "2026-07-31", "excludedWithoutCancellationDate": { "count": 0, "amount": 0 } },
"bookingsMatured": { "label": "...", "rows": [ ... ], "count": 3, "grossTotal": 498.82 },
"bookingsPending": { "label": "...", "rows": [ ... ], "count": 19, "grossTotal": 8204.97 },
"paymentsReceived": { "label": "...", "rows": [ ... ], "count": 0, "totalToDate": 0, "since": "2026-01-01T00:00:00.000Z" }
}Read balance — it is your real-time position
balance.due is what you owe now: every booking whose due date has passed, minus every payment received, applied oldest booking first — both counted from 1 January 2026 (paymentsReceived.since); the ledger before that date is closed. balance.pending is bookings not yet due. A negative due means you are in credit. Each booking row carries its dueDate, paid, outstanding, paymentStatus and the payments that settledBy it, and is labelled by bucket — current (due) or pending (not yet due). The rows add up to the balance.balanceAsRecorded is a deprecated alias carrying the same values under the old names; read balance. Bookings are approved against your credit limit at checkout.
POST /api/b2b/credit-line/settlement
Create a settlement intent and receive the SEPA details plus a unique reference to quote on the transfer. This records an intention, not a payment — no balance moves until the wire actually lands and is reconciled. Minimum 0.01; there is deliberately no maximum. Returns 201.
curl -X POST https://users.locktrip.com/api/b2b/credit-line/settlement \
-H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9..." \
-H "Content-Type: application/json" \
-d '{"amount": 2500.00}'
# Response (201 Created):
{
"success": true,
"settlement": {
"reference": "CL5I5ONKXR1FAMH", "status": "AWAITING_FUNDS",
"amount": 2500, "currency": "EUR",
"createdAt": "...", "expiresAt": "...",
"receivedAmount": null, "receivedAt": null, "recordedAt": null
},
"bankTransfer": {
"beneficiary": "...", "iban": "...", "bic": "...",
"amount": 2500, "currency": "EUR", "reference": "CL5I5ONKXR1FAMH"
},
"notice": "Anything you send above your outstanding balance is held as credit against your future bookings — it is not refunded."
}Quote the reference on the transfer. It is what matches your wire to your account. Show the notice to whoever authorises the payment: overpayment is accepted and carried as credit against future bookings, not refunded. If bank details are unconfigured the call fails 503 BANK_DETAILS_UNAVAILABLE rather than returning a blank IBAN — never treat a missing IBAN as payable.
GET /api/b2b/credit-line/settlements
Your intents, newest first, plus payable — the subset still open and unexpired — and the same bank block. Reopen an existing reference from here instead of creating a second intent for a payment you already have instructions for.
curl https://users.locktrip.com/api/b2b/credit-line/settlements \
-H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9..."
# Response (200 OK):
{ "success": true, "settlements": [ ... ], "payable": [ ... ],
"bank": { "beneficiary": "...", "iban": "...", "bic": "..." },
"notice": "..." }Expiry is a display rule only. An expired intent stops being offered as payable, but it is not cancelled and not voided — a wire that lands against it days later is still recorded in full. On a credit line the money is still owed.
POST /api/b2b/credit-line/settlement/{reference}/cancel
Abandon an intent you are not going to pay — a mistyped amount, typically. Only an AWAITING_FUNDS intent is cancellable; anything else answers 409 NOT_CANCELLABLE. The reference is scoped to your own credit line, so a leaked reference cannot cancel anyone else's intent. Do not cancel an intent you have already wired against — that becomes an anomaly for a human to unpick.
curl -X POST https://users.locktrip.com/api/b2b/credit-line/settlement/CL5I5ONKXR1FAMH/cancel \
-H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9..."
# Response (200 OK):
{ "success": true, "settlement": { "reference": "CL5I5ONKXR1FAMH", "status": "CANCELLED", ... } }Quick Reference
Date format: DD/MM/YYYY (e.g. "23/08/2027")
Page numbers: 1-based (start with page 1)
Completion field: isResultCompleted === true
hotelId type: NUMBER for getHotelRooms, STRING for hotelCancellationPolicies
packageId: quoteId.split('_')[0]
Guest structure: rooms[].adults[] with title, firstName, lastName
Guest count: Must match search exactly
First poll wait: 2+ seconds mandatory
Search time: 15–30 seconds typical
Session lifetime: ~30 minutes
Flow: locationSearch → hotelSearch → WAIT 2s → hotelSearchResults
→ getHotelRooms → hotelCancellationPolicies
→ hotelBookingPrepare → confirmB2bBooking
Manage: getUserBookings → getBookingDetails → cancelBookingRequestReady to integrate?
A complete end-to-end test script (e2e-graphql-test.ts) is available on request — it exercises the full flow from locationSearch through confirmed booking and cancellation. Contact your account manager or submit the business inquiry form to get B2B credentials.
Prefer an AI-native REST / JSON-RPC / SSE interface? See MCP docs | Back to Business
LockTrip B2B GraphQL API · Document v2.2.0