Payments
13 Nov 2026
11 min read

Gumroad Checkout Integration: From Purchase to Entitlement in Seconds

Complete guide to integrating Gumroad checkout overlay, handling success redirects, and verifying entitlements client-side.

Gumroad Checkout Integration

SpaceZero provides a complete Gumroad purchase flow.

Checkout Flow

code

User clicks "Buy" → Gumroad Checkout → Payment → Redirect → Verify Entitlement

Frontend Integration

1. Checkout Button

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):

tsx

// 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>

Success Redirect

Configure in Gumroad product settings:

  • Success URL: https://your-app.space/?purchase=success
  • Cancel URL: https://your-app.space/?purchase=cancel

Client-Side Verification

typescript

// 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;

}

}

}

};

Webhook Processing

The webhook (functions/api/webhooks/gumroad.ts) handles:

  1. Shared-Secret Verification: plain comparison against x-gumroad-signature / custom_fields.secret / ?secret= query parameter
  2. User Resolution: Three-tier lookup (custom_field → entitlement cache → auth)
  3. Idempotent Upsert: ON CONFLICT (gumroad_sale_id)
  4. Status Sync: active or revoked (refunds)

Testing Locally

bash

# 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

Entitlement Checking

The useEntitlements hook checks app_entitlements by product_id + user_id:

typescript

// src/hooks/useEntitlements.ts

const { hasAccess, loading, checkAccess } = useEntitlements(productId);

 

// Or check a specific product on demand:

const granted = await checkAccess('pro_plan');

Refund Handling

Automatic via webhook:

typescript

status: isRefunded ? 'revoked' : 'active'

Client should re-check on app load.

Configuration

typescript

// 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 || '

0',

originalPriceDisplay: import.meta.env.VITE_ORIGINAL_PRICE_DISPLAY || '

3',

planName: import.meta.env.VITE_PLAN_NAME || 'Lifetime Access',

env

VITE_GUMROAD_PRODUCT_ID=xxx

VITE_PRICE_DISPLAY=

0

Benefits

  • Zero Backend: Gumroad handles payments, VAT, fraud
  • Instant Access: Webhook fires in seconds
  • Global: Supports 135+ currencies
  • Compliance: Gumroad handles tax compliance