@hubbyesim/sdk 2.1.0
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/CHANGELOG.md +79 -0
- package/LICENSE +21 -0
- package/README.md +346 -0
- package/dist/index.cjs +1656 -0
- package/dist/index.d.cts +1877 -0
- package/dist/index.d.ts +1877 -0
- package/dist/index.js +1622 -0
- package/llms.txt +80 -0
- package/package.json +72 -0
- package/signing-vectors.json +87 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@hubbyesim/sdk` are recorded in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this package follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The SDK version is separate from the Partner API version it speaks (`SUPPORTED_API_VERSION`).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [2.1.0] - 2026-10-17
|
|
10
|
+
|
|
11
|
+
It includes everything prepared for 2.0.1, which was never published.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `valid_until` on `IssuedPackage` (`issuePackage`, `getPackage`) and on `EsimPackage` (`getEsim`'s `packages`): when the package's validity ends and its data stops working, activation plus its time allowance. Null until the package activates.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- The SDK now speaks Partner API 5.13.0 (`SUPPORTED_API_VERSION`), after 5.12.0: every refusal has a `code`, and a terminate spends the issue key.
|
|
20
|
+
- `getDestination` and `getDestinationBundles` take a destination as a ULID, an ISO 3166-1 alpha-2 or alpha-3 code, or its English name (exact, any letter case). The parameter is renamed from `code` to `destination`; it is positional, so your calls do not change. A value that names no destination you can sell answers `404` `not_found`.
|
|
21
|
+
- `expires_at` is described as what it is: the latest moment the package can still be activated, not when its validity ends. Use `valid_until` for that.
|
|
22
|
+
- A package that is `terminated` or `expired` answers `data_remaining_bytes: 0` (still `null` on unlimited). `data_used_bytes` stays what was used.
|
|
23
|
+
- A `404` with `code: not_found` answers the message `Not found.`. Branch on `code`, not on the message.
|
|
24
|
+
- The public surface does not change. The types change only by `valid_until`.
|
|
25
|
+
|
|
26
|
+
### Documentation
|
|
27
|
+
|
|
28
|
+
- README and `llms.txt`: "Build a shop": list destinations with `getDestinations()`, which answers `{ countries, regions }` with no `data`, and the fields of an entry; the plans of one with `getDestinationBundles()`. `getBundles()` is for a periodic sync, revalidated with its `ETag`.
|
|
29
|
+
- README and `llms.txt`: "Add a package to a traveller's eSIM": each traveller has one Universal eSIM; book again for the same `external_user_id`, then issue; the answer is `installed` once the eSIM is on the phone. There is no `topUp()` call. The Hubby apps offer a top-up only for a destination the traveller already holds, which the Partner API does not enforce, and where to find a package's destination.
|
|
30
|
+
- README: "What an issue answers" lists `valid_until` and says how it differs from `expires_at`.
|
|
31
|
+
|
|
32
|
+
- The README no longer carries Hubby's internal build and release steps, and no longer links to `AGENTS.md`, which the package does not ship. A test now fails when a shipped document links to a file the package does not ship.
|
|
33
|
+
- README and `llms.txt`: the booking example also specifies a package by destination, type and size. A new "Specify a package" section says what `bundle_id` books, the `destination` and `size` formats, the `package_type` values and both spellings, that an item specified by dimensions answers `bundle_id: null` until it is issued, the `package_strategy` refusal, and which bundle fields to pick by (sizes in binary units).
|
|
34
|
+
- README and `llms.txt`: a new "What an issue answers" section lists the fields of `issuePackage` and `getPackage`, says the package `id` is the one in `getEsim`'s `packages`, and that a second issue for the same traveller lands on the same eSIM. "What to show per `state`" says which states an item and a package answer, that an item is `queued` at once after your terminate, and that `getPackage` answers `404` `item_has_no_package` for an item with no package. The issue example says a key replays for 24 hours with `Idempotent-Replayed: true`, and is spent (`409` `idempotency_key_spent`, not retried) after a terminate.
|
|
35
|
+
- README, "Every call": which identifiers `{booking}` and `id` take, why `cancelBooking` answers `403` and how `Booking.is_cancellable` tells you first, the per-Partner rate limit on `getEsim(iccid, { refresh: true })`, and how the response and input types are named. "Errors": every refusal now has a `code` (Partner API 5.12.0), with a table of the codes you meet while integrating.
|
|
36
|
+
|
|
37
|
+
## [2.0.0] - 2026-10-16
|
|
38
|
+
|
|
39
|
+
A clean break: the generated operations are no longer the public API. Partner API 5.11.0, as before.
|
|
40
|
+
|
|
41
|
+
### Removed
|
|
42
|
+
|
|
43
|
+
- The generated `partnerApiV1*` functions. Call the matching `Hubby` method.
|
|
44
|
+
- `createHubbyClient` and its options type `HubbyClientOptions`. Use `new Hubby({ apiKey, apiSecret })`.
|
|
45
|
+
- The generated `Client` type.
|
|
46
|
+
- `orderStep` and its types. The README's "What to show per `state`" table replaces it.
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- `Hubby`: built once from the credentials, with one method per Partner API operation, named in the Partner guide's words (`getBundles`, `createBooking`, `issuePackage`, `getPackage`, `terminatePackage`, `getEsim` and the rest). Path identifiers come first, the request fields in one object. A method resolves with the response body; `cancelBooking` and `terminatePackage` resolve `undefined`.
|
|
51
|
+
- `HubbyApiError`, thrown on every non-`2xx` answer, with `status`, `code`, `message`, `errors`, `body` and `response`.
|
|
52
|
+
- A type for every input and response, by name (`CreateBookingInput`, `IssuePackageInput`, `BookingResponse`, `PackageResponse` and the rest), and the resource types.
|
|
53
|
+
- Per-call `signal` and `headers` on every method.
|
|
54
|
+
- Production (`https://core.hubbyesim.com/api`) is the default `baseUrl`.
|
|
55
|
+
|
|
56
|
+
## [1.0.1] - 2026-10-16 (unpublished)
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
|
|
60
|
+
- Staged from GitHub Actions through npm trusted publishing and approved on npm by a maintainer with 2FA.
|
|
61
|
+
- The README examples and the signing vectors use neutral sample names. The two request vectors with new inputs have new expected signatures. The signing scheme does not change.
|
|
62
|
+
- The code and the API are the same as in 1.0.0.
|
|
63
|
+
|
|
64
|
+
## [1.0.0] - 2026-10-16 (unpublished)
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
|
|
68
|
+
- First release, for Partner API 5.11.0.
|
|
69
|
+
- `createHubbyClient`: signs every request with the Partner API v1 HMAC scheme, adds an `Idempotency-Key` to every POST and reuses it on each retry, and retries `429` answers and network errors, waiting out `Retry-After`.
|
|
70
|
+
- `createHubbyClient` throws at construction, naming the option, for an empty `apiKey` or `apiSecret` or a `baseUrl` that is not an absolute URL.
|
|
71
|
+
- Every request sends `User-Agent: hubbyesim-sdk/<version>` unless the caller sets one.
|
|
72
|
+
- An abort during a retry wait rejects at once, and no further attempt is sent.
|
|
73
|
+
- A typed function for every Partner API operation, generated from its OpenAPI document.
|
|
74
|
+
- `signRequest` for sending with your own HTTP client, and `signing-vectors.json` with known-answer vectors.
|
|
75
|
+
- `verifyWebhook` for webhook signatures and timestamps.
|
|
76
|
+
- `orderStep`, which maps an item's `state` to the screen to show.
|
|
77
|
+
- `LICENSE` (MIT), this changelog, npm metadata (keywords, author, bug tracker, homepage), and a `./package.json` export.
|
|
78
|
+
- A security contact in the README.
|
|
79
|
+
- `llms.txt`, shipped in the package: a summary, one flow and the links a coding agent needs to integrate the SDK.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hubby
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# @hubbyesim/sdk
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@hubbyesim/sdk)
|
|
4
|
+
|
|
5
|
+
The official TypeScript SDK for the **Hubby Partner API**. Use it to sell Hubby eSIMs in your own booking flow or app: book, issue the eSIM, show the install screen, terminate, and read status and usage, with no Hubby app in front of the traveller.
|
|
6
|
+
|
|
7
|
+
The SDK:
|
|
8
|
+
|
|
9
|
+
- gives you one `Hubby` object, built from your credentials once, with a method for every Partner API operation, named in the Partner guide's words;
|
|
10
|
+
- returns the response body the guide documents, and throws a `HubbyApiError` on a refusal;
|
|
11
|
+
- signs every request with the Partner API v1 HMAC scheme, so you never build the canonical string;
|
|
12
|
+
- adds an `Idempotency-Key` to every POST and reuses it on each retry, so a retry after a timeout never books or issues twice;
|
|
13
|
+
- retries `429` answers and network errors, waiting out `Retry-After`;
|
|
14
|
+
- verifies webhook signatures and timestamps.
|
|
15
|
+
|
|
16
|
+
SDK 2.0.0 speaks Partner API **5.11.0** (`SUPPORTED_API_VERSION`). The SDK has its own version number. A new Partner API minor is additive, so this SDK keeps working against it.
|
|
17
|
+
|
|
18
|
+
Requires Node.js 24 or later (or any runtime with `fetch` and Web Crypto: Bun, Deno, edge workers). Use it on your server only. Your API secret must never reach a browser or a mobile app.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @hubbyesim/sdk
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Create a client
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { Hubby } from '@hubbyesim/sdk';
|
|
30
|
+
|
|
31
|
+
const hubby = new Hubby({
|
|
32
|
+
apiKey: process.env.HUBBY_API_KEY!, // your public key id
|
|
33
|
+
apiSecret: process.env.HUBBY_API_SECRET!,
|
|
34
|
+
// baseUrl: 'https://core-staging.hubbyesim.com/api', // staging
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Each method takes the path identifiers first, then one object with the request fields, then the per-call options. It resolves with the response body as the Partner guide documents it, and rejects with a `HubbyApiError` on a non-`2xx` answer (see "Errors").
|
|
39
|
+
|
|
40
|
+
Options:
|
|
41
|
+
|
|
42
|
+
| Option | Default | Meaning |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `baseUrl` | `https://core.hubbyesim.com/api` | The Partner API to call. Staging is `https://core-staging.hubbyesim.com/api`. A trailing slash is ignored. |
|
|
45
|
+
| `retry` | `{ maxAttempts: 3, maxDelayMs: 30000, baseDelayMs: 500 }` | Attempts per request, the first included. A `Retry-After` longer than `maxDelayMs` is returned to you rather than waited out. `false` sends each request once. |
|
|
46
|
+
| `fetch` | global `fetch` | The `fetch` to send with, for example one with a proxy or a timeout. |
|
|
47
|
+
|
|
48
|
+
`new Hubby(...)` throws at once, naming the option, when `apiKey` or `apiSecret` is empty or `baseUrl` is not an absolute URL. Every request sends `User-Agent: hubbyesim-sdk/<version>` unless you set your own `User-Agent` header on the call.
|
|
49
|
+
|
|
50
|
+
What is retried:
|
|
51
|
+
|
|
52
|
+
- `429` on any method, after `Retry-After` (else an exponential backoff from `baseDelayMs`).
|
|
53
|
+
- `409` with `code: idempotency_key_in_use`: the first attempt of the same request is still running on our side.
|
|
54
|
+
- A network error on `GET` and `POST`. A `PATCH` or `DELETE` is not repeated after a network error: a repeated terminate answers `404`, which would hide that the first one succeeded. Read the item back instead.
|
|
55
|
+
- Nothing after an abort: aborting the call's `signal` during a retry wait rejects at once with the signal's reason, and no further attempt is sent.
|
|
56
|
+
|
|
57
|
+
Every attempt is signed again, so a retry after a wait is still inside the 300-second timestamp window.
|
|
58
|
+
|
|
59
|
+
## Book, issue, install, terminate, read
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// 1. The catalogue: every bundle you can sell, with retail and net prices.
|
|
63
|
+
// Pick by destination.iso2, type, size_bytes and duration_days (see "Specify a package").
|
|
64
|
+
const catalogue = await hubby.getBundles();
|
|
65
|
+
|
|
66
|
+
// 2. Book. The SDK adds the Idempotency-Key.
|
|
67
|
+
const booked = await hubby.createBooking({
|
|
68
|
+
external_reference: 'PARTNER-LOC-4471',
|
|
69
|
+
depart_at: '2026-11-02T00:00:00Z',
|
|
70
|
+
return_at: '2026-11-09T00:00:00Z',
|
|
71
|
+
locale: 'es',
|
|
72
|
+
package_specifications: [
|
|
73
|
+
// A bundle from the catalogue: books exactly that bundle.
|
|
74
|
+
{ bundle_id: '01J…', external_user_id: 'partner-user-88213' },
|
|
75
|
+
// Or by destination, type and size: books the closest bundle.
|
|
76
|
+
{
|
|
77
|
+
destination: 'ES',
|
|
78
|
+
package_type: 'data-limited',
|
|
79
|
+
size: '3GB',
|
|
80
|
+
external_user_id: 'partner-user-88213',
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
const item = booked.data.items![0];
|
|
85
|
+
|
|
86
|
+
// 3. Issue the eSIM. The key is required on this call: create it once per issue
|
|
87
|
+
// attempt and store it with your own purchase record before the call, so a retry after a
|
|
88
|
+
// crash replays the same issue instead of being refused. Do not derive it from
|
|
89
|
+
// the item id: after a terminate, the next issue of the same item needs a new key.
|
|
90
|
+
// A key replays its first answer for 24 hours, with the header Idempotent-Replayed: true.
|
|
91
|
+
// After a terminate, the key is spent: see idempotency_key_spent under "Errors".
|
|
92
|
+
const idempotencyKey = await keyStoredWithThePurchase(); // for example crypto.randomUUID(), saved first
|
|
93
|
+
const issued = await hubby.issuePackage('PARTNER-LOC-4471', item.id, {
|
|
94
|
+
idempotencyKey,
|
|
95
|
+
external_user_id: 'partner-user-88213',
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// 4. Decide what to show from issued.data.state: see the table below.
|
|
99
|
+
// issued.data.esim.install_links.ios / .android: one-tap install on the phone itself.
|
|
100
|
+
// issued.data.esim.lpa: render as a QR code for a second device.
|
|
101
|
+
// issued.data.esim.smdp_address + matching_id: manual entry.
|
|
102
|
+
|
|
103
|
+
// 5. Terminate before the first network attach (withdrawal, changed plans).
|
|
104
|
+
// Resolves with nothing once the Partner API answers 204.
|
|
105
|
+
await hubby.terminatePackage('PARTNER-LOC-4471', item.id, {
|
|
106
|
+
reason: 'changed_plans',
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// 6. Status and remaining data. `refresh: true` reads the network now.
|
|
110
|
+
const esim = await hubby.getEsim(issued.data.esim.iccid, { refresh: true });
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Build a shop
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// The shop's list. The answer is { countries, regions }, with no `data` around it.
|
|
117
|
+
// Each entry: id, name, type, iso2, iso3, iso3s, i18n_name; a region also has slug and country_names.
|
|
118
|
+
const { countries, regions } = await hubby.getDestinations();
|
|
119
|
+
|
|
120
|
+
// The plans of the destination the traveller opens, for example 'ES'.
|
|
121
|
+
const plans = await hubby.getDestinationBundles(opened);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`getDestination` and `getDestinationBundles` take a destination as a ULID (its `id`), an ISO 3166-1 alpha-2 or alpha-3 code (`ES`, `ESP`), or its English name (`Spain`). A code or a name must match exactly, in any letter case; a translated name does not resolve. A value that names no destination you can sell answers `404` `not_found`.
|
|
125
|
+
|
|
126
|
+
Do not load `getBundles()` per screen: it answers your whole catalogue. Use it for a periodic sync, for example once a day. Its answer carries an `ETag`. Send that value back as `If-None-Match` in the call's `headers`; an unchanged catalogue answers `304` with no body, which rejects with a `HubbyApiError` whose `status` is `304`. The method resolves with the body only, so read the `ETag` in the `fetch` you pass to `new Hubby` (see "Create a client").
|
|
127
|
+
|
|
128
|
+
### Specify a package
|
|
129
|
+
|
|
130
|
+
A package specification names its package in one of two ways:
|
|
131
|
+
|
|
132
|
+
- **By `bundle_id`**, from `getBundles()`. It books exactly that bundle. `destination`, `package_type`, `size` and `package_duration` are read from the bundle and ignored when you send them. A bundle outside your catalogue answers `422`.
|
|
133
|
+
- **By dimensions**: `destination`, `package_type` (`data-limited`, `starter` or `unlimited`) and, for `data-limited`, `size` (`'512MB'`, `'1GB'`, `'3GB'`, …). It books the closest bundle. The item answers `bundle_id: null` until it is issued; its `size_bytes` shows the size it will get.
|
|
134
|
+
|
|
135
|
+
`destination` is a country or a region: an ISO 3166-1 alpha-2 or alpha-3 code (`ES`, `ESP`), a region's code (for example `EU`), or the destination's English name (`Spain`, `Europe`), in any letter case. The `iso2` or `iso3` of a catalogue bundle's `destination` works as it is.
|
|
136
|
+
|
|
137
|
+
When `getPartner()` reports a `package_strategy`, the strategy chooses the bundle: omit `bundle_id` (it answers `422`) and the fields the strategy sets.
|
|
138
|
+
|
|
139
|
+
To pick from the catalogue, read each bundle's `destination.iso2`, `type`, `size_bytes` and `duration_days`. Sizes count in binary units: `1GB` is `1073741824` bytes, `3GB` is `3221225472`.
|
|
140
|
+
|
|
141
|
+
A specification accepts the type as `data-limited` or `data_limited`; `Bundle.type` and an item's `package_type` answer `data_limited`, so you may copy the catalogue's value into a specification. (Webhooks spell it `data-limited`.)
|
|
142
|
+
|
|
143
|
+
### What an issue answers
|
|
144
|
+
|
|
145
|
+
`issuePackage` and `getPackage` answer the package: `id`, `state`, `bundle_id`, `issued_at`, `installed_at`, `activated_at`, `expires_at`, `valid_until`, `terminated_at`, and `esim` with `iccid`, `installed_at`, `install_links` (`ios`, `android`), `lpa`, `smdp_address` and `matching_id`. The package's `id` is the same `id` that `getEsim` lists under `packages`, so the two reads name one package.
|
|
146
|
+
|
|
147
|
+
`valid_until` is when the package's validity ends and its data stops working: activation plus the package's time allowance. It is null until the package activates (show "starts on first use"). `expires_at` is not that: it is the latest moment the package can still be activated. Each package in `getEsim`'s `packages` carries both too.
|
|
148
|
+
|
|
149
|
+
A second issue for the same traveller puts the new package on the same eSIM, with the same ICCID. Before the eSIM is on the phone, the answer is `redeemed` with the same install material again; once it is on the phone, `installed`.
|
|
150
|
+
|
|
151
|
+
### What to show per `state`
|
|
152
|
+
|
|
153
|
+
An item (in `createBooking`, `getBooking` and `getBookings`) and a package (in `issuePackage`, `getPackage` and `getEsim`'s `packages`) each have a `state`. An item answers its package's `state` once it has one; before that it is `queued`, `unavailable`, `redeemed` or `cancelled`. A package's `state` is one of `redeemed`, `installed`, `activated`, `expired` or `terminated`. Read the one you have and show:
|
|
154
|
+
|
|
155
|
+
| `state` | Show |
|
|
156
|
+
|---|---|
|
|
157
|
+
| `queued` | Issue: call `issuePackage`. |
|
|
158
|
+
| `unavailable` | Nothing to issue: cancel the booking or book again. |
|
|
159
|
+
| `redeemed` | Install: the one-tap `install_links` on the phone itself, `lpa` as a QR code for a second device, or `smdp_address` plus `matching_id` for manual entry. |
|
|
160
|
+
| `installed` | Ready: the eSIM is already on the phone. Skip the install screen and say the new package is ready. |
|
|
161
|
+
| `activated` | Usage: the remaining data from `getEsim`. |
|
|
162
|
+
| `expired` | Ended: offer a new booking. |
|
|
163
|
+
| `terminated` | The package ended before it attached. After your `terminatePackage`, its item answers `queued` at once. Issue again with a new key (see `idempotency_key_spent` under "Errors"). Or cancel the booking. |
|
|
164
|
+
| `cancelled` | Cancelled. `cancelBooking` cancels every `queued` item of the booking. |
|
|
165
|
+
| a state newer than this SDK | A neutral status. Do not fail. |
|
|
166
|
+
|
|
167
|
+
An item with no package has no package to read: see `item_has_no_package` under "Errors".
|
|
168
|
+
|
|
169
|
+
### Add a package to a traveller's eSIM
|
|
170
|
+
|
|
171
|
+
Each traveller has one Universal eSIM, and every package you issue for them attaches to it. To add a package, book again for the same `external_user_id`, then issue. There is no `topUp()` call; this is it.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
const topUp = await hubby.createBooking({
|
|
175
|
+
external_reference: 'PARTNER-LOC-5120',
|
|
176
|
+
package_specifications: [
|
|
177
|
+
{
|
|
178
|
+
destination: 'ES',
|
|
179
|
+
package_type: 'data-limited',
|
|
180
|
+
size: '3GB',
|
|
181
|
+
external_user_id: 'partner-user-88213', // the same traveller
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
const added = await hubby.issuePackage('PARTNER-LOC-5120', topUp.data.items![0].id, {
|
|
186
|
+
idempotencyKey: topUpKey, // a new key, stored first, as in step 3
|
|
187
|
+
external_user_id: 'partner-user-88213',
|
|
188
|
+
});
|
|
189
|
+
// added.data.esim.iccid is the traveller's eSIM, as before.
|
|
190
|
+
// added.data.state is 'installed' once the eSIM is on the phone: skip the install screen.
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The Hubby apps offer a top-up only for a destination the traveller already holds; any other destination is a purchase from the shop. The Partner API does not enforce this, so your app must. `getEsim` names only each package's `bundle_id` and `booking_id`, so take a package's destination from its booking's item (`getBooking`), or from your own record of what you sold.
|
|
194
|
+
|
|
195
|
+
### Every call
|
|
196
|
+
|
|
197
|
+
| Method | Partner API | Resolves with |
|
|
198
|
+
|---|---|---|
|
|
199
|
+
| `getPartner()` | `GET /v1/me` | your Partner and its settings |
|
|
200
|
+
| `getBundles()` | `GET /v1/bundles` | the Bundles you sell, for a periodic sync (see "Build a shop") |
|
|
201
|
+
| `getDestinations(filters?)`, `getDestination(destination)`, `getDestinationBundles(destination)` | `GET /v1/destinations…` | `{ countries, regions }` (no `data`), one destination, or its Bundles |
|
|
202
|
+
| `createBooking(input)` | `POST /v1/bookings` | the booking |
|
|
203
|
+
| `getBookings(filters?)`, `getBooking(id)` | `GET /v1/bookings…` | a page of bookings (pass `pagination.nextCursor` back as `cursor`), or one booking |
|
|
204
|
+
| `updateBooking(id, input)` | `PATCH /v1/bookings/{id}` | the booking |
|
|
205
|
+
| `cancelBooking(id)` | `DELETE /v1/bookings/{id}` | nothing (`undefined`): the API answers `200` with a message, which the method drops |
|
|
206
|
+
| `updatePackageSpecification(bookingId, uuid, input)`, `updatePackageSpecifications(bookingId, input)` | `PATCH /v1/bookings/{booking_id}/package-specifications…` | the changed specifications |
|
|
207
|
+
| `presignBookingImport(input)` | `POST /v1/bookings/imports/presign-upload` | an upload URL |
|
|
208
|
+
| `issuePackage(booking, item, input)` | `POST /v1/bookings/{booking}/items/{item}/package` | the package, with its eSIM |
|
|
209
|
+
| `getPackage(booking, item)` | `GET /v1/bookings/{booking}/items/{item}/package` | the package |
|
|
210
|
+
| `terminatePackage(booking, item, input?)` | `DELETE /v1/bookings/{booking}/items/{item}/package` | nothing (`undefined`): the API answers `204` |
|
|
211
|
+
| `getEsim(iccid, input?)`, `getEsimInstructions(iccid, input?)` | `GET /v1/esims/{iccid}…` | the eSIM and its packages, or its install instructions |
|
|
212
|
+
| `getCompatibleDevices()`, `checkDeviceCompatibility(input)` | `/v1/device-compatibility` | the devices that take an eSIM, or the answer for one |
|
|
213
|
+
| `createRedirectToken(input)` | `POST /v1/redirect-tokens` | a redirect token |
|
|
214
|
+
| `getAnalyticsSummary(query?)`, `getAnalyticsFunnel(query?)`, `getAnalyticsTimeseries(query?)`, `getAnalyticsBreakdowns(query?)` | `GET /v1/analytics/…` | the analytics for a date range |
|
|
215
|
+
|
|
216
|
+
`{booking}` (and `id` on the booking methods) is the booking's `id` or your `external_reference`; when several of your bookings share a reference, the most recently created one answers. `{item}` is the item's `id`.
|
|
217
|
+
|
|
218
|
+
`cancelBooking` answers `403` with a `code` when the booking cannot be cancelled. The codes are:
|
|
219
|
+
|
|
220
|
+
- `booking_departed`: the booking's `depart_at` has passed. The API compares that moment with the current time, not the date.
|
|
221
|
+
- `booking_has_redeemed_promo_codes`: an item holds an issued package that is not terminated. Terminate the package first.
|
|
222
|
+
- `booking_has_paid_packages`: a traveller paid for a package on the booking.
|
|
223
|
+
|
|
224
|
+
`Booking.is_cancellable` tells you before you call.
|
|
225
|
+
|
|
226
|
+
`getEsim(iccid, { refresh: true })` reads the network now, and is rate-limited per Partner, per minute. A refused refresh answers `429` with `Retry-After`, which the SDK waits out (see "What is retried"). A read without `refresh` is not limited and may be polled.
|
|
227
|
+
|
|
228
|
+
Every input and response has a type you can import by name:
|
|
229
|
+
|
|
230
|
+
- A method's response type ends in `Response`. Its `data` is the result. For example, `getPartner()` resolves with `PartnerResponse`, and its `data` is a `Partner`.
|
|
231
|
+
- `createBooking`, `getBooking` and `updateBooking` resolve with `BookingResponse`. Its `data` is a `Booking`.
|
|
232
|
+
- A list resolves with a `…ListResponse`. For example, `getBookings` resolves with `BookingListResponse`, and its `data` is a `Booking[]`.
|
|
233
|
+
- The response name does not always repeat the resource name. `issuePackage` and `getPackage` resolve with `PackageResponse`, and its `data` is an `IssuedPackage`.
|
|
234
|
+
- An input type has the name of the call, for example `CreateBookingInput` and `IssuePackageInput`.
|
|
235
|
+
- A filter or query type has the name of what it holds, for example `BookingListFilters` and `AnalyticsQuery`.
|
|
236
|
+
|
|
237
|
+
## Errors
|
|
238
|
+
|
|
239
|
+
A non-`2xx` answer rejects with a `HubbyApiError`:
|
|
240
|
+
|
|
241
|
+
| Field | Holds |
|
|
242
|
+
|---|---|
|
|
243
|
+
| `status` | The HTTP status, for example `409` or `422`. |
|
|
244
|
+
| `code` | The refusal code, for example `idempotency_key_in_use`. Every Partner API refusal has one. |
|
|
245
|
+
| `message` | The body's message, else `Partner API answered <status>`. |
|
|
246
|
+
| `errors` | The field errors of a `422`, keyed by field. |
|
|
247
|
+
| `body` | The parsed error body: JSON when it parses, else the text. |
|
|
248
|
+
| `response` | The `Response`. |
|
|
249
|
+
|
|
250
|
+
Branch on `code`, not on `message`. The codes you meet while integrating, on any call:
|
|
251
|
+
|
|
252
|
+
| `status` | `code` | When |
|
|
253
|
+
|---|---|---|
|
|
254
|
+
| `401` | `unauthenticated` | The key or the signature is wrong, a signing header is missing, or your Partner is not active. |
|
|
255
|
+
| `401` | `timestamp_out_of_window` | The request's timestamp is more than 300 seconds old, or ahead of the server's clock: check your clock. |
|
|
256
|
+
| `404` | `not_found` | The path is wrong (check `baseUrl`), or the booking, item or eSIM does not exist or is not yours. |
|
|
257
|
+
| `404` | `item_has_no_package` | `getPackage` or `terminatePackage` on one of your items that holds no package. For example, the item is `queued`, also right after your `terminatePackage`. Read the item's `state` from `getBooking` instead. |
|
|
258
|
+
| `409` | `idempotency_key_in_use` | A request with the same `Idempotency-Key` is still running. The SDK retries it. |
|
|
259
|
+
| `409` | `idempotency_key_spent` | The `Idempotency-Key` issued a package that you terminated after the issue. The key is spent. The SDK does not retry it. Issue again with a new key. |
|
|
260
|
+
| `422` | `validation_failed` | A field failed validation. `errors` holds the messages, keyed by field, for example `package_specifications.0.size`. |
|
|
261
|
+
| `429` | `rate_limited` | Too many requests. The SDK waits out `Retry-After` (see "What is retried"). |
|
|
262
|
+
|
|
263
|
+
The refusals of one operation, such as the `cancelBooking` codes above, are listed with it in the Partner guide.
|
|
264
|
+
|
|
265
|
+
One `catch` covers every call:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import { HubbyApiError } from '@hubbyesim/sdk';
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
await hubby.issuePackage(booking, item, { idempotencyKey, external_user_id });
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (error instanceof HubbyApiError) {
|
|
274
|
+
// error.status, error.code, error.errors
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
A call that gets no answer is not a `HubbyApiError`: a network error after the retries rejects with the underlying error, and an abort rejects with the signal's reason.
|
|
282
|
+
|
|
283
|
+
## Per-call options
|
|
284
|
+
|
|
285
|
+
Every method takes one more, optional, argument:
|
|
286
|
+
|
|
287
|
+
| Option | Meaning |
|
|
288
|
+
|---|---|
|
|
289
|
+
| `signal` | An `AbortSignal`. Aborting it cancels the call, a retry wait included, and the call rejects with the signal's reason. |
|
|
290
|
+
| `headers` | Extra headers to send. A `User-Agent` here replaces the SDK's. On `issuePackage`, the `idempotencyKey` you pass wins over an `Idempotency-Key` header. |
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
const bundles = await hubby.getBundles({ signal: AbortSignal.timeout(10_000) });
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Verify webhooks
|
|
297
|
+
|
|
298
|
+
Read the raw body. A parsed and re-serialised body does not verify.
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
import express from 'express';
|
|
302
|
+
import { verifyWebhook, WebhookVerificationError } from '@hubbyesim/sdk';
|
|
303
|
+
|
|
304
|
+
app.post('/hubby/webhooks', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
305
|
+
try {
|
|
306
|
+
const event = await verifyWebhook({
|
|
307
|
+
secret: process.env.HUBBY_WEBHOOK_SECRET!,
|
|
308
|
+
body: req.body, // a Buffer of the raw bytes
|
|
309
|
+
headers: req.headers,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Deduplicate on event.event_id: retries repeat it.
|
|
313
|
+
await handle(event.event, event.data);
|
|
314
|
+
res.sendStatus(204);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
if (error instanceof WebhookVerificationError) {
|
|
317
|
+
// error.reason: missing_signature | missing_timestamp | stale_timestamp | invalid_signature | invalid_body
|
|
318
|
+
res.sendStatus(400);
|
|
319
|
+
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
`verifyWebhook` checks that `x-hubby-timestamp` is within `toleranceSeconds` (default 300) of now, in either direction, and that one entry of `x-hubby-signature` equals `sha256=` plus the hex HMAC-SHA256 of `{x-hubby-timestamp}.{raw body}`. The comparison is constant-time. Answer `2xx` within a few seconds; anything else is retried with backoff.
|
|
329
|
+
|
|
330
|
+
## Signing without the client
|
|
331
|
+
|
|
332
|
+
`signRequest(secret, { method, url, timestamp, body })` returns the `X-Signature` for one request, if you send with your own HTTP client. `signing-vectors.json` in this package holds known-answer vectors (secret, method, URL, timestamp, body, expected signature) that Hubby's own server tests read too; use them to test a port to another language.
|
|
333
|
+
|
|
334
|
+
## More
|
|
335
|
+
|
|
336
|
+
The Partner API guide describes every call, field, refusal `code` and webhook event in full. The OpenAPI document this SDK is generated from is published with each Partner API release.
|
|
337
|
+
|
|
338
|
+
What changed between versions is in [`CHANGELOG.md`](CHANGELOG.md).
|
|
339
|
+
|
|
340
|
+
## Using AI agents
|
|
341
|
+
|
|
342
|
+
[`llms.txt`](llms.txt), shipped in this package, gives a coding agent the entry point, one flow and the links it needs to integrate the SDK.
|
|
343
|
+
|
|
344
|
+
## Security
|
|
345
|
+
|
|
346
|
+
Report a security problem to tech@hubbyesim.com, not in a public issue.
|