All integrations

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.

01

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.

CoinVoyage.tsx
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.",
  },
})
02

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:

Copy and paste
{"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:

Copy and paste
{"accent":"#d6296f","accentHover":"#b8225f","accentText":"#ffffff","radius":"16px"}

For PayKit:

Copy and paste
{"--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.

03

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.

Framer · Custom Code
<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.

04

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.

Before going live

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.