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
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 = {}
let theme = {}
try {
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 or theme 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 } : {}),
}
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" },
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.4/coinvoyage.js"
import { observeWidgets } from "YOUR_PLUGIN_SITE/coinvoyage/0.1.4/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.