{
 "openapi": "3.1.0",
 "info": {
  "title": "MOZG Caller API",
  "version": "1.0.0",
  "description": "Голосовой ассистент-порученец: звонит на телефон и решает задачу голосом на индонезийском, английском или русском — узнать цену, спросить свободное время, забронировать столик, записаться на приём. Звонок асинхронный: POST /v1/call возвращает conversation_id сразу, результат забирается через GET /v1/call/{conversation_id}."
 },
 "servers": [
  {
   "url": "https://caller.mozg.in",
   "description": "публичный вход"
  },
  {
   "url": "http://127.0.0.1:8099",
   "description": "с самого Гермеса"
  }
 ],
 "security": [
  {
   "bearerAuth": []
  }
 ],
 "components": {
  "securitySchemes": {
   "bearerAuth": {
    "type": "http",
    "scheme": "bearer",
    "description": "Токен из /root/callagent/.env, ключ CALLAPI_TOKEN. Можно и ?token=<токен> в query."
   }
  },
  "schemas": {
   "CallRequest": {
    "type": "object",
    "required": [
     "to"
    ],
    "properties": {
     "to": {
      "type": "string",
      "description": "Номер в международном формате. «+», пробелы и скобки допустимы.",
      "examples": [
       "+6281234567890"
      ]
     },
     "language": {
      "type": "string",
      "enum": [
       "id",
       "en",
       "ru"
      ],
      "default": "id",
      "description": "Язык первой фразы и голоса. Внутри разговора агент сам переключится на любой из трёх, если собеседник ответит на другом."
     },
     "place": {
      "type": "string",
      "description": "Куда звоним: название заведения, клиники, магазина. Агент называет его в разговоре.",
      "examples": [
       "Warung Bu Mi, Canggu"
      ]
     },
     "task": {
      "type": "string",
      "description": "ГЛАВНОЕ ПОЛЕ. Задача звонка своими словами, одним-двумя предложениями: что выяснить или о чём договориться."
     },
     "details": {
      "type": "string",
      "description": "Детали поручения: дата, время, сколько человек, имя для брони, бюджет, предпочтения. Всё, чего здесь нет, агент выдумывать не станет."
     },
     "callback_phone": {
      "type": "string",
      "description": "Телефон, который агент даёт для брони или обратной связи."
     },
     "context": {
      "type": "string",
      "description": "Что ещё известно: были ли раньше, откуда номер, чего опасаемся."
     },
     "principal": {
      "type": "string",
      "description": "Устаревшее. Роль и «от кого звонок» теперь целиком в persona. Поле принимается для совместимости, но ничего не навязывает."
     },
     "agent_name": {
      "type": "string",
      "description": "Имя ассистента для приветствия (в русском звонке — кириллицей, иначе синтез прочтёт латиницу по-английски). Не прислали — агент без имени."
     },
     "greeting": {
      "type": "string",
      "description": "Своя первая фраза целиком, вместо сгенерированной. На языке звонка."
     },
     "persona": {
      "type": "string",
      "description": "КТО агент и от кого звонит — задаёте здесь. Это главное поле роли: «Ты — Алекс, консьерж семьи Росси» / «You are a personal assistant calling on behalf of John». Зашитой личности у сервиса нет: не прислали persona — нейтральный вежливый ассистент без имени."
     },
     "prompt": {
      "type": "string",
      "description": "Весь системный промпт на этот звонок. Нужен редко: обычно хватает task и details."
     },
     "voice_id": {
      "type": "string",
      "description": "Другой голос ElevenLabs на этот звонок."
     },
     "dry": {
      "type": "boolean",
      "default": false,
      "description": "true — не набирать, а показать, что ушло бы. Так проверяют формулировки, не тратя звонок."
     },
     "reason": {
      "type": "string",
      "description": "Повод звонка одной короткой фразой НА ЯЗЫКЕ ЗВОНКА — попадает в первую фразу: «Здравствуйте! Хочу забронировать столик на сегодня. Подскажете?». Без него приветствие нейтральное. Фраза не на языке звонка отбрасывается, в ответе придёт reason_ignored."
     },
     "voice": {
      "type": "string",
      "description": "Код голоса, см. GET /v1/voices. Префикс задаёт движок: oa-* = OpenAI gpt-realtime-2.1 (голос и мозг одна модель), el-* = ElevenLabs. Например oa-marin, oa-cedar, el-eric, el-sarah. Без поля — oa-marin. Неизвестный код не роняет звонок: берётся голос движка по умолчанию, в ответе будет voice_warning.",
      "example": "oa-cedar"
     },
     "engine": {
      "type": "string",
      "enum": [
       "openai",
       "elevenlabs"
      ],
      "description": "Движок, если голос не указан. По умолчанию openai. Если указан voice — движок берётся из его префикса."
     }
    }
   },
   "CallResponse": {
    "type": "object",
    "properties": {
     "ok": {
      "type": "boolean",
      "description": "true — набор принят телефонией. Это ещё не «поговорили»: результат смотреть в GET /v1/call/{id}."
     },
     "conversation_id": {
      "type": "string",
      "description": "Идентификатор разговора. С ним забирают статус, результат и запись."
     },
     "sip_call_id": {
      "type": "string"
     },
     "to_number": {
      "type": "string"
     },
     "language": {
      "type": "string"
     },
     "message": {
      "type": "string",
      "description": "Что ответила телефония."
     },
     "line_busy": {
      "type": "boolean",
      "description": "Zadarma ответила 480: заняты все линии аккаунта (их три) или номер не маршрутизируется. Повторить через 15–30 секунд."
     },
     "busy": {
      "type": "boolean",
      "description": "SIP 486: занят сам абонент."
     },
     "no_answer": {
      "type": "boolean",
      "description": "Не поднял трубку за минуту."
     },
     "error": {
      "type": "string"
     },
     "reason_ignored": {
      "type": "string",
      "description": "Повод был не на языке звонка и заменён дефолтным."
     }
    }
   },
   "CallResult": {
    "type": "object",
    "properties": {
     "status": {
      "type": "string",
      "enum": [
       "initiated",
       "in-progress",
       "processing",
       "done",
       "failed"
      ],
      "description": "Пока не done — результата ещё нет. failed = разговора не было."
     },
     "call_successful": {
      "type": "string",
      "enum": [
       "success",
       "failure",
       "unknown"
      ],
      "description": "Оценка ElevenLabs: справился ли агент с задачей звонка."
     },
     "duration_secs": {
      "type": "integer"
     },
     "summary": {
      "type": "string",
      "description": "Резюме разговора."
     },
     "termination_reason": {
      "type": "string",
      "description": "Почему разговор закончился. Полезно при failed."
     },
     "result": {
      "type": "object",
      "description": "Вынутые из разговора факты. Пустых полей нет — чего не прозвучало, того и нет в ответе.",
      "properties": {
       "outcome": {
        "type": "string",
        "description": "Чем кончился звонок, одной фразой."
       },
       "task_done": {
        "type": "boolean",
        "description": "Задача выполнена полностью. false — нужен повторный звонок."
       },
       "booking_confirmed": {
        "type": "boolean",
        "description": "Бронь или запись подтверждена собеседником явно."
       },
       "booking_datetime": {
        "type": "string",
        "description": "Дата и время подтверждённой брони словами собеседника."
       },
       "price": {
        "type": "string",
        "description": "Названные цены с валютой и за что."
       },
       "options": {
        "type": "string",
        "description": "Свободные слоты, варианты, условия."
       },
       "contact_name": {
        "type": "string",
        "description": "С кем говорили."
       },
       "address": {
        "type": "string",
        "description": "Адрес, ориентир, часы работы."
       },
       "callback": {
        "type": "string",
        "description": "Когда просили перезвонить, что обещали прислать."
       },
       "notes": {
        "type": "string"
       }
      }
     },
     "transcript": {
      "type": "array",
      "description": "Реплики по порядку.",
      "items": {
       "type": "object",
       "properties": {
        "role": {
         "type": "string",
         "enum": [
          "agent",
          "user"
         ]
        },
        "text": {
         "type": "string"
        },
        "sec": {
         "type": "integer"
        }
       }
      }
     }
    }
   }
  }
 },
 "paths": {
  "/v1/call": {
   "post": {
    "operationId": "placeCall",
    "summary": "Позвонить и решить задачу голосом",
    "description": "Возвращается сразу, разговор идёт в фоне 40–150 секунд. Результат — GET /v1/call/{conversation_id}, опрашивать раз в 15 секунд, пока status не станет done или failed. Линий у телефонии три: два набора подряд ловят line_busy, ставь между наборами паузу.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/CallRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Набор принят",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/CallResponse"
        }
       }
      }
     },
     "401": {
      "description": "Нет или неверный токен"
     },
     "502": {
      "description": "Телефония набор не приняла — смотреть message, line_busy, busy"
     }
    }
   }
  },
  "/v1/call/{conversation_id}": {
   "get": {
    "operationId": "getCallResult",
    "summary": "Статус звонка, вынутые факты и транскрипт",
    "parameters": [
     {
      "name": "conversation_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "ok",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/CallResult"
        }
       }
      }
     }
    }
   }
  },
  "/v1/call/{conversation_id}/audio": {
   "get": {
    "operationId": "getCallAudio",
    "summary": "Запись разговора (mp3)",
    "parameters": [
     {
      "name": "conversation_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "audio/mpeg",
      "content": {
       "audio/mpeg": {
        "schema": {
         "type": "string",
         "format": "binary"
        }
       }
      }
     }
    }
   }
  },
  "/v1/calls": {
   "get": {
    "operationId": "listCalls",
    "summary": "Журнал наборов этого сервиса",
    "parameters": [
     {
      "name": "limit",
      "in": "query",
      "schema": {
       "type": "integer",
       "default": 20
      }
     }
    ],
    "responses": {
     "200": {
      "description": "ok"
     }
    }
   }
  },
  "/health": {
   "get": {
    "operationId": "health",
    "summary": "Живость, id агента и транка. Без токена.",
    "security": [],
    "responses": {
     "200": {
      "description": "ok"
     }
    }
   }
  },
  "/v1/voices": {
   "get": {
    "summary": "Список голосов с кодами",
    "responses": {
     "200": {
      "description": "ok",
      "content": {
       "application/json": {
        "example": {
         "ok": true,
         "default": {
          "openai": "oa-marin",
          "elevenlabs": "el-velora"
         },
         "voices": [
          {
           "code": "oa-marin",
           "engine": "openai",
           "gender": "f",
           "accent": "american",
           "note": "тёплый, спокойный"
          },
          {
           "code": "el-eric",
           "engine": "elevenlabs",
           "gender": "m",
           "accent": "american",
           "note": "гладкий, разговорный"
          }
         ]
        }
       }
      }
     }
    }
   }
  }
 }
}