@402md/checkout
Checkout widget SDK reference for vanilla JS and React
Installation
<script src="https://js.402.md/v1/checkout.js"></script>The script exposes a global FourOhTwo object.
npm install @402md/checkoutimport { createCheckout } from '@402md/checkout'createCheckout(config)
Creates a checkout instance.
const checkout = FourOhTwo.createCheckout({
publishableKey: 'pk_live_xxx',
skillId: 'your-skill-id',
amount: '49.00',
description: 'Premium Plan - 30 days',
type: 'subscription',
onSuccess: (payment) => {
console.log('Paid!', payment.txHash)
}
})CheckoutConfig
| Field | Type | Required | Description |
|---|---|---|---|
publishableKey | string | Yes | Publishable key from 402.md dashboard |
skillId | string | Yes | Skill ID to charge for |
amount | string | Yes | Amount in USDC (e.g. '49.00') |
description | string | Yes | Shown in the payment modal |
type | CheckoutType | No | 'api_call' | 'subscription' | 'product' | 'service' | 'content'. Defaults to 'api_call' |
metadata | Record<string, unknown> | No | Custom data passed to webhooks |
collectShipping | boolean | No | Show shipping address form (for products) |
apiBaseUrl | string | No | Override API URL. Defaults to 'https://api.402.md' |
onSuccess | (payment: PaymentResult) => void | No | Called when payment is confirmed |
onClose | () => void | No | Called when user closes the modal |
onError | (error: Error) => void | No | Called on error |
PaymentResult
interface PaymentResult {
sessionId: string // Checkout session ID
txHash: string // On-chain transaction hash
transactionId: string // 402.md internal transaction ID
amount: string // Confirmed USDC amount
ticket?: string // JWT ticket (subscriptions)
orderId?: string // Order ID (products)
}CheckoutSession
interface CheckoutSession {
sessionId: string // Unique session identifier
exactAmount: string // Amount with precision suffix for matching
expiresAt: string // ISO 8601 expiration timestamp
status: string // CONFIRMED | SETTLED | EXPIRED | CANCELLED
}Methods
| Method | Description |
|---|---|
checkout.open() | Opens the payment modal. Returns Promise<void> |
checkout.close() | Closes the modal and cleans up resources |
Internal flow
open()creates a checkout session viaPOST /api/v1/checkout/sessions- The API returns a
sessionId,exactAmount(with unique precision suffix), andexpiresAt - The modal displays a QR code, wallet address, exact amount, and countdown timer
- Payment detection runs in parallel:
- WebSocket — real-time
payment.confirmedevents - Polling — checks session status every 3 seconds (fallback)
- WebSocket — real-time
- On confirmation,
onSuccessfires and the modal shows a success state
Unique amount matching
Each session gets a unique exact amount (e.g., 49.000037 for a $49 payment). The random suffix lets 402.md match incoming USDC transfers to the correct session without needing a unique wallet address per session.
Session TTL
Sessions expire after 15 minutes. The modal shows a countdown timer. If the session expires before payment, the EXPIRED status is shown.
Browser wallet (EIP-1193)
If the user has an injected wallet (MetaMask, Coinbase Wallet, Rainbow), the modal shows a Connect Wallet button:
- Requests account access via
eth_requestAccounts - Switches to Base (chain ID
0x2105) if needed - Sends a USDC transfer to the checkout wallet
- Waits for on-chain confirmation
If no wallet is detected, the user can still pay via QR code or manual transfer.
@402md/checkout-react
React bindings for the checkout widget.
Installation
npm install @402md/checkout-react<CheckoutProvider>
Wraps your app and provides the publishable key to all child checkout components.
import { CheckoutProvider } from '@402md/checkout-react'
function App() {
return (
<CheckoutProvider publishableKey="pk_live_xxx">
<MyApp />
</CheckoutProvider>
)
}| Prop | Type | Required | Description |
|---|---|---|---|
publishableKey | string | Yes | Publishable key from dashboard |
apiBaseUrl | string | No | Override API URL |
children | ReactNode | Yes | Child components |
useCheckout(options)
Hook for programmatic checkout control.
import { useCheckout } from '@402md/checkout-react'
function PayButton() {
const { open, close, isOpen, isLoading, payment } = useCheckout({
skillId: 'your-skill-id',
amount: '49.00',
description: 'Premium Plan',
type: 'subscription',
onSuccess: (payment) => {
console.log('Paid!', payment.txHash)
}
})
return <button onClick={open} disabled={isLoading}>Pay Now</button>
}UseCheckoutOptions
| Field | Type | Required | Description |
|---|---|---|---|
skillId | string | Yes | Skill ID |
amount | string | Yes | Amount in USDC |
description | string | Yes | Payment description |
type | CheckoutType | No | Commerce type |
metadata | Record<string, unknown> | No | Custom data |
collectShipping | boolean | No | Collect shipping address |
onSuccess | (payment: PaymentResult) => void | No | Success callback |
onClose | () => void | No | Close callback |
onError | (error: Error) => void | No | Error callback |
UseCheckoutReturn
| Field | Type | Description |
|---|---|---|
open | () => void | Open the checkout modal |
close | () => void | Close the modal |
isOpen | boolean | Whether the modal is open |
isLoading | boolean | Loading state during checkout |
payment | PaymentResult | null | Last successful payment result |
<CheckoutButton>
Pre-built button component that opens a checkout modal.
import { CheckoutButton } from '@402md/checkout-react'
<CheckoutButton
skillId="your-skill-id"
amount="49.00"
description="Premium Plan"
checkoutType="subscription"
onSuccess={(payment) => {
window.location.href = `/welcome?ticket=${payment.ticket}`
}}
className="my-button"
>
Buy Now - $49.00 USDC
</CheckoutButton>| Prop | Type | Required | Description |
|---|---|---|---|
skillId | string | Yes | Skill ID |
amount | string | Yes | Amount in USDC |
description | string | Yes | Payment description |
checkoutType | CheckoutType | No | Commerce type |
metadata | Record<string, unknown> | No | Custom data |
collectShipping | boolean | No | Collect shipping address |
onSuccess | (payment: PaymentResult) => void | No | Success callback |
onClose | () => void | No | Close callback |
onError | (error: Error) => void | No | Error callback |
Accepts all standard HTML <button> attributes (className, disabled, style, etc.). Shows "Loading..." while processing. Default text is Pay $${amount} if no children are provided.
Complete React example
import { CheckoutProvider, CheckoutButton } from '@402md/checkout-react'
import { useRouter } from 'next/navigation'
function PricingPage() {
const router = useRouter()
return (
<CheckoutProvider publishableKey="pk_live_xxx">
<h1>Pricing</h1>
<CheckoutButton
skillId="analytics-dashboard"
amount="49.00"
description="Analytics Pro - 30 days"
checkoutType="subscription"
onSuccess={(payment) => {
router.push(`/dashboard?ticket=${payment.ticket}`)
}}
>
Subscribe - $49.00/mo
</CheckoutButton>
<CheckoutButton
skillId="widget-store"
amount="29.99"
description="Starter Widget"
checkoutType="product"
collectShipping
onSuccess={(payment) => {
router.push(`/orders/${payment.orderId}`)
}}
>
Buy Widget - $29.99
</CheckoutButton>
</CheckoutProvider>
)
}