{
  "openapi": "3.1.0",
  "info": {
    "title": "morphedra Whiteboard Video API",
    "version": "1.0.0",
    "summary": "Turn your own figures into narrated whiteboard videos.",
    "description": "Send 1 to 6 figures (PNG, JPG or WEBP) with your own scripts, or let the narration be composed from the figures and notes. The call returns at once; the video is made in the background. Poll the video or use a webhook. Price: $1.80 per minute of finished video, billed per second (shortest charge 10 seconds). Documentation: https://morphedra.com/api-docs",
    "termsOfService": "https://morphedra.com/api-terms",
    "contact": { "name": "morphedra support", "email": "support@morphedra.com" }
  },
  "servers": [{ "url": "https://api.morphedra.com/v1", "description": "Production" }],
  "security": [{ "bearerAuth": [] }],
  "tags": [
    { "name": "Videos" },
    { "name": "Account" }
  ],
  "paths": {
    "/videos": {
      "post": {
        "tags": ["Videos"],
        "operationId": "createVideo",
        "summary": "Create a video",
        "description": "Validates the request, stores your figures, reserves the maximum cost (max_duration_seconds x 3 cents) and queues the video. A failed video is not charged.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Up to 64 characters: letters, digits, - and _. The same key with the same request returns the first video; with a different request it returns 409.",
            "schema": { "type": "string", "maxLength": 64, "pattern": "^[A-Za-z0-9_-]{1,64}$" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateVideoRequest" } } }
        },
        "responses": {
          "202": {
            "description": "Accepted and queued.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Video" } } }
          },
          "200": {
            "description": "Repeat of an earlier request with the same Idempotency-Key (header Idempotent-Replay: true).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Video" } } }
          },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "402": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "409": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" },
          "500": { "$ref": "#/components/responses/Error" }
        }
      },
      "get": {
        "tags": ["Videos"],
        "operationId": "listVideos",
        "summary": "List videos, newest first",
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 } },
          { "name": "starting_after", "in": "query", "description": "A video id. Returns videos created before it.", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "A page of videos.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["object", "data", "has_more"],
                  "properties": {
                    "object": { "type": "string", "const": "list" },
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Video" } },
                    "has_more": { "type": "boolean" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/videos/{id}": {
      "get": {
        "tags": ["Videos"],
        "operationId": "getVideo",
        "summary": "Get a video",
        "description": "Poll every 5 to 10 seconds, or use a webhook. The limit is 30 requests per minute per key.",
        "parameters": [{ "$ref": "#/components/parameters/VideoId" }],
        "responses": {
          "200": { "description": "The video.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Video" } } } },
          "401": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/videos/{id}/cancel": {
      "post": {
        "tags": ["Videos"],
        "operationId": "cancelVideo",
        "summary": "Cancel a queued video",
        "description": "Works only while the status is queued. The full reserved amount is returned.",
        "parameters": [{ "$ref": "#/components/parameters/VideoId" }],
        "responses": {
          "200": { "description": "The cancelled video.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Video" } } } },
          "401": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" },
          "409": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/balance": {
      "get": {
        "tags": ["Account"],
        "operationId": "getBalance",
        "summary": "Get the account balance",
        "responses": {
          "200": { "description": "The balance.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Balance" } } } },
          "401": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/usage": {
      "get": {
        "tags": ["Account"],
        "operationId": "getUsage",
        "summary": "Get usage totals",
        "parameters": [{ "name": "days", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 90, "default": 30 } }],
        "responses": {
          "200": { "description": "Usage.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Usage" } } } },
          "401": { "$ref": "#/components/responses/Error" }
        }
      }
    }
  },
  "webhooks": {
    "videoEvent": {
      "post": {
        "summary": "A video finished (video.completed) or failed (video.failed)",
        "description": "Sent to the webhook_url of the video. Header morphedra-Signature is t=<unix seconds>,v1=<hex HMAC-SHA256 of t + '.' + raw body> using the webhook secret of the key. Answer with any 2xx within 10 seconds. Failed deliveries are retried after 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours.",
        "parameters": [
          { "name": "morphedra-Signature", "in": "header", "required": true, "schema": { "type": "string" } },
          { "name": "morphedra-Event", "in": "header", "required": true, "schema": { "type": "string", "enum": ["video.completed", "video.failed"] } }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookEvent" } } }
        },
        "responses": { "200": { "description": "Received." } }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Your API key: mk_live_... or mk_test_... Keys in the URL are refused."
      }
    },
    "parameters": {
      "VideoId": { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "examples": ["vid_pTcL5JlR5yGALqil"] } }
    },
    "responses": {
      "Error": {
        "description": "An error. The code is stable; the message is for people.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Figure": {
        "type": "object",
        "description": "Give exactly one of image_url or image_base64.",
        "properties": {
          "image_url": { "type": "string", "format": "uri", "description": "Public https link to a PNG, JPG or WEBP of at most 5 MB." },
          "image_base64": { "type": "string", "description": "The image as base64 (a data: prefix is accepted). At most 5 MB." },
          "script": { "type": "string", "maxLength": 10000, "description": "The words the voice reads for this figure, exactly as written. Give a script for every figure, or for none." }
        }
      },
      "CreateVideoRequest": {
        "type": "object",
        "required": ["figures"],
        "properties": {
          "figures": { "type": "array", "minItems": 1, "maxItems": 6, "items": { "$ref": "#/components/schemas/Figure" } },
          "notes": { "type": "string", "maxLength": 5000, "description": "Background for the narration. Used only when no scripts are given." },
          "voice": { "type": "string", "enum": ["Charon", "Orus", "Kore", "Aoede"], "default": "Charon" },
          "title": { "type": "string", "maxLength": 100 },
          "max_duration_seconds": { "type": "integer", "minimum": 10, "maximum": 600, "default": 60, "description": "The most you can be charged for. Test keys are limited to 15." },
          "webhook_url": { "type": "string", "format": "uri", "description": "Public https address for video.completed and video.failed." }
        }
      },
      "Video": {
        "type": "object",
        "required": ["id", "object", "status", "progress", "stage", "test", "aspect", "max_duration_seconds", "cost", "created_at"],
        "properties": {
          "id": { "type": "string" },
          "object": { "type": "string", "const": "video" },
          "status": { "type": "string", "enum": ["queued", "processing", "completed", "failed", "cancelled"] },
          "progress": { "type": "integer", "minimum": 0, "maximum": 100 },
          "stage": { "type": "string" },
          "test": { "type": "boolean" },
          "aspect": { "type": "string", "enum": ["landscape", "portrait"], "description": "Detected from your figures. Square counts as landscape." },
          "title": { "type": "string" },
          "max_duration_seconds": { "type": "integer" },
          "duration_seconds": { "type": ["number", "null"] },
          "video_url": { "type": ["string", "null"], "format": "uri", "description": "Set when completed. The file is deleted after 90 days." },
          "thumbnail_url": { "type": ["string", "null"], "format": "uri" },
          "transcript": { "type": ["string", "null"], "description": "Everything the voice says. Set when completed." },
          "cost": {
            "type": "object",
            "properties": {
              "currency": { "type": "string", "const": "usd" },
              "reserved_cents": { "type": "integer" },
              "charged_cents": { "type": "integer" }
            }
          },
          "error": {
            "type": ["object", "null"],
            "description": "Set when failed. You are not charged.",
            "properties": { "code": { "type": "string" }, "message": { "type": "string" } }
          },
          "created_at": { "type": "string", "format": "date-time" },
          "finished_at": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "required": ["id", "type", "created", "data"],
        "properties": {
          "id": { "type": "string", "description": "Stable per video. Use it to ignore repeats." },
          "type": { "type": "string", "enum": ["video.completed", "video.failed"] },
          "created": { "type": "integer", "description": "Unix seconds." },
          "data": { "$ref": "#/components/schemas/Video" }
        }
      },
      "Balance": {
        "type": "object",
        "properties": {
          "object": { "type": "string", "const": "balance" },
          "currency": { "type": "string", "const": "usd" },
          "balance_cents": { "type": "integer" },
          "test_videos_used": { "type": "integer" },
          "test_videos_limit": { "type": "integer" },
          "price_cents_per_second": { "type": "integer" },
          "minimum_billed_seconds": { "type": "integer" }
        }
      },
      "Usage": {
        "type": "object",
        "properties": {
          "object": { "type": "string", "const": "usage" },
          "days": { "type": "integer" },
          "totals": {
            "type": "object",
            "properties": { "jobs": { "type": "integer" }, "seconds": { "type": "number" }, "spent_cents": { "type": "integer" } }
          },
          "by_status": { "type": "object", "additionalProperties": { "type": "integer" } }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message", "request_id"],
            "additionalProperties": true,
            "properties": {
              "code": { "type": "string", "examples": ["script_too_long", "insufficient_balance", "invalid_api_key"] },
              "message": { "type": "string" },
              "request_id": { "type": "string" }
            }
          }
        }
      }
    }
  }
}
