Skip to content

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:

Authorization: Bearer <jwt_token>

REST Endpoints

Get Usage Limits

Returns current usage limits and remaining allowances for a user.

GET /usage/limits/{user_id}
Authorization: Bearer <token>

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.

GET /payment/status
Authorization: Bearer <token>

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