Subscription Access API Specification¶
Version: v1
Base URL: /api/v1
Last Updated: 2026-01-13
Overview¶
API endpoints for checking subscription status, usage limits, and free tier access controls.
Authentication¶
All API requests require authentication via Supabase JWT token in the Authorization header:
REST Endpoints¶
Get Usage Limits¶
Returns current usage limits and remaining allowances for a user.
Path Parameters: | Parameter | Type | Description | |-----------|------|-------------| | user_id | UUID | User identifier |
Response (200 OK) - Free User:
{
"subscription_status": "free",
"subscription_plan": null,
"messages": {
"limit": 50,
"remaining": 35,
"unlimited": false,
"resets_at": "midnight UTC"
},
"superpowers": {
"enabled": false,
"upgrade_message": "Upgrade to unlock superpowers"
}
}
Response (200 OK) - Subscribed User:
{
"subscription_status": "active",
"subscription_plan": "monthly",
"messages": {
"limit": null,
"remaining": null,
"unlimited": true,
"resets_at": null
},
"superpowers": {
"enabled": true,
"upgrade_message": null
}
}
Error Responses:
- 404 Not Found: User not found
- 401 Unauthorized: Missing or invalid token
- 500 Internal Server Error: Failed to get usage limits
Get Payment Status (Extended)¶
Returns payment status with free tier context.
Response (200 OK) - Free User:
{
"has_payment_method": false,
"subscription_status": "free",
"subscription_plan": null,
"subscription_id": null,
"current_period_end": null,
"cancel_at_period_end": false,
"trial_expires_at": null,
"credit_balance": 0,
"tier": "free",
"daily_messages_remaining": 35,
"superpowers_enabled": false,
"upgrade_benefits": [
"Unlimited messages",
"Access to superpowers (Gmail Mind Reader, Calendar Stress, etc.)",
"Priority support",
"Early access to new features"
]
}
Response (200 OK) - Subscribed User:
{
"has_payment_method": true,
"subscription_status": "active",
"subscription_plan": "monthly",
"subscription_id": "sub_xxxxx",
"current_period_end": "2026-02-13T00:00:00Z",
"cancel_at_period_end": false,
"trial_expires_at": null,
"credit_balance": 10000,
"tier": "monthly",
"daily_messages_remaining": null,
"superpowers_enabled": true,
"upgrade_benefits": null
}
Error Responses:
- 401 Unauthorized: Missing or invalid token
- 500 Internal Server Error: Failed to get payment status
Create Subscription Checkout¶
Creates a Stripe Checkout session for subscription.
POST /payment/checkout/subscription
Content-Type: application/json
Authorization: Bearer <token>
{
"plan": "monthly",
"success_url": "https://ikiro.ai/dashboard/billing?success=true",
"cancel_url": "https://ikiro.ai/dashboard/billing?canceled=true"
}
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | plan | string | Yes | "monthly" or "annual" | | success_url | string | No | Redirect URL on success | | cancel_url | string | No | Redirect URL on cancel |
Response (200 OK):
{
"session_id": "",
"url": "https://checkout.stripe.com/c/pay/xxxxx",
"publishable_key": "pk_live_xxxxx"
}
Error Responses:
- 400 Bad Request: Already has active subscription
- 401 Unauthorized: Missing or invalid token
- 503 Service Unavailable: Payment service not available
Internal Service API¶
These methods are used internally by the orchestrator and workflow engine.
SubscriptionAccessService¶
class SubscriptionAccessService:
"""Centralized subscription and access control checks."""
def is_subscribed(self, user: User) -> bool:
"""Check if user has active subscription."""
# Returns True for 'active' or 'trial' status
def check_message_limit(self, user: User) -> Tuple[bool, Optional[str], int]:
"""
Check if free user has messages remaining today.
Returns:
(allowed, error_code, remaining_count)
- remaining_count is -1 for unlimited (subscribed users)
"""
def can_use_superpower(
self,
user: User,
room_id: Optional[UUID] = None,
is_group: bool = False
) -> Tuple[bool, Optional[str]]:
"""
Check if user can use superpowers.
Rules:
- 1:1 chats: User must be subscribed
- Group chats: Group creator (room owner) must be subscribed
Returns:
(allowed, error_code)
"""
def record_message(self, user: User) -> None:
"""Record a message for rate limiting (free users only)."""
def get_usage_status(self, user: User) -> Dict[str, Any]:
"""Get comprehensive usage status for API response."""
RateLimitService¶
class RateLimitService:
"""Rate limiting for free tier."""
def check_free_tier_daily_limit(
self,
user_id: UUID,
limit: int = 50
) -> Tuple[bool, int]:
"""
Check if free user has daily messages remaining.
Args:
user_id: User UUID
limit: Max messages per day (default 50)
Returns:
(allowed, remaining_count)
"""
def record_free_tier_message(self, user_id: UUID) -> None:
"""Record a message for free tier user."""
Error Codes¶
| Code | HTTP Status | Description |
|---|---|---|
message_limit_reached |
200* | Daily message limit reached (returns upgrade message) |
superpower_requires_subscription |
200* | Superpower blocked - user needs subscription |
group_owner_not_subscribed |
200* | Superpower blocked - group owner needs subscription |
user_not_found |
404 | User ID not found in database |
unauthorized |
401 | Invalid or missing auth token |
*Note: Access control blocks return 200 with in-persona messages, not error status codes.
Rate Limits¶
| Endpoint | Limit | Window |
|---|---|---|
/payment/checkout/* |
3 requests | 1 minute |
/usage/* |
100 requests | 1 minute |
/payment/status |
100 requests | 1 minute |
Feature Flags¶
The free tier system is controlled by these feature flags:
| Flag | Default | Description |
|---|---|---|
free_tier.enabled |
true |
Master toggle for free tier restrictions |
free_tier.daily_message_limit |
50 |
Messages per day for free users |
free_tier.superpowers_disabled |
true |
Disable superpowers for free users in 1:1 |
Flags can be controlled via LaunchDarkly for gradual rollout.
Webhook Events¶
Subscription status is automatically updated via Stripe webhooks:
| Event | Handler | Status Update |
|---|---|---|
customer.subscription.created |
handle_subscription_created() |
→ active |
customer.subscription.updated |
handle_subscription_updated() |
Updates based on Stripe |
customer.subscription.deleted |
handle_subscription_deleted() |
→ canceled |
invoice.payment_failed |
handle_invoice_payment_failed() |
→ past_due |
Related Documentation¶
- Free Tier Architecture
- Payment Routes:
app/api/payment_routes.py - Usage Routes:
app/api/usage_routes.py