Skip to main content
This tutorial builds a document-generation tool to show how to charge per action using Profy’s PER_USE billing model.

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
Your app drives the whole charge flow through the 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, call report_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).
idempotencyKey is required (max 128 chars); event names are max 100 chars. The SDK auto-generates a key if omitted, but retries must reuse the same key (see Step 5).
Response fields:
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: use list_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).
Don’t “pre-check the balance” before charging — there is no balance endpoint, and concurrent operations invalidate any pre-check. Just report and catch InsufficientBalanceError.

Step 5: Handle charge failures

Errors are classified by HTTP status and all subclass ProfyApiError (with .statusCode / .status_code and .body).
Error handling strategy:
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.
Retries must reuse the same idempotencyKey. Generating a new key on each retry causes double charges.

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

Rule of thumb: if users would think of two operations as “one thing”, merge them into one event. If users would ask “why did this cost so much”, the granularity needs splitting.

FAQ

Q: What happens on a duplicate idempotency key? The API returns idempotent_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