# /api/v1/tenants (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



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                        | POST, GET                                                                      |
| Path                           | `/api/v1/tenants`                                                              |
| Path parameters                | None                                                                           |
| 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.

### CreateTenantRequest [#createtenantrequest]

```typescript
CreateTenantRequest = z.object({
  org_id: z.string(),
  project_id: z.string().optional(),
  name: z.string().min(1).max(100),
  slug: z
    .string()
    .regex(/^[a-z0-9-]+$/)
    .min(3)
    .max(63)
    .optional(), // Auto-generated if omitted
  plan: TenantPlan,
  product_type: ProductType.default('legacy').optional(),
  region: TenantRegion.optional().default('us-east-1'),
  config: TenantConfig.optional(),

  // Modular Services Extension (PRD §5.1)
  service_type: z
    .string()
    .optional(), // Specific service type (e.g., "kv", "graph", "memory_suite")
  bundle: z
    .string()
    .optional(), // Bundle name (e.g., "full_akasha", "kv_graph", "memory_suite")
  service_config: z
    .record(z.unknown())
    .optional(), // Service-specific configuration overrides
})
```

Source: `minds-ui/apps/api-service/types/tenant.ts:124`.

### CreateTenantRequest [#createtenantrequest-1]

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

Source: `minds-ui/apps/api-service/types/tenant.ts:151`.

### CreateTenantResponse [#createtenantresponse]

```typescript
CreateTenantResponse = z.object({
  tenant_id: z.string(),
  slug: z.string(),
  endpoint: z.string().url(),
  status: z.literal('provisioning'),
  created_at: z.string().datetime(),
  estimated_ready_in: z.number().int(), // seconds
})
```

Source: `minds-ui/apps/api-service/types/tenant.ts:158`.

### CreateTenantResponse [#createtenantresponse-1]

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

Source: `minds-ui/apps/api-service/types/tenant.ts:167`.

### ListTenantsQuery [#listtenantsquery]

```typescript
ListTenantsQuery = z.object({
  org_id: z.string().optional(),
  status: TenantStatus.optional(),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  offset: z.coerce.number().int().min(0).default(0),
})
```

Source: `minds-ui/apps/api-service/types/tenant.ts:190`.

### ListTenantsQuery [#listtenantsquery-1]

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

Source: `minds-ui/apps/api-service/types/tenant.ts:197`.

### ListTenantsResponse [#listtenantsresponse]

```typescript
ListTenantsResponse = z.object({
  tenants: z.array(
    Tenant.pick({
      id: true,
      org_id: true,
      project_id: true,
      name: true,
      slug: true,
      endpoint: true,
      plan: true,
      product_type: true,
      plan_memory_limit: true,
      plan_retrieval_quota: true,
      plan_graph_enabled: true,
      plan_node_partitioning: true,
      status: true,
      created_at: true,
    }),
  ),
  pagination: z.object({
    total: z.number().int(),
    limit: z.number().int(),
    offset: z.number().int(),
  }),
})
```

Source: `minds-ui/apps/api-service/types/tenant.ts:204`.

### ListTenantsResponse [#listtenantsresponse-1]

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

Source: `minds-ui/apps/api-service/types/tenant.ts:230`.

## 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.

### POST [#post]

```typescript
export async function POST(request: NextRequest) {
  const requestId = generateRequestId()
  let rateLimitInfo: RateLimitMetadata | undefined

  try {
    const authContext = await resolveAuthContext(request)

    const idempotencyKey = getIdempotencyKey(request)
    if (idempotencyKey) {
      const cached = await checkIdempotency(idempotencyKey)
      if (cached.found) {
        const evaluation = await enforceRateLimit({
          context: authContext,
        })
        rateLimitInfo = {
          ...evaluation.metadata,
          retryAfter: evaluation.retryAfter,
        }
        if (evaluation.blocked) {
          throw new RateLimitExceededError(evaluation.retryAfter ?? 60)
        }
        const response = apiSuccess(cached.response, cached.statusCode)
        return withRequestMetadata(response, requestId, rateLimitInfo)
      }
    }

    const body = await validateBody(request, CreateTenantRequest)

    const evaluation = await enforceRateLimit({
      context: authContext,
      tenantPlan: body.plan,
    })

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

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

    const tenant = await createTenant(authContext, body)

    if (idempotencyKey) {
      await storeIdempotencyResult(
        idempotencyKey,
        tenant,
        202,
        '/api/v1/tenants',
        'POST',
      )
    }

    const response = apiAccepted<CreateTenantResponse>(tenant)
    return withRequestMetadata(response, requestId, rateLimitInfo)
  } catch (error) {
    if (error instanceof ApiError) {
      const response = apiError(error, requestId)
      return withRequestMetadata(response, requestId, rateLimitInfo)
    }
    console.error('Failed to create tenant:', error)
    const response = apiError(
      new ApiError(500, 'INTERNAL_ERROR', 'Failed to create tenant'),
      requestId,
    )
    return withRequestMetadata(response, requestId, rateLimitInfo)
  }
}
```

Source: `minds-ui/apps/api-service/app/api/v1/tenants/route.ts:32`.

### GET [#get]

```typescript
export async function GET(request: NextRequest) {
  const requestId = generateRequestId()
  let rateLimitInfo: RateLimitMetadata | undefined

  try {
    console.log('[DEBUG] Starting GET /api/v1/tenants')
    const authContext = await resolveAuthContext(request)
    console.log('[DEBUG] Auth context:', { type: authContext.type, orgIds: authContext.orgIds })
    
    const query = validateQuery(
      request.nextUrl.searchParams,
      ListTenantsQuery,
    )
    console.log('[DEBUG] Query:', query)

    const evaluation = await enforceRateLimit({
      context: authContext,
    })

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

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

    console.log('[DEBUG] Calling listTenants with:', {
      orgId: query.org_id,
      status: query.status,
      limit: query.limit,
      offset: query.offset,
    })
    
    const result = await listTenants(authContext, {
      orgId: query.org_id,
      status: query.status,
      limit: query.limit,
      offset: query.offset,
    })
    
    console.log('[DEBUG] listTenants result:', result)

    const response = apiSuccess<ListTenantsResponse>(result)
    return withRequestMetadata(response, requestId, rateLimitInfo)
  } catch (error) {
    console.error('[DEBUG] Error in GET /api/v1/tenants:', error)
    if (error instanceof ApiError) {
      const response = apiError(error, requestId)
      return withRequestMetadata(response, requestId, rateLimitInfo)
    }
    console.error('Failed to list tenants:', error)
    const response = apiError(
      new ApiError(500, 'INTERNAL_ERROR', 'Failed to list tenants'),
      requestId,
    )
    return withRequestMetadata(response, requestId, rateLimitInfo)
  }
}
```

Source: `minds-ui/apps/api-service/app/api/v1/tenants/route.ts:102`.

## 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/route.ts:51`.

```typescript
apiSuccess(cached.response, cached.statusCode)
```

Source: `minds-ui/apps/api-service/app/api/v1/tenants/route.ts:53`.

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

Source: `minds-ui/apps/api-service/app/api/v1/tenants/route.ts:71`.

```typescript
apiAccepted<CreateTenantResponse>(tenant)
```

Source: `minds-ui/apps/api-service/app/api/v1/tenants/route.ts:86`.

```typescript
apiError(error, requestId)
```

Source: `minds-ui/apps/api-service/app/api/v1/tenants/route.ts:90`.

```typescript
apiError(
      new ApiError(500, 'INTERNAL_ERROR', 'Failed to create tenant'),
      requestId,
    )
```

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

```typescript
new ApiError(500, 'INTERNAL_ERROR', 'Failed to create tenant')
```

Source: `minds-ui/apps/api-service/app/api/v1/tenants/route.ts:95`.

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

Source: `minds-ui/apps/api-service/app/api/v1/tenants/route.ts:127`.

```typescript
apiSuccess<ListTenantsResponse>(result)
```

Source: `minds-ui/apps/api-service/app/api/v1/tenants/route.ts:146`.

```typescript
apiError(error, requestId)
```

Source: `minds-ui/apps/api-service/app/api/v1/tenants/route.ts:151`.

```typescript
apiError(
      new ApiError(500, 'INTERNAL_ERROR', 'Failed to list tenants'),
      requestId,
    )
```

Source: `minds-ui/apps/api-service/app/api/v1/tenants/route.ts:155`.

```typescript
new ApiError(500, 'INTERNAL_ERROR', 'Failed to list tenants')
```

Source: `minds-ui/apps/api-service/app/api/v1/tenants/route.ts:156`.

## 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/lib/idempotency.ts`
* `minds-ui/apps/api-service/types/tenant.ts`
* `minds-ui/apps/api-service/types/errors.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`

## 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/route.ts`. SHA-256: `69ec7f28850baf1f871da7b09242897c1932e8fef8ff97a512ef934173d56bcd`.
