@ic-pay/payload-plugin-icpay is a Payload CMS plugin that adds:
- ICPay server endpoints for creating and tracking payments
- Read-only
icpay-paymentsstorage populated by webhook/sync (not manual admin creation) - Global settings for API URL + publishable/secret keys
- React widget helpers powered by
@ic-pay/icpay-widget(payments, donations, topups) - Optional Lexical rich-text integration: insert ICPay Widget blocks inline (subpath
@ic-pay/payload-plugin-icpay/lexical) and render them with@ic-pay/payload-plugin-icpay/lexical-react - Optional bridge helpers for Payload ecommerce plugin payment hooks
This package is designed as a standalone GitHub/npm repository.
pnpm add @ic-pay/payload-plugin-icpayimport { buildConfig } from 'payload';
import icpayPayloadPlugin from '@ic-pay/payload-plugin-icpay';
export default buildConfig({
collections: [
// your collections
],
plugins: [
icpayPayloadPlugin({
enabled: true,
// Optional: `sdk` env defaults; otherwise configure Admin → Globals → icpay-settings.
// sdk: { publishableKey: '...', secretKey: '...', apiUrl: 'https://api.icpay.org' },
defaults: {
fiatCurrency: 'USD'
}
})
]
});By default (createCollections and createGlobalSettings are true):
- Collection
icpay-payments - Global
icpay-settings - Endpoints:
POST /api/icpay/create-paymentPOST /api/icpay/sync-paymentsGET /api/icpay/public-ledgers(admin session; proxies to ICPayGET /sdk/public/ledgers/all-with-pricesfor the Lexical token picker)POST /api/icpay/webhook
Requires @payloadcms/richtext-lexical (same major as your Payload app, e.g. ^3.82).
1. Admin — enable the block on your richText field (same idea as WordPress “insert block”):
import { lexicalEditor } from '@payloadcms/richtext-lexical';
import { icpayWidgetBlocksFeature } from '@ic-pay/payload-plugin-icpay/lexical';
{
name: 'content',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [...defaultFeatures, icpayWidgetBlocksFeature()],
}),
}Optional: add a standalone blocks field elsewhere using createIcpayWidgetsField() from the same subpath.
Each ICPay Widget block includes Metadata as repeatable key / value rows (Payload array field), not a raw JSON textarea—similar to WordPress-style meta. Rows are merged into one object for checkout. Legacy blocks that still store metadata as a JSON object are supported at render time (normalizeWidgetMetadata).
Filter allowed tokens (optional) matches the WordPress block: the admin UI loads verified ledgers from your ICPay API (using Globals → icpay-settings publishable key + API URL) via GET /api{apiBasePath}/public-ledgers (admin session required), groups them by chain, and stores selected shortcodes as a JSON string[]. Empty selection = all tokens. Helpers: normalizeAllowedTokenShortcodes on @ic-pay/payload-plugin-icpay/lexical (and lexical-react).
The plugin sets config.custom.icpayApiBasePath (default /icpay) so the admin picker matches your icpayPayloadPlugin({ apiBasePath }) routes.
2. Frontend — render Lexical JSON and map embedded widgets (e.g. Next.js App Router):
The lexical-react entry is a Client Component ('use client'), because @payloadcms/richtext-lexical/react’s RichText uses client hooks. You may import IcpayRichText from a Server Component (e.g. a page.tsx); Next will render it as a client boundary.
import { IcpayRichText } from '@ic-pay/payload-plugin-icpay/lexical-react';
<IcpayRichText
data={page.content}
widgetDefaults={{
publishableKey: settings?.publishableKey,
apiUrl: settings?.apiUrl,
fiatCurrency: settings?.defaultFiatCurrency,
defaultRecipientAddress: settings?.defaultRecipientAddress,
}}
/>Use a CSS class on the wrapper via className (default cms-rich-text) for typography.
Creates an ICPay payment via @ic-pay/icpay-sdk.
Example request:
{
"kind": "donation",
"amountUsd": 15,
"fiatCurrency": "USD",
"description": "Support our project",
"paymentMethod": "wallet",
"metadata": {
"campaign": "spring-2026"
}
}Pulls payments from icpay-api and upserts into icpay-payments.
Defaults:
- path:
/sdk/payments(icpay-apiSdkPaymentsController, secret key bearer — plainfetch, not the JS SDK) - auth:
Authorization: Bearer <secretKey>from Globals → icpay-settings (or pluginsdk) - source flag in DB:
sync
You can override sync behavior with plugin option:
sync: {
endpointPath: '/sdk/payments',
method: 'GET',
limit: 100 // optional `?limit=` for non–icpay-api backends
}Receives Stripe-shaped events from icpay-api (type, data.object, …). Headers:
X-ICPay-Signature(orx-icpay-signature/x-icpay-webhook-signature)
icpay-api signs with t=<unix>,v1=<hex>: v1 is HMAC-SHA256 of the UTF-8 string timestamp + '.' + rawJsonBody using the account SDK secret key (same as secretKey in Globals). Payload verifies using the exact raw POST bytes (important for HMAC).
If secretKey is unset, signature verification is skipped (same idea as the WooCommerce plugin with no secret). For older clients, a header value that is a single hex string (whole-body HMAC-SHA256, no t= / v1= parts) is also accepted.
Payload unwraps data.object, normalizes the flat payment row into the same shape as GET /sdk/payments, and upserts icpay-payments (source: webhook). Events without a resolvable paymentIntentId return 200 with { ignored: true } and do not write a row.
Admin: Globals → icpay-settings ends with an Inbound webhooks block showing the full webhook URL (from serverURL + /api + apiBasePath + /webhook). Override the path segment with NEXT_PUBLIC_ICPAY_API_BASE in the Next app if you change apiBasePath from /icpay.
icpay-paymentsis read-only from admin (create/update/deletedisabled). Collectiontimestamps: false:createdAt/updatedAtare explicit date fields filled only from ICPay API / webhook payloads (payment + intent + transaction), not Payload ingest time.- no separate webhook collection is created
- Sync button (default on): above the payments list, calls
POST /api/icpay/sync-paymentswith the admin session and refreshes the list. The component is exposed as the package subpath@ic-pay/payload-plugin-icpay/icpay-sync-payments(not a filesystem path) sopayload generate:importmapand Next resolve it in Docker and locally. Requiresnext,@payloadcms/ui, andtranspilePackages: ['@ic-pay/payload-plugin-icpay']innext.config. - Clear payments (default on): Globals → icpay-settings includes a
uifield with a normal button that callsPOST /api/icpay/clear-payments(admin session) after a browserconfirm()explaining the impact. Subpath:@ic-pay/payload-plugin-icpay/icpay-clear-payments. - Webhook URL help (default on): last block on icpay-settings shows the POST URL for icpay-api and how
x-icpay-signature/x-icpay-webhook-signaturerelate to secretKey. Subpath:@ic-pay/payload-plugin-icpay/icpay-webhook-help. - use the sync endpoint (or your own scheduler) to pull historical records from icpay-api
Configure publishable key, secret key, API URL in Globals → icpay-settings. Optional plugin sdk values merge as fallbacks when a global field is empty.
This package re-exports React wrappers powered by @ic-pay/icpay-widget:
import {
IcpayPaymentWidget,
IcpayDonationWidget,
IcpayTopupWidget
} from '@ic-pay/payload-plugin-icpay/widgets';Example:
<IcpayDonationWidget
publishableKey={process.env.NEXT_PUBLIC_ICPAY_PUBLISHABLE_KEY!}
goalUsd={5000}
defaultAmountUsd={25}
metadata={{ page: 'donations' }}
/>The bridge exposes generic hook handlers that you can map into your ecommerce integration:
import { createIcpayEcommerceBridge } from '@ic-pay/payload-plugin-icpay/commerce';
const bridge = createIcpayEcommerceBridge({
enabled: true,
sdk: {
publishableKey: process.env.ICPAY_PUBLISHABLE_KEY!,
secretKey: process.env.ICPAY_SECRET_KEY!,
apiUrl: process.env.ICPAY_API_URL
}
});
// attach bridge.beforeInitiatePayment / beforeConfirmOrder / afterConfirmOrder
// to your ecommerce payment hook pipelinepnpm install
pnpm build
pnpm typecheck- Update
package.jsonrepository/homepage/bugs URLs - Tag the GitHub repository with
payload-plugin - Publish npm package:
npm publish --access public