Basecamp Sherpa
AppsConsole

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:

FieldValue
NameSherpa
DescriptionBasecamp AI agent instance
Tax categorySaaS — Software as a Service

Under Pricing, select Subscription and create two variants:

VariantPriceBilling interval
Monthly$12/monthEvery 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:

SettingValue
Generate license keysEnabled
Activation limit1 (one instance per license)
License lengthLeave 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:

SettingValue
URLhttps://your-console-domain.com/api/webhooks/lemonsqueezy
Signing secretA random string between 6–40 characters
Eventssubscription_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-secret

The 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 ID

API 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.store and relationships.variant to identify the product
  • checkout_data.email prefilled from the user's account
  • checkout_data.custom with instance_id and user_id for webhook correlation
  • product_options.name overridden 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

  1. User creates an instance in the Console (status: pending)
  2. Console calls POST /v1/checkouts with instance_id and user_id in custom data
  3. User completes payment on the hosted checkout page
  4. LemonSqueezy sends a subscription_created webhook to the Console
  5. Console creates the subscription record and queues a provision job

Webhook processing

The Console webhook handler at /api/webhooks/lemonsqueezy processes three events:

EventAction
subscription_createdCreates a subscriptions record, sets instance to provisioning, queues a provision job
subscription_updatedUpdates subscription status and renews_at date
subscription_cancelledMarks 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)

Start the Console

pnpm console:dev

The Console runs on localhost:3001.

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 with ls_subscription_id = "1" and the correct status
  • provision_jobs — a provision job after subscription_created, or a destroy job after subscription_cancelled
  • instances — status changed to provisioning or destroying

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.