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:
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",
});| Option | Description |
|---|---|
source | The payment source's slug, as you created it in Studio. |
productKey | The product key the source sells. It must be mapped to a product. |
successUrl | Where your provider sends the buyer after paying. An http(s) URL. |
cancelUrl | Where 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 sayBoth 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.createwithout a browser, with your server library's test helper, such asprovider.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:
status | code | Cause |
|---|---|---|
400 | Api/SdkValidationError | No enabled source has that slug, the product key is not mapped under the source, or a return URL is not an http(s) URL. |
424 | Api/PaymentSourceEndpointError | The source has no endpoint connected, the endpoint does not implement checkout.create, refused it, or did not answer correctly. |
undefined | undefined | No 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;
}
}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():
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.