sabre-client 0.2.4 → 0.3.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.es.md +294 -0
- package/README.md +237 -91
- package/dist/index.d.ts +1160 -30424
- package/dist/index.js +1244 -307
- package/dist/index.js.map +1 -1
- package/package.json +22 -40
- package/dist/index.cjs +0 -397
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -30470
package/README.md
CHANGED
|
@@ -1,148 +1,294 @@
|
|
|
1
|
-
#
|
|
1
|
+
# sabre-client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
TypeScript SDK for the Sabre APIs.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[Leer en español](./README.es.md)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
> **`0.3.0` is a complete rewrite, not an incremental update to `0.2.4`.**
|
|
8
|
+
> It is built on a different architecture (a `Sabre` REST client with a managed
|
|
9
|
+
> `AuthManager`, a typed error hierarchy, injectable logging with mandatory redaction, and
|
|
10
|
+
> runtime-agnostic Web Standard APIs). Code written against `0.2.4` will not work unchanged.
|
|
11
|
+
> Older versions stay published as they are.
|
|
10
12
|
|
|
11
|
-
##
|
|
13
|
+
## Status
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
`0.3.0` is **not released yet**. It will be released once both Tier 1 and Tier 2 of the
|
|
16
|
+
Air Booking flow are done. Until then, `npm install sabre-client` installs `0.2.4`.
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
import { Sabre } from 'sabre-client';
|
|
18
|
+
Prereleases of the rewrite are published under the `next` tag:
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
password: 'PASSWORD_GOES_HERE',
|
|
21
|
-
organization: '7TZA', // pcc
|
|
22
|
-
});
|
|
20
|
+
```sh
|
|
21
|
+
npm install sabre-client@next
|
|
23
22
|
```
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
See http://developer.sabre.com
|
|
24
|
+
The API may still change before `0.3.0`.
|
|
28
25
|
|
|
29
|
-
|
|
26
|
+
Tier 1 covers the core Air Booking flow over REST:
|
|
30
27
|
|
|
31
|
-
|
|
28
|
+
| Step | Service | Method |
|
|
29
|
+
| - | - | - |
|
|
30
|
+
| Authentication | OAuth Token Create v3 / v2 | automatic |
|
|
31
|
+
| Shopping | Bargain Finder Max v5 | `sabre.air.bargainFinderMax` |
|
|
32
|
+
| Revalidation | Revalidate Itinerary v5 | `sabre.air.revalidateItinerary` |
|
|
33
|
+
| Booking | Create Booking (Booking Management API v1, ATPCO by `flightDetails`) | `sabre.booking.create` |
|
|
34
|
+
| Booking management | Get Booking and Cancel Booking (Booking Management API v1) | `sabre.booking.get`, `sabre.booking.cancel` |
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
await sabre.authentication.OAuthTokenV2();
|
|
35
|
-
```
|
|
36
|
+
Planned: Get Seats and Get Ancillaries (Tier 2), and a `SabreLegacy` client for SOAP services.
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
## Requirements
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
- Node.js `>=22.12`, or a server-side edge runtime with `fetch` and `crypto.subtle`
|
|
41
|
+
(Cloudflare Workers, Vercel Edge Functions, Deno Deploy). The package uses only Web
|
|
42
|
+
Standard APIs.
|
|
43
|
+
- ESM only.
|
|
44
|
+
- **Never run it in a browser.** Your Sabre credentials would be exposed to anyone with
|
|
45
|
+
devtools.
|
|
40
46
|
|
|
41
|
-
|
|
42
|
-
await sabre.booking.get({
|
|
43
|
-
confirmationId: 'GLEBNY',
|
|
44
|
-
returnOnly: [],
|
|
45
|
-
});
|
|
46
|
-
```
|
|
47
|
+
## Configuration
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
The SDK never reads environment variables. Pass the configuration explicitly:
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
```ts
|
|
52
|
+
import { Sabre } from 'sabre-client';
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
const sabre = new Sabre({
|
|
55
|
+
environment: 'sandbox', // 'sandbox' (CERT) or 'production'
|
|
56
|
+
credentials: {
|
|
57
|
+
clientId: process.env.SABRE_CLIENT_ID!,
|
|
58
|
+
clientSecret: process.env.SABRE_CLIENT_SECRET!,
|
|
59
|
+
username: process.env.SABRE_EPR_USERNAME!, // EPR user, e.g. `<user>-<PCC>-AA`
|
|
60
|
+
password: process.env.SABRE_EPR_PASSWORD!,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
54
63
|
```
|
|
55
64
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
| Option | Default | Description |
|
|
66
|
+
| - | - | - |
|
|
67
|
+
| `environment` | required | `'sandbox'` → `api.cert.platform.sabre.com`, `'production'` → `api.platform.sabre.com` |
|
|
68
|
+
| `authVersion` | `'v3'` | OAuth Token Create version. `'v3'` needs `clientId`, `clientSecret`, `username` and `password`; `'v2'` only needs the EPR `username` (`V1:<user>:<PCC>:AA`) and `password` |
|
|
69
|
+
| `credentials` | required | See above |
|
|
70
|
+
| `logger` | none | Any object with `debug`, `info`, `warn` and `error` (see [Logging](#logging)) |
|
|
71
|
+
| `retry` | `{ maxRetries: 3, baseDelayMs: 500 }` | Client-wide retry policy; every request can override it |
|
|
72
|
+
| `fetch` | global `fetch` | Custom `fetch` (proxies, tests, other runtimes) |
|
|
73
|
+
| `tokenStore` | in-memory only | Shares the token between processes (see [Token reuse](#token-reuse)) |
|
|
74
|
+
|
|
75
|
+
### Authentication
|
|
76
|
+
|
|
77
|
+
You never request a token yourself. The client gets one on the first call, caches it, renews
|
|
78
|
+
it about an hour before it expires, and shares a single in-flight request between concurrent
|
|
79
|
+
calls. If Sabre rejects a token with `401`, the client gets a new one and retries that request
|
|
80
|
+
once.
|
|
81
|
+
|
|
82
|
+
## Booking a flight
|
|
83
|
+
|
|
84
|
+
The full Tier 1 flow: search, revalidate the chosen itinerary, and book it.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { Sabre, buildRevalidateItineraryRequest, buildBookingFlightDetails } from 'sabre-client';
|
|
88
|
+
|
|
89
|
+
const sabre = new Sabre({ environment: 'sandbox', credentials });
|
|
90
|
+
|
|
91
|
+
// 1. Search with Bargain Finder Max. The request is the body Sabre documents.
|
|
92
|
+
const search = {
|
|
93
|
+
OTA_AirLowFareSearchRQ: {
|
|
94
|
+
Version: '5',
|
|
95
|
+
POS: { Source: [{ PseudoCityCode: 'XXXX', RequestorID: { Type: '1', ID: '1', CompanyName: { Code: 'TN' } } }] },
|
|
96
|
+
OriginDestinationInformation: [
|
|
97
|
+
{ DepartureDateTime: '2026-12-01T10:00:00', OriginLocation: { LocationCode: 'DFW' }, DestinationLocation: { LocationCode: 'LAX' } },
|
|
98
|
+
],
|
|
99
|
+
TravelerInfoSummary: { AirTravelerAvail: [{ PassengerTypeQuantity: [{ Code: 'ADT', Quantity: 1 }] }] },
|
|
100
|
+
TPA_Extensions: { IntelliSellTransaction: { RequestType: { Name: '50ITINS' } } },
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
const found = await sabre.air.bargainFinderMax(search);
|
|
104
|
+
|
|
105
|
+
// Responses come with references already resolved: every itinerary has its legs,
|
|
106
|
+
// schedules and fares inline, no `ref` lookups needed.
|
|
107
|
+
const group = found.itineraryGroups[0]!;
|
|
108
|
+
const itinerary = group.itineraries[0]!;
|
|
109
|
+
|
|
110
|
+
// 2. Revalidate the chosen itinerary (availability and price, no inventory held).
|
|
111
|
+
const revalidated = await sabre.air.revalidateItinerary(
|
|
112
|
+
buildRevalidateItineraryRequest({ search, group, itinerary }),
|
|
113
|
+
);
|
|
114
|
+
const confirmedGroup = revalidated.itineraryGroups[0]!;
|
|
115
|
+
const confirmed = confirmedGroup.itineraries[0]!;
|
|
116
|
+
|
|
117
|
+
// 3. Book it through the ATPCO path (`flightDetails`).
|
|
118
|
+
const booking = await sabre.booking.create({
|
|
119
|
+
flightDetails: buildBookingFlightDetails({ group: confirmedGroup, itinerary: confirmed }),
|
|
120
|
+
travelers: [{ givenName: 'JANE', surname: 'DOE', passengerCode: 'ADT' }],
|
|
121
|
+
contactInfo: { emails: ['jane@example.com'], phones: ['5550000000'] },
|
|
122
|
+
});
|
|
59
123
|
|
|
60
|
-
|
|
61
|
-
await sabre.booking.modify(options);
|
|
124
|
+
console.log(booking.confirmationId); // PNR locator
|
|
62
125
|
```
|
|
63
126
|
|
|
64
|
-
|
|
127
|
+
`buildBookingFlightDetails` maps the revalidated itinerary to the `flightDetails` block of
|
|
128
|
+
Create Booking. That mapping is not documented by Sabre and has not been checked against a
|
|
129
|
+
real sandbox booking yet, so review the request before you rely on it.
|
|
65
130
|
|
|
66
|
-
|
|
131
|
+
`sabre.booking.create` is **never retried automatically**, because it is not idempotent. If
|
|
132
|
+
Sabre creates the booking with warnings or non-fatal errors, the call does not throw. Check
|
|
133
|
+
`booking.warnings` and `booking.errors`.
|
|
67
134
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
### Void Flight Tickets
|
|
135
|
+
`sabre.booking.get` reads a booking by `confirmationId`. Unlike Create Booking, the response
|
|
136
|
+
has the booking fields at the top level (`bookingId`, `flights`, `travelers`, …), as Sabre
|
|
137
|
+
defines it. Use `returnOnly` to ask only for the sections you need, which also makes the call
|
|
138
|
+
faster:
|
|
73
139
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
140
|
+
```ts
|
|
141
|
+
const pnr = await sabre.booking.get({
|
|
142
|
+
confirmationId: booking.confirmationId,
|
|
143
|
+
returnOnly: ['FLIGHTS', 'TRAVELERS'],
|
|
144
|
+
});
|
|
78
145
|
```
|
|
79
146
|
|
|
80
|
-
|
|
147
|
+
It throws `SabreBusinessError` only when there is no booking (for example
|
|
148
|
+
`BOOKING_NOT_FOUND`). If the booking exists, partial problems come back in `pnr.warnings`
|
|
149
|
+
and `pnr.errors`; an unticketed booking usually carries a `RESOURCE_UNAVAILABLE` warning
|
|
150
|
+
because it has no fare rules yet. It is a read, so transient failures are retried.
|
|
81
151
|
|
|
82
|
-
|
|
152
|
+
`sabre.booking.cancel` cancels the whole booking (`cancelAll: true`) or specific items by the
|
|
153
|
+
`itemId` that `get` returns:
|
|
83
154
|
|
|
84
|
-
```
|
|
85
|
-
await sabre.
|
|
155
|
+
```ts
|
|
156
|
+
await sabre.booking.cancel({ confirmationId: booking.confirmationId, cancelAll: true });
|
|
157
|
+
await sabre.booking.cancel({ confirmationId: pnr.bookingId, flights: [{ itemId: '2' }] });
|
|
86
158
|
```
|
|
87
159
|
|
|
88
|
-
|
|
160
|
+
Like `create`, it is **never retried automatically**. Combinations that Sabre always rejects
|
|
161
|
+
(`cancelAll` together with item lists, no `cancelAll` and no list, `offerItemId` together with
|
|
162
|
+
`flightTicketOperation`) throw a `TypeError` before any request is sent. If Sabre answers
|
|
163
|
+
`NO_ITEMS_CANCELLED`, the call throws `SabreBusinessError` even though Sabre sends it as a
|
|
164
|
+
warning, because nothing was cancelled; the errors that explain why are in `error.errors`. An
|
|
165
|
+
`END_TRANSACTION_PROBLEM` warning means the confirmation is still pending: check the result
|
|
166
|
+
with `sabre.booking.get`.
|
|
89
167
|
|
|
90
|
-
|
|
168
|
+
Every request accepts an `AbortSignal`. Shopping calls also accept a per-request retry policy:
|
|
91
169
|
|
|
92
|
-
```
|
|
93
|
-
await sabre.
|
|
170
|
+
```ts
|
|
171
|
+
await sabre.air.bargainFinderMax(search, { signal, retry: { maxRetries: 1 } });
|
|
94
172
|
```
|
|
95
173
|
|
|
96
|
-
|
|
174
|
+
## Error handling
|
|
97
175
|
|
|
98
|
-
|
|
176
|
+
Every error thrown by the SDK extends `SabreError`:
|
|
99
177
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
178
|
+
| Class | When | Extra fields |
|
|
179
|
+
| - | - | - |
|
|
180
|
+
| `SabreGatewayError` | HTTP or gateway failure (`ERR.2SG.*`, `401`, `429`, `5xx`, OAuth errors) | `protocol`, `status`, `code` |
|
|
181
|
+
| `SabreOrchestrationError` | Orchestrated service error inside a `200` (`SP.*`) | `code` |
|
|
182
|
+
| `SabreBusinessError` | Error from a service's own catalog (e.g. `No Availability`, a booking that was not created) | `service`, `code`, `warnings`, `errors` |
|
|
183
|
+
| `SabreWarning` | Non-fatal warning (e.g. `category: WARNING` in Create Booking) | `service`, `code` |
|
|
103
184
|
|
|
104
|
-
|
|
185
|
+
All of them have `retryable`, which says whether the retry policy may try again.
|
|
105
186
|
|
|
106
|
-
|
|
187
|
+
```ts
|
|
188
|
+
import { SabreBusinessError, SabreGatewayError } from 'sabre-client';
|
|
107
189
|
|
|
108
|
-
|
|
109
|
-
await sabre.
|
|
190
|
+
try {
|
|
191
|
+
await sabre.booking.create(request);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error instanceof SabreBusinessError) {
|
|
194
|
+
console.error(error.service, error.code, error.message);
|
|
195
|
+
} else if (error instanceof SabreGatewayError) {
|
|
196
|
+
console.error(error.status, error.code);
|
|
197
|
+
}
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
110
200
|
```
|
|
111
201
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
202
|
+
> [!WARNING]
|
|
203
|
+
> `SabreError.raw` holds the original response fragment, untouched, for debugging. It **can
|
|
204
|
+
> contain credentials or passenger data**. It is not enumerable, so it does not show up in
|
|
205
|
+
> `JSON.stringify` or object spreads, but you can still read `error.raw`. Never send it to a
|
|
206
|
+
> logger, error tracker or monitoring service without sanitizing it first.
|
|
115
207
|
|
|
116
|
-
|
|
117
|
-
await sabre.search.revalidate(options);
|
|
118
|
-
```
|
|
208
|
+
## Raw response
|
|
119
209
|
|
|
120
|
-
|
|
210
|
+
Every REST operation (`air.bargainFinderMax`, `air.revalidateItinerary`, `booking.create`,
|
|
211
|
+
`booking.get` and `booking.cancel`) accepts an `onRawResponse` callback in its options, next to
|
|
212
|
+
`signal`. It receives Sabre's response as it arrived, for example to store it next to the typed
|
|
213
|
+
result:
|
|
121
214
|
|
|
122
|
-
|
|
215
|
+
```ts
|
|
216
|
+
import type { SabreRawResponse } from 'sabre-client';
|
|
123
217
|
|
|
124
|
-
|
|
125
|
-
await sabre.
|
|
218
|
+
let raw: SabreRawResponse | undefined;
|
|
219
|
+
const booking = await sabre.booking.get({ confirmationId }, { onRawResponse: (r) => (raw = r) });
|
|
220
|
+
// raw: { operation: 'booking.get', protocol: 'rest', status, headers, body, attempts }
|
|
126
221
|
```
|
|
127
222
|
|
|
128
|
-
|
|
223
|
+
- It is called **once**, with the last HTTP attempt, before the operation returns or throws.
|
|
224
|
+
It is also called when the operation throws `SabreBusinessError` (a `200` with `errors[]`) or
|
|
225
|
+
`SabreGatewayError` (a non-`2xx`). After retries, `attempts` counts every HTTP attempt.
|
|
226
|
+
- It is not called if no attempt got an HTTP response (network error, abort), nor for token
|
|
227
|
+
requests. After a `401`, it gets the response of the retry with the new token.
|
|
228
|
+
- It is synchronous and its return value is ignored. If it throws, the SDK catches it, logs
|
|
229
|
+
`sabre.request.rawResponseHookFailed` without the body, and the operation carries on: a
|
|
230
|
+
failing callback never makes `booking.create` lose its `confirmationId`. To persist
|
|
231
|
+
asynchronously, keep the value and write it after the call returns.
|
|
232
|
+
|
|
233
|
+
> [!WARNING]
|
|
234
|
+
> `body` is the raw response and **contains passenger data**. The SDK never sends it to the
|
|
235
|
+
> logger. Where you store it is up to you.
|
|
236
|
+
|
|
237
|
+
## Retries
|
|
238
|
+
|
|
239
|
+
Only idempotent operations are retried: Bargain Finder Max and Revalidate Itinerary (both
|
|
240
|
+
are read-only `POST`s). Create Booking is never retried. Retries use exponential backoff with
|
|
241
|
+
full jitter and a 500 ms floor, on `429`, `500`, `503`, `504`, `SP.PROVIDER_ERROR` and the
|
|
242
|
+
temporary errors in the Bargain Finder Max catalog.
|
|
243
|
+
|
|
244
|
+
## Token reuse
|
|
245
|
+
|
|
246
|
+
By default the token lives in memory, one per `Sabre` instance. To share it between
|
|
247
|
+
processes (scripts, serverless functions), pass a `tokenStore`. The SDK does not ship any
|
|
248
|
+
implementation:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
import type { SabreTokenStore } from 'sabre-client';
|
|
252
|
+
|
|
253
|
+
const tokenStore: SabreTokenStore = {
|
|
254
|
+
async get(key) {
|
|
255
|
+
return JSON.parse((await kv.get(key)) ?? 'null') ?? undefined;
|
|
256
|
+
},
|
|
257
|
+
async set(key, token) {
|
|
258
|
+
await kv.put(key, JSON.stringify(token));
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
```
|
|
129
262
|
|
|
130
|
-
|
|
263
|
+
`key` identifies the environment, the OAuth version and a hash of the credential identity,
|
|
264
|
+
so one store can serve several clients. It contains no secrets, but the stored token is one:
|
|
265
|
+
protecting it is up to you. If `get` or `set` throws, the SDK falls back to its in-memory
|
|
266
|
+
cache.
|
|
131
267
|
|
|
132
|
-
|
|
133
|
-
npm install
|
|
134
|
-
```
|
|
268
|
+
## Logging
|
|
135
269
|
|
|
136
|
-
|
|
270
|
+
Logging is off unless you pass a `logger`. It is compatible with `console` and `winston`;
|
|
271
|
+
for `pino`, swap the arguments:
|
|
137
272
|
|
|
138
|
-
```
|
|
139
|
-
|
|
273
|
+
```ts
|
|
274
|
+
const sabre = new Sabre({
|
|
275
|
+
environment: 'sandbox',
|
|
276
|
+
credentials,
|
|
277
|
+
logger: {
|
|
278
|
+
debug: (m, c) => pino.debug(c, m),
|
|
279
|
+
info: (m, c) => pino.info(c, m),
|
|
280
|
+
warn: (m, c) => pino.warn(c, m),
|
|
281
|
+
error: (m, c) => pino.error(c, m),
|
|
282
|
+
},
|
|
283
|
+
});
|
|
140
284
|
```
|
|
141
285
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
286
|
+
The SDK logs structured events (`sabre.request.start`, `sabre.request.end`,
|
|
287
|
+
`sabre.request.retry`, `sabre.request.rawResponseHookFailed`, `sabre.auth.token.refresh`,
|
|
288
|
+
`sabre.auth.token.store`), never raw
|
|
289
|
+
request or response bodies. Secrets, the `Authorization` header and passenger data are always
|
|
290
|
+
redacted before they reach your logger.
|
|
145
291
|
|
|
146
|
-
##
|
|
292
|
+
## Changelog
|
|
147
293
|
|
|
148
|
-
|
|
294
|
+
See [CHANGELOG.md](./CHANGELOG.md).
|