Corteksa
GuidesBilling

Best Practices

Billing — Best Practices

The recommended way to build a correct billing screen.

✓ Drive the whole screen from GET /usage

There is no separate "get subscription" call. /usage returns the subscription state, per-metric limits/used/remaining, active add-ons, and pack balances in one payload. Read it once and render everything from it.

✓ Keep plan and card separate

POST /change-plan changes the plan and never opens Paymob; POST /checkout is the only endpoint that returns a Paymob redirect_url. Don't expect a redirect from /change-plan, and don't ask for card details when changing plans.

✓ Poll /usage after anything that charges

The Paymob redirect and the immediate response are not proof of success — the state lands on the async webhook. After a /checkout return, or a /change-plan response with charged:true, poll /usage a few times over ~10–15s.

✓ Confirm the outcome with payment-status?payment_id=

On your Paymob callback page, call GET /payment-status?payment_id=… for the immediate success/failed/pending, then poll /usage for the final workspace state.

✓ Use the realtime usage:updated for metered spend

AI-credit and workflow-execution balances push over /data/events; render remaining straight from the event. Don't poll /usage on a timer for these two metrics, and don't open a second socket.

✓ Handle 402 globally

A blocked metered action returns 402 PLAN_LIMIT_EXCEEDED; a write on a read-only workspace returns 402 PAYMENT_REQUIRED. Catch both centrally and prompt an upgrade / add-on purchase / recover-payment flow.

✓ Gate the add-on buy button on /usage

Add-ons require status:"active" and has_payment_method:true. Disable the buy action unless both are true — the backend rejects it with 400 otherwise.

✓ Route lapsed accounts to /checkout

past_due / suspended / canceled can't change plans (PAYMENT_METHOD_REQUIRED). Send them to POST /checkout (a full-amount recovery charge) first; the webhook reactivates and lifts read-only.

✗ Don't trust the redirect, and don't verify webhooks yourself

The browser returning from Paymob doesn't mean the money settled — always reconcile via /usage. On the server, never disable PaymobWebhookGuard signature verification in production; a bad HMAC must 401.

✗ Don't add manual workspace_id filters on billing tables

Billing state is MAIN-db and keyed by user_workspace_id (not hyper-RLS). Metered writes must go through UsageModule (UsageCounterService / WorkspaceIdentityService), not hand-rolled SQL. See Backend Integration.

On this page