External Checkout — Endpoint Reference

Partner engineering teams implementing the External Checkout integration

This document is the technical reference for the two endpoints you must build on your side:

  1. Gateway URL — your checkout page, where Nuitée sends the guest and the signed booking handoff
  2. Refund URL — your refund endpoint, where Nuitée POSTs a signed refund notification on cancellation

For the integration overview, flow diagrams, and HMAC signing algorithm, see the External Checkout — Dashboard Setup Guide.


HMAC Signing Algorithm (quick reference)

Both endpoints receive signed payloads from Nuitée. Verify every request before processing it.

1. Take all payload fields except `signature`
2. Sort keys alphabetically (case-sensitive: uppercase before lowercase)
3. Concatenate: key1=value1&key2=value2&...
   (no URL-encoding)
4. Compute:
   HMAC-SHA256(
     key = sharedSecret,
     message = sortedString + sharedSecret
   )
5. Encode: lowercase hex

Full code examples (Node.js + Python) and a test vector are in the getting-started guide.


1. Gateway URL

What it is

Your checkout page. Nuitée redirects the guest here when they click Pay on the Nuitée checkout screen.

You configure this URL in the dashboard under Checkout behavior → Gateway URL.

How the request arrives

Nuitée submits a hidden HTML form via HTTP POST directly from the guest's browser. Your page receives a standard application/x-www-form-urlencoded POST body.

POST {your gatewayUrl}
Content-Type: application/x-www-form-urlencoded

clientId=Nuitée-ts-a3f2&prebookId=abc-123&hotelId=lp24354&...&signature=3a9f...
ℹ️

Why form POST and not a redirect? The payload is too large for a query string, and a POST keeps the signed data out of browser history and server logs.

Payload fields

All values are strings.

FieldExampleDescription
clientId"Nuitée-ts-a3f2"Your account identifier — matches what the dashboard shows under Connection details → Client ID
prebookId"abc-123"LiteAPI prebook reference. Use this to fetch room, rate, dates, and cancellation policy
hotelId"lp24354"LiteAPI hotel reference. Use this to fetch hotel name, photos, and address
amount"199.99"Supplier rate Nuitée will invoice you — two decimal places. This is not necessarily what you charge the guest
currency"USD"ISO 4217 currency code
bookingType"hotel"Always "hotel" for now
firstName"Jane"Guest first name
lastName"Doe"Guest last name
email"[email protected]"Guest email
okUrl"https://..."Where to redirect the guest after successful payment (see modes below)
failUrl"https://..."Where to redirect the guest after failed or cancelled payment
callbackUrl"https://..."Your server must POST the payment confirmation here (not a redirect — server-to-server)
timestamp"2026-05-14T10:00:00.000Z"ISO 8601 UTC — when this handoff was created
signature"3a9f..."HMAC-SHA256 of all above fields

What okUrl and failUrl point to

The destination depends on your integration mode:

ModeokUrlfailUrl
redirect-backNuitée loading screen — Nuitée polls until the booking is confirmed, then shows the confirmation pageNuitée error/retry page
headlessYour partnerOkUrl — the success page you configured in the dashboardYour partnerFailUrl

In both modes, you must append prebookId and status as query parameters when redirecting:

{okUrl}?prebookId=abc-123&status=success
{failUrl}?prebookId=abc-123&status=failed

Receiving and verifying the handoff


// Express example
app.post('/checkout/start', express.urlencoded({ extended: false }), (req, res) => {
  const payload = req.body; // all fields as strings

  // 1. Verify signature — reject immediately if invalid
  if (!verifySignature(payload, process.env.Nuitée_SHARED_SECRET)) {
    console.error('Invalid handoff signature', { prebookId: payload.prebookId });
    return res.status(400).send('Bad request');
  }

  // 2. Reject stale handoffs (replay protection)
  if (!isTimestampFresh(payload.timestamp)) {
    console.error('Stale handoff timestamp', { timestamp: payload.timestamp });
    return res.status(400).send('Bad request');
  }

  // 3. Store payload in session — you need callbackUrl, okUrl, failUrl later
  req.session.checkout = payload;

  // 4. Optionally fetch booking details to display to the guest
  //    GET https://api.liteapi.travel/v3.0/hotels/prebooks/{prebookId}
  //    GET https://api.liteapi.travel/v3.0/data/hotels?hotelIds={hotelId}

  // 5. Render your checkout page
  res.render('checkout', { booking: payload });
});

What you must do on this page

  1. Verify the signature before rendering anything
  2. Check the timestamp — reject if older than 5 minutes
  3. Display the booking details to the guest (use prebookId and hotelId to fetch from LiteAPI)
  4. Collect payment — apply discounts, loyalty points, promotions as you see fit
  5. After payment outcome, send the server-to-server callback to callbackUrl (see the getting-started guide, Step 4)
  6. Redirect the guest to okUrl or failUrl with the required query parameters

Session timeout

The prebook expires after 30 minutes (configurable in the dashboard under Callback TTL). If the guest does not complete checkout within this window, the booking is automatically expired. Enforce a matching session timeout on your checkout page.


2. Refund URL

What it is

Your server-to-server refund endpoint. When a guest cancels a confirmed booking on Nuitée, Nuitée calls LiteAPI to cancel the reservation and then POSTs a signed refund notification to this URL. Your backend should trigger the actual refund through your payment processor.

You configure this URL in the dashboard under Checkout behavior → Refund URL.

⚠️

Warning: If the Refund URL is not configured, Nuitée logs a warning and the refund must be handled manually. Configure this URL before going live if your integration supports cancellable bookings.

Request format

POST {your refundUrl}
Content-Type: application/json

Payload fields

All values are strings.

FieldExampleDescription
refundTxID"refund-your-tx-ref-4a2b"Nuitée-generated refund identifier. Your endpoint must be idempotent on this value — Nuitée may retry once on a 5xx response
transactionId"your-internal-tx-ref"The transaction ID your system sent in the original payment callback
amount"199.99"Refund amount from LiteAPI — two decimal places
currency"USD"ISO 4217 currency code
reason"guest_cancellation"Always "guest_cancellation" at this time
timestamp"2026-05-14T10:30:00.000Z"ISO 8601 UTC — when Nuitée triggered the refund
bookingId"litebooking-xyz"LiteAPI booking reference (included when available)
prebookId"abc-123"LiteAPI prebook reference (included when available)
signature"c4e8..."HMAC-SHA256 of all above fields

Use transactionId to look up the original payment in your system. Use refundTxID as your idempotency key.

Example payload

{
  "refundTxID": "refund-your-tx-ref-4a2b",
  "transactionId": "your-internal-tx-ref",
  "amount": "199.99",
  "currency": "USD",
  "reason": "guest_cancellation",
  "timestamp": "2026-05-14T10:30:00.000Z",
  "bookingId": "litebooking-xyz",
  "prebookId": "abc-123",
  "signature": "c4e8..."
}

Receiving and verifying the refund

// Express example
app.post('/refunds', express.json(), async (req, res) => {
  const payload = req.body;

  // 1. Verify signature
  if (!verifySignature(payload, process.env.Nuitée_SHARED_SECRET)) {
    console.error('Invalid refund signature', { refundTxID: payload.refundTxID });
    return res.status(401).send('Unauthorized');
  }

  // 2. Reject stale timestamps
  if (!isTimestampFresh(payload.timestamp)) {
    return res.status(401).send('Unauthorized');
  }

  // 3. Idempotency check — do nothing if already processed
  const alreadyProcessed = await db.refunds.findOne({
    refundTxID: payload.refundTxID
  });

  if (alreadyProcessed) {
    return res.status(200).json({ ok: true }); // safe to acknowledge again
  }

  // 4. Trigger refund via your payment processor
  //    Use payload.transactionId to find the original payment
  await paymentProcessor.refund({
    transactionId: payload.transactionId,
    amount: parseFloat(payload.amount),
    currency: payload.currency,
  });

  // 5. Record the refund
  await db.refunds.create({
    refundTxID: payload.refundTxID,
    ...payload
  });

  return res.status(200).json({ ok: true });
});

Response codes Nuitée expects

HTTPMeaning
2xxRefund notification accepted
5xxNuitée retries once after 500ms
4xxNuitée does not retry — logs the error and captures to Sentry for manual review
⚠️

Idempotency is required. Nuitée retries once on a 5xx. Your endpoint must check refundTxID before processing and return 2xx on a duplicate without re-processing the refund.

What happens if the Refund URL is not set

Nuitée logs a warning and sends a Sentry alert flagged for manual handling. The LiteAPI cancellation still completes — only the refund notification to your system is skipped. You will need to identify the cancellation from your Nuitée booking records and trigger the refund manually.


Summary

EndpointDirectionTriggered byAuth
Gateway URLNuitée → your server (browser form POST)Guest clicks PayHMAC signature in form body
Refund URLNuitée → your server (server-to-server JSON POST)Guest cancels confirmed bookingHMAC signature in JSON body

Both use the same sharedSecret and the same signing algorithm.


Demo repository

You can find a working example of this integration in a demo repository (GitHub - nuitee-white-label/wl-external-checkout-demo: Reference implementation of a partner-side checkout server for the Nuitee External Checkout payment provider. Handles handoff verification, payment processing, signed callbacks, and refund webhooks.) and see its behaviour at this URL External Checkout Demo - Incredible hotel deals


For integration support, contact the Nuitée engineering team.