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.