Skip to content

Metrics query ​

Exported from metrics.ts. Backs POST /v1/metrics/query — an API-key-authenticated endpoint for ad-hoc analytics over your own tenant's payment data (no id in the path; the tenant is implied by the calling key, same as every other /v1/* route), in the same spirit as a log/observability platform's query API (Datadog, Honeycomb, Axiom): you describe what you want — resource, aggregation, groupBy, filters, date range — and get back rows shaped that way. Not raw SQL, not GraphQL — every queryable field is a typed literal enum, checked both by the TypeScript compiler and by the server, so a query can only ever touch a field this package explicitly declares queryable.

MetricsQuerySchema ​

A discriminated union on resource — the fields you can filter/group/ aggregate by depend on which resource you're querying, and TypeScript narrows accordingly the moment you set resource:

ts
import { MetricsQuerySchema, type MetricsQueryRequest } from '@klappay/types'

const request: MetricsQueryRequest = {
  resource: 'charges',
  environment: 'live',
  dateRange: { field: 'createdAt', from: '2026-07-01T00:00:00.000Z', to: '2026-08-01T00:00:00.000Z' },
  groupBy: [{ type: 'date_bucket', field: 'createdAt', granularity: 'day' }],
  metrics: [
    { aggregation: 'sum', field: 'amount', alias: 'volume' },
    { aggregation: 'count' },
  ],
  filters: [{ field: 'status', operator: 'eq', value: 'confirmed' }],
}

MetricsQuerySchema.parse(request)

MetricsQueryRequest (z.input<>) is the pre-parse shape — groupBy, filters, and limit are all optional there (the schema defaults them to []/[]/100); use MetricsQuery (z.infer<>) for an already-parsed value where those are always present, same *Request/*Input split as CreateChargeSchema (see charges.md).

MetricsResourceSchema — resources and their queryable fields ​

resource: 'charges' | 'transactions' | 'distributions'. One row per underlying record: charges is one row per charge, transactions is one row per detected on-chain transfer (a charge paid in installments has more than one), distributions is one row per payout attempt for one (token, network) pair a charge settled across.

Every field name below is also exported as its own literal-union schema/type — import these directly for full autocomplete instead of typing the string by hand:

ResourceDimension fields (filter/groupBy)Numeric fields (sum/avg/min/max)Date fields
chargesstatus, source, apiKeyId, currency, isOverpaid, externalRef, escrowReleaserAddress — ChargesQueryFieldSchema/ChargesQueryFieldamount, amountReceived, feePercent, escrowFeePercent — ChargesMetricFieldSchema/ChargesMetricFieldcreatedAt, confirmedAt, lastActivityAt, expiresAt, escrowReleasedAt — ChargesDateFieldSchema/ChargesDateField
transactionsnetwork, token, source, causedTransition — TransactionsQueryFieldSchema/TransactionsQueryFieldamount — TransactionsMetricFieldSchema/TransactionsMetricFielddetectedAt — TransactionsDateFieldSchema/TransactionsDateField
distributionsstatus, network, token, distributorAddress — DistributionsQueryFieldSchema/DistributionsQueryFieldattempts — DistributionsMetricFieldSchema/DistributionsMetricFieldcreatedAt, processingStartedAt, completedAt — DistributionsDateFieldSchema/DistributionsDateField
ts
import { ChargesQueryFieldSchema, type ChargesQueryField } from '@klappay/types'

ChargesQueryFieldSchema.options // ['status', 'source', 'apiKeyId', 'currency', 'isOverpaid', 'externalRef', 'escrowReleaserAddress']

dateRange is required, and capped ​

Every query needs a dateRange — { field, from, to }, field one of that resource's date fields above. This isn't optional-defaulting-to- unbounded on purpose: it's what keeps a query from scanning your entire history. The span can't exceed MAX_METRICS_QUERY_DATE_RANGE_DAYS (366 days) — a superRefine on MetricsQuerySchema rejects anything wider, or from >= to.

groupBy — MetricsDateGranularitySchema ​

Up to METRICS_QUERY_MAX_GROUP_BY (3) entries, each either:

  • { type: 'field', field: <a dimension field> }
  • { type: 'date_bucket', field: <a date field>, granularity } — at most one of these per query. granularity is MetricsDateGranularitySchema/ MetricsDateGranularity: 'day' | 'week' | 'month'.

metrics — MetricsAggregationSchema ​

At least one, up to METRICS_QUERY_MAX_METRICS (10): { aggregation, field?, alias? }. aggregation is MetricsAggregationSchema/MetricsAggregation: 'count' | 'sum' | 'avg' | 'min' | 'max'. field is required unless aggregation is count (rejected otherwise), and must be one of the resource's numeric fields. alias names the output column — if you omit it, the column is named ${aggregation} for count, or ${aggregation}_${field} otherwise (e.g. sum_amount). Every alias in one query must be unique, must not collide with a groupBy field name (or the reserved word bucket) in the same query, and (since it becomes a SQL column alias for date-bucketed queries) must match ^[a-zA-Z_][a-zA-Z0-9_]*$ — letters, digits, and underscores, not starting with a digit.

filters — MetricsFilterOperatorSchema ​

Up to METRICS_QUERY_MAX_FILTERS (20): { field, operator, value }. field is one of that resource's dimension fields (not its numeric fields — filtering amount > 100 isn't supported yet, only grouping/aggregating on it). operator is MetricsFilterOperatorSchema/ MetricsFilterOperator: 'eq' | 'neq' | 'in' | 'gt' | 'gte' | 'lt' | 'lte' — in expects an array value (max 50 entries), every other operator a single scalar.

orderBy and limit ​

orderBy (optional): { key, direction }, where key is an output column name from this same query (a groupBy field name, or a metric's alias/default name) — must match ^[a-zA-Z_][a-zA-Z0-9_]*$, same pattern as alias above, though every real output column name already satisfies it. limit (default 100, max METRICS_QUERY_MAX_ROW_LIMIT = 1000) caps rows returned — if more rows matched, the response's meta.truncated is true.

MetricsQueryResultSchema ​

ts
import { type MetricsQueryResult } from '@klappay/types'

const result: MetricsQueryResult = {
  data: [
    { createdAt: '2026-07-01', volume: 4820.5, count: 12 },
    { createdAt: '2026-07-02', volume: 3110.0, count: 9 },
  ],
  meta: { resource: 'charges', environment: 'live', rowCount: 2, truncated: false },
}

data rows are shaped by your query — keys are whatever your groupBy fields and metric aliases resolved to, values are string | number | boolean | null.

That row type is exported on its own as MetricsQueryResultRowSchema / MetricsQueryResultRow (Record<string, string | number | boolean | null>). Reach for it when you're typing a function that formats or renders a single row without carrying the surrounding meta — the result schema is defined in terms of it, so the two can't drift. It is deliberately an open record rather than a generated per-resource type: the key set depends on the query you sent, not on the resource, so there's nothing narrower to promise at compile time.

Access control ​

Every valid API key gets unrestricted access to its own tenant's data — there's no member/role-based sub-scoping within a tenant, since Core has no membership or role concept at all (account/organization management lives in a separate Dashboard service, see docs/auth.md in the main repo). A key can only ever see data attributed to its own tenantId, never another tenant's.

See also ​

  • charges.md — the *Request/*Input split this schema also follows, and the underlying Charge fields this query engine reads.

Public docs for @klappay/types, mirrored from this package's own docs/ folder.