npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@letisim/connect-sdk

v0.2.0

Published

Official server-side TypeScript SDK for the Letisim Connect API — typed errors, safe retries, idempotency, cursor iterators and webhook verification

Readme

@letisim/connect-sdk

Official server-side client for Letisim Connect — the eSIM reseller API. It reaches every operation in its packaged runtime-derived OpenAPI document and adds typed errors, bounded retries, idempotency, cursor iterators, delivery polling and webhook verification. The package and production API may gain additive v2 operations independently; use the packaged contract for the exact surface supported by an installed SDK version.

The SDK is for trusted server code only. Never put a Letisim secret key in browser JavaScript, a mobile app or public source control. Browser storefronts use a short-lived WidgetSession minted by your server.

npm install @letisim/connect-sdk

Requires a runtime with fetch and WebCrypto: Node.js 20+, Bun, Deno, Cloudflare Workers or Vercel Edge. Both ESM (import) and CommonJS (require) are shipped.

Prove the integration before writing code

LETISIM_API_KEY=lts_test_… npx letisim-connect doctor

doctor authenticates, prints your environment, granted scopes, contract version and rate limit, then — on a Test key — runs a full sandbox purchase: offer → quote → order → delivery → eSIM. It never prints your key or the installation assets. It refuses to spend real money on a Live key.

First sandbox order

import { ConnectClient } from "@letisim/connect-sdk";

const connect = new ConnectClient({ apiKey: process.env.LETISIM_API_KEY! });

const offers = await connect.offers.list({ country: "TR", limit: 50 });
const selected = offers.data[0];
if (!selected) throw new Error("No executable offer for TR");

const quote = await connect.quotes.create(
  { offerId: selected.id },
  { idempotencyKey: "quote-booking-42" },
);

const order = await connect.orders.create({
  quoteId: quote.data.id,
  externalRef: "booking-42",
  deliveryMode: "partner",
  recipient: { reference: "traveller-42", locale: "ru" },
});

const delivered = await connect.orders.waitForDelivery(order.data.id);
if (!delivered.esim) throw new Error("Delivered order has no eSIM");

// `installation` carries the secret LPA/QR assets. Return them only to the intended customer,
// and never log this response.
const esim = await connect.esims.retrieve(delivered.esim.id);

const link = await connect.orders.deliveryLinks.create(
  delivered.id,
  { expiresInMinutes: 120, maxViews: 3 },
  { idempotencyKey: "delivery-booking-42" },
);
console.log(link.data.url);

Test and Live are selected by the API key. Test uses isolated simulated money and fulfilment; there is no environment header or parameter.

Client options

const connect = new ConnectClient({
  apiKey: process.env.LETISIM_API_KEY!,
  baseUrl: "https://api.letisim.com/partner/api/v2",
  timeoutMs: 15_000,        // deadline for one attempt, headers and body
  totalTimeoutMs: 60_000,   // hard deadline for the call including retries
  maxRetries: 3,
  retryBaseDelayMs: 250,
  retryMaxDelayMs: 5_000,
  maxRetryAfterMs: 60_000,  // ceiling applied to a server Retry-After
  appInfo: "acme-booking/2.4",  // appended to User-Agent
  fetch: globalThis.fetch,
});

Override baseUrl only for an approved edge, a local contract test or a private deployment.

Every method accepts the same options as its last argument, so a single call can carry its own idempotencyKey, signal, timeoutMs, maxRetries or extra headers.

Errors

Catch the class you care about; code stays the stable branch key.

import {
  ConnectApiError,
  ConnectConflictError,
  ConnectRateLimitError,
} from "@letisim/connect-sdk";

try {
  await connect.orders.create(input);
} catch (error) {
  if (error instanceof ConnectRateLimitError) {
    await sleep(error.retryAfterMs ?? 1_000);
  } else if (error instanceof ConnectConflictError && error.code === "quote_expired") {
    // Create a new Quote and ask the customer to reconfirm the price.
  } else if (error instanceof ConnectApiError) {
    console.error({ code: error.code, requestId: error.requestId });
  }
  throw error;
}

| Class | When | | --- | --- | | ConnectAuthError | 401 / 403 — missing, revoked, wrong-environment or under-scoped key | | ConnectValidationError | 400 — fix the request; params.field points at the input | | ConnectNotFoundError | 404 — not in this Account and environment | | ConnectConflictError | 409 — idempotency conflict, repriced or spent quote, unavailable action | | ConnectRateLimitError | 429 — carries retryAfterMs and live rateLimit | | ConnectServerError | 5xx — transient; idempotent work is safe to retry | | ConnectConnectionError | no HTTP response at all | | ConnectTimeoutError | attempt or total deadline elapsed | | ConnectDeliveryFailedError | an Order reached a terminal non-delivered state | | ConnectWebhookError | webhook timestamp, signature or body rejected |

Branch on code, never on a human message. Include requestId, your externalRef, the orderId and a timestamp when contacting support — never an API key, LPA string or full ICCID.

Retry policy

| Operation | Automatic retry | Recovery rule | | --- | --- | --- | | GET, DELETE, CSV export | timeout, 408, 429, transient 5xx | Exponential backoff with jitter. A server Retry-After is obeyed in full, up to maxRetryAfterMs. | | Writes with Idempotency-Key | same key and same body | Pass a booking-derived key when the operation maps to a business transaction. | | Natural-key writes (externalRef) | identical request | Orders, Accounts, Recipients and StorefrontApps deduplicate on externalRef. Changing the payload returns a conflict. | | PATCH, createPriceBook, createAccountKey | never | After an unknown response, read the resource or collection before deciding. | | validation, capability, balance and policy errors | never | Fix the request, refresh capability or price, fund the Account, or route to a person. |

No call can exceed totalTimeoutMs, whatever the retry budget says. When a Retry-After does not fit inside the remaining deadline, the SDK raises ConnectRateLimitError instead of sleeping past your own timeout.

createOrder is protected by the Account-scoped externalRef. After a timeout, repeat the same payload or reconcile Orders — never invent a new reference.

Response metadata

const { body, requestId, rateLimit, attempts } = await connect.orders.list().withResponse();
if (rateLimit.remaining !== null && rateLimit.remaining < 50) await slowDown();

Pagination

Collections expose page methods and iterators. Cursors are opaque: never parse, store or manufacture them.

for await (const offer of connect.offers.iterate({ country: "TR", limit: 100 })) {
  await upsertOffer(offer);
}

const recent = await connect.orders.iterate({ limit: 100 }).toArray({ limit: 1_000 });

Iterators exist for offers, catalog products, orders, recipients, esims, actionRequests and transactions. If the API returns cursor_expired, restart the same normalized query from the first page.

accounts.list() has no cursor: it is a bounded single-shot listing that reports truncation through meta.hasMore. Never treat a full page as the complete Account tree.

Delivery state

Signed webhooks are the primary production signal. waitForDelivery is a bounded recovery helper: it polls with an increasing interval, returns a delivered or completed Order, throws ConnectDeliveryFailedError for a terminal failure and ConnectWaitTimeoutError with lastOrder at its deadline. A timeout is not proof that no Order exists.

Webhooks

Verify the exact raw request bytes before JSON parsing. Verification is async and uses WebCrypto, so the same code runs on Node, Workers and Deno.

import { verifyWebhook, isOrderEvent } from "@letisim/connect-sdk";

async function receive(rawBody: Uint8Array, headers: Headers) {
  const event = await verifyWebhook({
    secret: process.env.LETISIM_WEBHOOK_SECRET!,
    rawBody,
    headers,
  });

  // Deliveries are at-least-once and may arrive out of order. Store event.id behind a unique
  // constraint, acknowledge duplicates, and apply only a newer data.stateRevision.
  if (isOrderEvent(event) && event.type === "order.delivered") {
    await markDelivered(event.data.orderId, event.data.stateRevision);
  }
  return event;
}

For Hono, Next route handlers, Workers, Deno and Bun there is a Fetch-API adapter:

import { verifyWebhookRequest } from "@letisim/connect-sdk";

export async function POST(request: Request) {
  const event = await verifyWebhookRequest(request, { secret: process.env.LETISIM_WEBHOOK_SECRET! });
  return Response.json({ received: event.id });
}

On Express, mount express.raw({ type: "application/json" }) for the webhook route and pass req.body together with req.headers. Parsing and re-serializing the body before verification invalidates the signature. See examples/.

Testing your integration

@letisim/connect-sdk/testing provides an in-memory Connect double: the sandbox purchase path with no network, no API key and no Letisim account.

import { createMockConnect } from "@letisim/connect-sdk/testing";

const mock = createMockConnect({ provisioningPolls: 2 });
const order = await issueTravelEsim(mock.client, booking);   // your code

expect(mock.requests).toContainEqual(expect.objectContaining({ method: "POST", path: "/orders" }));

// Drive your webhook handler with a correctly signed payload:
const signed = await mock.signWebhook(event, "whsec_test");
await receive(signed.body, signed.headers);

Use a Test key against the real API to validate the contract; use the double to test your own logic.

Compatibility policy

  • SDK and API versions are related but distinct. meta.apiVersion describes the server contract; the package version describes this client release.
  • While the SDK is 0.x, an incompatible client change ships in a new minor with a migration note in CHANGELOG.md. Standard SemVer applies after 1.0.
  • Removing or changing a public API field requires a new API major version and a migration guide.
  • Deprecated SDK members remain for at least two minor releases and 90 days, whichever is longer. The flat 0.1 method surface (connect.createOrder(...)) still works and will not be removed before 0.4.
  • Additive fields and enum values may appear. Ignore unknown response fields and keep an explicit fallback for unknown enum values at application boundaries.

Generating your own client

The contract is public and machine-readable:

https://api.letisim.com/partner/api/v2/openapi.json

The same document is packaged as @letisim/connect-sdk/openapi.json. If you work in PHP, Python, Go or C#, generate a client from that URL — the contract is identical to the one this SDK uses.

Maintainers regenerate the packaged document and its types from the runtime routes with:

npm run generate -w @letisim/connect-sdk

Generated files are never edited by hand, and a second generation must produce no diff before a release.

License

MIT — see LICENSE.