Skip to content

Billing Events (Lemon Squeezy)

Synapse receives subscription lifecycle events from Lemon Squeezy via webhooks. These events update your workspace's plan, limits, and billing status.


Webhook Endpoint

POST https://synapse-api.pyrx.tech/v1/webhooks/lemonsqueezy

This endpoint is secured by an HMAC-SHA256 signature in the x-signature header. See Signature Verification for details.


Events

EventDescriptionSynapse Action
subscription_createdNew subscription created after checkoutUpdate plan, set limits, store LS customer/subscription IDs, set renewal date
subscription_updatedPlan changed or renewal date updatedUpdate plan and limits if plan changed, update renewal date
subscription_cancelledSubscription cancelled by ownerLogged only -- access continues until current period ends
subscription_expiredSubscription period ended after cancellationDowngrade to free plan, clear LS subscription fields, reset limits
subscription_payment_failedRecurring payment attempt failedLogged as warning. Lemon Squeezy handles retry/dunning automatically.

Payload Schema

Lemon Squeezy webhooks use a standard envelope with meta and data sections:

json
{
"meta": {
"event_name": "subscription_created",
"custom_data": {
"tenant_id": "8f14e45f-ceea-467f-a83c-01a01ba3c5db",
"plan_id": "growth"
}
},
"data": {
"id": "12345",
"attributes": {
"customer_id": 67890,
"status": "active",
"renews_at": "2026-05-07T00:00:00.000Z"
}
}
}

Key Fields

FieldLocationDescription
meta.event_nameEnvelopeThe event type identifier
meta.custom_data.tenant_idEnvelopeSynapse workspace UUID (set at checkout time)
meta.custom_data.plan_idEnvelopePlan identifier (e.g., starter, growth)
data.idDataLemon Squeezy subscription ID
data.attributes.customer_idDataLemon Squeezy customer ID
data.attributes.statusDataSubscription status (active, cancelled, expired, etc.)
data.attributes.renews_atDataNext renewal date (ISO 8601)
data.attributes.ends_atDataSubscription end date (set on cancellation)

Event Details

subscription_created

Fired when a workspace owner completes checkout and payment succeeds.

Synapse action:

  • tenants.plan updated to the purchased plan (e.g., growth)
  • Plan limits synced: monthly_email_limit, contacts_limit, monthly_events_limit, data_retention_days
  • tenants.ls_customer_id and tenants.ls_subscription_id stored
  • tenants.ls_current_period_end set from renews_at
  • Workspace status set to active

subscription_updated

Fired when a subscription is modified (plan change, renewal date update).

Synapse action:

  • If the plan changed and status is active, update tenants.plan and sync limits
  • Update tenants.ls_current_period_end from renews_at

subscription_cancelled

Fired when the workspace owner cancels their subscription.

Synapse action:

  • Logged only. The workspace retains full access until the current billing period ends.
  • When the period ends, Lemon Squeezy fires subscription_expired.
Note

Cancellation does not immediately downgrade the workspace. The owner keeps their current plan until the period end date shown in the billing page.

subscription_expired

Fired when a cancelled subscription's billing period ends.

Synapse action:

  • tenants.plan downgraded to free
  • Limits reset to free-tier values
  • tenants.ls_subscription_id and tenants.ls_current_period_end cleared
  • tenants.ls_customer_id preserved (for future checkouts)

subscription_payment_failed

Fired when a recurring payment attempt fails.

Synapse action:

  • Logged as a warning. No plan or limit changes.
  • Lemon Squeezy handles automatic retry and dunning (payment failure emails to the customer).

Retry Behavior

The webhook handler always returns 200 OK with {"received": true}. This prevents Lemon Squeezy from retrying indefinitely. Internal processing failures are logged and alerted via Sentry.


Signature Verification

Lemon Squeezy signs every webhook request with an HMAC-SHA256 signature in the x-signature header. The signature is computed over the raw request body using your webhook secret.

python
import hashlib
import hmac
 
def verify_ls_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode("utf-8"),
payload,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)

See Signature Verification for more examples and best practices.