What you’ll build
A document-generation SaaS — every time a user performs a paid action, Profy deducts credits from their account:- Generate report (
generate_report) → 10 credits - Export PDF (
export_pdf) → 5 credits - Generate summary (
generate_summary) → 3 credits
ProfyApp SDK; Profy guarantees idempotency and atomicity.
Prerequisites
report_event uses OAuth and only a user’s OAuth token can call it (not an API Key). OAuth tokens hold only the events:write scope — they can report billing events but cannot invoke Experts/Chat (that’s what sk-pro- API Keys are for).Step 1: Configure metered events in Studio
Go to Profy Studio → your App → Billing Events and add these Meters:Event names are immutable once created — use
snake_case. Prices can change anytime and take effect immediately. At runtime, read the configured events and prices via list_meters().Step 2: Install and initialize the SDK
ProfyApp holds only the App identity (client_id / client_secret). The user token is passed per call — one App serves many users, each with a different token.
Step 3: Report billing events
When a user triggers a paid action, callreport_event. Pass that user’s OAuthToken, and use the on_refresh callback to persist the rotated token (every refresh rotates — the old refresh token is revoked immediately).
The charge amount is decided server-side from the Meter price — the request body has no amount/units field. To show prices in your UI, use
list_meters().Step 4: Show prices with Meters + track balance from responses
There is no standalone balance endpoint. The correct approach: uselist_meters() (public, no token) to show prices in your UI; track the user’s balance from each report_event response’s balance_remaining, and always handle InsufficientBalanceError (402).
Step 5: Handle charge failures
Errors are classified by HTTP status and all subclassProfyApiError (with .statusCode / .status_code and .body).
report_event handles token rotation for you: if the access token is expired (and on_refresh is set) it refreshes once proactively and calls back; on a 401 it also refreshes and retries once reactively. You just persist the new token in on_refresh.Step 6: Idempotency and safe retries
idempotencyKey ensures the same business action is never double-charged. On timeout or network jitter, retry safely with the same key.
Billing event design guide
Naming conventions
- Use
snake_case, start with a verb:generate_,export_,analyze_ - Avoid generic names (like
action,event); keep length ≤ 32 chars
Granularity
FAQ
Q: What happens on a duplicate idempotency key? The API returnsidempotent_replay: true and does not double-charge. charged is the amount from the first charge.
Q: How do I know the balance before charging?
There is no balance endpoint. Track it from each report_event response’s balance_remaining, and handle InsufficientBalanceError.
Q: Is metadata stored?
The metadata field is accepted but currently not persisted or returned — don’t rely on it for business logic; it’s reserved for future use.
Q: After a price change, which price applies to in-flight requests?
The latest price at the moment the request arrives. Changes take effect immediately with no transition period.
Q: Do refresh tokens expire?
They’re valid for 90 days and rotate on every refresh (the old one is revoked immediately). Persist the new token in on_refresh, or the App locks itself out after one refresh.
Next steps
SDK Quickstart
OAuth authorization and basic event reporting
Token management
OAuth authorization-code flow and token rotation persistence
Invoke platform Experts
Embed AI Expert capabilities in your App
API reference
Full Events API endpoint docs

