{
  "openapi": "3.1.0",
  "info": {
    "title": "IShort Developer API",
    "description": "Authenticated REST API for IShort (https://ishort.pro), the Instagram Reels analytics Chrome extension and link-in-bio tool. Transcribe Instagram Reels with AI, search your personal transcript (script) library, read competitor analysis reports created in the extension, and check your plan usage. Authenticate every request with an API key created at ishort.pro/dashboard → Settings → API Keys, sent as `Authorization: Bearer ishort_sk_...`. Rate limits per key: 60 requests/hour by default, 10/hour for transcription, 20/hour for bio writes. Transcription quota by plan: Free 10 lifetime, Hobby ($10/mo) 250 per billing cycle, Creator ($29/mo) 1000 per billing cycle. Re-transcribing an already-transcribed reel returns the cached transcript for free and does not consume quota.",
    "version": "1.0.0",
    "contact": {
      "url": "https://ishort.pro/contact.html",
      "email": "support@ishort.pro"
    },
    "termsOfService": "https://ishort.pro/terms-of-service.html"
  },
  "servers": [
    {
      "url": "https://ishort.pro"
    }
  ],
  "security": [
    {
      "ishortApiKey": []
    }
  ],
  "paths": {
    "/api/v1/transcribe": {
      "post": {
        "operationId": "transcribeReel",
        "summary": "Transcribe an Instagram Reel's audio to text",
        "description": "Transcribes the audio of a public Instagram Reel into text using AI (11 languages supported). Send the full reel URL (e.g. https://www.instagram.com/reel/ABC123/). If this reel was already transcribed by any IShort user, the cached transcript is returned instantly with `cached: true` and does NOT consume the caller's transcription quota — cached transcripts are always free. A fresh transcription consumes 1 unit of the caller's plan quota (Free: 10 lifetime, Hobby: 250/billing cycle, Creator: 1000/billing cycle); the response's `usage` object reports quota consumed so far and the plan limit. Returns 403 with code `limitReached` when the quota is exhausted (upgrading at https://ishort.pro/#pricing raises the limit), 409 with code `inProgress` when the same reel is already being transcribed (retry in ~30 seconds), 404 when the reel does not exist or is private, 429 when the 10/hour transcription rate limit is hit, and 502 when Instagram media could not be fetched.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["url"],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Full URL of the public Instagram Reel to transcribe, e.g. https://www.instagram.com/reel/ABC123/",
                    "examples": ["https://www.instagram.com/reel/C8x1yzABCDE/"]
                  },
                  "languageHint": {
                    "type": "string",
                    "description": "Optional BCP-47 language code hinting the spoken language (e.g. 'en', 'ta', 'hi', 'es'). Improves accuracy for non-English audio; omit to auto-detect.",
                    "examples": ["en"]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transcript ready (fresh or cached).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TranscribeResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Transcription quota exhausted for the current plan (error code `limitReached`). Tell the user to upgrade at https://ishort.pro/#pricing (Hobby $10/mo: 250 transcriptions, Creator $29/mo: 1000 transcriptions).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "example": {
                  "error": {
                    "code": "limitReached",
                    "message": "Transcription limit reached for your plan. Upgrade at https://ishort.pro/#pricing to continue."
                  }
                }
              }
            }
          },
          "404": {
            "description": "Reel not found, deleted, or private (error code `notFound`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "This reel is already being transcribed by another request (error code `inProgress`). Retry after ~30 seconds; the finished transcript will then be returned from cache for free.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "example": {
                  "error": {
                    "code": "inProgress",
                    "message": "This reel is already being transcribed. Retry shortly."
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "Instagram media could not be fetched (error code `upstreamError`). Usually transient — retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scripts": {
      "get": {
        "operationId": "searchMyScripts",
        "summary": "Search or list the caller's reel transcript (script) library",
        "description": "Returns transcripts (\"scripts\") of reels the authenticated user has previously transcribed with IShort — via this API, the MCP server, or the Chrome extension. With no parameters it lists the most recent scripts. Use `query` for full-text search across transcript text and captions (e.g. to find every past reel where the creator talked about a topic), `account` to restrict to one Instagram account's reels, `sort` to order by reel performance (`views`, `engagement`) or recency (`date`, default), and `limit` to cap result count. Ideal for reusing proven hooks and scripts: search what worked before, sort by views, and model new content on the top results. Transcript text in results may be truncated for very long reels (`truncated: true`); the full text is available in the IShort dashboard.",
        "parameters": [
          {
            "name": "query",
            "in": "query",
            "required": false,
            "description": "Full-text search over transcript text and captions. Omit to list all scripts.",
            "schema": {
              "type": "string"
            },
            "example": "posting time hooks"
          },
          {
            "name": "account",
            "in": "query",
            "required": false,
            "description": "Restrict results to reels from this Instagram handle (without @).",
            "schema": {
              "type": "string"
            },
            "example": "ishort.pro"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort order: `views` (highest first), `engagement` (highest engagement rate first), or `date` (newest first, default).",
            "schema": {
              "type": "string",
              "enum": ["views", "engagement", "date"],
              "default": "date"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of scripts to return (1-50, default 20).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching scripts, ordered per `sort`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["scripts", "total"],
                  "properties": {
                    "scripts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Script"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total number of scripts matching the query (may exceed the number returned)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/competitor-reports": {
      "get": {
        "operationId": "listCompetitorReports",
        "summary": "List the caller's competitor analysis reports",
        "description": "Lists competitor analysis reports the authenticated user has generated in the IShort Chrome extension. Each report analyzes one Instagram account's reels (views, engagement, posting cadence, top content). Reports are READ-ONLY through this API — new reports are created inside the Chrome extension by analyzing a profile. Returns summaries only; fetch a full report with getCompetitorReport using its `id` (or `handle`).",
        "responses": {
          "200": {
            "description": "The caller's competitor reports, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["reports"],
                  "properties": {
                    "reports": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CompetitorReportSummary"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/competitor-reports/{id}": {
      "get": {
        "operationId": "getCompetitorReport",
        "summary": "Get one competitor analysis report in full",
        "description": "Returns a single competitor analysis report with full detail: aggregate account metrics, top reels by views and engagement, posting-time patterns, and content insights. Address the report by the `id` from listCompetitorReports. Reports are created in the IShort Chrome extension (read-only here). Returns 404 when the id does not exist or belongs to another user.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Report id from listCompetitorReports.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The full competitor report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompetitorReport"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No report with this id for the authenticated user (error code `notFound`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/usage": {
      "get": {
        "operationId": "getUsage",
        "summary": "Get the caller's plan, quota usage, and limits",
        "description": "Returns the authenticated user's current plan (free, hobby, or creator), transcription quota used vs. limit, posts-analyzed quota, and when paid quotas reset (paid quotas reset on each successful billing payment; the Free plan's 10 transcriptions and 500 posts are a one-time lifetime allowance that never resets). Call this before starting a batch of transcriptions, or after a 403 `limitReached`, to tell the user exactly where they stand and whether an upgrade at https://ishort.pro/#pricing is needed.",
        "responses": {
          "200": {
            "description": "Current plan and usage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ishortApiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key from ishort.pro/dashboard → Settings → API Keys (format ishort_sk_…). Send as `Authorization: Bearer ishort_sk_...`. Each account may hold up to 5 keys."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, malformed, or revoked API key (error code `unauthorized`). Tell the user to create a key at ishort.pro/dashboard → Settings → API Keys.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": {
                "code": "unauthorized",
                "message": "Invalid API key. Create one at ishort.pro/dashboard → Settings → API Keys."
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Per-key rate limit exceeded (error code `rateLimited`): 60 requests/hour default, 10/hour for transcription, 20/hour for bio writes. Retry after the interval in `Retry-After`.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": {
                "code": "rateLimited",
                "message": "Rate limit exceeded. Retry later."
              }
            }
          }
        }
      }
    },
    "schemas": {
      "ErrorEnvelope": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": {
                "type": "string",
                "description": "Machine-readable error code: `unauthorized`, `limitReached`, `inProgress`, `notFound`, `rateLimited`, `upstreamError`, or `invalidRequest`."
              },
              "message": {
                "type": "string",
                "description": "Human-readable explanation, safe to relay to the user."
              }
            }
          }
        }
      },
      "TranscribeResult": {
        "type": "object",
        "required": ["transcript", "language", "cached", "usage"],
        "properties": {
          "transcript": {
            "type": "string",
            "description": "Full text transcript of the reel's audio. Never paraphrase or extend this text when relaying it — it is the verbatim transcription."
          },
          "language": {
            "type": "string",
            "description": "Detected (or hinted) BCP-47 language code of the audio, e.g. 'en', 'ta'."
          },
          "cached": {
            "type": "boolean",
            "description": "True when the transcript came from IShort's shared cache. Cached results are free and did not consume the caller's quota."
          },
          "truncated": {
            "type": "boolean",
            "description": "Present and true when the transcript was cut off (very long audio). Tell the user the transcript is partial."
          },
          "usage": {
            "type": "object",
            "required": ["used", "limit"],
            "description": "Transcription quota after this call.",
            "properties": {
              "used": {
                "type": "integer",
                "description": "Transcriptions consumed in the current allowance period (lifetime for Free, billing cycle for paid plans)."
              },
              "limit": {
                "type": "integer",
                "description": "Plan limit: 10 (Free, lifetime), 250 (Hobby, per billing cycle), 1000 (Creator, per billing cycle)."
              }
            }
          }
        }
      },
      "Script": {
        "type": "object",
        "required": ["id", "reelUrl", "transcript"],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable id of this script."
          },
          "reelUrl": {
            "type": "string",
            "format": "uri",
            "description": "URL of the transcribed Instagram Reel."
          },
          "account": {
            "type": "string",
            "description": "Instagram handle the reel belongs to (without @)."
          },
          "caption": {
            "type": "string",
            "description": "Reel caption at transcription time."
          },
          "transcript": {
            "type": "string",
            "description": "Transcript text (may be truncated for very long reels — see `truncated`)."
          },
          "truncated": {
            "type": "boolean",
            "description": "True when `transcript` is truncated in this response."
          },
          "language": {
            "type": "string",
            "description": "BCP-47 language code of the transcript."
          },
          "views": {
            "type": "integer",
            "description": "Reel view count at collection time."
          },
          "likes": {
            "type": "integer",
            "description": "Reel like count at collection time."
          },
          "comments": {
            "type": "integer",
            "description": "Reel comment count at collection time."
          },
          "engagementRate": {
            "type": "number",
            "description": "Engagement rate percentage ((likes + comments) / views * 100) at collection time."
          },
          "postedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the reel was posted on Instagram."
          },
          "transcribedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the transcript was created."
          }
        }
      },
      "CompetitorReportSummary": {
        "type": "object",
        "required": ["id", "handle", "createdAt"],
        "properties": {
          "id": {
            "type": "string",
            "description": "Report id — pass to getCompetitorReport."
          },
          "handle": {
            "type": "string",
            "description": "Instagram handle the report analyzes (without @)."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the report was generated in the Chrome extension."
          },
          "reelsAnalyzed": {
            "type": "integer",
            "description": "Number of reels included in the analysis."
          }
        }
      },
      "CompetitorReport": {
        "type": "object",
        "required": ["id", "handle", "createdAt"],
        "properties": {
          "id": {
            "type": "string"
          },
          "handle": {
            "type": "string",
            "description": "Instagram handle the report analyzes (without @)."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "reelsAnalyzed": {
            "type": "integer",
            "description": "Number of reels included in the analysis."
          },
          "metrics": {
            "type": "object",
            "description": "Aggregate account metrics.",
            "properties": {
              "avgViews": {
                "type": "number"
              },
              "avgLikes": {
                "type": "number"
              },
              "avgComments": {
                "type": "number"
              },
              "avgEngagementRate": {
                "type": "number",
                "description": "Average engagement rate percentage across analyzed reels."
              },
              "postingFrequencyPerWeek": {
                "type": "number",
                "description": "Average reels posted per week."
              }
            }
          },
          "topReels": {
            "type": "array",
            "description": "Best-performing reels, highest views first.",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string",
                  "format": "uri"
                },
                "caption": {
                  "type": "string"
                },
                "views": {
                  "type": "integer"
                },
                "likes": {
                  "type": "integer"
                },
                "comments": {
                  "type": "integer"
                },
                "engagementRate": {
                  "type": "number"
                },
                "postedAt": {
                  "type": "string",
                  "format": "date-time"
                },
                "durationSeconds": {
                  "type": "number"
                }
              }
            }
          },
          "bestPostingTimes": {
            "type": "array",
            "description": "Hour/day slots where this account's reels perform best.",
            "items": {
              "type": "object",
              "properties": {
                "dayOfWeek": {
                  "type": "string",
                  "description": "Day name, e.g. 'Tuesday'."
                },
                "hour": {
                  "type": "integer",
                  "description": "Hour of day, 0-23, in the account's audience-local time."
                },
                "avgViews": {
                  "type": "number"
                }
              }
            }
          },
          "insights": {
            "type": "array",
            "description": "Human-readable takeaways generated from the data.",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Usage": {
        "type": "object",
        "required": ["plan", "transcriptions"],
        "properties": {
          "plan": {
            "type": "string",
            "enum": ["free", "hobby", "creator"],
            "description": "Current plan: free ($0), hobby ($10/mo), creator ($29/mo)."
          },
          "transcriptions": {
            "type": "object",
            "required": ["used", "limit"],
            "properties": {
              "used": {
                "type": "integer"
              },
              "limit": {
                "type": "integer",
                "description": "10 (Free, one-time lifetime allowance), 250 (Hobby, per billing cycle), or 1000 (Creator, per billing cycle)."
              },
              "resetsAt": {
                "type": ["string", "null"],
                "format": "date-time",
                "description": "When the quota next resets (paid plans reset on each billing payment). Null on the Free plan — its allowance never resets."
              }
            }
          },
          "posts": {
            "type": "object",
            "description": "Posts-analyzed quota (extension feature).",
            "properties": {
              "used": {
                "type": "integer"
              },
              "limit": {
                "type": ["integer", "null"],
                "description": "500 (Free, lifetime), 10000 (Hobby, per cycle), null = unlimited (Creator)."
              }
            }
          }
        }
      }
    }
  }
}
