Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions apps/docs/content/docs/execution/advanced.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,47 @@ Monitor your usage and billing in Settings → Subscription:
- **Usage Limits**: Plan limits with visual progress indicators
- **Billing Details**: Projected charges and minimum commitments
- **Plan Management**: Upgrade options and billing history

### Programmatic Rate Limits & Usage (API)

You can query your current API rate limits and usage summary using your API key.

Endpoint:

```text
GET /api/users/me/usage-limits
```

Authentication:

- Include your API key in the `X-API-Key` header.

Response (example):

```json
{
"success": true,
"rateLimit": {
"sync": { "isLimited": false, "limit": 10, "remaining": 10, "resetAt": "2025-09-08T22:51:55.999Z" },
"async": { "isLimited": false, "limit": 50, "remaining": 50, "resetAt": "2025-09-08T22:51:56.155Z" },
"authType": "api"
},
"usage": {
"currentPeriodCost": 12.34,
"limit": 100,
"plan": "pro"
}
}
```

Example:

```bash
curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" https://sim.ai/api/users/me/usage-limits
```

Notes:

- `currentPeriodCost` reflects usage in the current billing period.
- `limit` is derived from individual limits (Free/Pro) or pooled organization limits (Team/Enterprise).
- `plan` is the highest-priority active plan associated with your user.
79 changes: 0 additions & 79 deletions apps/sim/app/api/users/me/rate-limit/route.ts

This file was deleted.

74 changes: 74 additions & 0 deletions apps/sim/app/api/users/me/usage-limits/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { type NextRequest, NextResponse } from 'next/server'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { checkServerSideUsageLimits } from '@/lib/billing'
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
import { getEffectiveCurrentPeriodCost } from '@/lib/billing/core/usage'
import { createLogger } from '@/lib/logs/console/logger'
import { createErrorResponse } from '@/app/api/workflows/utils'
import { RateLimiter } from '@/services/queue'

const logger = createLogger('UsageLimitsAPI')

export async function GET(request: NextRequest) {
try {
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
if (!auth.success || !auth.userId) {
return createErrorResponse('Authentication required', 401)
}
const authenticatedUserId = auth.userId

// Rate limit info (sync + async), mirroring /users/me/rate-limit
const userSubscription = await getHighestPrioritySubscription(authenticatedUserId)
const rateLimiter = new RateLimiter()
const triggerType = auth.authType === 'api_key' ? 'api' : 'manual'
const [syncStatus, asyncStatus] = await Promise.all([
rateLimiter.getRateLimitStatusWithSubscription(
authenticatedUserId,
userSubscription,
triggerType,
false
),
rateLimiter.getRateLimitStatusWithSubscription(
authenticatedUserId,
userSubscription,
triggerType,
true
),
])

// Usage summary (current period cost + limit + plan)
const [usageCheck, effectiveCost] = await Promise.all([
checkServerSideUsageLimits(authenticatedUserId),
getEffectiveCurrentPeriodCost(authenticatedUserId),
])

const currentPeriodCost = effectiveCost

return NextResponse.json({
success: true,
rateLimit: {
sync: {
isLimited: syncStatus.remaining === 0,
limit: syncStatus.limit,
remaining: syncStatus.remaining,
resetAt: syncStatus.resetAt,
},
async: {
isLimited: asyncStatus.remaining === 0,
limit: asyncStatus.limit,
remaining: asyncStatus.remaining,
resetAt: asyncStatus.resetAt,
},
authType: triggerType,
},
usage: {
currentPeriodCost,
limit: usageCheck.limit,
plan: userSubscription?.plan || 'free',
},
})
} catch (error: any) {
logger.error('Error checking usage limits:', error)
return createErrorResponse(error.message || 'Failed to check usage limits', 500)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export function ExampleCommand({
case 'rate-limits': {
const baseUrlForRateLimit = baseEndpoint.split('/api/workflows/')[0]
return `curl -H "X-API-Key: ${apiKey}" \\
${baseUrlForRateLimit}/api/users/me/rate-limit`
${baseUrlForRateLimit}/api/users/me/usage-limits`
}

default:
Expand Down Expand Up @@ -119,7 +119,7 @@ export function ExampleCommand({
case 'rate-limits': {
const baseUrlForRateLimit = baseEndpoint.split('/api/workflows/')[0]
return `curl -H "X-API-Key: SIM_API_KEY" \\
${baseUrlForRateLimit}/api/users/me/rate-limit`
${baseUrlForRateLimit}/api/users/me/usage-limits`
}

default:
Expand Down
43 changes: 42 additions & 1 deletion apps/sim/lib/billing/core/usage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { eq } from 'drizzle-orm'
import { eq, inArray } from 'drizzle-orm'
import { getEmailSubject, renderUsageThresholdEmail } from '@/components/emails/render-email'
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
import {
Expand Down Expand Up @@ -490,6 +490,47 @@ export async function getTeamUsageLimits(organizationId: string): Promise<
}
}

/**
* Returns the effective current period usage cost for a user.
* - Free/Pro: user's own currentPeriodCost (fallback to totalCost)
* - Team/Enterprise: pooled sum of all members' currentPeriodCost within the organization
*/
export async function getEffectiveCurrentPeriodCost(userId: string): Promise<number> {
const subscription = await getHighestPrioritySubscription(userId)

// If no team/org subscription, return the user's own usage
if (!subscription || subscription.plan === 'free' || subscription.plan === 'pro') {
const rows = await db
.select({ current: userStats.currentPeriodCost })
.from(userStats)
.where(eq(userStats.userId, userId))
.limit(1)

if (rows.length === 0) return 0
return rows[0].current ? Number.parseFloat(rows[0].current.toString()) : 0
}

// Team/Enterprise: pooled usage across org members
const teamMembers = await db
.select({ userId: member.userId })
.from(member)
.where(eq(member.organizationId, subscription.referenceId))

if (teamMembers.length === 0) return 0

const memberIds = teamMembers.map((m) => m.userId)
const rows = await db
.select({ current: userStats.currentPeriodCost })
.from(userStats)
.where(inArray(userStats.userId, memberIds))

let pooled = 0
for (const r of rows) {
pooled += r.current ? Number.parseFloat(r.current.toString()) : 0
}
return pooled
}

/**
* Calculate billing projection based on current usage
*/
Expand Down