{
  "openapi": "3.1.0",
  "info": {
    "title": "Ondoku (音読さん) Text-to-Speech API",
    "version": "2026-10-02",
    "summary": "Official text-to-speech APIs of Ondoku (音読さん).",
    "description": "Official APIs of Ondoku (音読さん), a text-to-speech service.\n\n- Standard TTS API (`POST /text_to_speech_api/`): official API. SLA: 99.0% monthly uptime.\n- Advanced TTS API (`POST /api/advanced-tts/`, `GET /api/advanced-tts/jobs/{job_id}/`): official API in Beta. SLA: 98.0% monthly uptime. Asynchronous jobs with emotion and speaking-style control.\n\nSLA: measured per calendar month in JST. A 5-minute interval counts as downtime when it had requests and all of them returned HTTP 5xx. For the Advanced TTS API, both job creation and polling are counted; failed speech generation (job status `failed`) is not counted. Excluded: announced scheduled maintenance, outages of speech-synthesis providers or cloud infrastructure, client-side causes (HTTP 4xx, including rate limits and insufficient characters), and force majeure. If the SLA is missed, paid-plan users who apply by the end of the following month receive 10% of their plan's monthly character allowance for that month (no cash refunds). The SLA covers uptime only, not response time. Definition: https://ondoku3.com/en/developer/api/standard-tts.html#sla (Japanese: https://ondoku3.com/ja/developer/api/standard-tts.html#sla)\n\nChanges: breaking changes (removing fields, changing types or meaning, retiring endpoints) are announced 30 days in advance in the changelog section of each documentation page. Notable backward-compatible changes such as new fields or voices are recorded in the same changelog after release (no advance notice). Urgent security fixes are an exception.\n\nAuthentication: send your 64-character access token in the `token` header. Advanced TTS job polling uses the per-job `X-Job-Token` header.\n\nHuman-readable documentation: https://ondoku3.com/en/developer/api/ (English) and https://ondoku3.com/ja/developer/api/ (Japanese), with the same terms.",
    "termsOfService": "https://ondoku3.com/ja/terms/",
    "contact": {
      "name": "Ondoku support",
      "url": "https://ondoku3.com/ja/contact/"
    }
  },
  "externalDocs": {
    "description": "API documentation (English). Japanese version: https://ondoku3.com/ja/developer/api/",
    "url": "https://ondoku3.com/en/developer/api/"
  },
  "servers": [
    {
      "url": "https://ondoku3.com"
    }
  ],
  "tags": [
    {
      "name": "Standard TTS",
      "description": "Official API. SLA: 99.0% monthly uptime.",
      "externalDocs": {
        "description": "Japanese version: https://ondoku3.com/ja/developer/api/standard-tts.html",
        "url": "https://ondoku3.com/en/developer/api/standard-tts.html"
      }
    },
    {
      "name": "Advanced TTS (Beta)",
      "description": "Official API in Beta. SLA: 98.0% monthly uptime.",
      "externalDocs": {
        "description": "Japanese version: https://ondoku3.com/ja/developer/api/advanced-tts.html",
        "url": "https://ondoku3.com/en/developer/api/advanced-tts.html"
      }
    }
  ],
  "paths": {
    "/text_to_speech_api/": {
      "post": {
        "tags": [
          "Standard TTS"
        ],
        "operationId": "createStandardSpeech",
        "summary": "Synthesize speech (Standard TTS API)",
        "description": "Synthesizes speech and returns the URL of the generated MP3. Download the audio from `url`. All four body fields (`text`, `voice`, `speed`, `pitch`) are required. Rate limit: 30 requests per 60 seconds per client IP address (shared across all tokens used from the same IP). Monthly character allowance depends on the plan. CORS is not enabled; call it from your server.",
        "security": [
          {
            "tokenAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StandardTTSRequest"
              },
              "example": {
                "voice": "ja-JP-NanamiNeural",
                "text": "こんにちは、音読さんです。",
                "speed": 1.0,
                "pitch": 0.0
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Generated audio. Some paths (for example, when the monthly character allowance is insufficient) also return HTTP 200 with the same shape, where `url` points to a guidance audio. Do not rely on the HTTP status alone; also check the returned `text` / `url`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StandardTTSResponse"
                },
                "example": {
                  "text": "こんにちは、音読さんです。",
                  "url": "https://storage.googleapis.com/ondoku3/voice/...",
                  "pk": 12345,
                  "share": 1
                }
              }
            }
          },
          "400": {
            "description": "Bad request. `voice parameter is required` (voice missing or not a string), `Unsupported voice requested.` (voice not in the voice list), `Please set \"voice\", \"speed\", \"pitch\", \"text\"` (a required field is missing), `Token does not exists` (token header missing or not 64 characters), `Token does not match` (invalid token), `\"speed\" and \"pitch\" must be finite numbers` (speed / pitch is NaN or Infinity). Do not retry; fix the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StandardTTSError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (30 requests per 60 seconds per client IP address, shared across tokens).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StandardTTSError"
                }
              }
            }
          },
          "500": {
            "description": "Server error. Retry later with exponential backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StandardTTSError"
                },
                "example": {
                  "error": "Internal server error"
                }
              }
            }
          }
        }
      }
    },
    "/api/advanced-tts/": {
      "post": {
        "tags": [
          "Advanced TTS (Beta)"
        ],
        "operationId": "createAdvancedSpeechJob",
        "summary": "Create a speech job (Advanced TTS API, Beta)",
        "description": "Creates an asynchronous speech job with emotion and speaking-style control. Poll `GET /api/advanced-tts/jobs/{job_id}/` with the returned `job_token` in the `X-Job-Token` header. Authenticate with the `token` header (no CSRF token needed). A logged-in session cookie plus `X-CSRFToken` and `Referer: https://ondoku3.com/ja/advanced-tts-beta/` is also accepted. The API accepts up to 400 characters per request on the free plan and 3,000 on paid plans. Rate limit: 10 job creations per 60 seconds per client IP address. Errors are JSON with `code` and `error` (except 403 CSRF failures and 405); branch on `code`.",
        "security": [
          {
            "tokenAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AdvancedTTSRequest"
              },
              "example": {
                "text": "こんにちは、音読さんです。",
                "voice": "Ruby",
                "tone": "warm and friendly"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Job accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdvancedTTSJobResponse"
                },
                "example": {
                  "job_id": "YOUR_JOB_ID",
                  "job_token": "YOUR_JOB_TOKEN",
                  "status": "pending",
                  "min_poll_after_ms": 4550,
                  "poll_url": "/api/advanced-tts/jobs/YOUR_JOB_ID/"
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Do not retry; fix the request. `code`: `invalid_token` (token header not 64 characters or no matching active user), `invalid_json` (body is not JSON), `validation_error` (text shorter than 10 characters, emoji only, or the same character repeated 20+ times), `invalid_speed` / `invalid_pitch` (not a number or out of range), `invalid_temperature` (an undocumented `temperature` field was sent with an invalid value), `invalid_seed` (not an integer or out of range), `invalid_model` (not `flash` / `pro`), `client_ip_unresolved` (no token, not logged in, and the client IP cannot be determined), `text_too_long` (over 400 characters on the free plan / 3,000 on paid plans), `invalid_voice` (unknown voice), `content_policy_violation` (text blocked by the content policy), `invalid_request` (other input errors, e.g. the text after user-dictionary replacement exceeds the limit).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdvancedTTSError"
                }
              }
            }
          },
          "403": {
            "description": "Session authentication without the `token` header: CSRF mismatch or not logged in. The body is HTML.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "`quota_exceeded` (not enough available characters; `available` holds the remaining characters) or `rate_limited` (10 job creations per 60 seconds per client IP address exceeded; wait `retry_after` seconds, also sent as the `Retry-After` header). Check `code` in the JSON body, not only the HTTP status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdvancedTTSError"
                },
                "example": {
                  "code": "rate_limited",
                  "error": "レート制限を超えました。しばらく経ってから再試行してください。",
                  "retry_after": 55
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (only with `rate_limited`).",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. No JSON body."
          },
          "500": {
            "description": "`service_not_configured` (server misconfiguration) or `internal_error` (server error; retry later).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdvancedTTSError"
                }
              }
            }
          },
          "503": {
            "description": "`unsupported_location` (speech synthesis is not available for the processing location; `error_code: UNSUPPORTED_LOCATION` is also returned for compatibility) or `tts_retry_exhausted` (temporarily unavailable; retry later).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdvancedTTSError"
                }
              }
            }
          }
        }
      }
    },
    "/api/advanced-tts/jobs/{job_id}/": {
      "get": {
        "tags": [
          "Advanced TTS (Beta)"
        ],
        "operationId": "getAdvancedSpeechJob",
        "summary": "Poll a speech job (Advanced TTS API, Beta)",
        "description": "Returns the job status. Send the `job_token` from the job creation response in the `X-Job-Token` header; the `?token=` query parameter is not supported. Wait at least `min_poll_after_ms` before the first poll and `poll_after_ms` between polls (3-5 seconds when absent). A failed job is returned as HTTP 200 with `status: failed`; check `status` / `code` in the JSON body.",
        "security": [
          {
            "jobToken": []
          }
        ],
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "description": "Job ID (UUID) returned by job creation.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Job status: `pending` -> `running` -> `succeeded` or `failed`. `url` and `pk` are returned only on success; `code` and `error` only on failure (see `AdvancedTTSJobFailureCode`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdvancedTTSPollResponse"
                },
                "examples": {
                  "pending": {
                    "value": {
                      "job_id": "YOUR_JOB_ID",
                      "status": "pending",
                      "poll_after_ms": 1000
                    }
                  },
                  "succeeded": {
                    "value": {
                      "job_id": "YOUR_JOB_ID",
                      "status": "succeeded",
                      "url": "https://storage.googleapis.com/ondoku3/voice/...",
                      "pk": 12345,
                      "seed": 123456789
                    }
                  },
                  "failed": {
                    "value": {
                      "job_id": "YOUR_JOB_ID",
                      "status": "failed",
                      "code": "internal_error",
                      "error": "内部サーバーエラーが発生しました"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`forbidden`: the `X-Job-Token` does not match the job (with a logged-in session: the job belongs to another user).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdvancedTTSPollError"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: the job does not exist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdvancedTTSPollError"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. No JSON body."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "tokenAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "token",
        "description": "64-character access token, available at https://ondoku3.com/ja/users/extension/ after signing in."
      },
      "jobToken": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Job-Token",
        "description": "Per-job token (`job_token`) returned by POST /api/advanced-tts/."
      }
    },
    "schemas": {
      "StandardTTSRequest": {
        "type": "object",
        "required": [
          "text",
          "voice",
          "speed",
          "pitch"
        ],
        "properties": {
          "voice": {
            "type": "string",
            "description": "Voice ID, e.g. `ja-JP-NanamiNeural`. See https://ondoku3.com/en/developer/api/voices.html (Japanese: https://ondoku3.com/ja/developer/api/voices.html)"
          },
          "text": {
            "type": "string",
            "description": "Text to read aloud. Must not be empty."
          },
          "speed": {
            "type": "number",
            "description": "Speed. Valid range 0.3 to 4.0 (e.g. 1.0). Out-of-range values are treated as the default 1.0."
          },
          "pitch": {
            "type": "number",
            "description": "Pitch. Valid range -20 to 20 (e.g. 0.0). Out-of-range values are treated as the default 0."
          }
        }
      },
      "StandardTTSResponse": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "description": "Text used for synthesis."
          },
          "url": {
            "type": "string",
            "description": "URL of the generated audio file, without the query string."
          },
          "pk": {
            "type": "number",
            "description": "ID of the generation history entry (Sentence)."
          },
          "share": {
            "type": "number",
            "description": "Share flag. Usually 1; 0 for some error audio."
          }
        }
      },
      "StandardTTSError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Error message."
          },
          "reset_in": {
            "type": "number",
            "description": "Returned only when the rate limit is exceeded."
          }
        }
      },
      "AdvancedTTSVoice": {
        "type": "string",
        "description": "Advanced TTS voice name (25 voices). Samples: https://ondoku3.com/ja/post/listen-advanced-tts-voice-sample/",
        "enum": [
          "Anna",
          "Chloe",
          "Ellis",
          "Emma",
          "Flora",
          "Iris",
          "Lena",
          "Luna",
          "Misa",
          "Ruby",
          "Sophie",
          "Tina",
          "Ash",
          "Chris",
          "Eden",
          "Gray",
          "Hope",
          "Hugo",
          "Kai",
          "Leo",
          "Noah",
          "Reid",
          "Roy",
          "Sam",
          "Yann"
        ]
      },
      "AdvancedTTSStatus": {
        "type": "string",
        "enum": [
          "pending",
          "running",
          "succeeded",
          "failed"
        ]
      },
      "AdvancedTTSRequest": {
        "type": "object",
        "required": [
          "text"
        ],
        "properties": {
          "text": {
            "type": "string",
            "description": "Text to read aloud. At least 10 characters. The API accepts up to 400 characters on the free plan and 3,000 on paid plans.",
            "minLength": 10
          },
          "voice": {
            "$ref": "#/components/schemas/AdvancedTTSVoice",
            "default": "Ruby"
          },
          "tone": {
            "type": "string",
            "maxLength": 100,
            "description": "Speaking style, e.g. `warm and friendly`, `storytelling`."
          },
          "speed": {
            "type": "number",
            "minimum": 0.5,
            "maximum": 1.5,
            "default": 1.0
          },
          "pitch": {
            "type": "number",
            "minimum": -1.0,
            "maximum": 1.0,
            "default": 0.0
          },
          "model": {
            "type": "string",
            "enum": [
              "flash",
              "pro"
            ],
            "description": "`flash` (faster) or `pro` (higher quality)."
          },
          "seed": {
            "type": "integer",
            "minimum": -2147483648,
            "maximum": 2147483647,
            "description": "Random seed that fixes the voice timbre."
          }
        }
      },
      "AdvancedTTSJobResponse": {
        "type": "object",
        "required": [
          "job_id",
          "job_token",
          "status",
          "min_poll_after_ms",
          "poll_url"
        ],
        "properties": {
          "job_id": {
            "type": "string",
            "description": "Job ID (UUID)."
          },
          "job_token": {
            "type": "string",
            "description": "Token required to poll this job. Send it in the `X-Job-Token` header."
          },
          "status": {
            "$ref": "#/components/schemas/AdvancedTTSStatus",
            "description": "Status right after creation. Usually `pending`."
          },
          "min_poll_after_ms": {
            "type": "number",
            "description": "Recommended wait in milliseconds before the first poll."
          },
          "poll_url": {
            "type": "string",
            "description": "Relative URL to poll."
          }
        }
      },
      "AdvancedTTSPollResponse": {
        "type": "object",
        "required": [
          "job_id",
          "status"
        ],
        "properties": {
          "job_id": {
            "type": "string",
            "description": "Job ID (UUID)."
          },
          "status": {
            "$ref": "#/components/schemas/AdvancedTTSStatus"
          },
          "poll_after_ms": {
            "type": "number",
            "description": "Returned while processing: recommended wait in milliseconds before the next poll."
          },
          "url": {
            "type": "string",
            "description": "Returned only on success: URL of the audio file (time-limited signed URL; download it promptly)."
          },
          "pk": {
            "type": "number",
            "description": "Returned only on success: ID of the generation history entry (Sentence)."
          },
          "seed": {
            "type": "number",
            "description": "Returned only when a seed is stored: the random seed used for generation."
          },
          "code": {
            "$ref": "#/components/schemas/AdvancedTTSJobFailureCode",
            "description": "Returned only on failure: error code."
          },
          "error": {
            "type": "string",
            "description": "Returned only on failure: error message."
          }
        }
      },
      "AdvancedTTSError": {
        "type": "object",
        "required": [
          "code",
          "error"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Machine-readable error code. New codes may be added without prior notice, so treat unknown codes as failures.",
            "anyOf": [
              {
                "enum": [
                  "invalid_token",
                  "invalid_json",
                  "validation_error",
                  "invalid_speed",
                  "invalid_pitch",
                  "invalid_temperature",
                  "invalid_seed",
                  "invalid_model",
                  "client_ip_unresolved",
                  "text_too_long",
                  "invalid_voice",
                  "content_policy_violation",
                  "invalid_request",
                  "quota_exceeded",
                  "rate_limited",
                  "service_not_configured",
                  "internal_error",
                  "unsupported_location",
                  "tts_retry_exhausted"
                ]
              },
              {
                "type": "string"
              }
            ]
          },
          "error": {
            "type": "string",
            "description": "Error message."
          },
          "retry_after": {
            "type": "integer",
            "description": "Seconds to wait before retrying (`rate_limited` only)."
          },
          "available": {
            "type": "integer",
            "description": "Remaining available characters (`quota_exceeded` only)."
          },
          "error_code": {
            "type": "string",
            "description": "`UNSUPPORTED_LOCATION`, kept for compatibility (`unsupported_location` only)."
          }
        }
      },
      "AdvancedTTSPollError": {
        "type": "object",
        "required": [
          "code",
          "error"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Machine-readable error code. New codes may be added without prior notice, so treat unknown codes as failures.",
            "anyOf": [
              {
                "enum": [
                  "forbidden",
                  "not_found"
                ]
              },
              {
                "type": "string"
              }
            ]
          },
          "error": {
            "type": "string",
            "description": "Error message."
          }
        }
      },
      "AdvancedTTSJobFailureCode": {
        "type": "string",
        "description": "`code` of a job with status `failed`. The speech-generation process may return other codes too; treat unknown codes as failures.",
        "examples": [
          "content_policy_violation",
          "unsupported_location",
          "tts_retry_exhausted",
          "invalid_request",
          "job_timeout",
          "enqueue_failed",
          "user_unavailable",
          "internal_error"
        ]
      }
    }
  }
}
