openapi: 3.0.3
info:
  title: Flikly Developer API
  version: 1.0.0
  description: |
    Server-to-server API for Team and Custom plans. Authenticate with a Bearer API key on every request.

    **Base URL:** `https://api.flikly.ai/api/v1/developer`

    **Plans:** Developer API v1 is available on **Team** and **Custom** plans only. Pro Limited API and social publishing are not available in v1.

    **API keys:** Create and manage keys in the [Developer Portal](https://developers.flikly.ai/dashboard/api-keys) (Clerk sign-in). Key CRUD is not available via API-key authentication.

    **Security:** Never embed API keys in browser or mobile clients. Use webhooks and server-side polling instead.

    **Test keys:** `flk_test_…` keys use a separate prefix for integration testing but share the same entitlement, credit billing, and job pipelines as `flk_live_…` keys — they are not a billing sandbox.

    **Credits:** Video and split jobs consume credits from your Flikly account (same pool as the web app). When credits are insufficient, the API returns `INSUFFICIENT_CREDITS` before queueing work. Buy credits in main app billing — the Developer API never initiates Stripe Checkout.

    **Auto-recharge:** When available, automatic credit recharge is managed in main app billing (`flikly.ai/dashboard/settings/billing`), not the Developer Portal.

    **Support:** support@flikly.com
  contact:
    email: support@flikly.com
  license:
    name: Proprietary
    url: https://flikly.ai/terms

servers:
  - url: https://api.flikly.ai
    description: Production

tags:
  - name: Context
    description: Organization and key context
  - name: Assets
    description: Upload and retrieve developer assets
  - name: Video Jobs
    description: AI text-to-video and image-to-video jobs
  - name: Split Jobs
    description: Long-form video to short clips (asset upload only in v1)
  - name: Jobs
    description: Unified job listing and detail (no signed URLs)
  - name: Webhooks
    description: HTTPS webhook endpoints and delivery
  - name: Usage
    description: Credits, rate limits, and usage metrics

security:
  - DeveloperApiKey: []

paths:
  /api/v1/developer/me:
    get:
      tags: [Context]
      summary: Get API context
      operationId: getDeveloperMe
      responses:
        '200':
          description: Organization, plan, scopes, and limits snapshot
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeveloperMe'
              examples:
                me:
                  value:
                    organization_id: 42
                    plan: team
                    api_access: true
                    environment: live
                    key_prefix: flk_live_abc1234567
                    scopes:
                      - assets:create
                      - assets:read
                      - video:create
                      - video:read
                      - split:create
                      - split:read
                      - jobs:read
                      - usage:read
                      - webhooks:read
                      - webhooks:write
                    limits:
                      requests_per_minute_per_key: 60
                      requests_per_day_per_organization: 10000
                      concurrent_video_jobs: 3
                      concurrent_split_jobs: 2
                      active_api_keys_per_organization: 10
                      webhook_endpoints: 3
                      max_asset_upload_size_bytes: 2147483648
                    usage_snapshot:
                      requests_this_minute: 2
                      requests_today: 120
                      active_video_jobs: 0
                      active_split_jobs: 1
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/developer/assets/upload-url:
    post:
      tags: [Assets]
      summary: Create presigned upload URL
      operationId: createAssetUploadUrl
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UploadUrlRequest'
            examples:
              videoSource:
                value:
                  filename: source.mp4
                  content_type: video/mp4
                  size_bytes: 10485760
                  purpose: split_source
      responses:
        '201':
          description: Upload URL created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadUrlResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/developer/assets/confirm-upload:
    post:
      tags: [Assets]
      summary: Confirm asset upload
      operationId: confirmAssetUpload
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [asset_id]
              properties:
                asset_id:
                  type: string
                  pattern: '^asset_'
            example:
              asset_id: asset_abc123xyz
      responses:
        '200':
          description: Asset ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Asset'
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/AssetNotFound'

  /api/v1/developer/assets/{id}:
    get:
      tags: [Assets]
      summary: Get asset metadata
      operationId: getAsset
      parameters:
        - $ref: '#/components/parameters/AssetId'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Asset'
        '404':
          $ref: '#/components/responses/AssetNotFound'

  /api/v1/developer/assets/{id}/download-url:
    get:
      tags: [Assets]
      summary: Get presigned download URL
      operationId: getAssetDownloadUrl
      parameters:
        - $ref: '#/components/parameters/AssetId'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AssetDownloadUrlResponse'
        '404':
          $ref: '#/components/responses/AssetNotFound'

  /api/v1/developer/video-jobs:
    post:
      tags: [Video Jobs]
      summary: Create video job
      operationId: createVideoJob
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateVideoJobRequest'
            examples:
              textToVideo:
                value:
                  prompt: A cinematic drone shot over the ocean at sunset
                  aspect_ratio: '9:16'
                  duration_seconds: 10
                  model: auto
              imageToVideo:
                value:
                  prompt: Gentle camera push-in with soft lighting
                  source_asset_id: asset_img123
                  aspect_ratio: '9:16'
                  duration_seconds: 5
                  model: auto
                  style: cinematic
      responses:
        '201':
          description: Job created (or idempotent replay)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VideoJob'
        '409':
          description: Idempotency conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: IDEMPOTENCY_CONFLICT
                  message: Idempotency key was already used with a different request body.
                  request_id: req_01abc
        '402':
          $ref: '#/components/responses/InsufficientCredits'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/developer/video-jobs/{id}:
    get:
      tags: [Video Jobs]
      summary: Get video job
      operationId: getVideoJob
      parameters:
        - $ref: '#/components/parameters/VideoJobId'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VideoJob'

  /api/v1/developer/video-jobs/{id}/result:
    get:
      tags: [Video Jobs]
      summary: Get video job result with download URL
      operationId: getVideoJobResult
      parameters:
        - $ref: '#/components/parameters/VideoJobId'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VideoJobResult'
        '404':
          $ref: '#/components/responses/VideoJobNotFound'
        '409':
          description: Job not ready or failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/v1/developer/split-jobs:
    post:
      tags: [Split Jobs]
      summary: Create split job
      description: |
        V1 requires `source_asset_id` from a prior asset upload. URL and YouTube inputs are not supported in v1.
      operationId: createSplitJob
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSplitJobRequest'
            example:
              source_asset_id: asset_vid123
              target_platforms: [tiktok, instagram_reels]
              clip_length_preset: under_60
              aspect_ratio: '9:16'
              max_clips: 5
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SplitJob'
        '409':
          description: Idempotency conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/v1/developer/split-jobs/{id}:
    get:
      tags: [Split Jobs]
      summary: Get split job
      operationId: getSplitJob
      parameters:
        - $ref: '#/components/parameters/SplitJobId'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SplitJob'

  /api/v1/developer/split-jobs/{id}/clips:
    get:
      tags: [Split Jobs]
      summary: List clips with signed download URLs
      operationId: getSplitJobClips
      parameters:
        - $ref: '#/components/parameters/SplitJobId'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SplitJobClipsResponse'

  /api/v1/developer/split-jobs/{id}/result:
    get:
      tags: [Split Jobs]
      summary: Get split job result
      operationId: getSplitJobResult
      parameters:
        - $ref: '#/components/parameters/SplitJobId'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SplitJobResult'

  /api/v1/developer/jobs:
    get:
      tags: [Jobs]
      summary: List jobs (unified)
      description: Cursor pagination. Does not return presigned download URLs.
      operationId: listJobs
      parameters:
        - name: type
          in: query
          schema:
            type: string
            enum: [video, split]
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/JobStatus'
        - name: created_after
          in: query
          schema:
            type: string
            format: date-time
        - name: created_before
          in: query
          schema:
            type: string
            format: date-time
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          schema:
            type: string
        - name: metadata_external_id
          in: query
          schema:
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/UnifiedJobListItem'
                  pagination:
                    $ref: '#/components/schemas/Pagination'

  /api/v1/developer/jobs/{id}:
    get:
      tags: [Jobs]
      summary: Get job detail
      operationId: getJob
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            description: video_job_* or split_job_*
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UnifiedJobDetail'

  /api/v1/developer/webhooks:
    post:
      tags: [Webhooks]
      summary: Create webhook endpoint
      operationId: createWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWebhookRequest'
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpointCreateResponse'
    get:
      tags: [Webhooks]
      summary: List webhook endpoints
      operationId: listWebhooks
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookEndpoint'

  /api/v1/developer/webhooks/{id}:
    get:
      tags: [Webhooks]
      summary: Get webhook endpoint
      operationId: getWebhook
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpoint'
    patch:
      tags: [Webhooks]
      summary: Update webhook endpoint
      operationId: patchWebhook
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchWebhookRequest'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpoint'
    delete:
      tags: [Webhooks]
      summary: Delete webhook endpoint
      operationId: deleteWebhook
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      responses:
        '204':
          description: Deleted

  /api/v1/developer/webhooks/{id}/test:
    post:
      tags: [Webhooks]
      summary: Send test webhook delivery
      operationId: testWebhook
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookTestResponse'

  /api/v1/developer/usage:
    get:
      tags: [Usage]
      summary: Usage summary
      operationId: getUsageSummary
      parameters:
        - $ref: '#/components/parameters/UsageQueryPeriod'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageSummary'

  /api/v1/developer/usage/credits:
    get:
      tags: [Usage]
      summary: Credit usage breakdown
      operationId: getUsageCredits
      parameters:
        - $ref: '#/components/parameters/UsageQueryPeriod'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreditUsage'

  /api/v1/developer/usage/rate-limits:
    get:
      tags: [Usage]
      summary: Rate limit usage and resets
      operationId: getUsageRateLimits
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateLimitUsage'

  /api/v1/developer/usage/jobs:
    get:
      tags: [Usage]
      summary: Job usage metrics
      operationId: getUsageJobs
      parameters:
        - $ref: '#/components/parameters/UsageQueryPeriod'
        - name: job_type
          in: query
          schema:
            type: string
            enum: [video, split]
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobUsage'

  /api/v1/developer/usage/webhooks:
    get:
      tags: [Usage]
      summary: Webhook delivery usage
      operationId: getUsageWebhooks
      parameters:
        - $ref: '#/components/parameters/UsageQueryPeriod'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookUsage'

components:
  securitySchemes:
    DeveloperApiKey:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: |
        `Authorization: Bearer flk_live_<prefix>_<secret>` or `flk_test_<prefix>_<secret>`.
        Manage keys in the Developer Portal — not via this API.

  parameters:
    AssetId:
      name: id
      in: path
      required: true
      schema:
        type: string
        pattern: '^asset_'
    VideoJobId:
      name: id
      in: path
      required: true
      schema:
        type: string
        pattern: '^video_job_'
    SplitJobId:
      name: id
      in: path
      required: true
      schema:
        type: string
        pattern: '^split_job_'
    WebhookId:
      name: id
      in: path
      required: true
      schema:
        type: string
        pattern: '^webhook_'
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: Optional idempotency key for create video/split jobs. Same key + same body returns the original job; same key + different body returns IDEMPOTENCY_CONFLICT.
      schema:
        type: string
        maxLength: 120
    UsageQueryPeriod:
      name: period
      in: query
      schema:
        type: string
        enum: [current_billing_period, last_7_days, last_30_days, custom]
        default: current_billing_period

  headers:
    X-RateLimit-Limit:
      schema:
        type: integer
      description: Per-key requests allowed in the current minute window.
    X-RateLimit-Remaining:
      schema:
        type: integer
      description: Per-key requests remaining in the current minute window.
    X-RateLimit-Reset:
      schema:
        type: integer
      description: Unix timestamp when the per-key minute window resets.
    X-RateLimit-Limit-Organization-Day:
      schema:
        type: integer
    X-RateLimit-Remaining-Organization-Day:
      schema:
        type: integer
    X-RateLimit-Reset-Organization-Day:
      schema:
        type: integer
    Retry-After:
      schema:
        type: integer
      description: Seconds to wait before retrying after HTTP 429.

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
            message:
              type: string
            request_id:
              type: string

    Pagination:
      type: object
      properties:
        next_cursor:
          type: string
          nullable: true

    JobStatus:
      type: string
      enum: [queued, processing, completed, failed, cancelled, expired]

    JobCredits:
      type: object
      required: [estimated, charged, refunded]
      properties:
        estimated:
          type: integer
          minimum: 0
        charged:
          type: integer
          minimum: 0
        refunded:
          type: integer
          minimum: 0

    JobResultSummary:
      type: object
      required: [available]
      properties:
        available:
          type: boolean
          description: True only when type-specific result routes can return data
        asset_id:
          type: string
          nullable: true
        clips_count:
          type: integer
          nullable: true

    DeveloperMe:
      type: object
      properties:
        organization_id:
          type: integer
        plan:
          type: string
          enum: [team, custom]
        api_access:
          type: boolean
        environment:
          type: string
          enum: [live, test]
        key_prefix:
          type: string
        scopes:
          type: array
          items:
            type: string
        limits:
          $ref: '#/components/schemas/DeveloperLimits'
        usage_snapshot:
          type: object

    DeveloperLimits:
      type: object
      properties:
        requests_per_minute_per_key:
          type: integer
        requests_per_day_per_organization:
          type: integer
        concurrent_video_jobs:
          type: integer
        concurrent_split_jobs:
          type: integer
        active_api_keys_per_organization:
          type: integer
        webhook_endpoints:
          type: integer
        max_asset_upload_size_bytes:
          type: integer

    UploadUrlRequest:
      type: object
      required: [filename, content_type, size_bytes, purpose]
      properties:
        filename:
          type: string
        content_type:
          type: string
          enum:
            - image/jpeg
            - image/png
            - image/webp
            - video/mp4
            - video/quicktime
            - video/webm
        size_bytes:
          type: integer
        purpose:
          type: string
          enum: [split_source, video_source, image_source, general]
        metadata:
          type: object
          additionalProperties: true

    UploadUrlResponse:
      type: object
      properties:
        asset_id:
          type: string
        upload_url:
          type: string
          format: uri
        upload_method:
          type: string
          enum: [PUT]
        upload_headers:
          type: object
          properties:
            Content-Type:
              type: string
        expires_at:
          type: string
          format: date-time

    Asset:
      type: object
      properties:
        id:
          type: string
        filename:
          type: string
        content_type:
          type: string
        size_bytes:
          type: integer
        purpose:
          type: string
        status:
          type: string
          enum: [pending, uploaded, failed]
        created_at:
          type: string
          format: date-time
        metadata:
          type: object
          nullable: true

    AssetDownloadUrlResponse:
      type: object
      properties:
        asset_id:
          type: string
        download_url:
          type: string
          format: uri
        expires_at:
          type: string
          format: date-time

    CreateVideoJobRequest:
      type: object
      required: [prompt, duration_seconds]
      properties:
        prompt:
          type: string
          maxLength: 1000
        source_asset_id:
          type: string
          pattern: '^asset_'
        aspect_ratio:
          type: string
          enum: ['9:16', '16:9', '1:1']
          default: '9:16'
        duration_seconds:
          type: integer
          enum: [5, 10, 15]
        model:
          type: string
          enum: [auto]
          default: auto
        style:
          type: string
          enum: [realistic, cinematic, animated]
        metadata:
          type: object

    VideoJob:
      type: object
      properties:
        id:
          type: string
        type:
          type: string
          enum: [video]
        status:
          $ref: '#/components/schemas/JobStatus'
        progress:
          type: number
          minimum: 0
          maximum: 100
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        credits:
          $ref: '#/components/schemas/JobCredits'
        input:
          type: object
        result:
          allOf:
            - $ref: '#/components/schemas/JobResultSummary'
            - type: object
              properties:
                download_url_available:
                  type: boolean
                  deprecated: true
                  description: Use `available` instead
        error:
          type: object
          nullable: true
        metadata:
          type: object
          nullable: true

    VideoJobResult:
      type: object
      properties:
        job_id:
          type: string
        status:
          $ref: '#/components/schemas/JobStatus'
        asset:
          type: object
          properties:
            asset_id:
              type: string
            content_type:
              type: string
            download_url:
              type: string
              format: uri
            expires_at:
              type: string
              format: date-time

    CreateSplitJobRequest:
      type: object
      required: [source_asset_id]
      properties:
        source_asset_id:
          type: string
        target_platforms:
          type: array
          items:
            type: string
            enum: [tiktok, instagram_reels, youtube_shorts, facebook_reels, linkedin, x]
        clip_length_preset:
          type: string
          enum: [under_30, under_60, under_90, auto]
        aspect_ratio:
          type: string
          enum: ['9:16', '1:1', '16:9']
        caption_style:
          type: string
          enum: [none, clean, bold_center, karaoke, platform_native]
        clip_style:
          type: string
          enum: [viral_hooks, educational, podcast, highlights, balanced]
        max_clips:
          type: integer
          minimum: 1
          maximum: 10
        language:
          type: string
        metadata:
          type: object

    SplitJob:
      type: object
      properties:
        id:
          type: string
        type:
          type: string
          enum: [split]
        status:
          $ref: '#/components/schemas/JobStatus'
        progress:
          type: number
          minimum: 0
          maximum: 100
        credits:
          $ref: '#/components/schemas/JobCredits'
        input:
          type: object
        result:
          allOf:
            - $ref: '#/components/schemas/JobResultSummary'
            - type: object
              properties:
                clips_available:
                  type: boolean
                  deprecated: true
                  description: Use `available` instead
        error:
          type: object
          nullable: true

    SplitJobClip:
      type: object
      properties:
        id:
          type: string
        status:
          type: string
          enum: [pending, processing, completed, failed]
        title:
          type: string
          nullable: true
        start_time_seconds:
          type: number
        end_time_seconds:
          type: number
        duration_seconds:
          type: number
        score:
          type: number
          nullable: true
        download_url:
          type: string
          format: uri
          nullable: true
        thumbnail_url:
          type: string
          format: uri
          nullable: true
        captions_url:
          type: string
          format: uri
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true

    SplitJobClipsResponse:
      type: object
      properties:
        job_id:
          type: string
        clips:
          type: array
          items:
            $ref: '#/components/schemas/SplitJobClip'

    SplitJobResult:
      type: object
      properties:
        job_id:
          type: string
        status:
          $ref: '#/components/schemas/JobStatus'
        clips_count:
          type: integer
        clips:
          type: array
          items:
            $ref: '#/components/schemas/SplitJobClip'

    UnifiedJobListItem:
      type: object
      properties:
        id:
          type: string
        type:
          type: string
          enum: [video, split]
        status:
          $ref: '#/components/schemas/JobStatus'
        progress:
          type: number
        credits:
          type: object
        result:
          type: object
          description: Availability flags only — no signed URLs
        error:
          type: object
          nullable: true

    UnifiedJobDetail:
      allOf:
        - $ref: '#/components/schemas/UnifiedJobListItem'
        - type: object
          properties:
            input:
              type: object

    CreateWebhookRequest:
      type: object
      required: [url, events]
      properties:
        url:
          type: string
          format: uri
          description: HTTPS URL only
        description:
          type: string
        events:
          type: array
          items:
            type: string
            enum: [job.completed, job.failed]

    PatchWebhookRequest:
      type: object
      properties:
        url:
          type: string
          format: uri
        description:
          type: string
          nullable: true
        events:
          type: array
          items:
            type: string
            enum: [job.completed, job.failed]
        status:
          type: string
          enum: [active, disabled]

    WebhookEndpoint:
      type: object
      properties:
        id:
          type: string
        url:
          type: string
        description:
          type: string
          nullable: true
        events:
          type: array
          items:
            type: string
        status:
          type: string
          enum: [active, disabled, deleted]
        failure_count:
          type: integer
        last_success_at:
          type: string
          format: date-time
          nullable: true
        last_failure_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    WebhookEndpointCreateResponse:
      allOf:
        - $ref: '#/components/schemas/WebhookEndpoint'
        - type: object
          properties:
            secret:
              type: string
              description: Signing secret — shown once at create

    WebhookTestResponse:
      type: object
      properties:
        delivery:
          type: object
          properties:
            id:
              type: string
            event_type:
              type: string
              enum: [webhook.test]
            status:
              type: string

    WebhookEventJobCompleted:
      type: object
      description: |
        Delivered with headers Flikly-Webhook-Id, Flikly-Webhook-Event, Flikly-Webhook-Timestamp, Flikly-Webhook-Signature (v1= HMAC-SHA256 of `${timestamp}.${rawBody}`).
      properties:
        id:
          type: string
        type:
          type: string
          enum: [job.completed]
        created_at:
          type: string
          format: date-time
        data:
          type: object
          properties:
            job:
              type: object
              properties:
                id:
                  type: string
                type:
                  type: string
                  enum: [video, split]
                status:
                  type: string
                  enum: [completed]
                result_url:
                  type: string
                  description: API path to fetch result (not a signed URL)
                clips_url:
                  type: string
                asset_id:
                  type: string

    UsageSummary:
      type: object
    CreditUsage:
      type: object
    RateLimitUsage:
      type: object
    JobUsage:
      type: object
    WebhookUsage:
      type: object

  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: API_KEY_INVALID
              message: The API key is invalid or expired.
              request_id: req_01abc
    Forbidden:
      description: Plan, scope, or access denied
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ValidationError:
      description: Request validation failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: VALIDATION_FAILED
              message: Request validation failed.
    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          $ref: '#/components/headers/Retry-After'
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/X-RateLimit-Remaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: RATE_LIMIT_EXCEEDED
              message: Developer API rate limit exceeded.
    InsufficientCredits:
      description: Not enough credits
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: INSUFFICIENT_CREDITS
              message: Insufficient credits to create this video job.
    AssetNotFound:
      description: Asset not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    VideoJobNotFound:
      description: Video job not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
