Zorveus LogoZorveusDOCS
Business

Product-user activity

View Markdown

List a customer's AI usage and credit movements with saved activity metadata and charge breakdowns.

GET/product-users/by-external-id/activity
Service Key (Bearer zrv_svc_...)

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 fieldContract
app_id, external_user_idRequired, scoped to the service key's organization and owned app
typeOptional usage, credit_added, credit_revoked, credit_expired, credit_refunded, credit_adjusted, or allowance_debt_repayment
fromInclusive timestamp with a timezone offset
toExclusive timestamp with a timezone offset
currencyOptional uppercase three-letter currency code
limitDefault 20, minimum 1, maximum 100
cursorOpaque 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 None

The 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

Was this page helpful?
Edit this page on GitHub

On this page