VoidhashDocs
DocumentationAPI reference
Dashboard
Get started
Introduction
Quickstart
Setup
Setup overview
Create a project
Products and perks
Paywall locations
CLI
Paywall components
Components
Develop a component
Editor panels
Component definition
Publishing
Installing and capabilities
SDKs
Swift
Kotlin
React Native
Web
Store setup
Connect the App Store
Connect Google Play
Connect Stripe
Record payments from other sources
API libraries
Node.js
PHP
Rust
Go
Resources
MCP server
Ad measurement
Meta ads
Migrate in observer mode
Delete a person's data
Web SDK
Web quickstart
Script tag
Identify users
Track events
Privacy and consent
Page views and attribution
Stripe revenue
Custom provider checkouts
Web-to-app handoff
First-party proxy
Tracking domain
Feature flags
Troubleshooting

Custom provider checkouts

Send a visitor to a checkout at your own payment provider, such as Paddle, and credit the payment to them.

When you sell through a payment source, such as Paddle, Lemon Squeezy or a provider of your own, the web SDK can start the checkout from your page. Voidhash asks your provider endpoint to create a checkout for the current visitor, and the SDK sends the browser to the URL your endpoint returns. The payment then reaches Voidhash through your backend's usual reports and is credited to that visitor.

Before you start a checkout, set up:

  • A payment source, for example with the slug paddle.
  • A provider endpoint connected to the source that implements checkout.create. The server libraries for Node.js, Go, PHP and Rust implement it for you.
  • The product key you sell, for example pri_monthly, mapped to a product under the source.

Start a checkout

Call redirectToCheckout() from a button. It starts the checkout and sends the browser to it:

pricing.ts
import { voidhash } from "./lib/voidhash";

await voidhash.redirectToCheckout({
  source: "paddle",
  productKey: "pri_monthly",
  successUrl: "https://app.example.com/welcome",
  cancelUrl: "https://app.example.com/pricing",
});
OptionDescription
sourceThe payment source's slug, as you created it in Studio.
productKeyThe product key the source sells. It must be mapped to a product.
successUrlWhere your provider sends the buyer after paying. An http(s) URL.
cancelUrlWhere your provider sends the buyer who leaves without paying.

All four are required. A blank one rejects with VoidhashInvalidArgumentError before anything is sent. Voidhash passes both URLs to your endpoint, which hands them to your provider.

To open the checkout yourself, for example in a new tab or an embedded frame, call createCheckout() with the same options. It resolves with the checkout instead of navigating:

const checkout = await voidhash.createCheckout({
  source: "paddle",
  productKey: "pri_monthly",
  successUrl: "https://app.example.com/welcome",
  cancelUrl: "https://app.example.com/pricing",
});

checkout.url; // where to send the buyer
checkout.expiresAt; // a Date when the link stops working, or null if your endpoint did not say

Both methods wait for a running initialize(). Each call creates a new checkout at your provider and is sent only once, so disable the button while a call is in progress.

Who the checkout is for

The checkout is for the SDK's current person: the signed-in user you identified, or the anonymous visitor. Voidhash passes that person's distinct ID to your endpoint, which should put it on the checkout, for example in Paddle's custom data. When your backend reports the payment with that distinct ID, the purchase is credited to the person who started the checkout.

When the buyer is signed in, call identify() first, so the purchase lands on your user rather than the anonymous visitor:

if (voidhash.getDistinctId() !== user.id) {
  await voidhash.identify(user.id, { email: user.email });
}

await voidhash.redirectToCheckout({
  source: "paddle",
  productKey: "pri_monthly",
  successUrl: "https://app.example.com/welcome",
  cancelUrl: "https://app.example.com/pricing",
});

A visitor who buys before signing up is fine too. The purchase lands on the anonymous visitor, and when they later sign up and you call identify(), it moves to the user with the rest of their history, including the campaign that brought them.

After the payment

Your provider sends the buyer to successUrl once they have paid, or to cancelUrl when they leave without paying. Arriving at successUrl does not mean Voidhash has recorded the payment yet: access is granted once your backend reports the payment, usually after your provider has notified it.

The web SDK does not read access in the browser. Check it on your server, for example with the Node.js library, and have the success page ask your server until the purchase shows up.

Test checkouts

A checkout always reaches your endpoint as a production request, so your endpoint creates it with your provider's live account. The web SDK has no sandbox option: it runs in the visitor's browser, where anyone can change what it sends, and a visitor who could choose your provider's test mode could pay with a test card for a sandbox purchase. In projects where sandbox purchases unlock access in production apps, that purchase would grant real access.

To test checkouts:

  • Test your endpoint's checkout.create without a browser, with your server library's test helper, such as provider.test() in Node.js. It sends a sandbox request by default.
  • Try the whole flow in a separate Voidhash project for staging, with its own publishable key and payment source. Point that source at a deployment of your endpoint that uses your provider's test account, and report the test payments from it.

Handle errors

Unlike track(), a checkout is not queued or retried: when it cannot be started, the call rejects with a VoidhashCheckoutError and the page stays where it is. Its message says what went wrong in words you can log, status is the HTTP status Voidhash answered with, and code is the API's error name:

statuscodeCause
400Api/SdkValidationErrorNo enabled source has that slug, the product key is not mapped under the source, or a return URL is not an http(s) URL.
424Api/PaymentSourceEndpointErrorThe source has no endpoint connected, the endpoint does not implement checkout.create, refused it, or did not answer correctly.
undefinedundefinedNo answer arrived within 15 seconds, for example because the visitor is offline.

Other failures, such as a publishable key Voidhash does not accept, carry their own status. Each failure is also reported as a CHECKOUT_FAILED diagnostic. Show the buyer a short message and let them try again:

import { VoidhashCheckoutError } from "@voidhash/web";

try {
  await voidhash.redirectToCheckout({
    source: "paddle",
    productKey: "pri_monthly",
    successUrl: "https://app.example.com/welcome",
    cancelUrl: "https://app.example.com/pricing",
  });
} catch (error) {
  if (error instanceof VoidhashCheckoutError) {
    console.warn("Checkout could not start", error.status, error.code, error.message);
    showMessage("We could not open the checkout. Please try again.");
  } else {
    throw error;
  }
}
A timeout may still have reached your endpoint

Voidhash sends your endpoint one request per call and never retries it. When no answer arrived in time, your endpoint may still have created a checkout at your provider that the buyer never saw. Letting them try again starts a new one.

React

In React, call the client from useVoidhash():

BuyButton.tsx
import { VoidhashCheckoutError } from "@voidhash/web";
import { useVoidhash } from "@voidhash/web/react";
import { useState } from "react";

export function BuyButton() {
  const { client } = useVoidhash();
  const [isPending, setIsPending] = useState(false);
  const [failure, setFailure] = useState<string>();

  const buy = async () => {
    setIsPending(true);
    setFailure(undefined);
    try {
      await client.redirectToCheckout({
        source: "paddle",
        productKey: "pri_monthly",
        successUrl: "https://app.example.com/welcome",
        cancelUrl: "https://app.example.com/pricing",
      });
    } catch (error) {
      setIsPending(false);
      setFailure(error instanceof VoidhashCheckoutError ? error.message : "Checkout failed.");
    }
  };

  return (
    <>
      <button type="button" disabled={isPending} onClick={buy}>
        Subscribe
      </button>
      {failure && <p role="alert">{failure}</p>}
    </>
  );
}

Script tag

With the script tag, call the methods on window.voidhash once the file has loaded; the loader snippet does not queue them. The error class is on the Voidhash global:

voidhash
  .redirectToCheckout({
    source: "paddle",
    productKey: "pri_monthly",
    successUrl: "https://app.example.com/welcome",
    cancelUrl: "https://app.example.com/pricing",
  })
  .catch(function (error) {
    if (error instanceof Voidhash.VoidhashCheckoutError) {
      alert("We could not open the checkout. Please try again.");
    }
  });

Example

The web example has a Buy with Nimbus Pay button that starts a checkout at a stand-in payment source and shows a failure on the page.

PreviousStripe revenueNextWeb-to-app handoff

On this page

Start a checkoutWho the checkout is forAfter the paymentTest checkoutsHandle errorsReactScript tagExample