SDK
The ICPay SDK provides a typed API for payments on the Internet Computer. It supports public client-side operations with a publishable key and private server-side operations with a secret key.
Intro
- Public operations: fetch account info, verified ledgers, pricing, start payments from a connected wallet.
- Private operations: account details, payment history, transactions. Use only on servers with your secret key.
Get your keys
Create an account in the icpay.org to obtain your Publishable Key (client) and Secret Key (server). Keep your Secret Key private.
Quickstart
Initialize the SDK with your publishable key on the client. Only when using createPayment or createPaymentUsd, you must provide an actorProvider and a connected wallet.
Client setup (TypeScript)
import { Icpay, IcpayError } from '@ic-pay/icpay-sdk'
const icpay = new Icpay({
publishableKey: process.env.NEXT_PUBLIC_ICPAY_PK!,
// actorProvider + connected wallet are required only when calling
// createPayment or createPaymentUsd (e.g. Plug, Internet Identity, Oisy)
})
try {
// Add your code here
} catch (e) {
if (e instanceof IcpayError) {
// Handle gracefully
}
}
Create a payment
There are two common ways to pay: fixed token amount or USD amount automatically converted to tokens.
Primary input going forward is the token shortcode. Each verified token ledger exposes a unique, human‑readable shortcode (e.g. ic_icp, base_usdcoin). Prefer sending tokenShortcode instead of symbol/ledgerCanisterId/chainId. Legacy fields still work for backward compatibility but are deprecated.
Note on chains:
chainId(optional, UUID): Target chain for the payment intent. If omitted, the intent defaults to the Internet Computer chain. Use this when directing payments to another supported chain.
Send fixed token amount (preferred: tokenShortcode)
import { Icpay } from '@ic-pay/icpay-sdk'
// If you want a drop-in wallet selector, use the widget's helper:
// import { createWalletSelect } from '@ic-pay/icpay-widget'
// const walletSelect = createWalletSelect()
const icpay = new Icpay({
publishableKey: process.env.NEXT_PUBLIC_ICPAY_PK!,
actorProvider: (canisterId, idl) => /* return agent-js actor from your wallet */
walletSelect.getActor({ canisterId, idl, requiresSigning: true, anon: false }),
connectedWallet: { owner: '<principal-id>' },
})
const tx = await icpay.createPayment({
tokenShortcode: 'ic_icp', // primary way (server resolves token ledger/chain from shortcode)
amount: '150000000', // smallest unit (e.g. 1.5 ICP with 8 decimals)
metadata: { myProductId: '511234', myOrderId: '511234', anyInternalField: 'allowed' },
})
Send by USD amount (auto conversion, preferred: tokenShortcode)
const res = await icpay.createPaymentUsd({
usdAmount: 5,
tokenShortcode: 'ic_icp',
metadata: { context: 'tip-jar', myProductId: '511234', myOrderId: '511234', anyInternalField: 'allowed' },
})
X402 payments (cards-enabled flow with fallback)
// Attempts X402 flow first; falls back to regular USD flow if not available
const out = await icpay.createPaymentX402Usd({
usdAmount: 10,
tokenShortcode: 'ic_icp',
metadata: { context: 'subscription' },
})
// If X402 responds with Payment Required (402), the SDK will handle header signing
// and settlement, then wait for terminal status, emitting events along the way.
Events are emitted throughout the flow when enableEvents is true. See Events section below.
Configuration
IcpayConfig options:
publishableKey(string): Public key for client operations.secretKey(string): Server-only key for private endpoints.environment('development' | 'production'): Default 'production'.apiUrl(string): Defaulthttps://api.icpay.org.connectedWallet(object): Connected wallet/principal for signing.icHost(string): IC network host for agent-js. Defaulthttps://icp-api.io.actorProvider(fn):(canisterId, idl) => Actorused to sign ICRC transfers.debug(boolean): Verbose logging in SDK internals.enableEvents(boolean): Emit SDK lifecycle events.
Price helpers and ledger info:
// Get list of Verified Ledgers
const ledgers = await icpay.getVerifiedLedgers()
// Get Ledger Canister Id by Symbol string. eg. ckUSDC or ICP
const bySymbol = await icpay.getLedgerCanisterIdBySymbol('ICP')
// Get all information (eg. decimals, current price, ...) about a ledger by passing a Ledger Canister Id (eg. ICP ledger id is 'ryjl3-tyaaa-aaaaa-aaaba-cai')
const info = await icpay.getLedgerInfo('ryjl3-tyaaa-aaaaa-aaaba-cai')
// Fetch all ledgers with current price for 1 token unit
const priced = await icpay.getAllLedgersWithPrices()
// Get number of token that is needed for a fixed price in USD
const calc = await icpay.calculateTokenAmountFromUSD({ usdAmount: 10, ledgerCanisterId: info.canisterId })
Types reference (balances)
type LedgerBalance = {
ledgerId: string
ledgerName: string
ledgerSymbol: string
canisterId: string
eip3009Version?: string | null
x402Accepts?: boolean
balance: string // smallest unit
formattedBalance: string // human-readable
decimals: number
currentPrice?: number
lastPriceUpdate?: Date
lastUpdated: Date
// Chain metadata (when available)
chainId?: string
chainName?: string | null
rpcUrlPublic?: string | null
chainUuid?: string | null
// Required amount helpers (when amount/amountUsd passed)
requiredAmount?: string
requiredAmountFormatted?: string
hasSufficientBalance?: boolean
logoUrl?: string | null
}
Wallet helpers
Helpers to connect and manage wallets when initiating payments:
showWalletModal()→ Prompts user to connect with the first available provider.connectWallet(providerId)→ Connect a specific provider:'internet-identity' | 'oisy' | 'plug'.getWalletProviders()→ Returns supported providers.isWalletProviderAvailable(providerId)→ Check availability in current environment.getAccountAddress()→ Principal/address of connected wallet.disconnectWallet()/isWalletConnected()/getConnectedWalletProvider()
const providers = icpay.getWalletProviders()
if (icpay.isWalletProviderAvailable('plug')) {
await icpay.connectWallet('plug')
}
const principal = icpay.getAccountAddress()
Public API reference
- Account and chains:
getAccountInfo()→ Public account info: id/live/canister/branding.getVerifiedLedgers()→ Verified ledgers with price metadata.getChains()→ Enabled chains (IC/EVM) with RPC/explorer data.getLedgerCanisterIdBySymbol(symbol)→ Resolve ICPay-verified symbol to canister id.
- Ledger info and prices:
getLedgerInfo(ledgerCanisterId)→ Detailed ledger metadata, price, decimals.getAllLedgersWithPrices()→ All ledgers including current price.calculateTokenAmountFromUSD({ usdAmount, ledgerCanisterId | ledgerSymbol })→ Price quote to smallest unit.
- Balances:
getSingleLedgerBalance(ledgerCanisterId)→ Connected wallet balance + price context for one ledger.getExternalWalletBalances({ network, address|principal, ... })→ Aggregate balances for an external EVM/IC wallet. Useful for showing “Pay with …” choices and sufficiency checks.
- Payments:
createPayment(request)→ Token-denominated payment.createPaymentUsd(request)→ USD-denominated payment (auto conversion).createPaymentX402Usd(request)→ X402 flow with automatic fallback to regular path.notifyPaymentIntentOnRamp({ paymentIntentId, ... })→ Poll an onramp intent until terminal status.triggerTransactionSync(canisterTransactionId)→ Ask ICPay to sync a canister tx to API DB now.
External wallet balances (public)
const all = await icpay.getExternalWalletBalances({
network: 'evm', // 'evm' | 'ic'
address: '0xabc...', // or principal: 'aaaa-bbbb...'
amountUsd: 10, // optional: compute requiredAmount per ledger
chainShortcodes: ['base-sep'],// optional filter
tokenShortcodes: ['base_usdcoin'], // optional filter by token shortcode
})
for (const b of all.balances) {
console.log(b.ledgerSymbol, b.formattedBalance, b.hasSufficientBalance)
}
Deprecated inputs (still supported)
symbol,ledgerCanisterId, andchainIdcontinue to work, but new integrations should usetokenShortcode. The server derives the ledger and chain from the shortcode automatically.
Single-ledger balance (connected wallet)
const b = await icpay.getSingleLedgerBalance('ryjl3-tyaaa-aaaaa-aaaba-cai')
console.log(b.ledgerSymbol, b.formattedBalance, b.currentPrice)
Low-level utilities
Advanced helpers used internally by flows; you can use them for custom UX:
pollTransactionStatus(canisterId, txId, accountCanisterId, indexReceived, intervalMs?, maxAttempts?)→ Anonymous polling for status.notifyLedgerTransaction(icpayCanisterId, ledgerCanisterId, blockIndex)→ Notify ICPay canister about a ledger tx.getTransactionStatusPublic(icpayCanisterId, canisterTxId, indexReceived, accountCanisterId)→ Public status endpoint via actor.getTransactionByFilter(canisterTxId)→ Fallback lookup by scanning transactions.sendFundsToLedger(ledgerCanisterId, toPrincipal, amount, memo?, host?)→ ICRC-1 transfer (requiresactorProvider).packEvmId(accountCanisterId, intentCode)→ Build bytes32 id used by EVM PaymentProcessor.
// Example: pack EVM id (bytes32) for custom contract calls
const idHex = icpay.packEvmId('42', 1234)
Events
Enable with enableEvents: true. Subscribe via on or standard addEventListener.
Event names:
icpay-sdk-erroricpay-sdk-transaction-createdicpay-sdk-transaction-updatedicpay-sdk-transaction-completedicpay-sdk-transaction-failedicpay-sdk-transaction-mismatchedicpay-sdk-connect-walleticpay-sdk-method-starticpay-sdk-method-successicpay-sdk-method-error
const unsubscribe = icpay.on('icpay-sdk-transaction-completed', (detail) => {
// e.g., show success toast, update UI
})
icpay.on('icpay-sdk-transaction-mismatched', (detail) => {
// detail.requestedAmount, detail.paidAmount
})
// Later
unsubscribe()
Types reference (payments)
Payment objects now include split-aware fields when applicable:
type SdkPayment = {
id: string
accountId: string
paymentIntentId: string
transactionId: string | null
transactionSplitId?: string | null
canisterTxId: number | null
amount: string
ledgerCanisterId: string
ledgerTxId?: string | null
accountCanisterId?: number | null
basePaymentAccountId?: string | null
status: 'pending' | 'completed' | 'failed' | 'canceled' | 'refunded' | 'mismatched'
// Optional clarity fields on webhook payloads and some responses
requestedAmount?: string | null // from payment_intent.amount
paidAmount?: string | null // from transaction.amount
invoiceId: string | null
metadata: Record<string, unknown>
createdAt: string
updatedAt: string
}
Public responses (publishable key) expose the same fields via PaymentPublic where returned.
Alternatively, DOM-style listeners:
const onCompleted = (evt: any) => {
const detail = evt && typeof evt === 'object' ? (evt as any).detail ?? evt : evt
// e.g., show success toast, update UI
}
icpay.addEventListener('icpay-sdk-transaction-completed', onCompleted)
// Later
icpay.removeEventListener('icpay-sdk-transaction-completed', onCompleted)
Event reference
| Event | When it fires | Payload (shape) |
|---|---|---|
icpay-sdk-error | Any SDK error is emitted (if enableEvents is true) | IcpayError instance (code, message, details?, retryable?, userAction?) |
icpay-sdk-transaction-created | After creating a payment intent before transfer | { paymentIntentId, amount, ledgerCanisterId, expectedSenderPrincipal } |
icpay-sdk-transaction-updated | While polling/awaiting status; intermediate updates | TransactionResponse-like: { transactionId, status, amount, recipientCanister, timestamp, description?, metadata?, payment? } |
icpay-sdk-transaction-completed |