Skip to content

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:

x-zid-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 = '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.

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

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.