Product-user activity
List a customer's AI usage and credit movements with saved activity metadata and charge breakdowns.
The service API returns AI usage and permanent credit movements in one paginated feed. Businesses can use it for a customer-facing activity screen. Organization-wide dashboard analytics keep their existing authentication requirements
Request activity
curl --fail-with-body --get https://api.zorveus.com/product-users/by-external-id/activity \
-H "Authorization: Bearer $ZORVEUS_SERVICE_KEY" \
--data-urlencode "app_id=$ZORVEUS_APP_ID" \
--data-urlencode 'external_user_id=user_123' \
--data-urlencode 'limit=20'Call this from the business backend with a service key that has product_users:read or a scope that includes it. Derive the external user ID from authenticated application state. Do not expose the service key in browser code or allow a customer to choose another customer's ID
| Query field | Contract |
|---|---|
app_id, external_user_id | Required, scoped to the service key's organization and owned app |
type | Optional usage, credit_added, credit_revoked, credit_expired, credit_refunded, credit_adjusted, or allowance_debt_repayment |
from | Inclusive timestamp with a timezone offset |
to | Exclusive timestamp with a timezone offset |
currency | Optional uppercase three-letter currency code |
limit | Default 20, minimum 1, maximum 100 |
cursor | Opaque next_cursor from the previous response |
Rows sort newest first, with stable ID and type ordering for equal timestamps. Reuse the same app, user, and filters with a cursor. Changing those values requires starting a new pagination sequence. A missing user returns HTTP 404 with zorveus_product_user_not_found by default. Add X-Zorveus-Upsert-Product-User: true to create one in an owned, non-deleted app and return an empty feed. Omit the header or use false for lookup-only behavior. Other values return HTTP 422. Existing profiles and statuses are preserved
Response and UI mapping
{
"product_end_user_id": "peu_123",
"items": [
{
"id": "ue_123",
"type": "usage",
"label": "WhatsApp agent replied",
"amount": "-4.000000000000",
"currency": "USD",
"status": "succeeded",
"created_at": "2026-10-26T12:00:00Z",
"model": "example-model",
"usage_metadata": {
"category": "customer_support",
"activity_id": "reply_123",
"activity_label": "WhatsApp agent replied"
},
"base_allowance_charge_amount": "1.000000000000",
"credit_charge_amount": "3.000000000000",
"uncovered_charge_amount": "0.000000000000",
"credit_allocations": [
{
"credit_grant_id": "grant_123",
"amount": "3.000000000000",
"currency": "USD",
"metadata": {"credits_per_usd": "1500"}
}
]
},
{
"id": "credit_entry_123",
"type": "credit_added",
"label": "Credit purchase",
"amount": "10.000000000000",
"currency": "USD",
"status": "completed",
"created_at": "2026-10-25T09:00:00Z",
"credit_grant_id": "grant_123",
"metadata": {"credits_per_usd": "1500"}
}
],
"next_cursor": null,
"has_more": false,
"limit": 20
}Display label, the signed amount, currency, status, and created_at in each row. Use (type, id) as the row identity. A non-empty string usage_metadata.activity_label becomes the usage label, otherwise the label is AI usage. Credit additions use the grant reason when present, otherwise a source-specific default. Other credit movements use their own labels
The usage amount is the negative product-user charge after the configured multiplier, not the organization wallet charge. Credit movements retain their recorded signs. Breakdown fields and allocation amounts are positive magnitudes. A request that consumes multiple grants appears once, with one allocation per grant
base_allowance_charge_amount records usage covered by the configured base allowance, or uncapped allowance. credit_charge_amount sums the settled grant deductions. uncovered_charge_amount is the remaining product-user charge after those two portions. It does not describe organization wallet debt or other organization cap overruns
Older events without a saved base snapshot return null for base and uncovered portions. Their recorded credit deductions remain available. Older events without product_user_charge_amount use their saved reference usage and multiplier, defaulting the multiplier to 1. Do not replace unknown breakdown values with zero
The feed includes recorded revocations, refunds, adjustments, expiration entries, and credit-funded allowance debt repayments. It excludes temporary reservation and release entries. It does not repeat credit settlement ledger entries as separate deductions alongside usage events. An expiration appears only if an expiration ledger movement was recorded
Use decimal arithmetic. Zorveus returns accounting currency amounts and the grant's saved metadata. To display purchased credit units, multiply each allocation amount by that grant's saved conversion factor and sum the results. A request can use grants with different factors. Plan allowance display follows the business's own unit policy. This endpoint does not perform FX lookups or infer business units
Render labels and metadata as plain text. Categories and activity IDs remain optional custom metadata fields. No category aggregation, activity grouping, total count, page numbers, or running balance is returned. Use has_more and next_cursor for Next or Load more, and retain prior cursors for Previous. Keep balance cards on the existing summary/profile endpoints
SDK methods
page = service.product_users.list_activity_by_external_id(
app_id=app_id,
external_user_id=authenticated_user_id,
limit=20,
)
next_page = service.product_users.list_activity_by_external_id(
app_id=app_id,
external_user_id=authenticated_user_id,
limit=20,
cursor=page.next_cursor,
) if page.has_more else NoneThe async Python service client exposes the same method with await. Optional Python filters are activity_type, from_date, to_date, and currency
const page = await service.productUsers.listActivityByExternalId({
appId,
externalUserId: authenticatedUserId,
limit: 20,
});Optional TypeScript filters are type, from, to, and currency. For opt-in creation, pass upsert_if_missing=True in Python or upsertIfMissing: true in the TypeScript request parameters. Both SDKs send the header automatically. An explicit False or false disables creation even if a conflicting raw header was supplied. Both SDKs preserve metadata, decimal strings, nullable historical breakdowns, and pagination fields
Errors
Malformed or mismatched cursors return HTTP 400 with zorveus_invalid_product_user_activity_cursor. A reversed or empty time window returns HTTP 400 with zorveus_invalid_product_user_activity_filter. Invalid enum values, limits, currencies, or timestamps return HTTP 422. Authentication and scope errors use the existing service-key contracts. An inaccessible or deleted app returns HTTP 404 without creating a user
Credit units charged
Usage items include nullable charged_credit_units. Zorveus saves this quantity at settlement from the actual base and grant allocations. For example, $0.01 from base allowance at 1,000 units/USD plus $0.01 from a grant at 1,500 units/USD produces charged_credit_units: "25"
The original amount remains the signed USD charge. The unit charge is a positive exact decimal string. Changing app defaults does not recalculate old usage. Reservations and releases are not settled charges. Refunds do not reprice the original usage row
Historical usage without a recorded quantity returns null. A charge with a missing conversion or funding that has not been allocated also returns null rather than a partial total. Known zero charges return zero