> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useautumn.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Schema Reference

> Every Lakehouse table and column, with the keys to join on.

> [!WARNING]
>
> **Available on request** — The Autumn Lakehouse is provisioned per customer. Contact us at [hey@useautumn.com](mailto:hey@useautumn.com) to get access. Any customer may use our [SQL endpoint](https://docs.useautumn.com/documentation/query-with-sql.md) instead.

Every object is delivered as a table named `v2_3_<object>`, grouped below by lane. The **one exception is events**: the event log is immutable and unversioned, delivered as `events` (not `v2_3_events`) — see [Events](#events).

## Type legend

Types below are **engine-neutral**. How each engine surfaces them:

| Logical type | ClickHouse | BigQuery |
| - | - | - |
| `string` | `Nullable(String)` | `STRING` |
| `number` | `Nullable(Decimal(38, …))` | `INT64` / `NUMERIC` |
| `number (epoch ms)` | `Nullable(Decimal(38, …))` | `INT64` |
| `boolean` | `Nullable(Bool)` | `BOOL` |
| `json (string)` | `Nullable(String)` | `STRING` |
| `json[] (string)` | `Array(Nullable(String))` | `ARRAY<STRING>` |
| `string[]` | `Array(Nullable(String))` | `ARRAY<STRING>` |
| `timestamp` | `Nullable(DateTime64)` | `TIMESTAMP` |

> [!NOTE]
>
> * **Timestamps are epoch-milliseconds** (`number (epoch ms)`), not native datetimes — convert before use. See [Querying → Timestamps](https://docs.useautumn.com/documentation/lakehouse/querying.md#timestamps). The one exception is `events.timestamp`, which is a real `timestamp`.
> * **JSON columns are stored as strings** — parse them with `JSONExtract*` (ClickHouse) or `JSON_VALUE` (BigQuery).

## Keys

* ⭐ marks the **stable, immutable key** for a table — join and filter on this.
* 🔗 marks a **stable foreign key** (`internal_customer_id`, `internal_feature_id`, `internal_product_id`, `internal_entity_id`, `internal_reward_id`) — join to the matching `internal_id`.
* External ids (`customer_id`, `plan_id`, `feature_id`, `entity_id`, `id`, …) are **mutable** — convenient for display, but don't rely on them as stable keys. See [Querying → Use internal ids](https://docs.useautumn.com/documentation/lakehouse/querying.md#use-internal-ids-not-external-ids).
* `org_id` is your tenant id (constant across your tables); `env` is `sandbox` or `live` (**not** `production` — filtering `env = 'production'` silently returns zero rows).

***

## Catalog

Your pricing model: features, plans, plan items, rewards, and referral programs.

> [accordion] v2\_3\_features — feature catalog
>
> | Column | Type | Notes |
> | - | - | - |
> | `internal_id` | string | ⭐ stable key |
> | `feature_id` | string | external id (e.g. `AI_CREDITS`), mutable |
> | `org_id` | string | your tenant id |
> | `env` | string | `sandbox` / `live` |
> | `name` | string | |
> | `type` | string | feature type |
> | `display` | json (string) | display config |
> | `config` | json (string) | metered / credit-system config |

> [accordion] v2\_3\_plans — plan headers with base pricing
>
> | Column | Type | Notes |
> | - | - | - |
> | `internal_id` | string | ⭐ stable key — one row **per plan version** |
> | `plan_id` | string | external id, mutable, shared across versions |
> | `org_id` | string | your tenant id |
> | `env` | string | `sandbox` / `live` |
> | `name` | string | |
> | `description` | string | |
> | `group` | string | |
> | `version` | number | plan version number |
> | `add_on` | boolean | |
> | `auto_enable` | boolean | |
> | `archived` | boolean | |
> | `base_variant_id` | string | |
> | `created_at` | number (epoch ms) | |
> | `config` | json (string) | |
> | `price_amount` | number | base recurring price |
> | `price_interval` | string | billing interval |
> | `price_interval_count` | number | |
> | `trial_duration_length` | number | |
> | `trial_duration_type` | string | |
> | `trial_card_required` | boolean | |
> | `trial_on_end` | string | |

> [accordion] v2\_3\_plan\_items — entitlements + linked prices
>
> | Column | Type | Notes |
> | - | - | - |
> | `internal_id` | string | ⭐ stable key |
> | `plan_id` | string | external plan id |
> | `internal_product_id` | string | 🔗 → `v2_3_plans.internal_id` |
> | `org_id` | string | your tenant id |
> | `env` | string | `sandbox` / `live` |
> | `feature_id` | string | external feature id |
> | `internal_feature_id` | string | 🔗 → `v2_3_features.internal_id` |
> | `included` | number | allowance (`0` if unlimited) |
> | `unlimited` | boolean | |
> | `reset_interval` | string | null for boolean / continuous-use |
> | `reset_interval_count` | number | null if count is 1 |
> | `has_price` | boolean | |
> | `price_amount` | number | single-tier only |
> | `price_tiers` | json (string) | multi-tier array (null if single/fixed) |
> | `price_tier_behavior` | string | graduated / stairstep |
> | `price_interval` | string | |
> | `price_interval_count` | number | |
> | `price_billing_units` | number | |
> | `price_billing_method` | string | prepaid / usage\_based |
> | `price_max_purchase` | number | usage\_limit − included |
> | `rollover_max` | number | |
> | `rollover_max_percentage` | number | |
> | `rollover_expiry_duration_type` | string | |
> | `rollover_expiry_duration_length` | number | |

> [accordion] v2\_3\_rewards — discounts, free products, promos
>
> | Column | Type | Notes |
> | - | - | - |
> | `internal_id` | string | ⭐ stable key |
> | `id` | string | external id, mutable |
> | `org_id` | string | your tenant id |
> | `env` | string | `sandbox` / `live` |
> | `name` | string | |
> | `type` | string | percentage\_discount / fixed\_discount / free\_product |
> | `created_at` | number (epoch ms) | |
> | `discount_value` | number | |
> | `discount_duration_type` | string | |
> | `discount_duration_value` | number | |
> | `discount_apply_to_all` | boolean | |
> | `discount_price_ids` | json (string) | string array (null for free-product) |
> | `free_product_id` | string | plan id (null for discount) |
> | `free_product_duration_type` | string | |
> | `free_product_duration_value` | number | |
> | `promo_codes` | json (string) | array of `{code, global_max_redemption}` |

> [accordion] v2\_3\_referral\_programs — referral configuration
>
> | Column | Type | Notes |
> | - | - | - |
> | `internal_id` | string | ⭐ stable key |
> | `id` | string | external id, mutable |
> | `org_id` | string | your tenant id |
> | `env` | string | `sandbox` / `live` |
> | `created_at` | number (epoch ms) | |
> | `reward_id` | string | external reward id |
> | `internal_reward_id` | string | 🔗 → `v2_3_rewards.internal_id` |
> | `trigger_event` | string | customer\_creation / checkout |
> | `received_by` | string | referrer / all |
> | `product_ids` | string\[] | external plan ids |
> | `exclude_trial` | boolean | |
> | `unlimited_redemptions` | boolean | |
> | `max_redemptions` | number | |

***

## Subjects

Who your plans apply to: customers and entities (sub-customers).

> [accordion] v2\_3\_customers — customer master record
>
> | Column | Type | Notes |
> | - | - | - |
> | `internal_id` | string | ⭐ stable key |
> | `customer_id` | string | external id, mutable (may be null) |
> | `org_id` | string | your tenant id |
> | `env` | string | `sandbox` / `live` |
> | `name` | string | |
> | `email` | string | |
> | `created_at` | number (epoch ms) | |
> | `fingerprint` | string | |
> | `stripe_id` | string | Stripe customer id |
> | `metadata` | json (string) | |
> | `send_email_receipts` | boolean | |
> | `billing_controls` | json (string) | auto\_topups, spend\_limits, usage\_alerts, overage\_allowed |
> | `config` | json (string) | |
> | `processors` | json (string) | stripe / vercel / revenuecat links |

> [accordion] v2\_3\_entities — sub-customers scoped to a feature
>
> | Column | Type | Notes |
> | - | - | - |
> | `internal_id` | string | ⭐ stable key |
> | `id` | string | external id, mutable |
> | `org_id` | string | your tenant id |
> | `env` | string | `sandbox` / `live` |
> | `customer_id` | string | parent customer external id |
> | `internal_customer_id` | string | 🔗 → `v2_3_customers.internal_id` |
> | `feature_id` | string | entity feature scope (external) |
> | `name` | string | |
> | `created_at` | number (epoch ms) | |
> | `deleted` | boolean | |
> | `billing_controls` | json (string) | spend\_limits, usage\_alerts, overage\_allowed |

***

## States

Active customer–product relationships: recurring subscriptions and one-off purchases.

> [accordion] v2\_3\_subscriptions — recurring customer products
>
> | Column | Type | Notes |
> | - | - | - |
> | `internal_id` | string | ⭐ stable key |
> | `id` | string | external id, mutable |
> | `customer_id` | string | external customer id |
> | `internal_customer_id` | string | 🔗 → `v2_3_customers.internal_id` |
> | `org_id` | string | your tenant id |
> | `env` | string | `sandbox` / `live` |
> | `plan_id` | string | external plan id |
> | `internal_product_id` | string | 🔗 → `v2_3_plans.internal_id` |
> | `add_on` | boolean | |
> | `auto_enable` | boolean | |
> | `status` | string | active / scheduled |
> | `past_due` | boolean | |
> | `canceled_at` | number (epoch ms) | null if not canceled |
> | `expires_at` | number (epoch ms) | null if no expiry |
> | `trial_ends_at` | number (epoch ms) | null if no trial |
> | `started_at` | number (epoch ms) | |
> | `quantity` | number | |
> | `current_period_start` | number (epoch ms) | null if none |
> | `current_period_end` | number (epoch ms) | null if none |
> | `entity_id` | string | external entity id (null = customer-scoped) |
> | `internal_entity_id` | string | 🔗 → `v2_3_entities.internal_id` (null = customer-scoped) |

> [accordion] v2\_3\_purchases — one-off purchases
>
> | Column | Type | Notes |
> | - | - | - |
> | `internal_id` | string | ⭐ stable key |
> | `id` | string | external id, mutable |
> | `customer_id` | string | external customer id |
> | `internal_customer_id` | string | 🔗 → `v2_3_customers.internal_id` |
> | `org_id` | string | your tenant id |
> | `env` | string | `sandbox` / `live` |
> | `plan_id` | string | external plan id |
> | `internal_product_id` | string | 🔗 → `v2_3_plans.internal_id` |
> | `expires_at` | number (epoch ms) | null if no expiry |
> | `started_at` | number (epoch ms) | |
> | `quantity` | number | |
> | `entity_id` | string | external entity id (null = customer-scoped) |
> | `internal_entity_id` | string | 🔗 → `v2_3_entities.internal_id` (null = customer-scoped) |

***

## Balances

Feature balances, their per-entitlement breakdowns, rollover grants, and boolean feature flags.

> [!WARNING]
>
> **These tables do not behave like a clean relational schema.** They are denormalized snapshots of Autumn's read-time balance computation, and they include **every** `customer_entitlement` row — across superseded product versions and past reset cycles — with **no active-status or current-cycle filter**. A naive `SELECT ... FROM v2_3_balances` therefore sums many lifetimes of a customer's history into one row. Before you query balances, breakdowns, or overage, read **[Working with balances](https://docs.useautumn.com/documentation/lakehouse/balance-semantics.md)** — it explains what each column means, why `usage`/`granted`/`remaining` don't form a closed arithmetic triple, where overage comes from (it is **not** stored), and the active + current-cycle filter you must apply.

> [!WARNING]
>
> **Rollover balances are not in `v2_3_balances` or `v2_3_breakdowns`.** They live in their own table, `v2_3_rollovers` (below), and `remaining` / `usage` on the other two tables **exclude** them. Any customer holding rollovers therefore reads low until you add the rollover balance in yourself — and rollovers **expire**, so you must filter `expires_at` when you do. See [Working with balances → Rollovers](https://docs.useautumn.com/documentation/lakehouse/balance-semantics.md#rollovers-live-in-their-own-table).

> [!NOTE]
>
> `entity_id` is the **scope** of the row: null means customer-scoped (pooled), non-null means the row belongs to that entity. In `v2_3_balances` and `v2_3_breakdowns` a given entitlement appears **exactly once**, under whichever scope it holds — the two kinds of row are disjoint, so filtering to `coalesce(entity_id, '') = ''` gives you customer-scoped rows only, and *dropping* entity-scoped ones. `v2_3_flags` is the exception: an entity-scoped flag is emitted **twice**, once at its own entity scope and once into the customer pool, so keep the pooled rows there to avoid double counting. (In ClickHouse use `coalesce`, **not** `entity_id IS NULL` — `IS NULL` throws on these Iceberg columns; BigQuery is unaffected. See [Querying → Pooled vs per-entity](https://docs.useautumn.com/documentation/lakehouse/querying.md#pooled-vs-per-entity-rows).)

> [accordion] v2\_3\_balances — aggregated balance per customer × feature × entity
>
> | Column | Type | Notes |
> | - | - | - |
> | `internal_customer_id` | string | ⭐🔗 part of key → `v2_3_customers.internal_id` |
> | `internal_feature_id` | string | ⭐🔗 part of key → `v2_3_features.internal_id` |
> | `entity_id` | string | ⭐ part of key (null = pooled) |
> | `customer_id` | string | external customer id |
> | `feature_id` | string | external feature id |
> | `internal_entity_id` | string | 🔗 → `v2_3_entities.internal_id` (null = pooled) |
> | `org_id` | string | your tenant id |
> | `env` | string | `sandbox` / `live` |
> | `unlimited` | boolean | true if any entitlement in the group is unlimited (then `granted`/`remaining`/`usage` are all `0`) |
> | `granted` | number | Σ(`included_grant` + `prepaid_grant`) over **all** entitlements in the group |
> | `remaining` | number | Σ(floored per-entitlement balance) — **floored at 0**, never negative. **Excludes rollovers** |
> | `usage` | number | Σ(per-entitlement `usage`). **Not** `granted − remaining`; see below. **Excludes rollovers** |
> | `reset_interval` | string | single interval or `multiple` |
> | `reset_interval_count` | number | null if `multiple` or count 1 |
> | `reset_resets_at` | number (epoch ms) | earliest reset across entitlements |
> | `next_reset_at` | number (epoch ms) | earliest next reset |
>
> > [!WARNING]
> >
> > `granted` / `remaining` / `usage` are **not** a closed arithmetic triple — `remaining ≠ granted − usage`. Each is aggregated **over every entitlement row for this customer × feature × scope**, including superseded product versions and past cycles, so a single row routinely sums many lifetimes of usage. `remaining` is floored per-entitlement, manual "Set Balance" decouples balance from grant, and rollover balances are omitted entirely (add them from `v2_3_rollovers`). **There is no overage column** — it is derived. Always read [Working with balances](https://docs.useautumn.com/documentation/lakehouse/balance-semantics.md) and apply the active + current-cycle filter before trusting these numbers.

> [accordion] v2\_3\_breakdowns — per-entitlement balance detail
>
> **One row per `customer_entitlement`** — `internal_id` alone is the key. (It used to be `(internal_id, entity_id)`, because one entitlement could emit a pooled row *plus* per-entity rows. That fan-out is gone; `entity_id` is now simply the scope this single row belongs to.)
>
> | Column | Type | Notes |
> | - | - | - |
> | `internal_id` | string | ⭐ stable key (customer\_entitlement id) |
> | `internal_customer_product_id` | string | 🔗 → `v2_3_subscriptions.internal_id` **or** `v2_3_purchases.internal_id` — the customer's own plan row |
> | `internal_product_id` | string | 🔗 → `v2_3_plans.internal_id` (the plan *version*, not the customer's copy of it) |
> | `internal_plan_item_id` | string | 🔗 → `v2_3_plan_items.internal_id` — the priced entitlement this balance came from |
> | `internal_customer_id` | string | 🔗 → `v2_3_customers.internal_id` |
> | `internal_feature_id` | string | 🔗 → `v2_3_features.internal_id` |
> | `internal_entity_id` | string | 🔗 → `v2_3_entities.internal_id` (null = customer-scoped) |
> | `id` | string | external id, mutable |
> | `customer_id` | string | external customer id |
> | `feature_id` | string | external feature id |
> | `entity_id` | string | external entity id (null = customer-scoped) |
> | `org_id` | string | your tenant id |
> | `env` | string | `sandbox` / `live` |
> | `unlimited` | boolean | |
> | `included_grant` | number | allowance grant (allowance × plan quantity + adjustment) |
> | `prepaid_grant` | number | prepaid quantity × billing units |
> | `remaining` | number | floored balance — `max(0, balance)`, **never negative**. **Excludes rollovers** |
> | `usage` | number | `included_grant` + `prepaid_grant` − raw (signed) balance. **Excludes rollovers** |
> | `expires_at` | number (epoch ms) | null if no expiry |
> | `reset_interval` | string | null for continuous-use; `one_off` for lifetime grants |
> | `reset_interval_count` | number | null if count 1 |
> | `reset_resets_at` | number (epoch ms) | next reset for **this** entitlement (use to pick the current cycle) |
>
> > [!WARNING]
> >
> > **`plan_id` is gone from this table.** Breakdowns no longer join out to the plans table, so the external, human-readable plan id is not carried here — a query that selects `plan_id` from `v2_3_breakdowns` today will fail outright rather than return nulls. Get it by joining `internal_product_id` → `v2_3_plans.internal_id` and reading `plan_id` there. (Prefer `internal_product_id` itself wherever you can: `plan_id` is mutable and shared across plan versions.)
>
> > [!WARNING]
> >
> > **`v2_3_breakdowns` has no `status` / `is_active` / `is_current` column** and mixes rows from superseded/cancelled product versions, past reset cycles, and pre-seeded future cycles. To get the rows the API would use, join `internal_customer_product_id` → `v2_3_subscriptions.internal_id` and keep the active ones, then pick the current cycle (earliest upcoming `reset_resets_at`); `one_off` rows are always live. This join is now **direct**: `internal_customer_product_id` is the customer's own subscription row, so one equality gets you its status. (Don't use `internal_product_id` for this — it points at the shared *plan version*, not at the customer's own copy of it.) See [Working with balances → The active + current-cycle filter](https://docs.useautumn.com/documentation/lakehouse/balance-semantics.md#the-active-current-cycle-filter).

> [accordion] v2\_3\_rollovers — rollover grants carried across reset cycles
>
> When a feature is configured to roll unused allowance into the next cycle, each carried-over grant lands here as its own row. **Nothing in `v2_3_balances` or `v2_3_breakdowns` includes these amounts** — you add them by joining `internal_breakdown_id` back to `v2_3_breakdowns.internal_id`.
>
> Rollovers are a separate table rather than extra columns on `v2_3_breakdowns` because they are **sparse**: roughly 6.8% of customer entitlements carry one (about 7.8M rollovers against 115M entitlements). Folding them in would put mostly-null columns on every breakdown row, so the \~93% of rows with no rollover would pay for the 7% that do. One entitlement can also hold several grants with different expiries, which a flat column set can't represent at all.
>
> | Column | Type | Notes |
> | - | - | - |
> | `internal_id` | string | ⭐ stable key (rollover id) |
> | `internal_breakdown_id` | string | 🔗 → `v2_3_breakdowns.internal_id` — the entitlement this rollover belongs to |
> | `internal_customer_product_id` | string | 🔗 → `v2_3_subscriptions.internal_id` **or** `v2_3_purchases.internal_id` |
> | `internal_plan_item_id` | string | 🔗 → `v2_3_plan_items.internal_id` (carries the `rollover_*` config) |
> | `internal_feature_id` | string | 🔗 → `v2_3_features.internal_id` |
> | `customer_id` | string | external customer id |
> | `org_id` | string | your tenant id |
> | `env` | string | `sandbox` / `live` |
> | `balance` | number | rollover amount still available |
> | `usage` | number | rollover amount already drawn down |
> | `expires_at` | number (epoch ms) | **null = never expires** |
> | `entities` | json (string) | raw per-entity map, `{entity_id: {id, balance, usage}}` |
>
> > [!WARNING]
> >
> > **Rollovers expire, and this table keeps the expired ones.** Summing `balance` without filtering overstates what a customer actually holds. Always filter on `expires_at`: in ClickHouse write `(coalesce(expires_at, 0) = 0 OR expires_at > <now epoch ms>)`, because `expires_at IS NULL` throws there, same as `entity_id`; in BigQuery plain `IS NULL` is fine. There is a worked query in [Querying → Balance including rollovers](https://docs.useautumn.com/documentation/lakehouse/querying.md#balance-including-rollovers).
>
> > [!NOTE]
> >
> > There is **no `entity_id` column** — per-entity rollover amounts are left packed in the `entities` JSON map rather than exploded into rows. Parse it if you need entity-level rollover detail.

> [accordion] v2\_3\_flags — boolean feature flags
>
> | Column | Type | Notes |
> | - | - | - |
> | `internal_id` | string | ⭐ part of key |
> | `entity_id` | string | ⭐ part of key (null = pooled) |
> | `id` | string | external id, mutable |
> | `customer_id` | string | external customer id |
> | `internal_customer_id` | string | 🔗 → `v2_3_customers.internal_id` |
> | `internal_entity_id` | string | 🔗 → `v2_3_entities.internal_id` (null = pooled) |
> | `feature_id` | string | external feature id |
> | `internal_feature_id` | string | 🔗 → `v2_3_features.internal_id` |
> | `internal_product_id` | string | 🔗 → `v2_3_plans.internal_id` |
> | `internal_customer_product_id` | string | 🔗 → `v2_3_subscriptions.internal_id` **or** `v2_3_purchases.internal_id` |
> | `subscription_ids_csv` | string | processor subscription ids, comma-separated (empty string if none) |
> | `expires_at` | number (epoch ms) | null if no expiry |
> | `org_id` | string | your tenant id |
> | `env` | string | `sandbox` / `live` |
>
> > [!WARNING]
> >
> > **`plan_id`, `customer_plan_status`, `customer_plan_starts_at`, and `customer_plan_ended_at` have been removed**, leaving the 14 columns above. Flags now follow the same rule as `v2_3_breakdowns`: **carry keys, never denormalized copies of data that lives on another table**. Resolve each of the four by joining outward —
> >
> > * `plan_id` → join `internal_product_id` to `v2_3_plans` and read `plan_id` there, exactly as you would from a breakdown.
> > * the three `customer_plan_*` columns → join `internal_customer_product_id` to `v2_3_subscriptions.internal_id` (or `v2_3_purchases.internal_id`) and read `status`, `started_at`, `ended_at` / `expires_at` from the plan row itself. There is a ready-made double-join in [Querying → The status of the plan behind a flag](https://docs.useautumn.com/documentation/lakehouse/querying.md#the-status-of-the-plan-behind-a-flag).
> >
> > Dropping `plan_id` removed the plans join from the flags query outright, so the table is cheaper to keep in sync as well as simpler to reason about. If you previously filtered `customer_plan_status = 'active'` directly on this table, that query needs the join added.
>
> > [!NOTE]
> >
> > The `subscription_ids` **array** was dropped — use `subscription_ids_csv`, which every engine handles identically; split on `,` if you need the individual ids.

***

## Invoices

Invoices and their line items.

> [accordion] v2\_3\_invoices — invoice master record
>
> | Column | Type | Notes |
> | - | - | - |
> | `id` | string | ⭐ stable key (invoice id) |
> | `org_id` | string | your tenant id |
> | `env` | string | `sandbox` / `live` |
> | `customer_id` | string | external customer id |
> | `internal_customer_id` | string | 🔗 → `v2_3_customers.internal_id` |
> | `entity_id` | string | external entity id (null = customer-scoped) |
> | `internal_entity_id` | string | 🔗 → `v2_3_entities.internal_id` (null = customer-scoped) |
> | `plan_ids` | string\[] | external plan ids |
> | `internal_product_ids` | string\[] | 🔗 → `v2_3_plans.internal_id` |
> | `stripe_id` | string | Stripe invoice id |
> | `processor_type` | string | stripe / … |
> | `status` | string | paid / open / void / draft |
> | `total` | number | invoice total |
> | `amount_paid` | number | |
> | `refunded_amount` | number | |
> | `currency` | string | ISO 4217 code |
> | `created_at` | number (epoch ms) | |
> | `hosted_invoice_url` | string | raw Stripe URL |
> | `discounts` | json\[] (string) | array of discount objects |

> [accordion] v2\_3\_invoice\_line\_items — invoice line detail
>
> | Column | Type | Notes |
> | - | - | - |
> | `id` | string | ⭐ stable key (line item id) |
> | `org_id` | string | your tenant id |
> | `env` | string | `sandbox` / `live` |
> | `invoice_id` | string | 🔗 → `v2_3_invoices.id` |
> | `customer_id` | string | external customer id |
> | `description` | string | |
> | `period_start` | number (epoch ms) | |
> | `period_end` | number (epoch ms) | |
> | `feature_id` | string | external feature id (null for non-feature lines) |
> | `internal_feature_id` | string | 🔗 → `v2_3_features.internal_id` |
> | `feature_name` | string | resolved feature name |
> | `plan_id` | string | external plan id |
> | `internal_product_id` | string | 🔗 → `v2_3_plans.internal_id` |
> | `amount` | number | pre-discount |
> | `amount_after_discounts` | number | |
> | `currency` | string | ISO 4217 code |
> | `direction` | string | charge / refund |
> | `billing_timing` | string | in\_advance / in\_arrear |
> | `prorated` | boolean | |
> | `discounts` | json\[] (string) | array of discount objects |

***

## Events

The append-only usage event log. This is the highest-volume table and is **append-only** (no updates).

> [!WARNING]
>
> **The events table is `events`, not `v2_3_events`.** Events are immutable and **unversioned** — they are *not* subject to the `v2_3` schema versioning that every other object uses. Address it as `` `<catalog>`.`<namespace>.events` ``. Querying `v2_3_events` returns `Unknown table expression identifier` because that table does not exist.

> [accordion] events — usage event log
>
> | Column | Type | Notes |
> | - | - | - |
> | `id` | string | ⭐ stable key (event id) |
> | `org_id` | string | your tenant id |
> | `org_slug` | string | |
> | `internal_customer_id` | string | 🔗 → `v2_3_customers.internal_id` |
> | `env` | string | `sandbox` / `live` |
> | `created_at` | number (epoch ms) | when Autumn recorded the event |
> | `timestamp` | timestamp | event time — a **real timestamp**, use directly |
> | `event_name` | string | |
> | `idempotency_key` | string | |
> | `value` | number | |
> | `entity_id` | string | external entity id (null = customer-scoped) |
> | `internal_entity_id` | string | 🔗 → `v2_3_entities.internal_id` |
> | `internal_product_id` | string | 🔗 → `v2_3_plans.internal_id` |
> | `customer_id` | string | external customer id |
> | `properties` | json (string) | event properties (e.g. `subtype`, `model`) |
> | `deductions` | json (string) | per-feature deductions for this event |

> [!NOTE]
>
> Unlike every other table, `events.timestamp` is a native `timestamp` — no epoch-ms conversion needed. `created_at` is still epoch-ms.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.