Skip to content

Salla billing bridge

Salla billing follows the existing Zid/Shopify subscription bridge: the Flask integration verifies the provider event, translates it into ConnectGain's normalized contract, signs the exact JSON bytes, and calls salla-billing-webhook. Only the Edge Function writes to Supabase.

Request

POST /functions/v1/salla-billing-webhook

{
 "organization_id": "organization UUID",
 "plan_id": "professional",
 "salla_store_id": "123456",
 "shop_domain": "merchant.example",
 "status": "active",
 "current_period_start": "2026-07-29T00:00:00.000Z",
 "current_period_end": "2026-08-29T00:00:00.000Z",
 "trial_start": null,
 "trial_end": null,
 "cancel_at_period_end": false
}

Required fields are organization_id, plan_id, salla_store_id, and status. Allowed statuses are trialing, active, past_due, and canceled. salla_store_id may arrive as a JSON string or number and is stored as text. shop_domain is optional and is persisted on the subscription row when sent.

Authentication

Both services hold SALLA_BILLING_WEBHOOK_SECRET. Flask calculates HMAC-SHA256 over the exact raw request body and sends:

x-salla-billing-signature: sha256=<lowercase hex digest>

The Edge Function validates the signature using the shared constant-time HMAC helper before parsing the body.

Persistence and idempotency

The Edge Function resolves the organization OWNER and maps plan_id through subscription_plans. It writes the plan's existing stripe_product_id and stripe_price_id as internal plan lookup metadata; no Stripe object is created or required.

Rows use billing_provider = 'salla' and are upserted on the globally unique salla_store_id. Replaying the same normalized event therefore updates the same subscription row.

The database upsert is atomic and only updates an existing row when the incoming current_period_start is greater than or equal to the stored value. Older deliveries return HTTP 200 with applied: false and reason: "stale_event", so they are acknowledged without regressing state or causing an endless retry loop.

{
 "ok": true,
 "applied": false,
 "reason": "stale_event"
}

Flask owns Salla verification, provider-status translation, delivery retries, and any provider reconciliation. It must treat non-2xx responses as failed deliveries. The Edge Function owns all database writes and returns:

{
 "ok": true,
 "applied": true,
 "organization_id": "organization UUID",
 "salla_store_id": "123456",
 "status": "active"
}

The current bridge, like Zid's and Shopify's, does not store provider event IDs or reject out-of-order events. Flask reconciliation must resend the latest authoritative state after ambiguous or failed deliveries.

Frontend

the app exposes isSallaBilled and sallaManageUrl. When subscription.billing_provider === "salla", createCheckout and openCustomerPortal never open Stripe Checkout or the Stripe Customer Portal.

Unlike Shopify, Salla has no official public manage-subscription URL to deep-link merchants to. getSallaManageUrl therefore does not hardcode any URL. It reads VITE_SALLA_MANAGE_SUBSCRIPTION_URL from the environment:

  • If unset (the default today), the merchant sees a toast telling them to manage their subscription from their Salla dashboard, under Apps or App Subscriptions.
  • If Salla later publishes an official manage-subscription URL, set VITE_SALLA_MANAGE_SUBSCRIPTION_URL and the app will open it directly, the same way shopifyManageUrl does for Shopify — no source change required.