Events and comments
An order's history is not derived from its columns. Every action route writes a row, and that row is also the domain event a workflow reacts to — so what a workflow saw and what an operator reads cannot diverge.
The trail
curl "https://api.revenexx.com/v1/orders/{id}/events?limit=50" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..."
Everything that has ever happened to this order, oldest first, each with the payload the action carried:
order.placed order.requested order.updated
order.acknowledged order.shipment.created order.held
order.payment_status.changed order.returned
order.completed order.cancelled order.comment.added
This is the trail an operator reads to answer "why is this order in this state".
| Detail | |
|---|---|
| Append-only | Rows are written by the action routes. There is no way to add, edit or remove one, and this route is read-only. |
| Paginated | An order's trail grows for as long as the order lives. page.hasMore says whether more exists. |
| Filters | Every query parameter is an exact match on the column it names. |
order_id | Not a filter. The route fixes it from the path, so a value sent for it is overwritten rather than honoured. |
payload | The jsonb column is not filterable — the data plane answers 400 for anything that is not a whole JSON document. |
events as well as items, for compatibility with the pre-envelope shape. Read items. The alias is removed in the next minor version.Each row carries name, payload, actor and created_at. actor is free text as the calling client supplied it, and null for a row the app wrote itself.
Comments
Notes are for what the state machine cannot record: what the customer said on the phone, why an exception was made, what the warehouse found in the box.
curl -X POST "https://api.revenexx.com/v1/orders/{id}/comments" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{
"body": "Customer asked for delivery after 15 August.",
"visibility": "internal",
"author": "desk@merchant.example"
}'
visibility is internal (for the service desk) or customer (text meant to be shown to the buyer). It defaults to the tenant's default_comment_visibility, which is internal out of the box, so a note is never accidentally customer-facing.
Adding one writes an order.comment.added event, so the trail shows that a note was made and its visibility without copying the text onto the event feed.
It changes nothing about the order, and it sends nothing to anybody: this stores a comment, it does not email the customer.
curl "https://api.revenexx.com/v1/orders/{id}/comments" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..."
The customer rollup
curl -X POST https://api.revenexx.com/v1/orders/reports/customer-rollup \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"organization_ids":["…","…"]}'
What each company has bought, as numbers another app can keep: order count, lifetime revenue, first and last order date, and the same count and revenue over the last 30, 90 and 365 days.
This is the hand-over that makes a segment like "bought for more than 100k last year" possible: revenue lives in the orders app, customer segments live in the customers app, and the two may not join across apps. The customers app materialises this answer into a local projection its segment rules query.
Three properties worth knowing:
- It answers about organizations only. A private or guest order carries none and is counted in
orders_without_organizationrather than attributed to anybody. - It converts nothing. An organization that ordered in two currencies gets both listed and one summed number to read with care.
- Every number is additive (count, sum, min, max), so partial answers merge. The average order value is deliberately not returned — it is
revenue_total / order_countover the merged parts.
Windows are anchored at as_of, which is echoed back so a loop measures one consistent picture.