Book an order on credit#
/v1/bookingsRe-prices the order and, only if its current total still matches acceptedTotal, runs the synchronous credit gate (402 on a denial, with the shortfall and available credit) before starting the booking. externalBookingId is the idempotency key: a replay with identical contents returns the same booking, charged once; a replay with different contents is 409, never a silent second booking. An order belonging to another agent group is 404, identically to one that doesn't exist.
202 is a real, expected outcome, not an edge case. The booking runs through a durable, external-first workflow; if it hasn't resolved by the time this call returns (e.g. the workflow started but hasn't finished), the response is 202 with status: "PROCESSING" — the credit charge IS authorized (chargedAmount reflects it), but bookingId is null until the booking actually lands. Poll GET /v1/bookings?externalBookingIds= with the same externalBookingId to discover the outcome once it resolves — this is the same idempotency key, so a repeat POST also converges on the resolved booking (same externalBookingId, same contents) rather than charging or booking a second time.
A replay never edits guests that are already on the order. Guests are matched on phone or email; anyone already present is left exactly as first submitted and only genuinely new guests are added. Re-POSTing to correct a typo in a guest's name, salutation or date of birth therefore has no effect, and the response still shows the original details — by replay time the booking may already have snapshotted them, and patching only one side would leave the order and the booking disagreeing. To change guest details, use a new order and a new externalBookingId. See the externalBookingId field docs.
Optional push callbacks. Supply callbackUrl and we POST the terminal outcome (booking.confirmed / booking.cancelled / booking.failed) to it instead of making you poll — signed with X-Elivaas-Signature: t=<unix-seconds>,v1=<hex> (HMAC-SHA256 over t + "." + rawBody, using your agent group's callback signing secret) and carrying a stable X-Elivaas-Event-Id because delivery is at-least-once. See the callbackUrl field docs for the full verification recipe, the payload shape, and the URL restrictions (https only, no private/metadata addresses, no redirects) that are enforced here with a 400. Polling is unaffected and stays the fallback: a callback that cannot be delivered is retried with backoff and then given up on.
Request
curl -X POST 'https://partner-api.elivaas.com/v1/bookings' \
-u "$ELIVAAS_API_KEY:" \
-H 'Content-Type: application/json' \
-d '{"orderId":"3fa85f64-5717-4562-b3fc-2c963f66afa6","externalBookingId":"po-48213-booking-1","acceptedTotal":0,"primaryGuest":{"guestId":"string","salutation":"string","firstName":"string","lastName":"string","email":"string","phone":"string","countryCode":"string","city":"string","dob":"2026-08-01","anniversary":"2026-08-01","position":0},"additionalGuests":[{"guestId":"string","salutation":"string","firstName":"string","lastName":"string","email":"string","phone":"string","countryCode":"string","city":"string","dob":"2026-08-01","anniversary":"2026-08-01","position":0}],"specialRequests":"string","callbackUrl":"https://example-pms.com/webhooks/bard-bookings"}'const res = await fetch('https://partner-api.elivaas.com/v1/bookings', {
method: 'POST',
headers: {
Authorization: 'Basic ' + Buffer.from(process.env.ELIVAAS_API_KEY + ':').toString('base64'),
'Content-Type': 'application/json',
},
body: JSON.stringify({"orderId":"3fa85f64-5717-4562-b3fc-2c963f66afa6","externalBookingId":"po-48213-booking-1","acceptedTotal":0,"primaryGuest":{"guestId":"string","salutation":"string","firstName":"string","lastName":"string","email":"string","phone":"string","countryCode":"string","city":"string","dob":"2026-08-01","anniversary":"2026-08-01","position":0},"additionalGuests":[{"guestId":"string","salutation":"string","firstName":"string","lastName":"string","email":"string","phone":"string","countryCode":"string","city":"string","dob":"2026-08-01","anniversary":"2026-08-01","position":0}],"specialRequests":"string","callbackUrl":"https://example-pms.com/webhooks/bard-bookings"}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const data = await res.json();import os, requests
res = requests.post(
"https://partner-api.elivaas.com/v1/bookings",
auth=(os.environ["ELIVAAS_API_KEY"], ""),
json={"orderId":"3fa85f64-5717-4562-b3fc-2c963f66afa6","externalBookingId":"po-48213-booking-1","acceptedTotal":0,"primaryGuest":{"guestId":"string","salutation":"string","firstName":"string","lastName":"string","email":"string","phone":"string","countryCode":"string","city":"string","dob":"2026-08-01","anniversary":"2026-08-01","position":0},"additionalGuests":[{"guestId":"string","salutation":"string","firstName":"string","lastName":"string","email":"string","phone":"string","countryCode":"string","city":"string","dob":"2026-08-01","anniversary":"2026-08-01","position":0}],"specialRequests":"string","callbackUrl":"https://example-pms.com/webhooks/bard-bookings"},
timeout=30,
)
res.raise_for_status()
data = res.json(){
"bookingId": "string",
"externalBookingId": "string",
"status": "CREATED",
"chargedAmount": 0,
"availableCreditAfter": 0
}Request body#
orderIdstring<uuid>requiredThe order to book — from a prior
POST /v1/orderscall.externalBookingIdstringrequiredCaller-supplied id for this booking attempt, unique per agent group. The idempotency key: replaying the same value with identical contents returns the same booking rather than creating a second one. Max 64 characters.
A replay will not edit an existing booking's guests. On a replay, any guest already on the order — matched on phone or email — is left exactly as first submitted; only guests not yet present are added. So re-POSTing with a corrected spelling, salutation or date of birth for a guest you already sent does NOT update them, and the response will still show the original details. This is deliberate: by the time you are replaying, the booking may already have snapshotted those guests, and silently patching one side would leave the order and the booking disagreeing with nothing to tell you which is right. Use a NEW
externalBookingIdagainst a new order if the guest details themselves need to change, and contact support to amend a booking that has already confirmed.acceptedTotalnumberrequiredThe order's grand total the caller is accepting, as last quoted. Compared against a fresh re-price at booking time; a mismatch is rejected rather than booking at a different price than the caller saw.
primaryGuestobjectrequiredA guest captured on the booking (primary or additional).
additionalGuestsobject[]Additional guests on the booking, if any.
specialRequestsstringFree-text special requests, passed through to the booking.
callbackUrlstringOptional. An
httpsURL we POST the booking's TERMINAL outcome to, so you do not have to poll a202 PROCESSINGbooking. Supplying it changes nothing else: pollingGET /v1/bookings?externalBookingIds=keeps working exactly as before and remains the system of record. Omit it and nothing is ever sent.When you get called. Once, per terminal outcome:
booking.confirmed,booking.cancelled, orbooking.failed(the booking never materialised and the credit authorization was released). IntermediatePROCESSINGis never pushed. A booking that is confirmed and later cancelled produces two callbacks.Body.
{ eventId, eventType, occurredAt, status, externalBookingId, bookingId, reasonCode, reason }.statusisCONFIRMED|CANCELLED|FAILED;reasonCodeis machine-readable and non-null only on a failure (BOOKING_NOT_COMPLETED).A later outcome supersedes an earlier one — order by
occurredAt, do not assume the first one you receive is final. Two cases are expected and neither is a bug. (1) A booking we could not confirm within its window is reportedbooking.failed; if its workflow then lands late, you also receivebooking.confirmedfor the sameexternalBookingId. The later CONFIRMED is the truth. (2) A booking that is confirmed and later cancelled sendsbooking.confirmedthenbooking.cancelled. Dedupe is pereventIdonly — it does NOT collapse different event types for one booking, by design.Delivery is at-least-once — you MUST dedupe. Every request carries
X-Elivaas-Event-Id: <uuid>, stable across all retries of the same outcome, and the same id is in the body aseventId. Treat a repeat of an id you have already processed as a no-op.Verifying the signature. Each request carries
X-Elivaas-Signature: t=<unix-seconds>,v1=<hex>. To verify: take the RAW request body exactly as received (do not parse and re-serialise — that changes key order and whitespace and breaks the MAC), buildt + "." + rawBody, computeHMAC-SHA256(secret, that)hex-encoded lowercase, and compare tov1in constant time. Then reject anything wheretis more than a few minutes old — that is what makes a captured request non-replayable. Thesecretis your agent group's callback signing secret, shown once when your API credential was issued; it is not your API key, and it is never retrievable from any read endpoint. The timestamp differs between retries of the same event, so verify each request on its own.Requirements, enforced at request time with
400. Scheme must behttps. The host must resolve exclusively to public addresses — loopback, RFC1918, link-local, unique-local, carrier-grade NAT, multicast, broadcast and cloud instance-metadata addresses are all rejected. Redirects are never followed: a3xxcounts as a failed delivery. Respond2xxto acknowledge; anything else is retried with exponential backoff (6 attempts over roughly four hours) and then recorded as permanently failed, at which point polling is your fallback.
Responses#
| Status | Meaning |
|---|---|
200 | The booking resolved synchronously — CREATED on first conversion, ALREADY_EXISTS on an idempotent replay. |
202 | The charge is authorized but the booking has not resolved yet (status: "PROCESSING", bookingId: null). Re-POST the identical request later to check again. |
400 | Validation failure. The message names the offending parameter. Limits are rejected, never silently clamped: an over-length id list, a page size above 100 or a date window longer than 365 days all land here rather than coming back quietly truncated. |
401 | Missing, malformed, revoked or unknown API key, or an agent group whose distribution access is disabled. Pass the key as the HTTP Basic username with an empty password. |
402 | Credit check failed: reasonCode is CREDIT_LIMIT_EXCEEDED (with shortfall) or ACCOUNT_ON_HOLD. availableCredit reflects the group's headroom at the moment of the decision. No booking is made and no charge is posted to the ledger. |
404 | No such resource on your channel. A resource that exists but belongs to another tenant is reported identically — the two are never distinguished, so this response can't be used to enumerate ids. |
409 | Either the order's price moved since it was quoted (expectedTotal/breakup populated — re-price and resubmit with the new total) or externalBookingId was already used for a booking with different contents (those two fields absent). No booking is made either way. |
429 | Rate limit exhausted for this credential. Wait Retry-After seconds before retrying; the budget is per credential, not per IP. |
See Errors for the error body shape and which statuses are worth retrying.