Digital Products
Sell software, ebooks, and courses with instant digital fulfillment
This recipe covers selling digital products through Hanzo Commerce -- software licenses, ebooks, online courses, and downloadable files -- with instant fulfillment after purchase.
What You Will Build
- Products configured for digital delivery (no shipping)
- Automatic fulfillment with download links
- License key generation for software products
- Access control for gated content after purchase
Create a Digital Product
Digital products skip shipping by setting type to digital. Use the SDK or the Admin dashboard.
import { Commerce } from '@hanzo/commerce'
const commerce = new Commerce({
apiKey: process.env.HANZO_API_KEY!,
environment: 'production',
})
const product = await commerce.products.create({
name: 'Pro License -- Annual',
slug: 'pro-license-annual',
type: 'digital',
price: 9900, // $99.00
currency: 'USD',
metadata: {
fulfillment: 'license_key',
maxActivations: '5',
},
})
console.log('Created product:', product.id)The type: 'digital' flag tells Commerce to:
- Skip shipping address collection at checkout
- Skip the fulfillment module's physical shipping flow
- Trigger digital fulfillment events on payment capture
Upload Downloadable Files
Attach files to the product using the Uploads API. Files are stored in your configured S3-compatible bucket.
await commerce.uploads.create({
productId: product.id,
file: fs.createReadStream('./dist/app-installer.dmg'),
filename: 'app-installer.dmg',
access: 'purchasers', // only customers who bought this product
})The access: 'purchasers' setting generates time-limited signed URLs that are only available to verified buyers.
License Key Generation
For software products, configure automatic license key generation on purchase.
await commerce.products.update(product.id, {
fulfillmentConfig: {
type: 'license_key',
generator: 'uuid-v4', // or 'custom' with a webhook
maxActivations: 5,
expiresIn: '365d',
},
})Supported generators:
| Generator | Format | Use Case |
|---|---|---|
uuid-v4 | 550e8400-e29b-41d4-a716-446655440000 | Simple unique keys |
alphanumeric | XXXX-XXXX-XXXX-XXXX | User-friendly keys |
custom | Webhook-generated | Complex licensing logic |
For custom generators, Commerce sends a POST to your webhook:
{
"event": "license.generate",
"orderId": "order_abc123",
"productId": "prod_xyz789",
"customerId": "cust_def456"
}Your webhook must return:
{
"licenseKey": "YOUR-CUSTOM-KEY",
"metadata": { "tier": "pro", "seats": 5 }
}Checkout Without Shipping
When a cart contains only digital products, Commerce automatically skips shipping:
const cart = await commerce.carts.create({
items: [{ productId: product.id, quantity: 1 }],
})
// Checkout will not request a shipping address
const session = await commerce.checkout.create({
cartId: cart.id,
successUrl: 'https://example.com/download?session={SESSION_ID}',
cancelUrl: 'https://example.com/pricing',
})Post-Purchase Fulfillment
After payment is captured, Commerce fires a order.fulfilled event. The order object contains the digital fulfillment details.
const order = await commerce.orders.getBySession(sessionId)
// For license key products
console.log(order.fulfillment.licenseKey)
// "550e8400-e29b-41d4-a716-446655440000"
// For downloadable products
console.log(order.fulfillment.downloads)
// [{ filename: "app-installer.dmg", url: "https://signed-url..." }]Download Page Example
import { commerce } from '@/lib/commerce'
import { Anchor, Card, H1, Paragraph, Text, YStack } from '@hanzo/gui'
export default async function DownloadPage({
searchParams,
}: {
searchParams: { session: string }
}) {
const order = await commerce.orders.getBySession(searchParams.session)
return (
<YStack
maxWidth={640}
width="100%"
marginHorizontal="auto"
paddingHorizontal="$4"
paddingVertical="$10"
gap="$6"
>
<H1>Your Purchase</H1>
{order.fulfillment.licenseKey && (
<Card backgroundColor="$color2" borderRadius="$4" padding="$6" gap="$1">
<Paragraph size="$2" theme="alt2">
License Key
</Paragraph>
<Text fontFamily="$mono" fontSize="$6">
{order.fulfillment.licenseKey}
</Text>
</Card>
)}
<YStack gap="$3">
{order.fulfillment.downloads?.map((file) => (
<Anchor key={file.filename} href={file.url} textDecorationLine="none">
<Card bordered padding="$4" hoverStyle={{ backgroundColor: '$color2' }}>
<Text>{file.filename}</Text>
</Card>
</Anchor>
))}
</YStack>
</YStack>
)
}Access Control for Courses
For course or content products, use the entitlements system to gate access:
const product = await commerce.products.create({
name: 'Advanced TypeScript Course',
slug: 'advanced-ts-course',
type: 'digital',
price: 4900,
currency: 'USD',
fulfillmentConfig: {
type: 'entitlement',
entitlementKey: 'course:advanced-ts',
},
})Check entitlements in your application:
const hasAccess = await commerce.customers.hasEntitlement(
customerId,
'course:advanced-ts'
)
if (!hasAccess) {
redirect('/pricing')
}Webhook for External Systems
If you need to trigger actions in external systems (e.g., grant access in a third-party LMS), configure a webhook:
await commerce.webhooks.create({
url: 'https://example.com/api/webhooks/commerce',
events: ['order.fulfilled'],
})Testing
- Create a digital product via the SDK or Admin dashboard
- Add the product to a cart and complete checkout with a Stripe test card
- Verify the order contains fulfillment data (license key or download URLs)
- Confirm download URLs are signed and time-limited
Next Steps
- Combine digital products with subscriptions for SaaS licensing
- Add multi-currency pricing for global sales
- Review the Fulfillment module for advanced configuration
How is this guide?