Complete guide to integrating Gumroad checkout overlay, handling success redirects, and verifying entitlements client-side.
SpaceZero provides a complete Gumroad purchase flow.
User clicks "Buy" → Gumroad Checkout → Payment → Redirect → Verify Entitlement
SpaceZero links straight to the Gumroad product page — no overlay script. VITE_GUMROAD_PRODUCT_ID accepts a full product URL, username/permalink, or the legacy bare permalink (only valid when the Gumroad username equals the permalink):
// src/components/ui/pricing.tsx — URL resolved in src/app.ts
const gumroadUrl =
gumroadProductUrl ||
(gumroadProductId
? `https://${gumroadProductId}.gumroad.com/l/${gumroadProductId}`
: '#');
<Button className="w-full" variant="action" href={gumroadUrl}>
Buy Now
</Button>
Configure in Gumroad product settings:
https://your-app.space/?purchase=successhttps://your-app.space/?purchase=cancel// In ProfileSettings or useEffect on app load
const verifyEntitlement = async () => {
if (new URLSearchParams(window.location.search).get('purchase') === 'success') {
// Poll for entitlement (webhook may not have fired yet)
for (let i = 0; i < 10; i++) {
await new Promise(r => setTimeout(r, 1000));
const { data } = await supabase
.from('app_entitlements')
.select('status')
.eq('email', user.email)
.eq('status', 'active')
.single();
if (data) {
toast.success('Purchase verified! Welcome aboard.');
// Clean URL
window.history.replaceState({}, '', '/');
break;
}
}
}
};
The webhook (functions/api/webhooks/gumroad.ts) handles:
x-gumroad-signature / custom_fields.secret / ?secret= query parameterON CONFLICT (gumroad_sale_id)active or revoked (refunds)# 1. Start ngrok
ngrok http 3000
# 2. Configure Gumroad webhook
# URL: https://your-ngrok.ngrok.io/api/webhooks/gumroad
# Secret: your-webhook-secret
# 3. Test purchase
# Use Gumroad test mode or $0.01 product
The useEntitlements hook checks app_entitlements by product_id + user_id:
// src/hooks/useEntitlements.ts
const { hasAccess, loading, checkAccess } = useEntitlements(productId);
// Or check a specific product on demand:
const granted = await checkAccess('pro_plan');
Automatic via webhook:
status: isRefunded ? 'revoked' : 'active'
Client should re-check on app load.
// src/app.ts
gumroadProductId: import.meta.env.VITE_GUMROAD_PRODUCT_ID || '',
// resolved in app.ts — accepts a full URL, user.gumroad.com/l/permalink, username/permalink, or legacy bare permalink
gumroadProductUrl: resolveGumroadUrl(import.meta.env.VITE_GUMROAD_PRODUCT_ID),
priceDisplay: import.meta.env.VITE_PRICE_DISPLAY || '
originalPriceDisplay: import.meta.env.VITE_ORIGINAL_PRICE_DISPLAY || '
planName: import.meta.env.VITE_PLAN_NAME || 'Lifetime Access',
VITE_GUMROAD_PRODUCT_ID=xxx
VITE_PRICE_DISPLAY=