Official TypeScript SDKs for the MoonPay Developer Platform.
Integrate crypto on/off-ramp capabilities into your web or mobile application with type-safe APIs, managed iframe/WebView frames, and encrypted credential exchange.
| Package | Description |
|---|---|
@moonpay/platform-protocol |
Shared protocol types — zero runtime, used by both MoonPay frames and SDKs |
@moonpay/platform-sdk-core |
Platform-agnostic logic — API client, crypto, frame orchestration |
@moonpay/platform-sdk-web |
Browser SDK — iframe + postMessage |
@moonpay/platform-sdk-react-native |
React Native SDK — WebView-based |
@moonpay/platform-sdk-node |
Server-side SDK — session creation |
import { createServerClient } from '@moonpay/platform-sdk-node';
const server = createServerClient({
apiKey: process.env.MOONPAY_SECRET_KEY,
});
const result = await server.createSession({
externalCustomerId: 'user-123',
deviceIp: req.ip,
});
if (!result.ok) {
// handle error
}
const { sessionToken } = result.value;
// Pass sessionToken to your frontendimport { createClient } from '@moonpay/platform-sdk-web';
// 1. Create client with session token from your server
const client = createClient({ sessionToken });
// 2. Check if the customer has an active connection
const conn = await client.getConnection();
if (conn.ok && conn.value.status === 'connectionRequired') {
// 3. Open the connect flow
const connect = await client.connect({
container: document.getElementById('moonpay-connect'),
onEvent: (event) => console.log(event),
});
}
// 4. Get a quote
const quote = await client.getQuote({
source: { asset: { code: 'USD' }, amount: '100' },
destination: { asset: { code: 'ETH' } },
wallet: { address: '0x...' },
paymentMethod: { type: 'apple_pay' },
feeBehavior: 'inclusive',
});
if (!quote.ok) {
// handle error
}
// 5. Set up a payment frame
const widget = await client.setupWidget({
quote: quote.value.data.signature,
container: document.getElementById('moonpay-widget'),
onEvent: (event) => {
if (event.kind === 'complete') {
console.log('Transaction complete:', event.payload.transaction);
}
},
});import { MoonPayProvider, useMoonPay } from '@moonpay/platform-sdk-react-native';
// Wrap your app
<MoonPayProvider sessionToken={sessionToken}>
<App />
</MoonPayProvider>
// Use in components
function BuyScreen() {
const { client } = useMoonPay();
const quote = await client.getQuote({
source: { asset: { code: 'USD' }, amount: '100' },
destination: { asset: { code: 'ETH' } },
wallet: { address: '0x...' },
paymentMethod: { type: 'apple_pay' },
});
}All SDK methods return a Result<T, E> instead of throwing exceptions:
const result = await client.getQuote(params);
if (result.ok) {
console.log(result.value);
} else {
console.error(result.error);
}Full guides and API reference: moonpay.mintlify.app
- Node.js >= 18
- Bun
bun install
bun run buildbun run build # Build all packages
bun run typecheck # Typecheck all packages
bun run clean # Remove dist/ from all packages
bun run openapi # Regenerate API types from OpenAPI specTo test SDK changes in a Bun consumer app without publishing:
bun run dev
# Link all dependened on packages
bun run link:local -- ~/dev/sdk-consumer-app
# Link a specific package
bun run link:local -- ~/dev/sdk-consumer-app @moonpay/platform-sdk-node
bun run unlink:local -- ~/dev/sdk-consumer-appCurrently Bun-only. link:local uses bun link --no-save, so it does not rewrite the consumer's package.json. unlink:local removes the local links and reinstalls from the consumer's existing manifest.
protocol ← core ← web
← react-native
← node
Build order matters: protocol must build before core, and core before web/react-native/node. Running bun run build at the root handles this automatically.
See AGENTS.md for detailed development guides covering:
- Adding a new REST API endpoint
- Adding a new frame type
- Adding a new connection status
- Updating API types from OpenAPI
- Adding parent-to-child commands
This repo publishes all 5 @moonpay/* packages to npm via changesets and npm Trusted Publishers (OIDC — no static publish token).
Two release channels, served by one workflow:
| Channel | Branch | Tag | npm install |
|---|---|---|---|
| stable | main |
latest |
@moonpay/platform-sdk-web |
| pre-release | next |
next |
@moonpay/platform-sdk-web@next |
Both channels run the same .github/workflows/publish.yml (triggered on push to either branch). A single workflow is intentional: npm Trusted Publishers matches the OIDC workflow_ref claim, which points at the calling workflow filename — not a reusable callee. One workflow → one TP binding per package → covers both channels.
The channel selection happens inside changesets: when .changeset/pre.json is present (on next), changeset publish ships under the pre-tag from that file (next) automatically. On main, no pre.json, so it ships to @latest.
- Land your code change as a PR. The
Changeset Requiredcheck enforces that every PR includes a changeset. - Add a changeset describing the user-facing impact:
All 5 packages are
bunx changeset
linkedin.changeset/config.json, so they version together. - When your PR merges to
main, the Release workflow opens (or updates) a Version PR titledVersion Packages. This PR contains the version bumps and changelog updates. - Merging the Version PR triggers the Release workflow again, this time publishing all bumped packages to npm.
Packages currently publish with access: "restricted" (org-only). They will become public via DEVXP-1098.
The next branch is long-lived and runs changesets pre mode. Versions look like 0.3.0-next.0, 0.3.0-next.1, etc., and resolve via the next dist-tag. Stable @latest is unaffected.
PR work targets next exactly like it would target main — add a changeset, merge, the Publish workflow opens a "Version Packages (next)" PR, merging it publishes pre-releases.
Initialising the channel (one-off, when next doesn't exist yet):
git checkout -b next main
bunx changeset pre enter next # writes .changeset/pre.json
git push -u origin nextThe .changeset/pre.json file is what makes changeset version produce pre-release numbers. It must remain on the next branch and must not exist on main.
Resyncing next after a stable release. When main ships a stable release, the next branch's base is behind. Until DEVXP-1095 automates this, the manual recovery is:
git checkout next
bunx changeset pre exit # remove .changeset/pre.json
git rebase main # bring in stable
bunx changeset pre enter next # re-enter pre mode on top of main
git push -f origin nextYes, this force-pushes next. The branch is a publishing channel, not a development branch — that's expected.
Promoting a next release to stable. Cherry-pick or merge the relevant commits onto main (with a fresh changeset describing the stable bump). The next release of main ships them under @latest.
Manual run: the workflow supports workflow_dispatch for emergencies / bootstrap. Trigger from the Actions tab and pick either main or next.
MIT — see individual package package.json files.