Event taxonomy

The names ReportsHQ recognises, and the properties each one carries.

This document is a contract in three directions: the integration packages (Stacks, Laravel) emit these names, the auto-report templates match on them, and a report built by hand can rely on them meaning the same thing in every project. Send a matching event and the reports appear on their own.

You are not limited to these. Any name works, and custom events are first-class in the builder. What the reserved names buy you is reports you did not have to build.

Shape

Names are lowercase, dot-separated, and read domain.subject.verb-in-past-tense:

commerce.order.created
user.registered
cms.post.published

The ingest folds case and collapses separators, so Commerce/Order Created arrives as commerce.order.created. Rely on that for forgiveness, not as a style: send the canonical form.

Two properties carry meaning everywhere they appear:

  • user_key is your own pseudonymous identifier for the person involved. Unique counts, repeat rate and retention are computed from it. Send a stable internal id or a hash, never an email address: this is not a table that should accumulate identities.
  • value is the number a report sums or averages, with currency alongside when it is money. Send minor units (4250 for $42.50) or major units consistently. Consistently is the part that matters.

commerce.*

Feeds the Commerce overview and Customers templates.

EventRequiredRecommended
commerce.order.createdvalue, currencyuser_key, properties.items, properties.order_id
commerce.order.paidvalue, currencyuser_key, properties.order_id, properties.method
commerce.order.refundedvalue, currencyuser_key, properties.order_id, properties.reason
commerce.order.cancelleduser_key, properties.order_id
commerce.checkout.starteduser_key, session_key, value, currency
commerce.cart.updateduser_key, session_key, properties.items
commerce.product.vieweduser_key, session_key, properties.sku
commerce.customer.createduser_key

Revenue is summed from commerce.order.paid where you send it, and falls back to commerce.order.created where you do not. Sending both without paying attention double-counts, which is the single most common integration mistake: if every order is paid immediately, send one of them.

The conversion funnel is commerce.product.viewed to commerce.checkout.started to commerce.order.created to commerce.order.paid, ordered by session_key. Steps you do not send are skipped rather than counted as zero.

user.*

Feeds the Users template: signups, active users, and a weekly retention grid.

EventRequiredRecommended
user.registereduser_key, properties.plan, properties.source
user.loginuser_key, session_key
user.logoutuser_key, session_key
user.deleteduser_key
user.inviteduser_key, properties.invited_by
user.subscription.startedvalue, currencyuser_key, properties.plan, properties.interval
user.subscription.cancelleduser_key, properties.plan, properties.reason

Active users and retention both need user_key. Without it the events still arrive and still count, but every person looks like a different person, and the retention grid is empty rather than wrong.

cms.*

Feeds the Content template, which only appears once these events exist.

EventRequiredRecommended
cms.post.publishedproperties.post_id, properties.author, properties.category
cms.post.viewedproperties.post_id, user_key, session_key
cms.comment.createdproperties.post_id, user_key

Custom events

Anything outside a reserved prefix is yours. Use the same shape so your own reports read like the built-in ones:

{
    "name": "support.ticket.resolved",
    "user_key": "cust_8812",
    "value": 42,
    "properties": { "queue": "billing", "first_response_minutes": 12 }
}

Avoid the reserved prefixes for a different meaning. Sending commerce.order.created for something that is not an order will produce a Commerce report that is confidently wrong, which is worse than no report.

Practical notes

Send events once. The stream is append-only and never deduplicated after the fact, because a report that changes retroactively is one nobody can act on. Corrections are new events: a refund, not an edited order.

Backfill with real timestamps. occurred_at is accepted up to 30 days back, so an import lands on the days things actually happened rather than all at once on the day you imported. Older than that is clamped, not rejected.

Property cardinality matters. A property with thousands of distinct values groups into thousands of series and reports as noise. Order ids belong in properties for lookup; they are not something to group by.