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

@m2c/checkout

v0.11.0

Published

Headless browser checkout SDK for M2C: run an auction, redirect to the winning vendor's hosted checkout, and reflect conversion status.

Readme

@m2c/checkout

Headless browser checkout SDK for M2C. It runs an M2C auction, redirects the customer to the winning vendor's hosted checkout, and reflects the conversion status back to your UI when they return.

It is headless: no components, no styles. You render every pixel (including the trigger button); the SDK gives you state, events, and a typed terminal result.

It holds no secrets and does no cryptography. The signed, retried merchant webhook to your backend remains the source of truth for fulfillment - a return to success_url proves the customer came back, not that they paid. Treat this SDK's status as advisory UX.

npm install @m2c/[email protected]

ESM, zero runtime dependencies, ships .d.ts types. Targets evergreen browsers (ES2020). Importing the module is SSR-safe; the lifecycle methods need a DOM.

Two modes

Backend-initiated (recommended)

Your backend creates the checkout with its secret key (the existing POST /api/v1/auction call is the create step) and forwards checkout_url, request_id, and ttl to the browser. No key ships in the client.

import { createClient } from '@m2c/checkout';

const client = createClient({
  // Your backend already receives the M2C webhook, so it is the authoritative
  // status source. Point the SDK at your own endpoint.
  statusSource: { kind: 'url', template: 'https://shop.example/checkout-status?ref={request_id}' },
});

// session = { checkoutUrl, requestId, ttl } from your backend
await client.startFromSession(session);

Client-initiated (no-backend shortcut)

The browser calls the auction directly with a publishable key. For merchants without a backend. Reliable only for low-stakes, instant, in-session digital delivery: a customer who never returns is never confirmed client-side.

Live publishable keys require HTTPS origins. Test publishable keys (pub_test_...) may also allow loopback HTTP origins such as http://127.0.0.1:5173 for local sandbox testing.

const client = createClient({
  publishableKey: 'pub_...',
  // default statusSource is { kind: 'm2c' } - poll the M2C advisory read endpoint
});

await client.start({
  transactionValue: 49.99, // major units, e.g. dollars
  currency: 'USD',
  description: 'Pro plan',
  successUrl: 'https://shop.example/checkout/return',
  cancelUrl: 'https://shop.example/checkout/canceled',
});

The SDK automatically attaches checkout-context metadata to this auction: platform: "web" and a coarse device_type (mobile / desktop) detected from the browser. Both are metadata only - never auth or fulfillment - with no caller override.

Shop sessions

Create one vendor-owned shop visit with a publishable key. launchExpiresAt is the authoritative server deadline for opening the shop URL; ttl is the vendor's original lifetime measured from its response time. The SDK enforces a conservative, clock-skew-immune launch budget beginning before session creation, so request time shortens the local budget and never extends the vendor's URL lifetime. expiresAt is the later deadline for the vendor to report purchases, so do not conflate them.

const client = createClient({ publishableKey: 'pub_...' });
const session = await client.startShopSession({
  currency: 'USD',
  returnUrl: 'https://game.example/shop/closed',
  segments: ['returning_customer'],
});

// Creation launches immediately. Relaunches are allowed while the SDK's
// conservative local launch budget remains valid.
session.open();
const status = await client.readShopSessionStatus(session.sessionId);
// { sessionId, status: 'active' | 'expired', completedPurchases }

completedPurchases counts purchases successfully reported so far. It includes purchases that are later refunded or charged back, and a zero count is not final while the session remains active. The read is advisory; signed merchant webhooks remain the authoritative purchase and reversal source.

In popup or new-tab mode, if the shopper closes the pre-opened window while the session request is in flight, startShopSession() still resolves with the created session. Its initial launch was skipped; call open() from a later user gesture to open it in a new window while the launch budget remains valid.

For live sessions, M2C forwards the normalized segments (from M2C's fixed list; see SupportedSegment in openapi.yaml) and the returnUrl verbatim to every eligible bidding vendor, not only the winner. Treat every eligible vendor as a third-party recipient. Do not include personal or sensitive customer attributes, secrets, or session tokens in the return URL; use a token-free return URL.

No checkout resume record or polling loop is created. Purchase webhooks to the merchant backend remain authoritative.

The redirect / resume model

The default launch is a same-tab full-page redirect (location.assign), for maximum compatibility with vendor 3-D Secure / bank redirect flows that break inside popups and iframes.

Because the redirect tears down the page, start / startFromSession navigate away and do not resolve in place. You get the outcome on your return page by calling resume():

// On your success_url and cancel_url pages:
const client = createClient({ publishableKey: 'pub_...' });

const result = await client.resume({ outcome: 'success' }); // or 'cancel'
// result is null if no checkout was in progress, otherwise one of:
//   { status: 'completed',       requestId }
//   { status: 'failed',          requestId }
//   { status: 'canceled',        requestId }
//   { status: 'pending_timeout', requestId }  <- poll window elapsed; ask your webhook

The SDK cannot tell success_url from cancel_url on its own, so pass outcome. A cancel return resolves canceled immediately; a success return polls the status source to a terminal result.

start* writes a small resume record to sessionStorage (namespaced by storageKey, default m2c.checkout) before navigating. It is read and cleared on the return page, so a refresh does not re-poll a stale checkout.

If the customer instead presses Back from the vendor checkout, the originating page is restored (from the back-forward cache) and the start / startFromSession promise resolves { status: 'window_closed', requestId }. This is an ambiguous browser outcome, not a payment status; keep using the webhook/status source as the truth. The resume record remains in storage, so if the customer goes Forward and completes the hosted checkout, the return page can still call resume() and resolve the terminal payment result.

Popup / new-tab launch

For apps that must keep the original page alive, such as web games, opt into a separate checkout window:

const client = createClient({
  publishableKey: 'pub_...',
  launchMode: 'popup', // or 'new_tab'
  returnTimeoutMs: 60000, // optional fallback for abandoned popup/new-tab flows
});

Call start() directly from a user click/tap. The SDK pre-opens a blank window before the auction request, clears opener, then navigates that window after the checkout URL is known. If the browser blocks the window, start() rejects with InvalidRequest before creating the auction.

The default storage for popup/new-tab launch writes the resume record to localStorage as well as sessionStorage, so the return page in the checkout window can call resume(). If you inject custom storage, make sure it is visible to both the opener page and the return page. Use the same client factory on the opener and return page, or at least pass the same launchMode and storage configuration:

const client = createClient({
  publishableKey: 'pub_...',
  launchMode: 'popup', // same value used before start()
});

// On your success_url / cancel_url page in the checkout window:
await client.resume({ outcome: 'success' });

When the return page calls resume(), the SDK also broadcasts the terminal result back to the original same-origin page. In popup/new-tab mode, the start() / startFromSession() promise in the original page resolves with the same result.

This handoff does not make successUrl authoritative. completed and failed still come from the configured status source, and fulfillment should still be driven server-side from webhook state.

If the checkout window observably closes before a return page publishes a result, the original page resolves start() / startFromSession() with { status: 'window_closed', requestId }. This is not a payment status and does not mean the purchase failed or was canceled. Some hosted checkout pages sever opener observability while the window is still alive; in that case the SDK keeps waiting for the return handoff, then treats opener focus as an abandonment signal if no return result appears.

Set returnTimeoutMs if you want popup/new-tab mode to stop waiting after a fixed window when no return handoff arrives. This is an advisory UX fallback; choose a value long enough for a normal customer checkout, and keep server-side webhook state as the fulfillment source of truth.

To reconcile an ambiguous outcome after the fact - a window_closed or pending_timeout that may have completed server-side - re-read the status for the requestId with checkStatus, a one-shot read against the configured status source:

const status = await client.checkStatus(requestId);
// 'processing' | 'completed' | 'failed' | 'canceled'

checkStatus does not change the client's lifecycle state, and (like the poll) it is advisory - your webhook remains the source of truth.

For example:

const result = await client.start({
  transactionValue: 49.99,
  successUrl: 'https://shop.example/checkout/return',
  cancelUrl: 'https://shop.example/checkout/canceled',
});

The checkout window still lands on your return page because that is where the vendor redirects the customer. The original page receives the result through a same-origin BroadcastChannel plus a storage handoff: localStorage with the default storage, or only your injected storage when you provide one. The SDK does not rely on window.opener.

Progress events

const unsubscribe = client.onStateChange((state, ctx) => {
  // state: idle | creating | ready | launching | awaiting_return | returned
  //        | polling | completed | failed | canceled | pending_timeout
  //        | window_closed | error
  // ctx:   { requestId?, error? }   (error is set only on the 'error' state)
  render(state);
});

Status sources

Configure where status is read from after the return:

  • { kind: 'm2c' } - poll the M2C advisory read endpoint with the publishable key (client-initiated only). The default.
  • { kind: 'url', template } - poll your HTTPS endpoint; the literal {request_id} token is required and every occurrence is substituted. Answer from your webhook-recorded state. Recommended for backend-initiated mode.
  • { kind: 'callback', checkStatus } - call your async function on the backoff schedule. A function cannot survive a full-page redirect, so re-supply it on the return page via createClient({ statusSource }) or resume({ statusSource }).

For local development only, allowInsecureUrls: true also permits a loopback HTTP status template such as http://localhost:3000/status/{request_id}.

Status readers may return the client statuses (processing, completed, failed, canceled) or webhook-native statuses from your own store (pending, abandoned, refunded, chargedback). The SDK maps them to the client result enum before resolving.

The poll uses bounded exponential backoff (immediate, then 1s, 2s, 4s, 8s, capped at 8s, ~90s total window; override with poll). Correlation is always request_id, which you get from the auction response and your webhook records - no pre-coordination needed.

Every auction request, status fetch, and callback read is also bounded by requestTimeoutMs (default 15 seconds). During polling, the remaining poll window is the stricter ceiling, so a fetch or callback that never settles cannot extend the ~90-second window. HTTP response bodies are streamed with a 64 KiB ceiling.

const client = createClient({
  publishableKey: 'pub_...',
  requestTimeoutMs: 10_000,
});

A push (subscribe) adapter is reserved in the type surface but not implemented yet; use m2c, url, or callback.

M2C status fallback (opt-in). When your own source (url / callback) has not gone terminal within a threshold, the SDK can also read the M2C endpoint once per poll cycle and resolve on a terminal result - useful when a customer returns before your backend has processed the conversion webhook. Your own source stays authoritative and it is advisory UX only; the signed webhook remains the source of truth.

const client = createClient({
  publishableKey: 'pub_...',            // required: the M2C fallback read uses it
  statusSource: { kind: 'url', template: 'https://shop.example/status?ref={request_id}' },
  statusFallback: { afterMs: 8000 },    // also check M2C once the primary lags 8s
});

It requires a publishableKey (a missing one when statusFallback is set is a configuration error, not a silent no-op). In the default full-page redirect the fallback runs on the return page, so give the return-page createClient the same statusFallback and publishableKey (just as you re-supply statusSource) or it will not run.

Recommended afterMs: 5000-15000. The checkout-to-webhook pipeline usually needs a few seconds before your own endpoint can know the outcome, so a lower threshold mostly re-reads processing from M2C while masking a broken status integration during your own testing - the fallback should rescue an outage, not become the primary. The SDK accepts any positive afterMs because code implies an informed choice; the Unity SDK clamps its equivalent setting to 5-60 seconds because an inspector slider does not.

Error handling

Rejections are M2CCheckoutError with a typed code:

| code | meaning | |---|---| | InvalidRequest | bad input / unsupported currency / bad URL (400, or client-side) | | AuthenticationFailed | the server rejected the publishable key as invalid or revoked (401) | | OriginNotAllowed | the page origin is not on the key's allowlist (403) | | AccountSuspended | the merchant account is suspended (403) | | NoVendorsAvailable | no linked/eligible vendor produced a bid (404) | | SessionNotFound | the requested shop session was not found for this account/mode (404) | | RateLimited | slow down; see retryAfterSeconds (429) | | ServiceUnavailable | transient backend failure (5xx) | | CheckoutExpired | the TTL lapsed before launch (backend mode cannot re-mint) | | Network | fetch, timeout, or bounded response-read failure | | Unknown | an unexpected response with no specific mapping |

pending_timeout is not an error - it resolves as a terminal result. During polling, transient read failures (rate limit, 5xx, network, a not-yet-visible status row) are swallowed and retried; a developer-actionable failure (bad key, bad origin, suspended account, invalid request) rejects so you see it. M2C status responses must carry the exact active request_id; merchant status endpoints may omit it.

Framework recipes

Vanilla:

button.onclick = () => client.start({ /* ... */ }).catch(showError);
client.onStateChange((s) => (statusEl.textContent = s));

React (drive a state variable from the event; resume on the return route):

useEffect(() => client.onStateChange(setState), []);
const onBuy = () => client.start({ /* ... */ }).catch(setError);

// On the return route:
useEffect(() => {
  client.resume({ outcome: 'success' }).then(setResult).catch(setError);
}, []);

Test mode

Use test keys (pub_test_...) and forward a backend session created with a sec_test_... key. The auction runs against the in-server sandbox vendors and returns a synthetic M2C-hosted checkout_url; the status read is scoped to the test flag derived from the key. No special code path - the same flow drives the sandbox.

Server-side / non-browser usage

All DOM access is guarded, so importing the module on a server never throws. start / startFromSession / resume throw InvalidRequest if no sessionStorage / navigation is available; inject storage, navigate, and fetch to drive the SDK in a non-browser environment (this is how the test suite runs).

What this SDK does not do

  • Capture card data or render the vendor's checkout page (M2C is a router; the vendor owns PCI scope).
  • Decide fulfillment. Grant goods server-side off the M2C webhook. See the fulfillment webhook receiver examples that ship alongside the SDKs.
  • Ship UI. Headless by design.

Packaging note

This package currently publishes ESM only, matching the rest of the M2C SDK workspace and its plain-tsc, zero-dependency build. Every modern bundler and Node 18+ consume it directly; a CJS build can be added without an API change if a consumer needs it.

Privacy & data handling

The SDK is tracking-free by design. Your site remains responsible for deciding which consent, disclosure, and retention rules apply to its integration.

  • Sent to M2C per checkout: transaction value and currency, checkout platform and coarse device class (derived from the user agent), and your optional context fields. The connection IP reaches M2C like any HTTPS request; M2C uses it server-side to resolve the customer's country and does not store the IP.
  • Stored on the device: purchase-scoped checkout-continuity state only - the return-bridge hand-off (popup/new-tab launch modes) and resume state in local/session storage, cleared as it is consumed. Account for this storage in the consent and disclosure rules that apply to your site.
  • Never: cookies, advertising or device identifiers, analytics, or fingerprinting. The SDK sets no third-party requests other than to your configured M2C endpoint and the vendor checkout the user is sent to.

Account for M2C as a payment-processing service provider in the privacy notices that apply to your integration.