{
  "openapi": "3.1.0",
  "info": {
    "title": "Narrator Check API",
    "version": "1.0.0",
    "summary": "ACX check and fix of audiobook chapters, loudness normalization and proof-listening text-to-speech, over HTTP.",
    "description": "The tools on narratorcheck.com never upload your audio; this API is the separate, opt-in surface that processes files key holders send.\n\nEvery POST is bodyless: parameters go in the query string, and the file goes to S3 on the presigned PUT the create call returns, with exactly the headers it returns.\nUploads are deleted when the job finishes; results expire within 48 hours.\nA 403 or 404 whose body is HTML is the CDN's error page for a wrong path, never a refusal from this API.\n\nPlans:\n- Free: 60 audio minutes and 10,000 TTS characters a month, 1 job a minute, 20 jobs a day.\n- Pro, $12 a month (free during beta): 1,200 audio minutes and 200,000 TTS characters a month, 5 jobs a minute, no daily job limit.\nAcross everybody, at most 3,000 audio minutes and 500,000 TTS characters a day.\nLimits per file: 400 MiB, 120 minutes, 96 kHz, 2 channels; TTS 10,000 characters.",
    "contact": {
      "email": "support@narratorcheck.com",
      "url": "https://narratorcheck.com/developers/"
    },
    "termsOfService": "https://narratorcheck.com/terms/"
  },
  "servers": [
    {
      "url": "https://narratorcheck.com"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "paths": {
    "/api/v1/jobs": {
      "post": {
        "operationId": "createJob",
        "summary": "Create an audio job and get a presigned upload URL",
        "description": "Bodyless. Creates the job, then answers with a presigned S3 PUT; the job runs when the upload lands. acx-check measures a chapter against a delivery target; acx-fix corrects it and returns a 192 kbps CBR MP3 re-measured from its own bytes; loudness-fix normalizes integrated loudness (LUFS) to a preset. WAV, AIFF and MP3 only: M4A, M4B and AAC are refused with 422 and a link to /m4a-to-mp3/.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "name": "kind",
            "in": "query",
            "required": true,
            "description": "What to do with the file.",
            "schema": {
              "type": "string",
              "enum": [
                "acx-check",
                "acx-fix",
                "loudness-fix"
              ]
            }
          },
          {
            "name": "target",
            "in": "query",
            "required": false,
            "description": "Delivery target for acx-check and acx-fix. Default acx.",
            "schema": {
              "type": "string",
              "enum": [
                "acx",
                "inaudio",
                "spotify",
                "authorsRepublic"
              ],
              "default": "acx"
            }
          },
          {
            "name": "preset",
            "in": "query",
            "required": false,
            "description": "Loudness preset for loudness-fix. Default podcast (-16 LUFS, -1 dBTP).",
            "schema": {
              "type": "string",
              "enum": [
                "podcast",
                "mono",
                "voiceover"
              ],
              "default": "podcast"
            }
          },
          {
            "name": "bytes",
            "in": "query",
            "required": true,
            "description": "Exact size of the audio file you will PUT, in bytes. S3 refuses an upload of any other length.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 419430400
            }
          },
          {
            "name": "sha256",
            "in": "query",
            "required": true,
            "description": "SHA-256 of the audio file, base64 (44 characters, the value S3 calls x-amz-checksum-sha256). S3 refuses any other content.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9+/]{43}=$"
            }
          },
          {
            "name": "ext",
            "in": "query",
            "required": true,
            "description": "The file's format. M4A, M4B and AAC are refused.",
            "schema": {
              "type": "string",
              "enum": [
                "wav",
                "aiff",
                "mp3"
              ]
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Optional, 1-64 characters from [A-Za-z0-9_-]. Repeating a create with the same key within 24 hours returns the same job and a fresh upload URL instead of a second job; the same key with different parameters is 409 idempotency_key_reused.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Created. PUT the file to upload.url with exactly upload.headers, then poll.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobCreated"
                }
              }
            }
          },
          "401": {
            "description": "No key, or a key that is unknown or revoked. Codes: key_required, key_unknown, key_revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          },
          "409": {
            "description": "The Idempotency-Key was used for a different request. Codes: idempotency_key_reused.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          },
          "422": {
            "description": "A parameter is missing or out of range, the format is refused, or the file cannot fit the plan. Codes: bad_parameter, format_refused, too_large, cost_exceeds_limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          },
          "429": {
            "description": "A limit: per-minute or per-day jobs, the monthly quota, or the site's daily ceiling. Retry-After says when. Codes: rate_limited, daily_limit, quota_exceeded, site_daily_ceiling.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          },
          "503": {
            "description": "The API is not open yet, or not configured on this deployment. Codes: not_open, metering_not_configured, jobs_not_configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listJobs",
        "summary": "List your recent jobs, newest first",
        "description": "Jobs are kept for three days.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "How many, 1-50. Default 20.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Your jobs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobList"
                }
              }
            }
          },
          "401": {
            "description": "No key, or a key that is unknown or revoked. Codes: key_required, key_unknown, key_revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          },
          "422": {
            "description": "A parameter is out of range. Codes: bad_parameter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          },
          "503": {
            "description": "The API is not open yet, or not configured on this deployment. Codes: not_open, metering_not_configured, jobs_not_configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tts": {
      "post": {
        "operationId": "createTts",
        "summary": "Create a text-to-speech job for proof-listening and get a presigned upload URL",
        "description": "Bodyless. Upload the text as UTF-8 plain text. The result is an MP3 read by a synthetic voice (Kokoro-82M), labelled synthetic in its ID3 tags. It is for proof-listening a manuscript; ACX does not accept synthetic narration, and nothing here clones a voice.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "name": "voice",
            "in": "query",
            "required": true,
            "description": "One of the built-in English voices.",
            "schema": {
              "type": "string",
              "enum": [
                "af_heart",
                "af_bella",
                "af_nicole",
                "am_michael",
                "am_fenrir",
                "bf_emma",
                "bm_george",
                "bm_fable"
              ]
            }
          },
          {
            "name": "chars",
            "in": "query",
            "required": true,
            "description": "Characters in the text, at most 10,000. The job is charged on the characters it actually reads.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            }
          },
          {
            "name": "bytes",
            "in": "query",
            "required": true,
            "description": "Exact size of the UTF-8 text you will PUT, in bytes. S3 refuses an upload of any other length.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 40000
            }
          },
          {
            "name": "sha256",
            "in": "query",
            "required": true,
            "description": "SHA-256 of the UTF-8 text, base64 (44 characters, the value S3 calls x-amz-checksum-sha256). S3 refuses any other content.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9+/]{43}=$"
            }
          },
          {
            "name": "master",
            "in": "query",
            "required": false,
            "description": "none returns the voice as synthesized; acx also runs the ACX level fix on it. Default none.",
            "schema": {
              "type": "string",
              "enum": [
                "none",
                "acx"
              ],
              "default": "none"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Optional, 1-64 characters from [A-Za-z0-9_-]. Repeating a create with the same key within 24 hours returns the same job and a fresh upload URL instead of a second job; the same key with different parameters is 409 idempotency_key_reused.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Created. PUT the text to upload.url with exactly upload.headers, then poll.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobCreated"
                }
              }
            }
          },
          "401": {
            "description": "No key, or a key that is unknown or revoked. Codes: key_required, key_unknown, key_revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          },
          "409": {
            "description": "The Idempotency-Key was used for a different request. Codes: idempotency_key_reused.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          },
          "422": {
            "description": "A parameter is missing or out of range. Codes: bad_parameter, too_large, cost_exceeds_limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          },
          "429": {
            "description": "A limit: per-minute or per-day jobs, the monthly quota, or the site's daily ceiling. Retry-After says when. Codes: rate_limited, daily_limit, quota_exceeded, site_daily_ceiling.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          },
          "503": {
            "description": "The API is not open yet, or not configured on this deployment. Codes: not_open, metering_not_configured, jobs_not_configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/jobs/{id}": {
      "get": {
        "operationId": "getJob",
        "summary": "Read a job, with a download URL once it has succeeded",
        "description": "Poll every few seconds. A job that no longer exists, or is not yours, is 410 job_gone.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The job id returned by the create call.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{16,64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "401": {
            "description": "No key, or a key that is unknown or revoked. Codes: key_required, key_unknown, key_revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          },
          "410": {
            "description": "No such job for this key: never created, expired after three days, or not yours. Codes: job_gone.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          },
          "503": {
            "description": "The API is not open yet, or not configured on this deployment. Codes: not_open, metering_not_configured, jobs_not_configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "cancelJob",
        "summary": "Cancel a job that has not finished",
        "description": "Send no body: CloudFront refuses a DELETE that carries one. A cancelled job is not charged unless its work had already been measured.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The job id returned by the create call.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{16,64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled, or already cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "401": {
            "description": "No key, or a key that is unknown or revoked. Codes: key_required, key_unknown, key_revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          },
          "409": {
            "description": "The job has already finished. Codes: job_finished.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          },
          "410": {
            "description": "No such job for this key. Codes: job_gone.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          },
          "503": {
            "description": "The API is not open yet, or not configured on this deployment. Codes: not_open, metering_not_configured, jobs_not_configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/usage": {
      "get": {
        "operationId": "usage",
        "summary": "Your plan and this month's usage on each meter",
        "description": "Audio in minutes; text-to-speech in thousands of characters.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Usage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                }
              }
            }
          },
          "401": {
            "description": "No key, or a key that is unknown or revoked. Codes: key_required, key_unknown, key_revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          },
          "503": {
            "description": "The API is not open yet, or not configured on this deployment. Codes: not_open, metering_not_configured, jobs_not_configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "Create a key at /account/. Never send it as Authorization: Bearer; the CDN overwrites that header."
      }
    },
    "schemas": {
      "Refusal": {
        "type": "object",
        "required": [
          "code"
        ],
        "properties": {
          "code": {
            "type": "string"
          },
          "detail": {},
          "plan": {
            "type": "string"
          }
        }
      },
      "Upload": {
        "type": "object",
        "required": [
          "method",
          "url",
          "headers",
          "expiresAt"
        ],
        "properties": {
          "method": {
            "const": "PUT"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "headers": {
            "type": "object",
            "required": [
              "content-length",
              "x-amz-checksum-sha256"
            ],
            "properties": {
              "content-length": {
                "type": "string"
              },
              "x-amz-checksum-sha256": {
                "type": "string"
              }
            }
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "JobCreated": {
        "type": "object",
        "required": [
          "jobId",
          "state",
          "upload",
          "poll"
        ],
        "properties": {
          "jobId": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{16,64}$"
          },
          "state": {
            "const": "pending"
          },
          "upload": {
            "$ref": "#/components/schemas/Upload"
          },
          "poll": {
            "type": "string",
            "description": "The path to GET for the job's state."
          },
          "replayed": {
            "type": "boolean",
            "description": "True when an Idempotency-Key returned an existing job."
          }
        }
      },
      "Job": {
        "type": "object",
        "required": [
          "id",
          "kind",
          "state",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "acx-check",
              "acx-fix",
              "loudness-fix",
              "tts"
            ]
          },
          "state": {
            "type": "string",
            "enum": [
              "pending",
              "running",
              "succeeded",
              "failed",
              "cancelled"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "startedAt": {
            "type": "string",
            "format": "date-time"
          },
          "finishedAt": {
            "type": "string",
            "format": "date-time"
          },
          "progress": {
            "type": "object",
            "properties": {
              "fraction": {
                "type": "number"
              },
              "message": {
                "type": "string"
              }
            }
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              }
            }
          },
          "result": {
            "type": "object",
            "properties": {
              "ext": {
                "type": "string"
              },
              "bytes": {
                "type": "integer"
              },
              "sha256": {
                "type": "string"
              },
              "contentType": {
                "type": "string"
              },
              "meta": {
                "type": "object",
                "description": "The report: the verdict and measurements, or the TTS provenance."
              }
            }
          },
          "charge": {
            "type": "object",
            "properties": {
              "meter": {
                "type": "string"
              },
              "units": {
                "type": "integer"
              }
            }
          },
          "download": {
            "type": "object",
            "description": "Present once the job has succeeded and while the result exists.",
            "properties": {
              "method": {
                "const": "GET"
              },
              "url": {
                "type": "string",
                "format": "uri"
              },
              "expiresAt": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "JobList": {
        "type": "object",
        "required": [
          "jobs"
        ],
        "properties": {
          "jobs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Job"
            }
          }
        }
      },
      "Usage": {
        "type": "object",
        "required": [
          "plan",
          "meters"
        ],
        "properties": {
          "plan": {
            "type": "string"
          },
          "meters": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "month": {
                  "type": "string"
                },
                "used": {
                  "type": "number"
                },
                "limit": {
                  "type": "number"
                },
                "remaining": {
                  "type": "number"
                },
                "unit": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  }
}
