LemonSqueezy
Set up LemonSqueezy payments for Sherpa subscriptions.
LemonSqueezy handles subscription billing for Sherpa instances. When a user creates an instance in the Console, they're redirected to a LemonSqueezy checkout. On successful payment, a webhook triggers instance provisioning automatically.
Create a Store
Sign up
Go to app.lemonsqueezy.com and create an account. Complete the onboarding to set up your store (name, currency, payout details).
Create the Sherpa product
Navigate to Store → Products and click + New Product:
| Field | Value |
|---|---|
| Name | Sherpa |
| Description | Basecamp AI agent instance |
| Tax category | SaaS — Software as a Service |
Under Pricing, select Subscription and create two variants:
| Variant | Price | Billing interval |
|---|---|---|
| Monthly | $12/month | Every 1 month |
| Yearly | $10/month ($120/year) | Every 1 year |
Configure license keys
For each variant, enable Generate license keys. This creates a unique license key (UUID format, e.g. 80e15db5-c796-436b-850c-8f9c98a48abe) for every subscription, which you can use to gate access to the Sherpa agent.
Configure the license key settings per variant:
| Setting | Value |
|---|---|
| Generate license keys | Enabled |
| Activation limit | 1 (one instance per license) |
| License length | Leave unlimited (tied to subscription lifecycle) |
For subscription products, the license key status automatically syncs with the subscription — it activates on payment, and expires when the subscription is cancelled or lapses. You don't need to set a manual license length.
License keys are delivered to customers in their order receipt email and available in the LemonSqueezy customer portal under My Orders.
Note your IDs
You need two IDs from the product you just created:
- Variant ID — visible in the URL when editing a variant, or via the Variants API. Set this to the default variant customers should check out with (typically "Monthly").
- Store ID — found under Settings → Stores in the dashboard.
The checkout dynamically renames the product to Sherpa — {instance name} using the product_options.name attribute, so the generic product name is only visible in your dashboard.
Generate an API Key
Create the key
Go to Settings → API in the LemonSqueezy dashboard and generate a new API key.
Set the environment variable
LEMONSQUEEZY_API_KEY=eyJ0eXA...This key has full access to your store. Keep it secret and never commit it to version control.
Configure Webhooks
Webhooks are how LemonSqueezy notifies the Console about subscription lifecycle events.
Create a webhook
In the LemonSqueezy dashboard, go to Settings → Webhooks and click the + icon:
| Setting | Value |
|---|---|
| URL | https://your-console-domain.com/api/webhooks/lemonsqueezy |
| Signing secret | A random string between 6–40 characters |
| Events | subscription_created, subscription_updated, subscription_cancelled |
You can generate a secret with openssl rand -hex 20. LemonSqueezy requires the secret to be 6–40 characters.
LemonSqueezy supports additional subscription events (subscription_resumed, subscription_expired, subscription_paused, subscription_unpaused, subscription_payment_failed, subscription_payment_success, subscription_payment_recovered) — subscribe to more if you need them.
Set the webhook secret
Use the same secret you entered in the LemonSqueezy dashboard:
LEMONSQUEEZY_WEBHOOK_SECRET=your-random-secretThe Console verifies every incoming webhook by computing an HMAC-SHA256 hash of the raw request body with this secret and comparing it to the X-Signature header. Requests with an invalid or missing signature are rejected with 401.
Retry behavior
LemonSqueezy expects an HTTP 200 response. Any other status triggers automatic retries — up to 4 attempts total. After that, the webhook is marked as failed in the dashboard.
Environment Variables
Add all four variables to your Console deployment:
LEMONSQUEEZY_API_KEY=eyJ0eXA... # API key from Settings → API
LEMONSQUEEZY_WEBHOOK_SECRET=abc123... # Webhook signing secret (6–40 chars)
LEMONSQUEEZY_STORE_ID=12345 # Store ID from Settings → Stores
LEMONSQUEEZY_VARIANT_ID=67890 # Product variant IDAPI Reference
The Console uses the LemonSqueezy REST API directly (no SDK). All requests go to https://api.lemonsqueezy.com/v1 with these headers:
Accept: application/vnd.api+json
Content-Type: application/vnd.api+json
Authorization: Bearer {LEMONSQUEEZY_API_KEY}The API is rate-limited to 300 requests per minute. Rate limit status is returned in X-Ratelimit-Limit and X-Ratelimit-Remaining response headers.
Create checkout
POST /v1/checkouts — creates a hosted checkout session. The Console sends:
relationships.storeandrelationships.variantto identify the productcheckout_data.emailprefilled from the user's accountcheckout_data.customwithinstance_idanduser_idfor webhook correlationproduct_options.nameoverridden to"Sherpa — {instance name}"
The response includes a url where the user is redirected to complete payment.
Cancel subscription
DELETE /v1/subscriptions/{id} — cancels an active subscription. Called when a user destroys an instance from the Console. LemonSqueezy then sends a subscription_cancelled webhook to confirm.
How It Works
Checkout flow
- User creates an instance in the Console (status:
pending) - Console calls
POST /v1/checkoutswithinstance_idanduser_idin custom data - User completes payment on the hosted checkout page
- LemonSqueezy sends a
subscription_createdwebhook to the Console - Console creates the subscription record and queues a provision job
Webhook processing
The Console webhook handler at /api/webhooks/lemonsqueezy processes three events:
| Event | Action |
|---|---|
subscription_created | Creates a subscriptions record, sets instance to provisioning, queues a provision job |
subscription_updated | Updates subscription status and renews_at date |
subscription_cancelled | Marks subscription as cancelled, queues a destroy job if instance is running |
The webhook payload follows the JSON:API format. Custom data passed during checkout is available at payload.meta.custom_data.
Local Development
You can test the full webhook flow locally without a tunnel by simulating LemonSqueezy webhooks with curl.
Set environment variables
Add test credentials to .env:
LEMONSQUEEZY_API_KEY=eyJ0eXA... # generate under Settings → API (in test mode)
LEMONSQUEEZY_WEBHOOK_SECRET=testsecret # any string, 6–40 chars
LEMONSQUEEZY_STORE_ID=12345 # same store, test mode
LEMONSQUEEZY_VARIANT_ID=67890 # variant ID (may differ in test mode)Simulate subscription_created
Send a webhook to your local Console. The script computes the HMAC-SHA256 signature the same way LemonSqueezy does:
SECRET="testsecret"
BODY='{"meta":{"event_name":"subscription_created","custom_data":{"instance_id":"YOUR_INSTANCE_UUID","user_id":"YOUR_USER_UUID"}},"data":{"id":"1","type":"subscriptions","attributes":{"store_id":12345,"customer_id":1,"status":"active","renews_at":"2026-07-15T00:00:00.000000Z"}}}'
SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -X POST http://localhost:3001/api/webhooks/lemonsqueezy \
-H "Content-Type: application/json" \
-H "X-Signature: $SIGNATURE" \
-d "$BODY"Replace YOUR_INSTANCE_UUID and YOUR_USER_UUID with real IDs from your local Supabase (check instances and profiles tables in Studio).
A successful response returns:
{"received": true}Simulate subscription_updated
SECRET="testsecret"
BODY='{"meta":{"event_name":"subscription_updated"},"data":{"id":"1","type":"subscriptions","attributes":{"status":"active","renews_at":"2026-08-15T00:00:00.000000Z"}}}'
SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -X POST http://localhost:3001/api/webhooks/lemonsqueezy \
-H "Content-Type: application/json" \
-H "X-Signature: $SIGNATURE" \
-d "$BODY"Simulate subscription_cancelled
SECRET="testsecret"
BODY='{"meta":{"event_name":"subscription_cancelled"},"data":{"id":"1","type":"subscriptions","attributes":{"status":"cancelled"}}}'
SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -X POST http://localhost:3001/api/webhooks/lemonsqueezy \
-H "Content-Type: application/json" \
-H "X-Signature: $SIGNATURE" \
-d "$BODY"Verify in Supabase Studio
Open localhost:54323 and check:
subscriptions— a new row withls_subscription_id = "1"and the correct statusprovision_jobs— aprovisionjob aftersubscription_created, or adestroyjob aftersubscription_cancelledinstances— status changed toprovisioningordestroying
To test the full checkout flow with a real LemonSqueezy checkout page, you'll need a tunnel (e.g. ngrok or Cloudflare Tunnel) so LemonSqueezy can reach your local Console. Enable test mode in the dashboard and use card 4242 4242 4242 4242 with any future expiry and CVC.