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:
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.
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_URLand the app will open it directly, the same wayshopifyManageUrldoes for Shopify — no source change required.