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:
- Gateway URL — your checkout page, where Nuitée sends the guest and the signed booking handoff
- 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 hexFull 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.
| Field | Example | Description |
|---|---|---|
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:
| Mode | okUrl | failUrl |
|---|---|---|
redirect-back | Nuitée loading screen — Nuitée polls until the booking is confirmed, then shows the confirmation page | Nuitée error/retry page |
headless | Your partnerOkUrl — the success page you configured in the dashboard | Your 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=failedReceiving 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
- Verify the signature before rendering anything
- Check the timestamp — reject if older than 5 minutes
- Display the booking details to the guest (use
prebookIdandhotelIdto fetch from LiteAPI) - Collect payment — apply discounts, loyalty points, promotions as you see fit
- After payment outcome, send the server-to-server callback to
callbackUrl(see the getting-started guide, Step 4) - Redirect the guest to
okUrlorfailUrlwith 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/jsonPayload fields
All values are strings.
| Field | Example | Description |
|---|---|---|
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
| HTTP | Meaning |
|---|---|
2xx | Refund notification accepted |
5xx | Nuitée retries once after 500ms |
4xx | Nuitée does not retry — logs the error and captures to Sentry for manual review |
Idempotency is required. Nuitée retries once on a5xx. Your endpoint must checkrefundTxIDbefore processing and return2xxon 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
| Endpoint | Direction | Triggered by | Auth |
|---|---|---|---|
| Gateway URL | Nuitée → your server (browser form POST) | Guest clicks Pay | HMAC signature in form body |
| Refund URL | Nuitée → your server (server-to-server JSON POST) | Guest cancels confirmed booking | HMAC 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.
Updated about 2 hours ago