Zid billing bridge¶
Zid billing follows the existing Shopify subscription bridge: the Flask
integration verifies the provider event, translates it into ConnectGain's
normalized contract, signs the exact JSON bytes, and calls
zid-billing-webhook. Only the Edge Function writes to Supabase.
Request¶
POST /functions/v1/zid-billing-webhook
{
"organization_id": "organization UUID",
"plan_id": "professional",
"zid_store_id": "123456",
"shop_domain": "merchant.example",
"status": "active",
"current_period_start": "2026-07-22T00:00:00.000Z",
"current_period_end": "2026-08-22T00:00:00.000Z",
"trial_start": null,
"trial_end": null,
"cancel_at_period_end": false
}
Required fields are organization_id, plan_id, zid_store_id, and
status. Allowed statuses are trialing, active, past_due, and
canceled. zid_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 ZID_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 = 'zid' and are upserted on the globally unique
zid_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 Zid 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",
"zid_store_id": "123456",
"status": "active"
}
The current bridge, like 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.