{
  "openapi": "3.1.0",
  "info": {
    "title": "Alfie REST API",
    "version": "1.0.0",
    "description": "Turn audio, video and YouTube links into notes without a browser. Authenticate with an API key created at /settings/api-keys, sent as `Authorization: Bearer alfie_...`. Credits and plan limits are the same as in the web app.\n\nRate limits: 120 requests per minute per key, plus 10 job submissions per minute per key. Every authenticated response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds).\n\nBreaking changes will ship as /api/v2."
  },
  "servers": [{ "url": "/api/v1" }],
  "security": [{ "bearerAuth": [] }],
  "paths": {
    "/me": {
      "get": {
        "operationId": "getMe",
        "summary": "Read your plan and remaining credits",
        "description": "Requires the `notes:read` scope.",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "Plan and remaining credits.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Me" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/uploads": {
      "post": {
        "operationId": "createUpload",
        "summary": "Get a pre-signed URL to upload a file",
        "description": "Requires `notes:write`. PUT the file bytes to `upload_url` with the `required_headers` before `expires_at` (10 minutes), then submit a job with `type: \"upload\"` and the `upload_id`. Files over 800 MB are rejected at job submission with `validation_error`. The job transcribes the bytes present when it is submitted; later PUTs to the same URL do not affect it.\n\n**Accepted formats.** Uploads are charged by their real audio length, so only uncompressed WAV (PCM or IEEE float, RIFF/WAVE) is accepted: `content_type` must be `audio/wav`, `audio/x-wav` or `audio/wave` (other types get 400 `validation_error` with `details.allowed`), and a job whose file is not such a WAV fails with 422 `unsupported_media`. Convert other media to WAV first, for example `ffmpeg -i in.m4a -ac 1 -ar 16000 out.wav`. Compressed formats may be accepted again in a later version.",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["filename", "content_type"],
                "properties": {
                  "filename": { "type": "string", "minLength": 1, "maxLength": 500 },
                  "content_type": {
                    "type": "string",
                    "description": "The file's MIME type. Only WAV is accepted; see the operation description.",
                    "enum": ["audio/wav", "audio/x-wav", "audio/wave"]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Upload URL created.",
            "headers": { "Idempotent-Replayed": { "$ref": "#/components/headers/IdempotentReplayed" } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Upload" } } }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "409": { "$ref": "#/components/responses/IdempotencyConflict" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/jobs": {
      "post": {
        "operationId": "createJob",
        "summary": "Submit an upload or a YouTube link for transcription",
        "description": "Requires `notes:write`. Credits for the media's duration are reserved atomically; a WAV is charged from its size, not its header. There is no refund if processing later fails (same as the web app). One item per request. Poll `GET /jobs/{id}` until `status` is `succeeded` or `failed`.",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  { "$ref": "#/components/schemas/UploadJobRequest" },
                  { "$ref": "#/components/schemas/YoutubeJobRequest" }
                ],
                "discriminator": { "propertyName": "type" }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Job accepted.",
            "headers": { "Idempotent-Replayed": { "$ref": "#/components/headers/IdempotentReplayed" } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JobEnvelope" } } }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/InsufficientCredits" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "409": { "$ref": "#/components/responses/IdempotencyConflict" },
          "422": { "$ref": "#/components/responses/UnsupportedMedia" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/jobs/{id}": {
      "get": {
        "operationId": "getJob",
        "summary": "Poll a job",
        "description": "Requires `notes:read`. The job id is also the note id.",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "$ref": "#/components/parameters/Id" }],
        "responses": {
          "200": {
            "description": "Job status.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JobEnvelope" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/notes": {
      "get": {
        "operationId": "listNotes",
        "summary": "List your notes, newest first",
        "description": "Requires `notes:read`. Auto-deleted notes are excluded. Pass `next_cursor` back as `cursor` to get the next page.",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque cursor from a previous response's `next_cursor`.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of notes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data", "next_cursor"],
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/NoteSummary" } },
                    "next_cursor": { "type": ["string", "null"] }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/notes/{id}": {
      "get": {
        "operationId": "getNote",
        "summary": "Fetch a note with its transcript and latest summary",
        "description": "Requires `notes:read`. Another user's note returns 404.",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "$ref": "#/components/parameters/Id" }],
        "responses": {
          "200": {
            "description": "The note.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["note"],
                  "properties": { "note": { "$ref": "#/components/schemas/NoteDetail" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/notes/{id}/sharing": {
      "put": {
        "operationId": "setNoteSharing",
        "summary": "Share or unshare a note",
        "description": "Requires `notes:write`. Idempotent: setting the current value again succeeds.",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "$ref": "#/components/parameters/Id" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["public"],
                "properties": { "public": { "type": "boolean" } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new sharing state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["id", "is_public"],
                  "properties": { "id": { "type": "string" }, "is_public": { "type": "boolean" } }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/notes/{id}/export": {
      "get": {
        "operationId": "exportNote",
        "summary": "Export a transcript as plain text",
        "description": "Requires `notes:read`. One `[hh:mm:ss] Speaker: text` line per segment. Returns 400 `validation_error` when the note has no transcript yet.",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/Id" },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "enum": ["txt"], "default": "txt" }
          }
        ],
        "responses": {
          "200": {
            "description": "The transcript.",
            "content": { "text/plain": { "schema": { "type": "string" } } }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An Alfie API key (`alfie_` followed by 43 characters). Scopes: `notes:read`, `notes:write`."
      }
    },
    "parameters": {
      "Id": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": { "type": "string" }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "1–255 printable ASCII characters, kept for 24 hours. Replaying the same request returns the original response with `Idempotent-Replayed: true`; reusing the key with a different body returns 409 `idempotency_conflict`.",
        "schema": { "type": "string", "minLength": 1, "maxLength": 255 }
      }
    },
    "headers": {
      "IdempotentReplayed": {
        "description": "`true` when this response is a stored replay.",
        "schema": { "type": "string", "enum": ["true"] }
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying.",
        "schema": { "type": "integer" }
      }
    },
    "responses": {
      "ValidationError": {
        "description": "`validation_error`: the request is invalid.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Unauthorized": {
        "description": "`unauthorized`: the API key is missing, malformed, unknown or revoked.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "InsufficientCredits": {
        "description": "`insufficient_credits`: not enough remaining credits for this media.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Forbidden": {
        "description": "`insufficient_scope`: the key lacks the required scope; or `plan_limit`: the media is longer than your plan allows.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NotFound": {
        "description": "`not_found`: no such resource for this key's user (another user's id also returns 404).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "UnsupportedMedia": {
        "description": "`unsupported_media`: the uploaded file is not in a format accepted right now (see POST /uploads). No credits were reserved.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "MethodNotAllowed": {
        "description": "`method_not_allowed`.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "IdempotencyConflict": {
        "description": "`idempotency_conflict`: the key was used with a different request; or `idempotency_in_progress`: the first request is still running (see Retry-After).",
        "headers": { "Retry-After": { "$ref": "#/components/headers/RetryAfter" } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "RateLimited": {
        "description": "`rate_limited`: too many requests for this key.",
        "headers": { "Retry-After": { "$ref": "#/components/headers/RetryAfter" } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "InternalError": {
        "description": "`internal_error`.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "unauthorized",
                  "insufficient_scope",
                  "not_found",
                  "validation_error",
                  "insufficient_credits",
                  "plan_limit",
                  "unsupported_media",
                  "method_not_allowed",
                  "idempotency_conflict",
                  "idempotency_in_progress",
                  "rate_limited",
                  "internal_error"
                ]
              },
              "message": { "type": "string" },
              "details": { "type": "object", "additionalProperties": true }
            }
          }
        }
      },
      "Me": {
        "type": "object",
        "required": ["plan", "remaining_usage_seconds"],
        "properties": {
          "plan": { "type": "string" },
          "remaining_usage_seconds": { "type": "integer" }
        }
      },
      "Upload": {
        "type": "object",
        "required": ["upload_id", "upload_url", "expires_at", "required_headers"],
        "properties": {
          "upload_id": { "type": "string" },
          "upload_url": { "type": "string", "format": "uri" },
          "expires_at": { "type": "string", "format": "date-time" },
          "required_headers": {
            "type": "object",
            "required": ["Content-Type"],
            "properties": { "Content-Type": { "type": "string", "description": "The content_type from the request. Send it exactly on the PUT." } }
          }
        }
      },
      "UploadJobRequest": {
        "type": "object",
        "required": ["type", "upload_id"],
        "properties": {
          "type": { "type": "string", "const": "upload" },
          "upload_id": { "type": "string" },
          "filename": { "type": "string", "maxLength": 500 },
          "speaker_count": { "type": "integer", "minimum": 1, "maximum": 20 },
          "language": { "type": "string", "maxLength": 32 },
          "prompt": { "type": "string", "maxLength": 4000 },
          "region": { "type": "string", "enum": ["us", "eu"] }
        }
      },
      "YoutubeJobRequest": {
        "type": "object",
        "required": ["type", "url"],
        "properties": {
          "type": { "type": "string", "const": "youtube" },
          "url": { "type": "string", "maxLength": 2048 },
          "language": { "type": "string", "maxLength": 32 }
        }
      },
      "JobStatus": {
        "type": "string",
        "enum": ["queued", "processing", "succeeded", "failed", "expired"]
      },
      "Job": {
        "type": "object",
        "required": ["id", "note_id", "status", "created_at"],
        "properties": {
          "id": { "type": "string" },
          "note_id": { "type": "string" },
          "status": { "$ref": "#/components/schemas/JobStatus" },
          "created_at": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "JobEnvelope": {
        "type": "object",
        "required": ["job"],
        "properties": { "job": { "$ref": "#/components/schemas/Job" } }
      },
      "NoteSummary": {
        "type": "object",
        "required": ["id", "title", "status", "duration_seconds", "source", "created_at", "is_public"],
        "properties": {
          "id": { "type": "string" },
          "title": { "type": "string" },
          "status": { "$ref": "#/components/schemas/JobStatus" },
          "duration_seconds": { "type": ["integer", "null"] },
          "source": { "type": "string" },
          "created_at": { "type": "string", "format": "date-time" },
          "is_public": { "type": "boolean" }
        }
      },
      "TranscriptSegment": {
        "type": "object",
        "required": ["start", "end", "text", "speaker"],
        "properties": {
          "start": { "type": "number", "description": "Seconds from the start." },
          "end": { "type": "number" },
          "text": { "type": "string" },
          "speaker": { "type": ["string", "null"] }
        }
      },
      "NoteDetail": {
        "allOf": [
          { "$ref": "#/components/schemas/NoteSummary" },
          {
            "type": "object",
            "required": ["region", "transcribed_at", "transcript", "artifact"],
            "properties": {
              "region": { "type": ["string", "null"] },
              "transcribed_at": { "type": ["string", "null"], "format": "date-time" },
              "transcript": { "type": "array", "items": { "$ref": "#/components/schemas/TranscriptSegment" } },
              "artifact": {
                "description": "The latest generated summary, or null.",
                "oneOf": [
                  { "type": "null" },
                  {
                    "type": "object",
                    "required": ["id", "content", "created_at"],
                    "properties": {
                      "id": { "type": "string" },
                      "content": {},
                      "created_at": { "type": "string", "format": "date-time" }
                    }
                  }
                ]
              }
            }
          }
        ]
      }
    }
  }
}
