Events and metering
Driptab's usage-based billing is driven by events. Your backend sends an event every time a billable action occurs. At period close, those events are aggregated into invoice line items.
What is an event?
An event is a single billable occurrence: an email sent, a gigabyte stored, an API call made. Events have:
| Field | Description |
|---|---|
transaction_id |
Your stable, unique ID for this event. Must be unique per subscription per organization. |
subscription_id |
Which subscription to attribute the usage to (or external_id prefixed with ext:). |
code |
Which billable metric this event contributes to (e.g. mail_out). |
value |
Numeric value as a string (e.g. "1", "24576"). Used by sum, max, latest aggregations. |
properties |
Optional JSON object with additional context. |
timestamp |
When the event occurred (ISO-8601 UTC). |
Sending an event
dtab ev ingest \
--transaction-id msg_01HX4K2MNPQRSTUVWXYZ000001 \
--subscription ext:your-sub-id \
--code mail_out \
--prop count=1
POST /v1/events
Authorization: Bearer dtab_…
X-Project-Id: <project_id>
{
"transaction_id": "msg_01HX4K2MNPQRSTUVWXYZ000001",
"subscription_id": "ext:your-sub-id",
"code": "mail_out",
"value": "1",
"timestamp": "2026-06-08T14:23:00Z"
}
Response:
{ "accepted": true, "duplicate": false }
If the same transaction_id is sent again:
{ "accepted": true, "duplicate": true }
The event is acknowledged but not counted again. Your backend can retry event ingestion safely.
Idempotency
Idempotency for events is enforced at two levels:
-
The Durable Object — the
SubscriptionMeterDO maintains a set of seentransaction_ids for the current period. Concurrent or retried events are serialized by the DO; a duplicate is never double-counted. -
D1 unique constraint —
UNIQUE(organization_id, transaction_id)on the events table acts as a second safety net, catching any path that bypasses the DO.
The dedup set in the DO is reset when the period closes, so it does not grow without
bound. transaction_id uniqueness is per-period per-subscription.
How aggregation works
Each billable metric has an aggregation_type that determines how raw event values
roll up into the period total:
| Aggregation | How it works | Example use |
|---|---|---|
sum |
Add all value values in the period |
Email volume, bytes stored |
count |
Count the number of events | API calls (ignoring value) |
count_unique |
Count distinct values of field_name |
Unique active users |
max |
Peak value in the period |
High-water-mark storage billing |
latest |
Most recent value in the period |
Current seat count |
Aggregation does not happen in SQL. It happens in the SubscriptionMeter Durable
Object. The DO maintains a running counter per metric for the current period and
updates it on each accepted event.
The Durable Object architecture
Each active subscription has its own SubscriptionMeter Durable Object named
{organization_id}:{subscription_id}. The DO is:
- Single-threaded: all events to the same subscription are processed one at a time. No concurrent counter updates, no locking.
- Serialized: the DO's inbox queues concurrent requests rather than running them in parallel.
- Strongly consistent: the DO's SQLite storage is local to the DO; reads and writes are not eventually consistent.
This means you can fire thousands of events at the same subscription concurrently and the final counter will be exactly correct. There is no need for an external queue or distributed lock at current event volumes.
At extremely high event rates for a single subscription, a Queue can be placed in front
of the DO with no change to callers (see ARCHITECTURE.md).
Raw event storage
Every accepted event is also appended to the D1 events table for:
- Audit: who sent what, when, with what properties.
- Recompute: if a DO is lost or corrupted, the raw events can be replayed to reconstruct counters.
- Analytics: query patterns like "top 10 customers by mail_out this month".
The events table is append-only. At high scale, it migrates to R2 + Analytics Engine.
Period close
When the DO's alarm() fires at current_period_end:
- Final per-metric counters are read from the DO.
- Fees are computed for each charge:
standardcharges:units × properties.amount, rounded to cents once.flat_feecharges: the catalog item's price (or override).
- An
invoicerow and itsfeesrows are written to D1 in one atomic transaction. - Counters and the dedup set are reset.
current_period_start/endadvances by one interval.- The next alarm is scheduled.
- An
invoice.createdwebhook fires.
Late events
An event whose timestamp falls before current_period_start arrives after the
relevant period has already closed. Driptab's MVP behavior is to reject late events
with an error. Do not silently roll them into the current period — that would
misrepresent when the usage occurred. An explicit adjustment workflow will be added
in a future release.