Skip to content

Commit d357280

Browse files
feat(usage-api): make external endpoint to query usage (#1285)
* feat(usage-api): make external endpoint to query usage * add docs * consolidate endpoints with rate-limits one * update docs * consolidate code * remove unused route
1 parent 5218dd4 commit d357280

File tree

5 files changed

+162
-82
lines changed
  • apps
    • docs/content/docs/execution
    • sim
      • app
        • api/users/me
        • workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command
      • lib/billing/core

5 files changed

+162
-82
lines changed

apps/docs/content/docs/execution/advanced.mdx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,47 @@ Monitor your usage and billing in Settings → Subscription:
212212
- **Usage Limits**: Plan limits with visual progress indicators
213213
- **Billing Details**: Projected charges and minimum commitments
214214
- **Plan Management**: Upgrade options and billing history
215+
216+
### Programmatic Rate Limits & Usage (API)
217+
218+
You can query your current API rate limits and usage summary using your API key.
219+
220+
Endpoint:
221+
222+
```text
223+
GET /api/users/me/usage-limits
224+
```
225+
226+
Authentication:
227+
228+
- Include your API key in the `X-API-Key` header.
229+
230+
Response (example):
231+
232+
```json
233+
{
234+
"success": true,
235+
"rateLimit": {
236+
"sync": { "isLimited": false, "limit": 10, "remaining": 10, "resetAt": "2025-09-08T22:51:55.999Z" },
237+
"async": { "isLimited": false, "limit": 50, "remaining": 50, "resetAt": "2025-09-08T22:51:56.155Z" },
238+
"authType": "api"
239+
},
240+
"usage": {
241+
"currentPeriodCost": 12.34,
242+
"limit": 100,
243+
"plan": "pro"
244+
}
245+
}
246+
```
247+
248+
Example:
249+
250+
```bash
251+
curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" https://sim.ai/api/users/me/usage-limits
252+
```
253+
254+
Notes:
255+
256+
- `currentPeriodCost` reflects usage in the current billing period.
257+
- `limit` is derived from individual limits (Free/Pro) or pooled organization limits (Team/Enterprise).
258+
- `plan` is the highest-priority active plan associated with your user.

apps/sim/app/api/users/me/rate-limit/route.ts

Lines changed: 0 additions & 79 deletions
This file was deleted.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { type NextRequest, NextResponse } from 'next/server'
2+
import { checkHybridAuth } from '@/lib/auth/hybrid'
3+
import { checkServerSideUsageLimits } from '@/lib/billing'
4+
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
5+
import { getEffectiveCurrentPeriodCost } from '@/lib/billing/core/usage'
6+
import { createLogger } from '@/lib/logs/console/logger'
7+
import { createErrorResponse } from '@/app/api/workflows/utils'
8+
import { RateLimiter } from '@/services/queue'
9+
10+
const logger = createLogger('UsageLimitsAPI')
11+
12+
export async function GET(request: NextRequest) {
13+
try {
14+
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
15+
if (!auth.success || !auth.userId) {
16+
return createErrorResponse('Authentication required', 401)
17+
}
18+
const authenticatedUserId = auth.userId
19+
20+
// Rate limit info (sync + async), mirroring /users/me/rate-limit
21+
const userSubscription = await getHighestPrioritySubscription(authenticatedUserId)
22+
const rateLimiter = new RateLimiter()
23+
const triggerType = auth.authType === 'api_key' ? 'api' : 'manual'
24+
const [syncStatus, asyncStatus] = await Promise.all([
25+
rateLimiter.getRateLimitStatusWithSubscription(
26+
authenticatedUserId,
27+
userSubscription,
28+
triggerType,
29+
false
30+
),
31+
rateLimiter.getRateLimitStatusWithSubscription(
32+
authenticatedUserId,
33+
userSubscription,
34+
triggerType,
35+
true
36+
),
37+
])
38+
39+
// Usage summary (current period cost + limit + plan)
40+
const [usageCheck, effectiveCost] = await Promise.all([
41+
checkServerSideUsageLimits(authenticatedUserId),
42+
getEffectiveCurrentPeriodCost(authenticatedUserId),
43+
])
44+
45+
const currentPeriodCost = effectiveCost
46+
47+
return NextResponse.json({
48+
success: true,
49+
rateLimit: {
50+
sync: {
51+
isLimited: syncStatus.remaining === 0,
52+
limit: syncStatus.limit,
53+
remaining: syncStatus.remaining,
54+
resetAt: syncStatus.resetAt,
55+
},
56+
async: {
57+
isLimited: asyncStatus.remaining === 0,
58+
limit: asyncStatus.limit,
59+
remaining: asyncStatus.remaining,
60+
resetAt: asyncStatus.resetAt,
61+
},
62+
authType: triggerType,
63+
},
64+
usage: {
65+
currentPeriodCost,
66+
limit: usageCheck.limit,
67+
plan: userSubscription?.plan || 'free',
68+
},
69+
})
70+
} catch (error: any) {
71+
logger.error('Error checking usage limits:', error)
72+
return createErrorResponse(error.message || 'Failed to check usage limits', 500)
73+
}
74+
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export function ExampleCommand({
7979
case 'rate-limits': {
8080
const baseUrlForRateLimit = baseEndpoint.split('/api/workflows/')[0]
8181
return `curl -H "X-API-Key: ${apiKey}" \\
82-
${baseUrlForRateLimit}/api/users/me/rate-limit`
82+
${baseUrlForRateLimit}/api/users/me/usage-limits`
8383
}
8484

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

125125
default:

apps/sim/lib/billing/core/usage.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { eq } from 'drizzle-orm'
1+
import { eq, inArray } from 'drizzle-orm'
22
import { getEmailSubject, renderUsageThresholdEmail } from '@/components/emails/render-email'
33
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
44
import {
@@ -490,6 +490,47 @@ export async function getTeamUsageLimits(organizationId: string): Promise<
490490
}
491491
}
492492

493+
/**
494+
* Returns the effective current period usage cost for a user.
495+
* - Free/Pro: user's own currentPeriodCost (fallback to totalCost)
496+
* - Team/Enterprise: pooled sum of all members' currentPeriodCost within the organization
497+
*/
498+
export async function getEffectiveCurrentPeriodCost(userId: string): Promise<number> {
499+
const subscription = await getHighestPrioritySubscription(userId)
500+
501+
// If no team/org subscription, return the user's own usage
502+
if (!subscription || subscription.plan === 'free' || subscription.plan === 'pro') {
503+
const rows = await db
504+
.select({ current: userStats.currentPeriodCost })
505+
.from(userStats)
506+
.where(eq(userStats.userId, userId))
507+
.limit(1)
508+
509+
if (rows.length === 0) return 0
510+
return rows[0].current ? Number.parseFloat(rows[0].current.toString()) : 0
511+
}
512+
513+
// Team/Enterprise: pooled usage across org members
514+
const teamMembers = await db
515+
.select({ userId: member.userId })
516+
.from(member)
517+
.where(eq(member.organizationId, subscription.referenceId))
518+
519+
if (teamMembers.length === 0) return 0
520+
521+
const memberIds = teamMembers.map((m) => m.userId)
522+
const rows = await db
523+
.select({ current: userStats.currentPeriodCost })
524+
.from(userStats)
525+
.where(inArray(userStats.userId, memberIds))
526+
527+
let pooled = 0
528+
for (const r of rows) {
529+
pooled += r.current ? Number.parseFloat(r.current.toString()) : 0
530+
}
531+
return pooled
532+
}
533+
493534
/**
494535
* Calculate billing projection based on current usage
495536
*/

0 commit comments

Comments
 (0)