openapi: 3.0.3 info: title: FlatRender Identity Service (internal) version: 1.0.0 description: | Internal API for the Identity Service. Owned by .NET service. Handles users, tenants, plans, payments, MFA, sessions. Called by the Gateway and by other services via service tokens. servers: - url: http://identity-svc.internal/v1 security: - BearerAuth: [] - ServiceToken: [] # Common types are referenced from ../common/types.yaml # In production this is bundled via openapi-merge or similar. tags: - name: Auth - name: Users - name: Tenants - name: Plans - name: Payments - name: Discounts - name: ApiKeys - name: Webhooks - name: Gamification - name: Admin paths: # ===================== AUTH ===================== /auth/register: post: tags: [Auth] summary: Register a new user (email/password) requestBody: required: true content: application/json: schema: type: object required: [tenant_slug, password] properties: tenant_slug: { type: string } email: { type: string, format: email } phone_number: { type: string } password: { type: string, minLength: 8 } full_name: { type: string } affiliate_code: { type: string } accept_terms: { type: boolean } responses: '201': description: Created (verification email/SMS sent) content: application/json: schema: type: object properties: user_id: { type: string, format: uuid } verification_required: { type: boolean } /auth/login: post: tags: [Auth] summary: Login with email/phone + password requestBody: required: true content: application/json: schema: type: object required: [tenant_slug, password] properties: tenant_slug: { type: string } email: { type: string } phone_number: { type: string } password: { type: string } device_id: { type: string } device_name: { type: string } responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/AuthTokens' '401': { description: Invalid credentials } '403': { description: MFA required, returns mfa_token } /auth/login/oauth/{provider}: post: tags: [Auth] summary: OAuth login (google/telegram) parameters: - name: provider in: path required: true schema: { type: string, enum: [google, telegram] } requestBody: required: true content: application/json: schema: type: object required: [tenant_slug, code] properties: tenant_slug: { type: string } code: { type: string } redirect_uri: { type: string } responses: '200': description: OK content: application/json: schema: { $ref: '#/components/schemas/AuthTokens' } /auth/refresh: post: tags: [Auth] summary: Exchange refresh token for new access token requestBody: required: true content: application/json: schema: type: object required: [refresh_token] properties: refresh_token: { type: string } responses: '200': content: application/json: schema: { $ref: '#/components/schemas/AuthTokens' } /auth/logout: post: tags: [Auth] summary: Revoke current session responses: '204': { description: Logged out } /auth/sessions: get: tags: [Auth] summary: List active sessions for current user responses: '200': content: application/json: schema: type: object properties: sessions: type: array items: { $ref: '#/components/schemas/Session' } /auth/sessions/{session_id}: delete: tags: [Auth] summary: Revoke a specific session parameters: - { name: session_id, in: path, required: true, schema: { type: string, format: uuid } } responses: '204': { description: Revoked } /auth/verify/email: post: tags: [Auth] summary: Verify email via OTP code requestBody: required: true content: application/json: schema: type: object required: [token, code] properties: token: { type: string } code: { type: string } responses: '200': { description: Verified } /auth/verify/phone: post: tags: [Auth] summary: Verify phone via OTP code requestBody: required: true content: application/json: schema: type: object required: [token, code] properties: token: { type: string } code: { type: string } responses: '200': { description: Verified } /auth/password/reset/request: post: tags: [Auth] summary: Request password reset (email/SMS) requestBody: required: true content: application/json: schema: type: object required: [tenant_slug] properties: tenant_slug: { type: string } email: { type: string } phone_number: { type: string } responses: '202': { description: Sent if account exists } /auth/password/reset/confirm: post: tags: [Auth] summary: Reset password with token requestBody: required: true content: application/json: schema: type: object required: [token, new_password] properties: token: { type: string } new_password: { type: string, minLength: 8 } responses: '200': { description: OK } /auth/password/change: post: tags: [Auth] summary: Change password (logged-in) requestBody: required: true content: application/json: schema: type: object required: [current_password, new_password] properties: current_password: { type: string } new_password: { type: string, minLength: 8 } responses: '204': { description: Changed } # MFA /auth/mfa/setup: post: tags: [Auth] summary: Initiate MFA setup (returns TOTP secret/QR) requestBody: required: true content: application/json: schema: type: object required: [factor_type] properties: factor_type: { type: string, enum: [TOTP, SMS] } label: { type: string } responses: '200': content: application/json: schema: type: object properties: factor_id: { type: string, format: uuid } secret: { type: string } qr_code_url: { type: string } recovery_codes: { type: array, items: { type: string } } /auth/mfa/verify: post: tags: [Auth] summary: Verify MFA factor with first code requestBody: required: true content: application/json: schema: type: object required: [factor_id, code] properties: factor_id: { type: string, format: uuid } code: { type: string } responses: '200': { description: Verified } /auth/mfa/challenge: post: tags: [Auth] summary: Complete login after MFA prompt requestBody: required: true content: application/json: schema: type: object required: [mfa_token, code] properties: mfa_token: { type: string } code: { type: string } responses: '200': content: application/json: schema: { $ref: '#/components/schemas/AuthTokens' } # Push subscriptions (PWA) /auth/push/subscribe: post: tags: [Auth] summary: Register browser Web Push subscription requestBody: required: true content: application/json: schema: type: object required: [endpoint, keys] properties: endpoint: { type: string } keys: type: object required: [p256dh, auth] properties: p256dh: { type: string } auth: { type: string } user_agent: { type: string } responses: '201': { description: Created } /auth/push/unsubscribe: post: tags: [Auth] summary: Remove a push subscription requestBody: required: true content: application/json: schema: type: object properties: endpoint: { type: string } responses: '204': { description: Removed } # ===================== USERS ===================== /users/me: get: tags: [Users] summary: Current user profile responses: '200': content: application/json: schema: { $ref: '#/components/schemas/User' } patch: tags: [Users] summary: Update profile requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/UserUpdate' } responses: '200': content: application/json: schema: { $ref: '#/components/schemas/User' } /users/me/balance: get: tags: [Users] summary: Get current balance + affiliate balance responses: '200': content: application/json: schema: type: object properties: balance_minor: { type: integer, format: int64 } affiliate_balance_minor: { type: integer, format: int64 } currency: { type: string } daily_remain_render_count: { type: integer } parallel_rendering_ceiling: { type: integer } /users/me/avatar: post: tags: [Users] summary: Pick avatar (from library) or upload custom requestBody: required: true content: application/json: schema: type: object properties: avatar_id: { type: string, format: uuid } avatar_url: { type: string } responses: '200': { description: Updated } /users/{user_id}: get: tags: [Users] summary: (Admin) Get any user parameters: - { name: user_id, in: path, required: true, schema: { type: string, format: uuid } } responses: '200': content: application/json: schema: { $ref: '#/components/schemas/User' } /users: get: tags: [Users] summary: (Admin) Search users parameters: - { name: q, in: query, schema: { type: string } } - { name: tenant_id, in: query, schema: { type: string, format: uuid } } - { name: page, in: query, schema: { type: integer, default: 1 } } - { name: page_size, in: query, schema: { type: integer, default: 20 } } responses: '200': content: application/json: schema: type: object properties: data: { type: array, items: { $ref: '#/components/schemas/User' } } meta: { $ref: '#/components/schemas/PaginationMeta' } /users/{user_id}/ban: post: tags: [Admin] summary: Ban a user parameters: - { name: user_id, in: path, required: true, schema: { type: string, format: uuid } } requestBody: required: true content: application/json: schema: type: object required: [reason] properties: reason: { type: string } unblock_date: { type: string, format: date-time } responses: '204': { description: Banned } # ===================== TENANTS ===================== /tenants: get: tags: [Tenants] summary: (Admin) List tenants responses: '200': content: application/json: schema: type: object properties: data: { type: array, items: { $ref: '#/components/schemas/Tenant' } } meta: { $ref: '#/components/schemas/PaginationMeta' } post: tags: [Tenants] summary: Create a tenant (Admin or signup flow) requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/TenantCreate' } responses: '201': content: application/json: schema: { $ref: '#/components/schemas/Tenant' } /tenants/by-slug/{slug}: get: tags: [Tenants] summary: Resolve tenant by slug/domain (used by Gateway) parameters: - { name: slug, in: path, required: true, schema: { type: string } } responses: '200': content: application/json: schema: { $ref: '#/components/schemas/Tenant' } /tenants/{tenant_id}: get: tags: [Tenants] parameters: - { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } } responses: '200': content: application/json: schema: { $ref: '#/components/schemas/Tenant' } patch: tags: [Tenants] summary: Update tenant settings/contact parameters: - { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } } requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/TenantUpdate' } responses: '200': content: application/json: schema: { $ref: '#/components/schemas/Tenant' } /tenants/{tenant_id}/branding: get: tags: [Tenants] parameters: - { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } } responses: '200': content: application/json: schema: { $ref: '#/components/schemas/TenantBranding' } put: tags: [Tenants] parameters: - { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } } requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/TenantBranding' } responses: '200': content: application/json: schema: { $ref: '#/components/schemas/TenantBranding' } /tenants/{tenant_id}/domains/verify: post: tags: [Tenants] summary: Start domain verification (returns DNS challenge) parameters: - { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } } requestBody: required: true content: application/json: schema: type: object required: [domain] properties: domain: { type: string } method: { type: string, enum: [DNS_TXT, HTTP_FILE], default: DNS_TXT } responses: '200': content: application/json: schema: type: object properties: verification_id: { type: string, format: uuid } challenge_record: { type: string } expires_at: { type: string, format: date-time } /tenants/{tenant_id}/usage: get: tags: [Tenants] summary: Usage time-series for billing/dashboard parameters: - { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } } - { name: from, in: query, schema: { type: string, format: date } } - { name: to, in: query, schema: { type: string, format: date } } responses: '200': content: application/json: schema: type: object properties: data: type: array items: { $ref: '#/components/schemas/TenantUsageDay' } # ===================== API KEYS ===================== /tenants/{tenant_id}/api-keys: get: tags: [ApiKeys] parameters: - { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } } responses: '200': content: application/json: schema: type: object properties: data: type: array items: { $ref: '#/components/schemas/ApiKey' } post: tags: [ApiKeys] summary: Create new API key (full secret returned ONCE) parameters: - { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } } requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/ApiKeyCreate' } responses: '201': content: application/json: schema: allOf: - $ref: '#/components/schemas/ApiKey' - type: object properties: secret_key: type: string description: Shown only on creation. Format fr_live_xxx... or fr_test_xxx... /tenants/{tenant_id}/api-keys/{api_key_id}: delete: tags: [ApiKeys] summary: Revoke API key parameters: - { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } } - { name: api_key_id, in: path, required: true, schema: { type: string, format: uuid } } requestBody: content: application/json: schema: type: object properties: reason: { type: string } responses: '204': { description: Revoked } /api-keys/validate: post: tags: [ApiKeys] summary: (Internal) Validate API key + signature (called by Gateway) requestBody: required: true content: application/json: schema: type: object required: [key_prefix, key_hash] properties: key_prefix: { type: string } key_hash: { type: string } ip_address: { type: string } responses: '200': content: application/json: schema: type: object properties: valid: { type: boolean } tenant_id: { type: string, format: uuid } scopes: { type: array, items: { type: string } } rate_limit_rpm: { type: integer } # ===================== WEBHOOKS ===================== /tenants/{tenant_id}/webhooks: get: tags: [Webhooks] parameters: - { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } } responses: '200': content: application/json: schema: type: object properties: data: { type: array, items: { $ref: '#/components/schemas/Webhook' } } post: tags: [Webhooks] parameters: - { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } } requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/WebhookCreate' } responses: '201': content: application/json: schema: { $ref: '#/components/schemas/Webhook' } /tenants/{tenant_id}/webhooks/{webhook_id}: delete: tags: [Webhooks] parameters: - { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } } - { name: webhook_id, in: path, required: true, schema: { type: string, format: uuid } } responses: '204': { description: Deleted } /tenants/{tenant_id}/webhooks/{webhook_id}/deliveries: get: tags: [Webhooks] summary: Recent delivery attempts parameters: - { name: tenant_id, in: path, required: true, schema: { type: string, format: uuid } } - { name: webhook_id, in: path, required: true, schema: { type: string, format: uuid } } responses: '200': content: application/json: schema: type: object properties: data: type: array items: { $ref: '#/components/schemas/WebhookDelivery' } # ===================== PLANS ===================== /plans: get: tags: [Plans] summary: List active plans (tenant-scoped via auth) parameters: - { name: scope, in: query, schema: { type: string, enum: [User, Tenant] } } responses: '200': content: application/json: schema: type: object properties: data: { type: array, items: { $ref: '#/components/schemas/Plan' } } /plans/{plan_id}: get: tags: [Plans] parameters: - { name: plan_id, in: path, required: true, schema: { type: string, format: uuid } } responses: '200': content: application/json: schema: { $ref: '#/components/schemas/Plan' } /users/me/plan: get: tags: [Plans] summary: Current active plan responses: '200': content: application/json: schema: { $ref: '#/components/schemas/UserPlan' } /users/me/plan/purchase: post: tags: [Plans] summary: Start purchase flow (returns payment redirect URL) requestBody: required: true content: application/json: schema: type: object required: [plan_id] properties: plan_id: { type: string, format: uuid } gateway: { type: string, enum: [ZarinPal, IdPay, Bazaar, Stripe, Balance] } discount_code: { type: string } responses: '200': content: application/json: schema: type: object properties: payment_id: { type: string, format: uuid } redirect_url: { type: string } # ===================== PAYMENTS ===================== /payments: get: tags: [Payments] summary: List user's payments parameters: - { name: page, in: query, schema: { type: integer } } responses: '200': content: application/json: schema: type: object properties: data: { type: array, items: { $ref: '#/components/schemas/Payment' } } meta: { $ref: '#/components/schemas/PaginationMeta' } /payments/{payment_id}: get: tags: [Payments] parameters: - { name: payment_id, in: path, required: true, schema: { type: string, format: uuid } } responses: '200': content: application/json: schema: { $ref: '#/components/schemas/Payment' } /payments/callback/zarinpal: get: tags: [Payments] summary: ZarinPal callback (verifies payment) parameters: - { name: Authority, in: query, schema: { type: string } } - { name: Status, in: query, schema: { type: string } } responses: '302': { description: Redirect to UI } /payments/callback/stripe: post: tags: [Payments] summary: Stripe webhook responses: '200': { description: OK } /payments/refund: post: tags: [Payments] summary: (Internal) Issue refund (called by render service on failure) security: [ServiceToken: []] requestBody: required: true content: application/json: schema: type: object required: [payment_id, reason] properties: payment_id: { type: string, format: uuid } amount_minor: { type: integer, format: int64 } reason: { type: string } refund_to: { type: string, enum: [Balance, OriginalMethod, Plan] } responses: '200': content: application/json: schema: type: object properties: refund_id: { type: string, format: uuid } status: { type: string } # ===================== DISCOUNTS ===================== /discounts/validate: post: tags: [Discounts] summary: Validate a discount code requestBody: required: true content: application/json: schema: type: object required: [code] properties: code: { type: string } plan_id: { type: string, format: uuid } responses: '200': content: application/json: schema: type: object properties: valid: { type: boolean } discount_minor: { type: integer, format: int64 } kind: { type: string } value: { type: number } /discounts: get: tags: [Discounts] summary: (Admin) List discounts responses: '200': { description: OK } post: tags: [Discounts] summary: (Admin) Create discount requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/DiscountCreate' } responses: '201': { description: Created } # ===================== GAMIFICATION ===================== /quests: get: tags: [Gamification] summary: Active quests for current user responses: '200': content: application/json: schema: type: object properties: data: { type: array, items: { $ref: '#/components/schemas/Quest' } } /quests/{quest_id}/claim: post: tags: [Gamification] summary: Claim a completed quest prize parameters: - { name: quest_id, in: path, required: true, schema: { type: string, format: uuid } } responses: '200': { description: Claimed } /gifts/earned: get: tags: [Gamification] summary: List earned gifts not yet used responses: '200': content: application/json: schema: type: object properties: data: { type: array, items: { $ref: '#/components/schemas/EarnedGift' } } /gifts/earned/{earned_gift_id}/use: post: tags: [Gamification] summary: Claim/use an earned gift parameters: - { name: earned_gift_id, in: path, required: true, schema: { type: string, format: uuid } } responses: '200': { description: Used } components: securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: JWT ServiceToken: type: http scheme: bearer schemas: PaginationMeta: type: object properties: page: { type: integer } page_size: { type: integer } total: { type: integer } has_more: { type: boolean } AuthTokens: type: object required: [access_token, refresh_token, expires_in] properties: access_token: { type: string } refresh_token: { type: string } token_type: { type: string, enum: [Bearer] } expires_in: { type: integer, description: seconds } user: { $ref: '#/components/schemas/User' } tenant: { $ref: '#/components/schemas/Tenant' } User: type: object properties: id: { type: string, format: uuid } tenant_id: { type: string, format: uuid } email: { type: string, nullable: true } email_verified: { type: boolean } phone_number: { type: string, nullable: true } phone_verified: { type: boolean } full_name: { type: string, nullable: true } avatar_url: { type: string, nullable: true } is_admin: { type: boolean } is_tenant_admin: { type: boolean } register_mode: { type: string } last_active_date: { type: string, format: date-time } balance_minor: { type: integer, format: int64 } affiliate_balance_minor: { type: integer, format: int64 } loyalty_score: { type: integer } daily_remain_render_count: { type: integer } max_daily_render_count: { type: integer } parallel_rendering_ceiling: { type: integer } used_storage_bytes: { type: integer, format: int64 } register_date: { type: string, format: date-time } UserUpdate: type: object properties: full_name: { type: string } slogan: { type: string } about_me: { type: string } company_name: { type: string } website_name: { type: string } birth_date: { type: string, format: date } gender: { type: string, enum: [Male, Female, Other, PreferNotToSay] } email_tell_me: { type: boolean } sms_tell_me: { type: boolean } push_tell_me: { type: boolean } telegram_tell_me: { type: boolean } Session: type: object properties: id: { type: string, format: uuid } device_name: { type: string } user_agent: { type: string } ip_address: { type: string } issued_at: { type: string, format: date-time } last_used_at: { type: string, format: date-time } is_current: { type: boolean } Tenant: type: object properties: id: { type: string, format: uuid } slug: { type: string } name: { type: string } kind: { type: string, enum: [Internal, Reseller, Enterprise] } status: { type: string, enum: [Active, Trial, Suspended, Cancelled] } custom_domain: { type: string, nullable: true } domain_verified: { type: boolean } contact_email: { type: string } max_users: { type: integer, nullable: true } max_storage_gb: { type: integer, nullable: true } monthly_render_qty: { type: integer, nullable: true } trial_ends_at: { type: string, format: date-time, nullable: true } created_at: { type: string, format: date-time } TenantCreate: type: object required: [slug, name, contact_email] properties: slug: { type: string } name: { type: string } kind: { type: string, enum: [Reseller, Enterprise] } contact_name: { type: string } contact_email: { type: string, format: email } contact_phone: { type: string } TenantUpdate: type: object properties: name: { type: string } contact_name: { type: string } contact_email: { type: string } contact_phone: { type: string } billing_email: { type: string } allowed_origins: { type: array, items: { type: string } } TenantBranding: type: object properties: display_name: { type: string } logo_url: { type: string } logo_dark_url: { type: string } favicon_url: { type: string } og_image_url: { type: string } primary_color: { type: string } secondary_color: { type: string } accent_color: { type: string } background_color: { type: string } font_family: { type: string } email_from_name: { type: string } email_from_address: { type: string } email_reply_to: { type: string } email_footer_html: { type: string } support_url: { type: string } terms_url: { type: string } privacy_url: { type: string } embed_enabled: { type: boolean } embed_allowed_hosts: { type: array, items: { type: string } } watermark_text: { type: string } watermark_image_url: { type: string } watermark_enabled: { type: boolean } TenantUsageDay: type: object properties: usage_date: { type: string, format: date } renders_completed: { type: integer } render_seconds: { type: integer, format: int64 } storage_bytes: { type: integer, format: int64 } api_calls: { type: integer, format: int64 } active_users: { type: integer } amount_billed_minor: { type: integer, format: int64 } billing_currency: { type: string } billing_status: { type: string } ApiKey: type: object properties: id: { type: string, format: uuid } tenant_id: { type: string, format: uuid } name: { type: string } environment: { type: string, enum: [Live, Test] } key_prefix: { type: string } last4: { type: string } scopes: { type: array, items: { type: string } } allowed_ips: { type: array, items: { type: string } } rate_limit_rpm: { type: integer } is_active: { type: boolean } expires_at: { type: string, format: date-time, nullable: true } last_used_at: { type: string, format: date-time, nullable: true } usage_count: { type: integer, format: int64 } created_at: { type: string, format: date-time } ApiKeyCreate: type: object required: [name, scopes] properties: name: { type: string } environment: { type: string, enum: [Live, Test] } scopes: type: array items: type: string enum: - renders:create - renders:read - renders:cancel - projects:read - projects:write - templates:read - exports:read - exports:download - users:read - users:write - webhooks:manage allowed_ips: { type: array, items: { type: string } } rate_limit_rpm: { type: integer } expires_at: { type: string, format: date-time, nullable: true } Webhook: type: object properties: id: { type: string, format: uuid } name: { type: string } url: { type: string } events: { type: array, items: { type: string } } is_active: { type: boolean } last_triggered_at: { type: string, format: date-time, nullable: true } last_status_code: { type: integer, nullable: true } consecutive_failures: { type: integer } created_at: { type: string, format: date-time } WebhookCreate: type: object required: [name, url, events] properties: name: { type: string } url: { type: string, format: uri } events: type: array items: { type: string } WebhookDelivery: type: object properties: id: { type: string, format: uuid } event_type: { type: string } request_url: { type: string } response_status: { type: integer, nullable: true } response_body: { type: string, nullable: true } duration_ms: { type: integer } attempt: { type: integer } succeeded: { type: boolean } error_message: { type: string, nullable: true } delivered_at: { type: string, format: date-time, nullable: true } created_at: { type: string, format: date-time } Plan: type: object properties: id: { type: string, format: uuid } code: { type: string } name: { type: string } description: { type: string } price_minor: { type: integer, format: int64 } before_price_minor: { type: integer, format: int64, nullable: true } currency: { type: string } billing_period: { type: string } seconds_charge: { type: integer } monthly_renders_quota: { type: integer, nullable: true } storage_gb: { type: integer } parallel_renders: { type: integer } max_resolution: { type: string } render_speed_factor: { type: number } icon: { type: string } is_featured: { type: boolean } features: { type: object, additionalProperties: true } UserPlan: type: object properties: id: { type: string, format: uuid } plan_id: { type: string, format: uuid } plan_code: { type: string } plan_name: { type: string } initial_seconds_charge: { type: integer } remain_charge_sec: { type: integer } monthly_renders_used: { type: integer } starts_at: { type: string, format: date-time } expires_at: { type: string, format: date-time } cancelled_at: { type: string, format: date-time, nullable: true } auto_renew: { type: boolean } Payment: type: object properties: id: { type: string, format: uuid } gateway: { type: string } status: { type: string } action: { type: string } amount_minor: { type: integer, format: int64 } currency: { type: string } title: { type: string } description: { type: string } card_last4: { type: string } confirmed_at: { type: string, format: date-time, nullable: true } failed_at: { type: string, format: date-time, nullable: true } failure_reason: { type: string, nullable: true } created_at: { type: string, format: date-time } DiscountCreate: type: object required: [name, code, kind, value] properties: name: { type: string } code: { type: string } kind: { type: string, enum: [Percentage, FixedAmount, FreeMonths, RenderCredits] } value: { type: number } owner_user_id: { type: string, format: uuid } owner_profit_percentage: { type: number } max_use_count: { type: integer } applies_to_plan_ids: { type: array, items: { type: string, format: uuid } } starts_at: { type: string, format: date-time } expires_at: { type: string, format: date-time } Quest: type: object properties: id: { type: string, format: uuid } title: { type: string } challenge: { type: string } why: { type: string } hint: { type: string } icon: { type: string } quest_type: { type: string, enum: [OneTime, Daily, Weekly, Onboarding, Milestone] } target_count: { type: integer } current_count: { type: integer } is_completed: { type: boolean } prize_claimed: { type: boolean } prize_type: { type: string } prize_amount: { type: integer, format: int64 } expires_at: { type: string, format: date-time, nullable: true } EarnedGift: type: object properties: id: { type: string, format: uuid } gift_id: { type: string, format: uuid } name: { type: string } description: { type: string } prize_type: { type: string } value: { type: integer, format: int64 } unit: { type: string } earned_at: { type: string, format: date-time } expires_at: { type: string, format: date-time, nullable: true } is_used: { type: boolean }