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

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.

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:

Provider options JSON
{"language":"de-DE","hideTooltips":true,"overlayBlur":4}

Use Debug mode for SDK diagnostics; leave it off for normal use.

OptionValue
languageA supported locale, such as en-US, de-DE or fr-FR.
hideTooltips, hideQuestionMarkCTA, hideRecentBadgetrue or false to hide each UI element.
walletConnectCTA"link", "modal" or "both".
disclaimerPlain text, or null to clear it. HTML is displayed as text.
overlayBlurA finite number of zero or greater.
optimisticConfirmationtrue or false. Browser completion still requires server verification.
bufferPolyfilltrue or false. Leave unset unless you need to control the Buffer polyfill.
experimentalFeatures.cardPaymentstrue 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.