Hanzo
CommerceRecipes

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:

GeneratorFormatUse Case
uuid-v4550e8400-e29b-41d4-a716-446655440000Simple unique keys
alphanumericXXXX-XXXX-XXXX-XXXXUser-friendly keys
customWebhook-generatedComplex 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

  1. Create a digital product via the SDK or Admin dashboard
  2. Add the product to a cart and complete checkout with a Stripe test card
  3. Verify the order contains fulfillment data (license key or download URLs)
  4. Confirm download URLs are signed and time-limited

Next Steps

How is this guide?

Last updated on

On this page