# /api/v1/tenants/[id]/usage (management)

Source-derived management endpoint contract, authentication, request validation, responses, and errors.

Source: https://docs.minds.sh/docs/api/platform/reference/management--api-v1-tenants-id-usage



This is the **management** service route. Its origin and authentication are described in [Platform API overview](/docs/api/platform) and [Authentication](/docs/api/platform/authentication). A path shared by another service is a different endpoint.

| Property                       | Contract                                                                       |
| ------------------------------ | ------------------------------------------------------------------------------ |
| Methods                        | GET                                                                            |
| Path                           | `/api/v1/tenants/[id]/usage`                                                   |
| Path parameters                | `id`                                                                           |
| Query parameters read in route | None read directly; schema validation below may define additional fields       |
| Headers read in route          | No additional direct header reads; authentication helpers may read credentials |

## Authentication and access [#authentication-and-access]

Management auth context: validated x-api-key or configured session authentication; trusted gateway integration may select its auth pipeline. Organization/tenant checks are separate.

## Request and response definitions [#request-and-response-definitions]

The following named definitions are imported or declared by this route. Optional markers, defaults, bounds, and enum values are shown exactly as implemented. A response type is a source contract, not an example populated with real account data.

### UsageResponse [#usageresponse]

```typescript
UsageResponse = z.object({
  tenant_id: z.string(),
  plan: TenantPlan,
  billing_period: BillingPeriod,
  usage: z.object({
    storage_gb: UsageMetric,
    requests: UsageMetric,
    hrm_inferences: UsageMetric,
    vector_searches: z.object({
      total: z.number().int(),
      cost: z.number(),
    }),
    graph_queries: z.object({
      total: z.number().int(),
      cost: z.number(),
    }),
  }),
  estimated_cost: z.object({
    base: z.number(), // Plan base cost
    usage: z.number(), // Overage cost
    total: z.number(), // Total estimated
    currency: z.literal('USD'),
  }),
})
```

Source: `minds-ui/apps/api-service/types/usage.ts:32`.

### UsageResponse [#usageresponse-1]

```typescript
export type UsageResponse = z.infer<typeof UsageResponse>
```

Source: `minds-ui/apps/api-service/types/usage.ts:57`.

### apiRequestCounter [#apirequestcounter]

```typescript
apiRequestCounter = new promClient.Counter({
  name: 'api_requests_total',
  help: 'Total number of API requests',
  labelNames: ['method', 'path', 'status', 'tenant_id'],
  registers: [register],
})
```

Source: `minds-ui/apps/api-service/lib/services/prometheus.ts:27`.

### apiRequestDuration [#apirequestduration]

```typescript
apiRequestDuration = new promClient.Histogram({
  name: 'api_request_duration_seconds',
  help: 'API request duration in seconds',
  labelNames: ['method', 'path', 'tenant_id'],
  buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
  registers: [register],
})
```

Source: `minds-ui/apps/api-service/lib/services/prometheus.ts:34`.

### apiRequestErrors [#apirequesterrors]

```typescript
apiRequestErrors = new promClient.Counter({
  name: 'api_request_errors_total',
  help: 'Total number of API request errors',
  labelNames: ['method', 'path', 'error_type', 'tenant_id'],
  registers: [register],
})
```

Source: `minds-ui/apps/api-service/lib/services/prometheus.ts:42`.

### RouteContext [#routecontext]

```typescript
interface RouteContext {
  params: Promise<{ id: string }>
}
```

Source: `minds-ui/apps/api-service/app/api/v1/tenants/[id]/usage/route.ts:26`.

## Method behavior [#method-behavior]

The handler excerpt preserves field validation, response envelopes, cookie changes, and exception branches. Values returned by service helpers retain their named response type above; for passthrough routes, the upstream response is authoritative.

### GET [#get]

```typescript
export async function GET(request: NextRequest, context: RouteContext) {
  const requestId = generateRequestId()
  const { id: rawTenantId } = await context.params
  const pathLabel = '/api/v1/tenants/[id]/usage'
  const requestLogger = createRequestLogger(request)
  const traceContext = extractTraceContext(request.headers)
  const span = tracer.startSpan('tenant.usage.fetch', traceContext, {
    'http.method': 'GET',
    'http.route': pathLabel,
    'request.id': requestId,
    'tenant.id': rawTenantId,
  })
  const stopTimer = apiRequestDuration.startTimer()
  const startedAt = Date.now()

  let statusCode = 200
  let tenantIdForLog = rawTenantId
  let rateLimitInfo: RateLimitMetadata | undefined
  let planForLog: string | undefined

  try {
    const tenantId = validateTenantId(rawTenantId)
    tenantIdForLog = tenantId
    tracer.setSpanAttributes(span.spanId, { 'tenant.id': tenantId })

    requestLogger.info('Fetching tenant usage', {
      requestId,
      tenantId,
    })

    const authContext = await resolveAuthContext(request)
    const usage = await getTenantUsage(authContext, tenantId)
    planForLog = usage?.plan ?? undefined

    const evaluation = await enforceRateLimit({
      context: authContext,
      tenantId,
      tenantPlan: usage?.plan as string | undefined,
    }) ?? { metadata: { limit: 0, remaining: 0 }, retryAfter: 0 }

    rateLimitInfo = {
      ...evaluation.metadata,
      retryAfter: evaluation.retryAfter,
    }

    tracer.setSpanAttributes(span.spanId, {
      'rate.limit.limit': evaluation.metadata?.limit ?? 0,
      'rate.limit.remaining': evaluation.metadata?.remaining ?? 0,
      'tenant.plan': usage?.plan as string | undefined ?? '',
    })

    if (evaluation.blocked) {
      throw new RateLimitExceededError(evaluation.retryAfter ?? 60)
    }

    tenantOperations.inc({ tenant_id: tenantId, operation_type: 'usage_read' })
    tracer.addSpanEvent(span.spanId, 'usage.fetched', { 'tenant.id': tenantId })

    requestLogger.info('Tenant usage fetched', {
      requestId,
      tenantId,
      metadata: { plan: usage?.plan as string | undefined },
    })

    const response = apiSuccess<UsageResponse>(usage as UsageResponse)
    return withRequestMetadata(response, requestId, rateLimitInfo)
  } catch (error) {
    const normalizedError =
      error instanceof ApiError
        ? error
        : new ApiError(500, 'INTERNAL_ERROR', 'Failed to get usage')

    statusCode = normalizedError.statusCode

    apiRequestErrors.inc({
      method: 'GET',
      path: pathLabel,
      tenant_id: tenantIdForLog,
      error_type: normalizedError.code,
    })

    tracer.recordError(span.spanId, normalizedError)

    requestLogger.error('Failed to fetch tenant usage', {
      requestId,
      tenantId: tenantIdForLog,
      error: normalizedError,
      metadata: { plan: planForLog },
    })

    const response = apiError(normalizedError, requestId)
    return withRequestMetadata(response, requestId, rateLimitInfo)
  } finally {
    stopTimer({
      method: 'GET',
      path: pathLabel,
      tenant_id: tenantIdForLog,
    })

    apiRequestCounter.inc({
      method: 'GET',
      path: pathLabel,
      tenant_id: tenantIdForLog,
    })

    const durationMs = Date.now() - startedAt
    requestLogger.logResponse('GET', pathLabel, statusCode, durationMs, {
      requestId,
      tenantId: tenantIdForLog,
      metadata: { plan: planForLog, rateLimit: rateLimitInfo },
    })

    tracer.setSpanAttributes(span.spanId, {
      'http.status_code': statusCode,
      'tenant.id': tenantIdForLog,
      'tenant.plan': planForLog ?? '',
    })

    tracer.endSpan(span.spanId)
  }
}
```

Source: `minds-ui/apps/api-service/app/api/v1/tenants/[id]/usage/route.ts:30`.

## Response and error branches [#response-and-error-branches]

Literal HTTP statuses in the route: 500. Shared management error classes are defined in [Errors and responses](/docs/api/platform/errors).

```typescript
new RateLimitExceededError(evaluation.retryAfter ?? 60)
```

Source: `minds-ui/apps/api-service/app/api/v1/tenants/[id]/usage/route.ts:82`.

```typescript
apiSuccess<UsageResponse>(usage as UsageResponse)
```

Source: `minds-ui/apps/api-service/app/api/v1/tenants/[id]/usage/route.ts:94`.

```typescript
new ApiError(500, 'INTERNAL_ERROR', 'Failed to get usage')
```

Source: `minds-ui/apps/api-service/app/api/v1/tenants/[id]/usage/route.ts:100`.

```typescript
tracer.recordError(span.spanId, normalizedError)
```

Source: `minds-ui/apps/api-service/app/api/v1/tenants/[id]/usage/route.ts:111`.

```typescript
apiError(normalizedError, requestId)
```

Source: `minds-ui/apps/api-service/app/api/v1/tenants/[id]/usage/route.ts:120`.

## Service dependencies [#service-dependencies]

* `minds-ui/apps/api-service/lib/api-response.ts`
* `minds-ui/apps/api-service/lib/validation.ts`
* `minds-ui/apps/api-service/types/usage.ts`
* `minds-ui/apps/api-service/types/errors.ts`
* `minds-ui/apps/api-service/lib/observability/logger.ts`
* `minds-ui/apps/api-service/lib/observability/tracer.ts`
* `minds-ui/apps/api-service/lib/services/auth-context.ts`
* `minds-ui/apps/api-service/lib/services/tenant-service.ts`
* `minds-ui/apps/api-service/lib/services/rate-limit.ts`
* `minds-ui/apps/api-service/lib/services/prometheus.ts`

## Evidence [#evidence]

Generated from the local source snapshot on 2026-09-12. This page documents implemented code and does not certify a deployed service, permissions configuration, or successful provider operation.

Source file: `minds-ui/apps/api-service/app/api/v1/tenants/[id]/usage/route.ts`. SHA-256: `0efd397a334e481339c83e0e59a0595754db750b437cfdfaebd2a9175efbc621`.
