SETUP GUIDE
CoinVoyage for Framer
Accept deposits with PayKit and add token swaps with SwapKit to your published Framer site.
Requirements
Your Framer site needs Custom Code access. A CoinVoyage public API key connects the widgets to your account. Widgets run on published pages; the Framer canvas displays a placeholder.
Get your CoinVoyage API key
Follow the API key setup guide to create your public API key.
PayKit supports crypto and eligible card payments. SwapKit is for token swaps.
An easier way to sell products is coming
Accept deposits and offer token swaps today. You can also sell products now if you connect PayKit to your own server. We're building a simpler checkout so you can sell from Framer without setting up a server.
Add the component
Create a new Code Component in Framer. Copy the complete source below, replace the default component code, and place the component on your page.
import { addPropertyControls, ControlType, RenderTarget } from "framer"
import type { CSSProperties } from "react"
type Props = {
widget: "swap" | "pay"
apiKey: string
orderId: string
buttonText?: string
optionsJson?: string
debugMode?: boolean
paymentMethod?: "" | "CARD" | "WALLET" | "DEPOSIT_ADDRESS"
closeOnSuccess?: boolean
resetOnSuccess?: boolean
mode: "auto" | "light" | "dark"
walletConnectProjectId: string
paymentConfig: string
accent?: string
accentHover?: string
accentText?: string
background?: string
surface?: string
text?: string
radius?: string
fontFamily?: string
themeJson?: string
style?: CSSProperties
}
/** @framerSupportedLayoutWidth any @framerSupportedLayoutHeight any */
export default function CoinVoyage(props: Props) {
if (RenderTarget.current() === RenderTarget.canvas) {
return (
<div style={{ ...props.style, minHeight: 80, display: "grid", placeItems: "center" }}>
CoinVoyage {props.widget === "pay" ? "PayKit" : "SwapKit"} ? available on the published site
</div>
)
}
let payment: Record<string, unknown>
let theme: Record<string, unknown>
let options: Record<string, unknown> | undefined
try {
if (props.widget === "pay" && props.optionsJson?.trim()) {
options = JSON.parse(props.optionsJson)
if (!options || typeof options !== "object" || Array.isArray(options)) throw new Error("Invalid provider options")
}
theme = props.themeJson?.trim() ? JSON.parse(props.themeJson) : {}
if (!theme || typeof theme !== "object" || Array.isArray(theme)) throw new Error("Invalid theme")
payment = props.orderId
? { orderId: props.orderId }
: props.paymentConfig
? { deposit: JSON.parse(props.paymentConfig) }
: {}
} catch {
return <div role="alert">Invalid CoinVoyage deposit, theme or provider options JSON.</div>
}
const appearance = Object.fromEntries(
[
"accent",
"accentHover",
"accentText",
"background",
"surface",
"text",
"radius",
...(props.widget === "swap" ? ["fontFamily"] : []),
]
.map((key) => [key, props[key as keyof Props]])
.filter(([, value]) => typeof value === "string" && value.trim() !== "")
)
const config = {
appearance,
...(props.widget === "pay" ? { customTheme: theme } : { theme }),
widget: props.widget,
apiKey: props.apiKey,
environment: "production",
mode: props.mode,
...(props.walletConnectProjectId ? { walletConnectProjectId: props.walletConnectProjectId } : {}),
...(props.widget === "pay"
? {
...payment,
buttonText: props.buttonText,
options,
debugMode: props.debugMode,
...(props.paymentMethod ? { paymentMethod: props.paymentMethod } : {}),
closeOnSuccess: props.closeOnSuccess,
resetOnSuccess: props.resetOnSuccess,
}
: {}),
}
return <div style={props.style} data-coinvoyage={JSON.stringify(config)} />
}
addPropertyControls(CoinVoyage, {
widget: { type: ControlType.Enum, options: ["swap", "pay"], defaultValue: "swap" },
apiKey: { type: ControlType.String, title: "Public API key" },
buttonText: { type: ControlType.String, title: "Button text", defaultValue: "Pay" },
optionsJson: {
type: ControlType.String,
title: "Provider options",
displayTextArea: true,
description: 'PayKit options JSON, for example {"language":"de-DE","hideTooltips":true}.',
hidden: (props) => props.widget !== "pay",
},
debugMode: {
type: ControlType.Boolean,
title: "Debug mode",
defaultValue: false,
hidden: (props) => props.widget !== "pay",
},
paymentMethod: {
type: ControlType.Enum,
title: "Payment method",
options: ["", "CARD", "WALLET", "DEPOSIT_ADDRESS"],
optionTitles: ["Default", "Pay with card", "Wallet", "Deposit address"],
defaultValue: "",
hidden: (props) => props.widget !== "pay",
},
closeOnSuccess: {
type: ControlType.Boolean,
title: "Close on success",
defaultValue: false,
hidden: (props) => props.widget !== "pay",
},
resetOnSuccess: {
type: ControlType.Boolean,
title: "Reset on success",
defaultValue: false,
hidden: (props) => props.widget !== "pay",
},
orderId: { type: ControlType.String, title: "Existing order" },
paymentConfig: { type: ControlType.String, title: "Deposit JSON" },
mode: { type: ControlType.Enum, options: ["auto", "light", "dark"], defaultValue: "auto" },
walletConnectProjectId: { type: ControlType.String, title: "WalletConnect ID" },
accent: { type: ControlType.Color, title: "Accent", optional: true },
accentHover: { type: ControlType.Color, title: "Accent hover", optional: true },
accentText: { type: ControlType.Color, title: "Button color", optional: true },
background: { type: ControlType.Color, title: "Background", optional: true },
surface: { type: ControlType.Color, title: "Surface", optional: true },
text: { type: ControlType.Color, title: "Text color", optional: true },
radius: {
type: ControlType.String,
title: "Radius",
placeholder: "12px",
description: "PayKit button radius; SwapKit corner radius. Leave blank for the default.",
},
fontFamily: {
type: ControlType.String,
title: "Font family",
hidden: (props) => props.widget !== "swap",
description: "Load custom fonts on your site.",
},
themeJson: {
type: ControlType.String,
title: "Theme JSON",
displayTextArea: true,
description: "Advanced SDK theme properties override the basic controls. Leave blank for defaults.",
},
})
Choose your widget
In the component's properties, choose swap or pay and enter your Public API key. Choose a color mode. The integration always uses production.
For PayKit, set Button text to a label such as “Donate now” or “Pay now”. Leave it blank to use “Pay”.
For PayKit deposits, leave Existing order empty and paste this JSON into Deposit JSON, replacing the recipient address:
{"toChain":1,"toAmount":0.01,"toAddress":"YOUR_RECIPIENT_ADDRESS"}A deposit does not automatically verify a product purchase.
Match your site’s appearance
Use Accent, Accent hover, Button color, Background, Surface, Text color, and Radius in the component properties. Radius styles PayKit buttons or SwapKit corners; SwapKit also has a Font family field. Leave controls empty to keep the defaults, including SwapKit’s transparent outer background.
For advanced styling, paste the widget’s SDK theme object into Theme JSON. For SwapKit:
{"accent":"#d6296f","accentHover":"#b8225f","accentText":"#ffffff","radius":"16px"}For PayKit:
{"--ck-primary-button-background":"#d6296f","--ck-primary-button-color":"#ffffff","--ck-body-background":"#ffffff","--ck-body-color":"#111827"}Advanced values override matching basic controls. Use literal CSS values; URLs and CSS declarations are not accepted. Load custom fonts on your site. Custom colors apply in both light and dark modes, so choose matching background and text colors.
Have your own server? Start selling products now
Your server can create a new purchase order (called a SALE order in the API) for each checkout using your production API key and API secret. Validate the product, price, and recipient on your server, then return the order ID to your Framer site and pass it to PayKit through the component's Existing order property.
The component accepts an order ID but does not call your backend automatically. Connect your checkout flow to supply a new order ID for each purchase; do not reuse a single order for multiple buyers.
Keep the API secret on your server and verify payment completion there before fulfillment.
Add the site script
Open your site's Custom Code settings. Add this snippet at the end of the body, on all pages, and set it to run once.
<script type="module">
import * as runtime from "YOUR_PLUGIN_SITE/coinvoyage/0.1.7/coinvoyage.js"
import { observeWidgets } from "YOUR_PLUGIN_SITE/coinvoyage/0.1.7/loader.js"
observeWidgets(runtime)
</script>Copy code fills in this site’s address. For a published Framer site, copy from the deployed HTTPS version of this page.
The script loads the hosted CoinVoyage integration and handles page navigation automatically.
Publish a test page
Publish your Framer site and check both the widget and your chosen wallets. If you use WalletConnect, configure its project ID and site origin.
Published Framer wallet and mobile return testing is still in progress. Test your target wallets before offering this to customers. Keep your API secret on your backend.
Payment options
These controls require integration version 0.1.6 or later. The downloads on this page are version 0.1.7. When upgrading, replace both the Code Component and the versioned Custom Code imports.
Open checkout on Pay with card
Choose the pay widget in the Framer component's properties. Set Payment method to Pay with card. You can also choose Wallet, Deposit address or Default to keep the normal starting screen.
Card payments must be enabled and available for your account and order. If unavailable, PayKit shows payment method selection. Choosing a method selects the first screen when the modal opens. Customers still click the payment button to open it.
Close on success closes the modal after payment; Reset on success clears the widget's payment state. A new purchase still needs a fresh server-created order; resetting a widget does not create one.
Configure PayKit options and debug mode
Paste JSON into the Framer component's Provider options field. For example:
{"language":"de-DE","hideTooltips":true,"overlayBlur":4}Use Debug mode for SDK diagnostics; leave it off for normal use.
| Option | Value |
|---|---|
language | A supported locale, such as en-US, de-DE or fr-FR. |
hideTooltips, hideQuestionMarkCTA, hideRecentBadge | true or false to hide each UI element. |
walletConnectCTA | "link", "modal" or "both". |
disclaimer | Plain text, or null to clear it. HTML is displayed as text. |
overlayBlur | A finite number of zero or greater. |
optimisticConfirmation | true or false. Browser completion still requires server verification. |
bufferPolyfill | true or false. Leave unset unless you need to control the Buffer polyfill. |
experimentalFeatures.cardPayments | true or false. An explicit card-payments setting takes precedence. |
Omit options to keep SDK defaults. Unknown fields and invalid values are rejected. hideNoWalletCTA is accepted by PayKit but currently has no visible effect.