diff --git a/api-reference/openapi.json b/api-reference/openapi.json index 4bd0cba..0f05cd1 100644 --- a/api-reference/openapi.json +++ b/api-reference/openapi.json @@ -413,6 +413,71 @@ } } }, + "/v1/usage": { + "get": { + "summary": "Get usage", + "description": "Per-inbox usage for metering and pass-through billing. Counts and attachment bytes are scoped to the [from, to) window; storedBytes is the current point-in-time storage for the inbox. Defaults to the last 30 days. Max window is 92 days.", + "operationId": "getUsage", + "parameters": [ + { + "name": "from", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + }, + "description": "ISO 8601 start (inclusive). Defaults to 30 days ago." + }, + { + "name": "to", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + }, + "description": "ISO 8601 end (exclusive). Defaults to now." + }, + { + "name": "group_by", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "inbox", + "account" + ], + "default": "inbox" + }, + "description": "`inbox` (default) returns a per-inbox breakdown plus totals; `account` returns totals only." + } + ], + "responses": { + "200": { + "description": "Usage report", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsageReport" + } + } + } + }, + "400": { + "description": "Invalid or too-large date range", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, "/v1/inboxes/{id}/send": { "post": { "summary": "Send email", @@ -999,6 +1064,78 @@ } } }, + "InboxUsage": { + "type": "object", + "properties": { + "inboxId": { + "type": "string" + }, + "address": { + "type": "string", + "format": "email" + }, + "inbound": { + "type": "integer", + "description": "Inbound messages received in the window" + }, + "outbound": { + "type": "integer", + "description": "Outbound messages sent in the window" + }, + "attachmentBytes": { + "type": "integer", + "description": "Sum of attachment bytes across messages in the window" + }, + "storedBytes": { + "type": "integer", + "description": "Current stored bytes for the inbox (point-in-time)" + } + } + }, + "UsageReport": { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "date-time" + }, + "to": { + "type": "string", + "format": "date-time" + }, + "groupBy": { + "type": "string", + "enum": [ + "inbox", + "account" + ] + }, + "inboxes": { + "type": "array", + "description": "Per-inbox breakdown. Empty when group_by=account.", + "items": { + "$ref": "#/components/schemas/InboxUsage" + } + }, + "totals": { + "type": "object", + "properties": { + "inbound": { + "type": "integer" + }, + "outbound": { + "type": "integer" + }, + "attachmentBytes": { + "type": "integer" + }, + "storedBytes": { + "type": "integer" + } + } + } + } + }, "Message": { "type": "object", "properties": { diff --git a/concepts/usage.mdx b/concepts/usage.mdx new file mode 100644 index 0000000..639b2b7 --- /dev/null +++ b/concepts/usage.mdx @@ -0,0 +1,73 @@ +--- +title: "Usage" +description: "Per-inbox metering for pass-through and pay-as-you-go billing" +icon: gauge +--- + +OpenMail exposes granular usage so you can meter cost per inbox. If you treat each agent as its own inbox, this gives you per-agent metering out of the box — useful for passing cost through to your own users. + +## The usage endpoint + +`GET /v1/usage` returns a per-inbox breakdown plus account totals. + +```bash +curl "https://api.openmail.sh/v1/usage?from=2026-05-01T00:00:00Z&to=2026-06-01T00:00:00Z" \ + -H "Authorization: Bearer $OPENMAIL_API_KEY" +``` + +```json +{ + "from": "2026-05-01T00:00:00.000Z", + "to": "2026-06-01T00:00:00.000Z", + "groupBy": "inbox", + "inboxes": [ + { + "inboxId": "inb_abc", + "address": "agent-1@yourco.com", + "inbound": 128, + "outbound": 64, + "attachmentBytes": 5242880, + "storedBytes": 7340032 + } + ], + "totals": { + "inbound": 128, + "outbound": 64, + "attachmentBytes": 5242880, + "storedBytes": 7340032 + } +} +``` + +## Query parameters + +| Parameter | Description | +|-----------|-------------| +| `from` | ISO 8601 start (inclusive). Defaults to 30 days ago. | +| `to` | ISO 8601 end (exclusive). Defaults to now. | +| `group_by` | `inbox` (default) returns the per-inbox breakdown plus totals. `account` returns totals only. | + +The maximum window is 92 days. + +## Metrics + +| Field | Meaning | +|-------|---------| +| `inbound` | Inbound messages received in the window. | +| `outbound` | Outbound messages sent in the window. | +| `attachmentBytes` | Sum of attachment bytes across messages in the window (transferred volume). | +| `storedBytes` | Current stored bytes for the inbox right now (point-in-time), not bound to the window. | + + + `inbound`, `outbound`, and `attachmentBytes` are **windowed** — they reflect activity in `[from, to)`. `storedBytes` is **point-in-time** — it reflects what is occupying storage now, which is the right basis for storage billing. + + +## Metering pattern + +For pay-as-you-go pass-through: + +- Bill **per email** from `inbound + outbound` over a billing window. +- Bill **per attachment volume** from `attachmentBytes` over the window. +- Bill **per storage** from the latest `storedBytes` (snapshot it at the end of each period). + +Poll `GET /v1/usage` on your billing cadence (for example daily or hourly) and store the deltas. Inboxes with no activity still appear with zeroed counts, so you always get a complete per-agent list. diff --git a/docs.json b/docs.json index 1b96546..df3c8c7 100644 --- a/docs.json +++ b/docs.json @@ -51,7 +51,8 @@ "concepts/attachments", "concepts/suppressions", "concepts/sender-rules", - "concepts/rate-limits" + "concepts/rate-limits", + "concepts/usage" ] }, { diff --git a/scripts/generate-openapi.js b/scripts/generate-openapi.js index 5d8cad8..2e1bb18 100644 --- a/scripts/generate-openapi.js +++ b/scripts/generate-openapi.js @@ -255,6 +255,50 @@ const spec = { }, }, }, + "/v1/usage": { + get: { + summary: "Get usage", + description: + "Per-inbox usage for metering and pass-through billing. Counts and attachment bytes are scoped to the [from, to) window; storedBytes is the current point-in-time storage for the inbox. Defaults to the last 30 days. Max window is 92 days.", + operationId: "getUsage", + parameters: [ + { + name: "from", + in: "query", + required: false, + schema: { type: "string", format: "date-time" }, + description: "ISO 8601 start (inclusive). Defaults to 30 days ago.", + }, + { + name: "to", + in: "query", + required: false, + schema: { type: "string", format: "date-time" }, + description: "ISO 8601 end (exclusive). Defaults to now.", + }, + { + name: "group_by", + in: "query", + required: false, + schema: { type: "string", enum: ["inbox", "account"], default: "inbox" }, + description: + "`inbox` (default) returns a per-inbox breakdown plus totals; `account` returns totals only.", + }, + ], + responses: { + "200": { + description: "Usage report", + content: { + "application/json": { schema: { $ref: "#/components/schemas/UsageReport" } }, + }, + }, + "400": { + description: "Invalid or too-large date range", + content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }, + }, + }, + }, + }, "/v1/inboxes/{id}/send": { post: { summary: "Send email", @@ -570,6 +614,45 @@ const spec = { createdAt: { type: "string", format: "date-time" }, }, }, + InboxUsage: { + type: "object", + properties: { + inboxId: { type: "string" }, + address: { type: "string", format: "email" }, + inbound: { type: "integer", description: "Inbound messages received in the window" }, + outbound: { type: "integer", description: "Outbound messages sent in the window" }, + attachmentBytes: { + type: "integer", + description: "Sum of attachment bytes across messages in the window", + }, + storedBytes: { + type: "integer", + description: "Current stored bytes for the inbox (point-in-time)", + }, + }, + }, + UsageReport: { + type: "object", + properties: { + from: { type: "string", format: "date-time" }, + to: { type: "string", format: "date-time" }, + groupBy: { type: "string", enum: ["inbox", "account"] }, + inboxes: { + type: "array", + description: "Per-inbox breakdown. Empty when group_by=account.", + items: { $ref: "#/components/schemas/InboxUsage" }, + }, + totals: { + type: "object", + properties: { + inbound: { type: "integer" }, + outbound: { type: "integer" }, + attachmentBytes: { type: "integer" }, + storedBytes: { type: "integer" }, + }, + }, + }, + }, Message: { type: "object", properties: {