| name | icpay |
| description | Integrates and extends the ICPay crypto payments platform. Use when working with icpay-widget, icpay-sdk, payment links, merchant accounts, relay payments (recipient EVM/IC/Solana), X402 v2, X402 up-to (`x402Upto`, `x402UptoSkipSettlementWait`, `icpay-x402-upto-submitted`, webhook `x402_upto_authorization_received`, pay-button progress), refunds, split payments, email notifications, webhooks, demo.icpay.org, betterstripe.com sandbox (testnets), filter tokens/chains, WalletConnect QR and deep links, wallet adapters, currency for payment links and profile, WordPress plugins (Instant Crypto Payments, WooCommerce), registration on icpay.org, creating an account, API keys (publishable and secret), .env for keys, SDK events (icpay-sdk-transaction-completed for success, transaction lifecycle, method start/success/error), account and wallet balances (user-wallets/with-balances, SDK getAccountWalletBalances and getWalletsWithBalances), or any ICPay-related code in the icpay monorepo. |
| source | https://github.com/icpay/icpay-sdk/tree/master/skills/icpay |
ICPay Skill
Instruction manual for working with the ICPay project: SDK, widget, payment links, accounts, webhooks, relay payments, X402 v2, refunds, split payments, email notifications, demo site, and integrations (WordPress, WooCommerce, Shopify).
Feature overview
- Relay payments — Per-chain recipient addresses (EVM, IC, Solana); funds forwarded to your specified addresses; optional relay fee in account settings.
- X402 v2 — HTTP 402 “Payment Required” flow for IC, EVM, and Solana; sign authorization, ICPay facilitator settles; card/onramp-friendly.
- Currency — Payment links have
fiatCurrencyId; user/account profile can set default fiat for payment (USD, EUR, etc.).
- QR and mobile — WalletConnect QR for desktop; deep links for mobile browsers so users can pay with mobile phone wallet apps.
- Wallet adapters — EVM: MetaMask, Coinbase, Brave, Rabby, OKX, WalletConnect. Solana: Phantom, Backpack. IC: Plug, Internet Identity (II), Oisy, NFID. Configurable enable/disable per adapter.
- Split payments — Optional: multiple merchants share revenue via split rules (target account + percentage in basis points).
- Refunds — Refund completed payments; execute-refunds worker; webhook
payment.refunded; email notification for refund completed.
- Email notifications — Payment completed and refund completed emails to account; configurable templates; process-notifications worker.
- Webhooks — Merchant endpoint receives payment/refund events; HMAC-SHA256 verification.
- demo.icpay.org — Live demo/playground for building and testing custom widgets (all components, configurable options).
- betterstripe.com — Sandbox environment: same features as icpay.org but on testnets (Solana devnet, Base Sepolia, Ark network testnet, and other testnets) for developers.
- Filter tokens/chains — Widget config:
tokenShortcodes, chainShortcodes, chainTypes to show only specific tokens or chains.
Public Project layout
- icpay-sdk —
@ic-pay/icpay-sdk: typed client for browser/server; create payments, wallet helpers, events.
- icpay-widget — Web Components + React wrappers: pay-button, amount-input, tip-jar, paywall, etc.
- icpay-docs — Documentation site (docs.icpay.org); MDX under
src/app/.
Use pnpm for install/build across the repo. Main technical reference: repo root technical.md.
Keys and auth
- Publishable key (
pk_live_* / pk_test_*): safe for client; used by widget and public SDK calls.
- Secret key: server-only; required for protected API (payments list, account info, webhook verification). Never expose in browser.
- Auth:
Authorization: Bearer <key> for both keys. API base: https://api.icpay.org (or env API_URL).
SDK (@ic-pay/icpay-sdk)
Install: pnpm add @ic-pay/icpay-sdk.
Browser (publishable key):
import { Icpay } from '@ic-pay/icpay-sdk';
const icpay = new Icpay({
publishableKey: 'pk_live_xxx',
apiUrl: 'https://api.icpay.org',
debug: false,
});
Create payment (USD): Prefer tokenShortcode (e.g. ic_icp, base_usdc). For createPaymentUsd / createPayment you must provide wallet context: actorProvider + connectedWallet (IC) or evmProvider + connected address (EVM).
const tx = await icpay.createPaymentUsd({
amountUsd: 5,
metadata: { orderId: 'ORDER-123' },
});
Wait for completion by payment intent id (SSE first, polling fallback):
const final = await icpay.waitForPaymentIntentCompletion('pi_123');
X402 v2 (IC, EVM, Solana): Use createPaymentX402Usd(request) for sign-and-settle flows; SDK builds EIP-712 (EVM) or Solana message/transaction, sends to ICPay facilitator, returns terminal status. Fallback to regular createPaymentUsd when X402 not available. When you have an existing payment intent (e.g. from a pay link), pass paymentIntentId or paymentIntent in config or in the request so the SDK sends it to the x402 intent endpoint and the API reuses that intent instead of creating a second one.
X402 up-to (agentic / usage-based):
-
Frontend (publishable key) can request an X402 up-to scheme by setting x402Upto: true in createPaymentX402Usd:
const icpay = new Icpay({ publishableKey: 'pk_test_xxx', apiUrl: 'https://api.icpay.org' });
const x402Init = await icpay.createPaymentX402Usd({
usdAmount: 25,
tokenShortcode: 'base_usdc',
metadata: { orderId: 'job-123', context: 'agentic-x402' },
x402Upto: true,
});
- ICPay creates a payment intent with
x402_upto = true and returns an X402 acceptance with scheme: 'upto' and maxAmountRequired (cap).
- SDK drives X402 authorization (wallet signs EIP‑712 or Solana message) but does not auto-settle via
/sdk/public/payments/x402/settle for up-to.
- For EVM up-to flows, the SDK builds a signed X402 v2 header and exposes it on the response:
x402Init.payment.paymentHeader — base64-encoded header containing the signed EIP‑712/EIP‑3009 payload (including maxAmount, validBefore, nonce, etc.).
x402Init.payment.paymentRequirements — the exact requirement used for signing (same shape as items in accepts[]).
- Use this when you want the user/agent to sign a maximum budget but decide final cost later; forward
paymentHeader and paymentIntentId to your backend.
-
Backend (secret key) settles later once the real cost in USD is known, via icpay.settleX402Upto (or the same call on icpay.protected.settleX402Upto):
const icpayBackend = new Icpay({
secretKey: process.env.ICPAY_SECRET_KEY!,
apiUrl: process.env.ICPAY_API_URL!,
});
await icpayBackend.settleX402Upto({
paymentIntentId: 'pi_123',
settledAmountUsd: usageUsd,
});
- This hits
POST /sdk/payments/x402/upto/settle on icpay-api, which:
- Verifies the intent belongs to the account and has
x402_upto = true.
- Converts
settledAmountUsd to token units using the ledger’s current USD price and decimals.
- Enforces
0 < tokenAmount <= maxAmountRequired (the authorized cap).
- If
paymentHeader is provided and not already stored, decodes and persists it (including validBefore) on the payment intent for icpay-services to consume.
- Triggers icpay-services →
PaymentProcessor.payWithSignatureUpto (EVM): EIP-3009 pull for the signed max, platform fee and splits on the settled token amount, refund of unused cap to the payer in the same tx (PaymentProcessor v1.3.0+).
- Combine this pattern with
icpay.protected.getPaymentById and/or webhooks when building agentic or usage-based payment flows.
Webhook (ICPay dashboard): x402_upto_authorization_received
- Emitted when
POST /sdk/public/payments/intents/x402/upto/confirm succeeds (EVM, publishable key): ICPay persisted the signed X402 header on the intent.
event.type: x402_upto_authorization_received. data.object: payment-intent-shaped payload (id / paymentIntentId, status, amount, amountUsd, ledgerCanisterId, payer, metadata, intentCode, …) — see icpay-docs Webhooks.
- Use this to sync your server with the browser: do not rely only on
onX402UptoIntent or icpay-x402-upto-submitted from the client. Especially with x402UptoSkipSettlementWait, the widget may not poll until payment.completed; this webhook is the authoritative signal that authorization is stored before settlement.
- Still handle
payment.completed for final fulfillment after settleX402Upto and on-chain settlement.
Widget (icpay-pay-button) — X402 up-to progress and x402UptoSkipSettlementWait:
- With
x402Upto: true and the default progress bar, icpay-progress-bar shows up-to-specific steps: wallet ready → sign authorization → submit authorization (after SDK returns signed header; EVM confirm to API) → settlement (poll until intent terminal, or pending if skipping wait).
- After EVM
POST /sdk/public/payments/intents/x402/upto/confirm, the pay button dispatches a window event icpay-x402-upto-submitted (CustomEvent, bubbles, composed) with detail: { paymentIntentId, amountUsdMax }. The progress bar listens for this to advance the “submit” step and either start settlement loading + poll (default) or show the “authorization saved” banner + pending last step when skipping wait (below).
x402UptoSkipSettlementWait?: boolean (pay button config, forwarded to <icpay-progress-bar .x402UptoSkipSettlementWait>):
false / omitted: Poll GET /sdk/public/payments/intents/:id until terminal; then icpay-sdk-transaction-completed and onSuccess. Success copy uses paymentIntent.amountUsd when the API returns it (settled fiat after merchant settlement), not only the widget’s max cap.
true: No poll after confirm. onSuccess runs with { id: 0, status: 'authorized_pending_settlement', paymentIntentId }; pay button succeeded is set. Progress shows a green banner and keeps Settlement as pending (user can close). Fulfillment when the intent later completes: webhooks or server-side polling — not the widget poll.
- Optional
onX402UptoIntent on pay button config: invoked after confirm with paymentIntentId, cap amountUsd, accepts, paymentHeader, paymentRequirements, metadata — forward to your backend to start work / store the header.
Example (React wrapper / same shape as plain config object):
<IcpayPayButton
config={{
publishableKey: process.env.NEXT_PUBLIC_ICPAY_PK!,
amountUsd: 10,
x402Upto: true,
x402UptoSkipSettlementWait: true,
progressBar: { enabled: true },
onX402UptoIntent: async (info) => {
await fetch('/api/jobs/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
paymentIntentId: info.paymentIntentId,
maxUsd: info.amountUsd,
paymentHeader: info.paymentHeader,
paymentRequirements: info.paymentRequirements,
}),
});
},
}}
onSuccess={(d) => {
if (d.status === 'authorized_pending_settlement') {
console.log('Header saved; settle with secret key later', d.paymentIntentId);
} else {
console.log('Completed', d);
}
}}
/>
Docs: icpay-docs → Agentic X402 up-to payments (sections Progress UI & optional skip settlement wait, Webhook: x402_upto_authorization_received).
Server (secret key): Use for icpay.protected.*: getPaymentById, listPayments, getPaymentHistory, getDetailedAccountInfo, getVerifiedLedgersPrivate, getAccountWalletBalances, getWalletsWithBalances, etc.
Account and wallet balances (how to get balances):
- User-wallets with-balances (JWT): The current way to get balances for a specific wallet when you have the wallet id and user auth. Call
GET https://api.icpay.org/user-wallets/<walletId>/with-balances with Authorization: Bearer <JWT>. When using the icpay.org web app, requests go through the proxy: GET https://icpay.org/api/proxy/user-wallets/<walletId>/with-balances (session cookie is sent automatically). Response: wallet object with a balances array (per-ledger balance, formattedBalance, decimals, currentPrice, etc.).
- SDK secret key (recommended for server/agents): Use the protected API with the account’s secret key:
- Aggregated account balances:
icpay.protected.getAccountWalletBalances() — returns balances across all wallets of the account (same as GET https://api.icpay.org/sdk/account/wallet-balances with Authorization: Bearer <secret_key>).
- All account wallets with balances:
icpay.protected.getWalletsWithBalances() — returns all ledger balances for the account (all internal wallets; same shape as getAccountWalletBalances). Same as GET https://api.icpay.org/sdk/wallets/with-balances with Authorization: Bearer <secret_key>.
Prefer SDK methods over raw fetch. Handle errors via IcpayError; subscribe to SDK events for lifecycle (see SDK events below).
Widget (@ic-pay/icpay-widget)
Install: pnpm add @ic-pay/icpay-widget @ic-pay/icpay-sdk.
Components: icpay-pay-button, icpay-amount-input, icpay-tip-jar, icpay-premium-content, icpay-article-paywall, icpay-coffee-shop, icpay-donation-thermometer, icpay-progress-bar.
HTML (bundler):
<script type="module"> import '@ic-pay/icpay-widget'; </script>
<icpay-pay-button
id="pay"
publishableKey="YOUR_PK"
tokenShortcodes="base_usdc"
amountUsd="5"
></icpay-pay-button>
Set config on the element (object with publishableKey, tokenShortcode, amountUsd, etc.). Listen for icpay-pay, icpay-error on the element or window.
React: Use wrappers from @ic-pay/icpay-widget/react (e.g. IcpayPayButton, IcpayTipJar) with config prop and onSuccess / onError.
Hosted embed (no bundler): Script from https://widget.icpay.org/v{VERSION}/embed.min.js; then ICPay.create('pay-button', { publishableKey, amountUsd, defaultSymbol, ... }).mount('#el').
Filter tokens/chains: In config set tokenShortcodes (e.g. ['ic_icp','base_usdc']), chainShortcodes (e.g. ['ic','base']), or chainTypes (e.g. ['ic','evm','sol']) to restrict which tokens or chains are shown in the widget.
Relay payments: Set recipientAddresses: { evm?: string, ic?: string, sol?: string } in config; funds are relayed to those addresses. Optional relay fee is set per account in dashboard (Settings → ICPay Fees → Relay Fee).
QR and deep links: Payment links support showWalletConnectQr (default true) and showBaseWalletQr; WalletConnect shows QR on desktop and deep links on mobile so users can open wallet apps.
Handle success/error via events, not console. Theming: CSS variables on :root or component (e.g. --icpay-primary, --icpay-surface). See widget-reference.md for options, wallet adapters, and component-specific config.
SDK events (icpay-sdk)
The SDK emits named events so agents and apps can react to payment lifecycle and method outcomes without polling. Subscribe with icpay.on(type, (detail) => { ... }); unsubscribe with icpay.off(type, listener). Events can be disabled via config: { enableEvents: false } (default is true). In browsers the SDK uses EventTarget/CustomEvent; in Node it uses an in-memory emitter.
Success event (crucial for apps)
icpay-sdk-transaction-completed — Fired when a payment has successfully completed. This is the primary event apps should listen to to fulfill orders, unlock content, or show confirmation.
- When it fires: After the payment is confirmed (on-chain and/or backend reconciliation). Emitted from
createPayment, createPaymentUsd, createPaymentX402Usd, and from polling/notify flows when status becomes completed.
- Payload (detail): A TransactionResponse-shaped object:
transactionId (number) — Canister/backend transaction id.
status: 'completed'.
amount (string) — Amount in smallest unit.
recipientCanister (string).
timestamp (Date).
description?, metadata? (e.g. your orderId).
payment? — When present, includes paymentId, paymentIntentId, status, canisterTxId, transactionId (from PublicNotifyResponse).
- What to do: Use
detail.paymentIntentId or detail.payment?.paymentIntentId and detail.payment?.paymentId for idempotency. Fulfill the order, persist success, show a success UI. Do not rely only on the widget callback; listening to this event ensures you capture completion even if the user navigates or the widget is unmounted.
Example (SDK instance):
const unbind = icpay.on('icpay-sdk-transaction-completed', (detail) => {
const paymentIntentId = detail.payment?.paymentIntentId ?? detail.paymentIntentId;
const paymentId = detail.payment?.paymentId;
});
Example (window — e.g. when using the widget, which forwards SDK events to window):
function handleSuccess(e: CustomEvent) {
const detail = e.detail ?? e;
const paymentIntentId = detail.payment?.paymentIntentId ?? detail.paymentIntentId;
const paymentId = detail.payment?.paymentId;
}
window.addEventListener('icpay-sdk-transaction-completed', handleSuccess as EventListener);
Transaction lifecycle events
icpay-sdk-transaction-created — Payment intent created; user still has to send funds. Detail: { paymentIntentId, amount, ledgerCanisterId, expectedSenderPrincipal?, accountCanisterId? }. Not emitted for onramp-only flows.
icpay-sdk-transaction-updated — Status changed (e.g. pending → processing). Detail: same shape as TransactionResponse (or with extra status, requestedAmount, paidAmount when relevant). Use for progress UI.
icpay-sdk-transaction-failed — Payment failed (rejected, timeout, or backend marked failed). Detail: TransactionResponse-like; check status: 'failed' and optional reason for messaging.
icpay-sdk-transaction-mismatched — Paid amount does not match requested amount. Detail: includes requestedAmount, paidAmount plus TransactionResponse fields. Followed by icpay-sdk-transaction-updated with status: 'mismatched'. Use to prompt user to correct or refund/partial-fulfill per business rules.
Method lifecycle events (generic)
Every SDK method that uses the internal emitter fires:
icpay-sdk-method-start — Method invoked. Detail: { name: string, args?: any } (e.g. name: 'createPayment', args: { request: { amountUsd, ... } }).
icpay-sdk-method-success — Method finished successfully. Detail: { name: string, result?: any } (e.g. name: 'createPayment', result is the return value or a summary).
icpay-sdk-method-error — Method threw. Detail: { name: string, error: any }.
Method names include: notifyPayment, subscribePaymentIntentStatus, waitForPaymentIntentCompletion, getAccountInfo, quoteAtxpRequest, payAtxpRequest, executeAtxpRequest, getVerifiedLedgers, getChains, getLedgerCanisterIdBySymbol, triggerTransactionSync, showWalletModal, connectWallet, getWalletProviders, isWalletProviderAvailable, getAccountAddress, getLedgerBalance, createPayment, createPaymentUsd, createPaymentX402Usd, pollTransactionStatus, notifyLedgerTransaction, getTransactionStatusPublic, sendFundsToLedger, getTransactionByFilter, getExternalWalletBalances, getSingleLedgerBalance, calculateTokenAmountFromUSD, getLedgerInfo, getAllLedgersWithPrices, and protected API method names when using icpay.protected.*.
For payment success, prefer icpay-sdk-transaction-completed over icpay-sdk-method-success for createPayment/createPaymentUsd/createPaymentX402Usd, because the transaction-completed event carries the final payment state and is emitted at the right semantic time.
Error event
icpay-sdk-error — Any SDK error (including from method-error). Detail: an IcpayError-like object (code, message, details?). Use for logging and user-facing error messages.
Optional / internal
icpay-sdk-onramp-intent-created — Emitted when an onramp-only flow creates an intent (e.g. Transak). Detail: { paymentIntentId, amountUsd?, onramp? }. Useful for UI that shows “redirect to onramp” state.
Widget-only (window)
icpay-x402-upto-submitted — Fired by icpay-pay-button after EVM up-to confirm succeeds (not part of @ic-pay/icpay-sdk’s icpay.on() API). Listen with window.addEventListener. Detail: { paymentIntentId: string, amountUsdMax: number }. Used by icpay-progress-bar together with x402UptoSkipSettlementWait to show “authorization saved” vs. advance to settlement polling. Custom apps can listen for the same milestone.
Summary table
| Event | When | Detail (main fields) |
|---|
| icpay-sdk-transaction-completed | Payment succeeded | TransactionResponse + payment? — use for fulfillment |
| icpay-sdk-transaction-created | Intent created | paymentIntentId, amount, ledgerCanisterId, … |
| icpay-sdk-transaction-updated | Status changed | TransactionResponse (+ status/requestedAmount/paidAmount when relevant) |
| icpay-sdk-transaction-failed | Payment failed | TransactionResponse, optional reason |
| icpay-sdk-transaction-mismatched | Amount mismatch | TransactionResponse + requestedAmount, paidAmount |
| icpay-sdk-method-start | Method called | name, args |
| icpay-sdk-method-success | Method resolved | name, result |
| icpay-sdk-method-error | Method rejected | name, error |
| icpay-sdk-error | Any SDK error | IcpayError-like |
| icpay-sdk-onramp-intent-created | Onramp intent created | paymentIntentId, amountUsd?, onramp? |
| icpay-x402-upto-submitted (window) | Pay button: EVM up-to confirm saved | paymentIntentId, amountUsdMax |
Payment links
Payment links are per-account entities with a unique shortcode. Public pay page: https://icpay.org/pay/<shortcode>.
- Create (user/dashboard): API
PaymentLinksService.createForAccount(accountId, dto); DTO includes name, description, amountUsd, collectEmail, requireEmail, widgetOptions, showWalletConnectQr, etc. Shortcode is generated (unique). User endpoints: POST /user/payment-links with JWT (see Flow: I am an agent).
- Create (POS / publishable key):
POST /sdk/public/payment-links with publishable key in Authorization: Bearer <pk_...>. Body: CreatePosPaymentLinkDto — amountUsd (required, min 0.01), optional name, description, tokenShortcodes (array; when exactly one, the payment intent is created with that token; otherwise intent has no token and user selects on pay page), showWalletConnectQr (default false for POS), showBaseWalletQr (default true). Creates the link and an associated payment intent; response includes shortcode and paymentIntentId. Use the pay page URL https://icpay.org/pay/<shortcode> (optionally ?paymentIntentId=<id> for pre-filled intent). POS-only: recipient addresses are not set on the link.
- Public fetch:
GET /public/payment-links/:shortcode returns { link, account } (link config + account publishableKey/branding). Used by icpay-web pay page.
- Merchant UI:
icpay-web → Payment Links → create/edit; link to /pay/<shortcode> shown after create.
Payment link entity: icpay-api/src/entities/payment-link.entity.ts. Fields: amountUsd, shortcode, fiatCurrencyId (display currency for the link), collect/require for email, name, address, phone, quantity (min/max/default), widgetOptions (JSON), showWalletConnectQr, showBaseWalletQr, isActive.
Payment intent: reusing by id (widget, SDK, X402)
When you already have a payment intent id (e.g. from a pay link or POS flow) but not the full intent object, pass paymentIntentId so the SDK and API reuse that intent instead of creating a new one.
- Widget: In pay-button config (or when creating the SDK via the widget), set
paymentIntentId when the full paymentIntent object is not loaded. The widget passes it into the SDK config; the SDK uses it in getOrResolvePaymentIntent and when calling the x402 intent endpoint.
- SDK config: You can pass
paymentIntent (full object) or paymentIntentId (string). When only the id is set, the SDK fetches the intent by id via GET /sdk/public/payments/intents/:id and uses it for createPayment, createPaymentUsd, and createPaymentX402Usd. This avoids creating a second intent when the user pays on a pay link (first intent created with the link, second avoided by passing the id).
- X402 intent endpoint: When calling
POST /sdk/public/payments/intents/x402, the SDK includes paymentIntentId in the request body when it has an existing intent (from config or request). The API reuses that intent when body.paymentIntentId is present: it loads the intent, verifies it belongs to the account and is in a reusable state (requires_payment or processing), merges icpay_x402: true into metadata, and returns that intent for the x402 response instead of creating a new one. Use this so a single payment intent is used end-to-end (e.g. pay link + x402 EVM flow) and metadata (e.g. icpayPaymentLink) is preserved.
Accounts
- User: Register via
POST /auth/register (firstName, lastName, email, password, etc.). Login: POST /auth/login; JWT returned.
- Account (merchant): Created per user via
POST /user-accounts with JWT; body: CreateAccountDto (name, email, country, businessName, accountType, businessType). User can own multiple accounts; each account has publishableKey and secret (managed by backend).
- Dashboard:
icpay-web: signup → create account → dashboard; create payment links, view payments, settings (including default fiat currency for the account). Switch account via auth/account context.
Currency: Payment links have fiatCurrencyId; account/user profile can set default fiat for display (e.g. USD, EUR). Used in widget as fiat_currency for amount display.
Account creation: icpay-api/src/accounts/accounts.service.ts → createAccount(userId, dto). Frontend: icpay-web/src/app/account/create/page.tsx and AccountsService.create().
Flow: I am an agent (fully programmatic via API)
Use this when you are an AI agent that must create the user, verify email, log in, create an account, and generate API keys entirely via requests to the ICPay API. Base URL: https://api.icpay.org
-
Register the user
- Endpoint:
POST https://api.icpay.org/auth/register
- Body (JSON):
RegisterDto — firstName, lastName, email, password (min 8 chars), dateOfBirth (ISO8601), addressLine1, addressLine2 (optional), city, stateOrProvince, postalCode, country (2-letter ISO), phone (optional), nationality (optional).
- Response:
201 with { message: "Registration successful. Please check your email to verify your account." }.
- A verification email is sent. It contains text like "Please verify your email address to complete your registration" and a "Verify Email" link. The link is the web URL, e.g.
https://icpay.org/auth/verify-email?token=<JWT>. The JWT is in the query parameter token.
-
Activate the account (verify email)
- Get the token: From the verification email, take the "Verify Email" link URL and read the
token query parameter (the JWT).
- Endpoint:
POST https://api.icpay.org/auth/verify-email
- Body (JSON):
{ "token": "<JWT from the link>" }
- Response:
200 with { message: "..." }. After this, the user's email is verified and they can log in.
-
Log in (start OTP)
- Endpoint:
POST https://api.icpay.org/auth/login
- Body (JSON):
{ "email": "<email used in registration>", "password": "<password>" }
- Response:
200 with { requires2fa: true, challengeId: "<sid>", user: { id, firstName, lastName, email, ... } }. No JWT yet. An email is sent to the user's email with a verification code, e.g. "icpay.org authentication email code" / "The verification code for icpay.org is: 313215". The agent must obtain this code (e.g. read from mail or have the user provide it).
-
Complete login (submit OTP code)
- Endpoint:
POST https://api.icpay.org/auth/verify-login-otp
- Body (JSON):
{ "email": "<same email>", "code": "<6-digit code from email>" }
- Response:
200 with { access_token: "<JWT>", user: { ... } }. Use access_token as the Bearer token for all following requests.
-
Use the Bearer token for all further requests
- Send header:
Authorization: Bearer <access_token> on every request to the API.
-
Create one account
- Endpoint:
POST https://api.icpay.org/user-accounts
- Headers:
Authorization: Bearer <access_token>, Content-Type: application/json
- Body (JSON):
CreateAccountDto — name, email, and optionally country (2-letter), accountType, businessName, businessType.
- Response:
201 with the created account object; note id (account ID).
-
Generate API keys (publishable + secret)
- Endpoint:
POST https://api.icpay.org/user-accounts/:accountId/generate-secret-key
- Headers:
Authorization: Bearer <access_token>
- Body: none (or
{}).
- Response:
200 with { secretKey: "sk_...", publicKey: "pk_..." }.
- Publishable key (
pk_...): Safe to store in .env (e.g. NEXT_PUBLIC_ICPAY_PK) or use directly in client code; it is public.
- Secret key (
sk_...): Store only in .env (e.g. ICPAY_SECRET_KEY). Never embed in source code or commit to git; use only on the server.
-
Use in the app
- Publishable key: Use with icpay-widget to build any frontend that accepts crypto payments (pay button, tip jar, paywall, etc.).
- Secret key: Use with icpay-sdk on the server for protected operations (e.g.
icpay.protected.getPaymentById, listPayments, getDetailedAccountInfo) and to verify payment state. Alternatively (or in addition), register a webhook URL in the ICPay dashboard; verify X-ICPay-Signature and handle payment.completed and payment.refunded for fulfillment. For X402 up-to, also subscribe to x402_upto_authorization_received so your backend syncs when the signed header is stored (before payment.completed).
-
Other API endpoints (all require Authorization: Bearer <access_token>)
With the JWT from step 4, the agent can perform all account operations via the API without using icpay.org. Base URL: https://api.icpay.org. Send Content-Type: application/json where a body is used.
Email code for sensitive actions: For payouts, split rules (create/update/delete/replace), the API requires an email verification code. First call POST /user/security/email-challenge/start (optional body { reason?, metadata? }); the user receives an email with a code. Then include that code in the request body as emailSecurityCode or securityCode when calling the endpoint below. Alternatively use the same code from the login OTP flow if still valid for the same user.
-
User profile
GET /users/profile — Get current user profile.
PATCH /users/profile — Update profile. Body: SelfUpdateUserDto (optional: firstName, lastName, phone, dateOfBirth, avatarUrl, nationality, address { line1, line2, city, stateOrProvince, postalCode, country }, fiatCurrencyId). Email cannot be changed via this endpoint.
-
Switch account
POST /auth/switch-account — Body: { "accountId": "<uuid>" }. Returns new token scoped to that account; use for subsequent requests if the user has multiple accounts.
-
List user's accounts
GET /user-account-users/my-accounts — List accounts the user belongs to.
GET /user-account-users/my-pending-invitations — List pending invitations.
POST /user-account-users/invite — Invite a user to an account. Body: CreateInvitationDto — accountId (string), email (string), optional role (e.g. "owner" | "admin" | "viewer", default "viewer"), optional permissions (string[]). invitedBy is not accepted from the client; it is set server-side from the authenticated user (JWT).
POST /user-account-users/:id/accept — Accept invitation.
POST /user-account-users/:id/decline — Decline invitation.
GET /user-account-users/account/:accountId — List users for an account (must be member).
-
Account (user-accounts)
PATCH /user-accounts/:id — Update account (JWT: owner or admin of the account). Body: UpdateAccountDto — all optional, only the following are intended for user/owner use (admin-only and system fields are not listed): name, email, country, accountType, businessName, businessType, isActive, isLive, taxId, businessProfile (object: name, url, mcc, supportEmail, supportPhone, supportUrl), capabilities (object: cardPayments, transfers, taxReporting), requirements (object: currentlyDue, eventuallyDue, pastDue, disabledReason), primaryDomain, branding (object: logoUrl, faviconUrl, primaryColor, secondaryColor), address, billingAddress, taxInfo, relayFeeBps (number, basis points), settings.
POST /user-accounts/:id/regenerate-secret-key — Regenerate API keys; returns { secretKey, publicKey } (show secret once).
POST /user-accounts/:id/phone-change/start — Start phone change; body { phone }.
POST /user-accounts/:id/phone-change/verify — Body { challengeId, code } to confirm phone change.
-
Payment links
GET /user/payment-links?accountId=<uuid> — List payment links for account.
GET /user/payment-links/:id — Get one payment link.
POST /user/payment-links — Create. Body: CreatePaymentLinkDto — name, description, amountUsd, optional fiatCurrencyId, accountId (or use query ?accountId=), collect/require (email, name, address, phone, business, shipping), quantity (allow, default, min, max), maxRedemptions, widgetOptions, showWalletConnectQr, showBaseWalletQr, isActive. Shortcode is generated.
PUT /user/payment-links/:id — Update. Body: UpdatePaymentLinkDto (same fields as create, partial).
DELETE /user/payment-links/:id — Delete payment link.
GET /user/payment-links/:id/submissions — List submissions for the link.
-
User wallets and balances
GET /user-wallets/account/:accountId — List wallets for account (JWT). Requires membership.
GET /user-wallets/:walletId/with-balances — Get one wallet with per-ledger balances (JWT). Current way to get balances when using user auth. When using icpay.org, call via proxy: GET https://icpay.org/api/proxy/user-wallets/<walletId>/with-balances (session cookie). Direct API: GET https://api.icpay.org/user-wallets/<walletId>/with-balances with Authorization: Bearer <JWT>.
- SDK (secret key) — Use when you have the account’s secret key (server/agent):
GET /sdk/account/wallet-balances — Aggregated balances for all wallets of the account. Header: Authorization: Bearer <secret_key>.
GET /sdk/wallets/with-balances — All ledger balances for the account (all internal wallets). Header: Authorization: Bearer <secret_key>.
-
Payments (list, get, refund)
GET /user/payments?accountId=<uuid> — List payments (with pagination).
GET /user/payments/summary — Payment summary for account.
GET /user/payments/stats — Payment stats.
GET /user/payments/:id — Get payment by ID.
GET /user/payments/:id/refund-precheck — Check if refund is allowed.
POST /user/payments/:id/refund — Create a refund for the payment. No body required (refund is full). Refunds are processed by the execute-refunds worker; webhook payment.refunded is sent when done.
-
Payouts (require email code)
GET /user/payouts?accountId=<uuid> — List payouts.
POST /user/payouts — Create payout. Body: accountId, amount (decimal string), ledgerId, optional ledgerCanisterId, accountCanisterId, toWalletAddress, toWalletSubaccount, and emailSecurityCode or securityCode (code from email). Only OWNER or admin with payouts permission.
POST /user/payouts/:id/execute — Execute a created payout.
-
Webhook endpoints
GET /user/webhook-endpoints?accountId=<uuid> — List webhook endpoints.
GET /user/webhook-endpoints/:id — Get one endpoint.
POST /user/webhook-endpoints — Create. Body: CreateWebhookEndpointDto — endpointUrl, eventTypes (array, e.g. ["x402_upto_authorization_received","payment.completed","payment.refunded"] for up-to flows), optional accountId, isActive, secretKey, description, retryCount, timeoutSeconds, headers.
PUT /user/webhook-endpoints/:id — Update. Body: UpdateWebhookEndpointDto (partial: endpointUrl, eventTypes, isActive, description, retryCount, timeoutSeconds, headers).
DELETE /user/webhook-endpoints/:id — Delete webhook endpoint.
POST /user/webhook-endpoints/:id/test — Send a test event to the endpoint.
-
Webhook events
GET /user/webhook-events — List webhook events (query filters).
GET /user/webhook-events/:id — Get one event.
-
Split rules (require email code for create/update/delete)
GET /user/accounts/:accountId/split-rules — List split rules for account.
POST /user/accounts/:accountId/split-rules — Create split rule. Body: CreateSplitRuleDto — targetAccountCanisterId (number), percentageBps (0–10000), optional targetAccountId; and emailSecurityCode or securityCode. OWNER only.
PUT /user/accounts/:accountId/split-rules/:id — Update split rule. Body: UpdateSplitRuleDto (partial) + emailSecurityCode or securityCode. OWNER only.
PUT /user/accounts/:accountId/split-rules — Replace all rules atomically. Body: { rules: CreateSplitRuleDto[], emailSecurityCode?: string, securityCode?: string }. OWNER only.
DELETE /user/accounts/:accountId/split-rules/:id — Delete split rule. Body: emailSecurityCode or securityCode. OWNER only.
GET /user/accounts/:accountId/transactions/:transactionId/splits — Get splits for a transaction.
-
Transactions
GET /user-transactions?accountId=<uuid> — List transactions.
GET /user-transactions/:id — Get transaction by ID.
-
Notifications
GET /user/notification-templates — List notification templates.
GET /user/accounts/:accountId/subscriptions — List subscription for account.
POST /user/accounts/:accountId/subscriptions — Subscribe; body { templateId }.
DELETE /user/accounts/:accountId/subscriptions/:templateId — Unsubscribe.
-
Security (email challenge — get a code for sensitive actions)
POST /user/security/email-challenge/start — Request an email with a verification code. Optional body: { reason?, metadata? }. Response includes challengeId (optional).
POST /user/security/email-challenge/verify — Verify the code. Body: { code: "<6-digit>" }. Use the same code in payout/split/wallet requests as emailSecurityCode or securityCode.
With these endpoints, the agent can create and manage payment links, webhooks, splits, payouts, refunds, wallets, and user profile entirely via the API, without using the icpay.org dashboard.
Flow: I am the human (getting started myself)
Use this when you are the developer/user and want to register on icpay.org, create one account, get API keys, and accept crypto payments.
- Sign up — Go to https://icpay.org and complete Sign up (first name, last name, email, password, and any other required fields such as address, country).
- Verify email — Check the email inbox for the address you used. You will either receive:
- A link — Click “Verify Email” (or similar) to complete verification; or
- A code — Enter that code on the verification page when prompted.
Do not skip this step; you cannot log in until email is verified.
- Log in — Go to https://icpay.org/auth/login and sign in with your email and password.
- Create one account — After login, create a business account (name, email, country, business name, business type). This is your merchant account for receiving payments.
- Generate API keys — In the dashboard, go to Settings (or API Keys). Click Generate (or Generate secret key). You will see:
- Publishable key (
pk_live_... or pk_test_...) — Safe to use in frontend or put in .env (e.g. NEXT_PUBLIC_ICPAY_PK).
- Secret key (
sk_...) — Shown once; copy it immediately. Store it only in .env (e.g. ICPAY_SECRET_KEY=sk_...). Never put the secret key in your source code or commit it to git.
- Use in your app — Use the publishable key with icpay-widget (e.g.
@ic-pay/icpay-widget) to add pay buttons, tip jars, paywalls, or other components that accept crypto. To verify that a payment succeeded, use either:
- icpay-sdk on your server with the secret key (e.g.
icpay.protected.getPaymentById(id)), or
- A webhook URL registered in the ICPay dashboard; verify the
X-ICPay-Signature header and handle payment.completed (and optionally payment.refunded).
Webhooks
- Endpoint: Merchant registers URL in ICPay dashboard; events posted to that URL.
- Security: Verify
X-ICPay-Signature = HMAC-SHA256(raw body, webhook secret) using constant-time compare. Reject if invalid.
- Payload: JSON body;
event.type e.g. payment.completed, payment.failed, payment.refunded, x402_upto_authorization_received (X402 up-to: header saved on intent). Use event/payment IDs for idempotency.
- Retries: Backoff and retries by backend; handle duplicate deliveries idempotently.
Refunds
- Refunds are requested/executed via API or dashboard; execute-refunds worker processes them.
- Webhook
payment.refunded is sent when a refund completes. Email notification (refund completed) can be sent to the account; templates in notification_templates (e.g. email_refund_completed_account).
Split payments (optional)
- Split rules let multiple merchants share revenue: per account, define target account(s) and percentage (basis points). Entity:
SplitRule (accountId, targetAccountId / targetAccountCanisterId, percentageBps). Services distribute funds according to rules.
- API: splits module (
icpay-api/src/splits/) — user/sdk controllers for split rules (create, update, list). Optional feature; when not used, 100% goes to the receiving account.
Email notifications
- Payment completed and refund completed emails can be sent to the account. process-notifications worker and notification templates in the API; user/account can have default fiat for amount in emails.
- Configure in dashboard/account settings; templates editable via migrations or admin.
Example verification (Node):
const crypto = require('node:crypto');
const sig = req.headers['x-icpay-signature'] || '';
const raw = req.body;
const expected = crypto.createHmac('sha256', process.env.ICPAY_WEBHOOK_SECRET).update(raw).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return res.status(401).send();
Events (Widget & SDK)
Widget: icpay-pay (payment done), icpay-error, icpay-unlock (paywall), icpay-tip, icpay-donation. SDK (on window): icpay-sdk-method-start|success|error, icpay-sdk-transaction-created|updated|completed|failed|mismatched, icpay-sdk-wallet-connected|disconnected|cancelled|error. Subscribe on the widget element or window to drive UI/analytics; do not rely on console.
Demo / playground (demo.icpay.org)
- https://demo.icpay.org — Live demo app (
icpay-demo/) for building and testing custom widgets. All widget types, configurable options, copy-paste snippets. Use for quick experiments and sharing configs (e.g. with publishableKey in query).
Sandbox (betterstripe.com)
- https://betterstripe.com — Sandbox environment for developers. Same functionality as icpay.org (dashboard, payment links, widget, API, webhooks, relay, X402, splits, refunds, email notifications) but uses testnets as well as mainnets, so you can test without mainnet funds if you need to.
- Networks: Solana devnet, Base Sepolia, Ark network testnet, and other supported testnets. Mainnet chains (e.g. Solana mainnet, Base mainnet, IC mainnet);
- Use case: Integrate the widget or SDK against the sandbox API and pay page; create test accounts and payment links; verify webhooks, relay, and refund flows with testnet tokens.
- Keys: Sandbox uses test keys (e.g.
pk_test_*); keep sandbox and production keys separate. API base and pay page are sandbox-specific (betterstripe.com); switch to icpay.org and production API when going live.
WordPress plugins
Two plugins live under icpay-integrations/:
- icpay-payments — Standalone: Gutenberg block + shortcodes for all widgets; settings for publishable/secret key; webhook receiver; sync payments. Webhook URL:
/wp-json/icpay-payments/v1/webhook.
- instant-crypto-payments-for-woocommerce — WooCommerce gateway: checkout/order-pay pay button; webhook updates order status; reuses ICPay keys from main plugin if present. Webhook URL:
/wp-json/instant-crypto-payments-for-woocommerce/v1/wc/webhook.
Both verify webhooks with HMAC-SHA256. Widget script: assets/js/icpay-embed.min.js (built from icpay-widget). See wordpress.md for build and shortcode/block usage.
Conventions
- Use pnpm for install/build.
- Prefer SDK over direct HTTP for payment/account operations. Prefer widget events for success/error handling.
- Token identification: Use
tokenShortcode (e.g. ic_icp, base_eth); legacy symbol/ledgerCanisterId/chainId still supported.
- Errors: Catch
IcpayError; check code and message; surface user-friendly messages; log details server-side only when needed.
Additional resources
- API, entities, workers, splits, refunds, X402: reference.md
- Widget components, config, wallet adapters, filter tokens: widget-reference.md
- WordPress build and usage: wordpress.md
- Docs site: https://docs.icpay.org
- Demo: https://demo.icpay.org
- Sandbox (testnets): https://betterstripe.com — Solana devnet, Base Sepolia, Ark testnet, etc.
- This skill (source): https://github.com/icpay/icpay-sdk/tree/master/skills/icpay — npm: @ic-pay/icpay-sdk, @ic-pay/icpay-widget