Locktrip
← Back to Business

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

EnvironmentURL
ProductionPOST 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
  • hotelId is a NUMBER for getHotelRooms — use parseInt()
  • Guest count in booking must exactly match search (2 adults searched → 2 adults booked)
  • Extract packageId from quoteId: quoteId.split('_')[0]

Error Handling

ErrorCauseSolution
0 results after 30 pollsPolling started too fastWait 2+ seconds before first poll
Guest count mismatchAdults count differs from searchMatch exactly: 2 adults searched = 2 in booking
User is not b2b userAccount lacks B2B statusContact LockTrip to upgrade the account
400 on cancellation policyPassed full quoteIdExtract packageId: quoteId.split('_')[0]
hotelId must be a numberString hotelId passedUse parseInt(hotelId, 10) for getHotelRooms
Session expiredFlow took longer than ~30 minRestart from hotelSearch
Invalid date formatUsed ISO or US formatUse DD/MM/YYYY (e.g. "23/08/2027")

Operation Reference

Every GraphQL query and mutation with sample variables and real responses.

locationSearch

Query · Public

Find the regionId for a destination. Never hardcode regionId — IDs can change.

query LocationSearch($query: String!) {
  locationSearch(query: $query) {
    locationData
  }
}

Variables

FieldTypeRequiredDescription
queryString!YesCity, 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 · Public

Initiate 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

FieldTypeRequiredDescription
regionIdStringNoFrom locationSearch. Required unless lat/lng provided.
latitudeFloatNoLatitude (-90 to 90). Use with longitude instead of regionId.
longitudeFloatNoLongitude (-180 to 180).
radiusInMetersIntNoSearch radius in meters (default: 30000, max: 100000). Only with lat/lng.
startDateString!YesCheck-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.
endDateString!YesCheck-out date — DD/MM/YYYY
currencyString!YesREQUIRED — currency code (e.g. "EUR", "USD"). Omitting it fails validation with BAD_USER_INPUT.
rooms[RoomSearchInput!]!YesArray of room configurations
rooms[].adultsInt!YesNumber of adults (1–4)
rooms[].children[ChildrenInput!]NoChild 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.
uuidString!YesUnique ID per search (client-generated, for dedup)
natStringNoNationality — 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 · Public

Poll 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

FieldTypeRequiredDescription
searchKeyString!YesFrom hotelSearch
pageIntNoPage number — 1-based (default: 1)
sizeIntNoResults per page (default: 100, max: 5000)
filtersObjectNoOptional filters (price range, stars, amenities, boards)
sortParams[String!]NoSort field + direction (e.g. ["price", "asc"])
singleHotelIdIntNoRestrict 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 · Public

Get 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

FieldTypeRequiredDescription
searchKeyString!YesFrom hotelSearch
hotelIdFloat!YesexternalId as NUMBER — use parseInt(). The schema declares it Float; send a number, not a string.
startDateString!YesDD/MM/YYYY. MUST be in the future; a past date is rejected. Example dates are illustrative — substitute your own future window.
endDateString!YesDD/MM/YYYY
regionIdString!YesFrom locationSearch
rooms[RoomSearchInput!]!YesSame shape as hotelSearch
currencyString!YesCurrency code
natString!Yes2-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 · Public

Get 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

FieldTypeRequiredDescription
searchKeyString!YesFrom hotelSearch
hotelIdString!YesexternalId as STRING (note: this is string here, not number)
packageIds[String!]!YesArray 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 required

Create 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

FieldTypeRequiredDescription
quoteIdStringNoFull 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!]NoOne 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!]!NoAdult 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[].titleStringNo"Mr", "Mrs", or "Ms". Nullable in the schema, but send it — suppliers reject some bookings without a title.
rooms[].adults[].firstNameStringNoFirst name. Nullable in the schema; send it — the supplier needs a real guest name.
rooms[].adults[].lastNameStringNoLast name. Nullable in the schema; send it — the supplier needs a real guest name.
rooms[].children[ChildInput!]!NoChild 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.
contactPersonContactPersonInputNoLead contact for the booking. Nullable in the schema; always send it — the supplier and the confirmation email both need a reachable contact.
contactPerson.emailStringNoContact email. Nullable in the schema; send it — it is where the booking confirmation goes.
contactPerson.phoneStringNoPhone 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 required

Confirm 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. message carries its verdict verbatim: Insufficient credit line., Credit line not active., No credit line available., User not found., or User 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 getBookingDetails before 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

FieldTypeRequiredDescription
bookingInternalIdString!YespreparedBookingId from hotelBookingPrepare
quoteIdString!YesSame quoteId used in prepare
paymentMethodString!YesREQUIRED — 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.
ipAddressStringNoOriginating client IP, for fraud scoring. Optional but recommended when you are booking on behalf of an end customer.
lockTripPlatformStringNoFree-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 required

List 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

FieldTypeRequiredDescription
typeString!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 required

Full 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

FieldTypeRequiredDescription
bookingIdString!YesBooking ID from getUserBookings or preparedBookingId

cancelBookingRequest

Mutation · Auth required

Request 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

FieldTypeRequiredDescription
bookingIdString!YesBooking ID to cancel
confirmedBoolean!Yesfalse = dry-run (preview), true = execute cancellation

Sample response

{
  "data": {
    "cancelBookingRequest": {
      "isCancellationRequested": true
    }
  }
}

Credit line: statement & settlement

REST · Auth required

Read 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 → cancelBookingRequest

Ready 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

B2B GraphQL API Documentation - Hotel Booking Integration | LockTrip.com