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'
export default async function DownloadPage({
searchParams,
}: {
searchParams: { session: string }
}) {
const order = await commerce.orders.getBySession(searchParams.session)
return (
<main className="mx-auto max-w-2xl px-4 py-16">
<h1 className="text-3xl font-bold mb-6">Your Purchase</h1>
{order.fulfillment.licenseKey && (
<div className="bg-gray-50 rounded-lg p-6 mb-6">
<p className="text-sm text-gray-500 mb-1">License Key</p>
<code className="text-lg font-mono">
{order.fulfillment.licenseKey}
</code>
</div>
)}
{order.fulfillment.downloads?.map((file) => (
<a
key={file.filename}
href={file.url}
className="block border rounded-lg p-4 mb-3 hover:bg-gray-50"
>
{file.filename}
</a>
))}
</main>
)
}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?
Last updated on