ListenHubOpenAPI
API 레퍼런스

팟캐스트

1인 또는 2인 화자 팟캐스트 에피소드를 생성합니다. 한 번에 생성하거나 스크립트 생성과 오디오 렌더링의 2단계 워크플로로 나눌 수 있으며, quick·deep·debate 세 가지 모드를 지원합니다.

팟캐스트 API는 프롬프트와 선택적인 참고 소스를 완성된 한 편의 에피소드로 바꿉니다. 결과물은 화자가 배정된 텍스트 스크립트, 렌더링된 오디오, 그리고 자막입니다. 한 번의 호출로 전부 생성할 수도 있고, 두 단계로 나눠서 스크립트를 먼저 생성하고 검토·편집한 뒤에 오디오를 렌더링할 수도 있습니다.

모든 요청은 base URL https://api.marswave.ai/openapi/v1을 사용하며 API key가 필요합니다:

Authorization: Bearer $LISTENHUB_API_KEY

키는 listenhub.ai/settings/api-keys에서 만듭니다. 모든 응답은 { "code": 0, "message": "", "data": { ... } } 형태로 감싸여 있으며, code가 0이 아니면 오류입니다.


팟캐스트 생성

POST /v1/podcast/episodes

한 번의 호출로 완전한 에피소드(스크립트 + 오디오)를 생성합니다. 요청은 즉시 episodeId를 반환하고 생성은 비동기로 진행되므로, 완료될 때까지 에피소드를 폴링하세요.

요청 파라미터

필드타입필수설명
querystring아니요생성의 기반이 되는 프롬프트 또는 주제. sources가 소재를 담고 있다면 비워 둘 수 있습니다.
sourcesarray아니요참고 소재. 각 항목은 { "type": "text" | "url", "content": "..." } 형태입니다. url이면 content는 링크이고, textcontent는 원본 텍스트입니다.
speakersarray1~2명의 화자, 각 항목은 { "speakerId": "..." }. 화자가 한 명이면 독백, 두 명이면 대화가 됩니다. debate 모드는 정확히 2명이 필요합니다.
languagestring아니요출력 언어. 예: en, zh, ja. 생략하면 입력에서 언어를 추론합니다.
modestring아니요생성 모드. quick, deep, debate 중 하나. 기본값은 quick.

querysources 중 최소 하나는 제공해야 합니다. speakerId 값은 Speakers API에서 조회하세요.

모드

모드화자 수적합한 용도
quick1 또는 2시의성이 중요한 콘텐츠를 빠르게 뽑아낼 때. 기본값입니다.
deep1 또는 2전문적인 주제를 다루는 심층·리서치형 에피소드.
debate정확히 2두 화자가 서로 다른 입장을 주장하는 양측 토론.

크레딧 비용은 모드와 길이에 따라 달라집니다. 고정 가격을 가정하지 마세요. 에피소드의 credits 필드가 실제 청구액을 반영하며, 실시간 잔액은 GET /v1/user/subscription으로 확인할 수 있습니다. 크레딧과 기능의 대응 관계는 요금을 참고하세요.

단일 화자 예제

quick 모드의 독백:

curl -X POST "https://api.marswave.ai/openapi/v1/podcast/episodes" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "Give a short technology news briefing for today.",
    "speakers": [
      {"speakerId": "<SPEAKER_ID_1>"}
    ],
    "language": "en",
    "mode": "quick"
  }'
const response = await fetch('https://api.marswave.ai/openapi/v1/podcast/episodes', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: 'Give a short technology news briefing for today.',
    speakers: [{ speakerId: '<SPEAKER_ID_1>' }],
    language: 'en',
    mode: 'quick',
  }),
});
const data = await response.json();
console.log(data);
import os
import requests

response = requests.post(
    'https://api.marswave.ai/openapi/v1/podcast/episodes',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
    json={
        'query': 'Give a short technology news briefing for today.',
        'speakers': [{'speakerId': '<SPEAKER_ID_1>'}],
        'language': 'en',
        'mode': 'quick',
    }
)
data = response.json()
print(data)

2인 화자 예제

두 명의 진행자가 등장하는 deep 에피소드:

curl -X POST "https://api.marswave.ai/openapi/v1/podcast/episodes" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "Analyze the technical foundations and future outlook of large language models.",
    "speakers": [
      {"speakerId": "<SPEAKER_ID_1>"},
      {"speakerId": "<SPEAKER_ID_2>"}
    ],
    "language": "en",
    "mode": "deep"
  }'
const response = await fetch('https://api.marswave.ai/openapi/v1/podcast/episodes', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: 'Analyze the technical foundations and future outlook of large language models.',
    speakers: [
      { speakerId: '<SPEAKER_ID_1>' },
      { speakerId: '<SPEAKER_ID_2>' },
    ],
    language: 'en',
    mode: 'deep',
  }),
});
const data = await response.json();
console.log(data);
import os
import requests

response = requests.post(
    'https://api.marswave.ai/openapi/v1/podcast/episodes',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
    json={
        'query': 'Analyze the technical foundations and future outlook of large language models.',
        'speakers': [
            {'speakerId': '<SPEAKER_ID_1>'},
            {'speakerId': '<SPEAKER_ID_2>'},
        ],
        'language': 'en',
        'mode': 'deep',
    }
)
data = response.json()
print(data)

debate 모드

debate는 정확히 2명의 화자가 필요합니다:

curl -X POST "https://api.marswave.ai/openapi/v1/podcast/episodes" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "Should remote work become the default model?",
    "speakers": [
      {"speakerId": "<SPEAKER_ID_1>"},
      {"speakerId": "<SPEAKER_ID_2>"}
    ],
    "language": "en",
    "mode": "debate"
  }'
const response = await fetch('https://api.marswave.ai/openapi/v1/podcast/episodes', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: 'Should remote work become the default model?',
    speakers: [
      { speakerId: '<SPEAKER_ID_1>' },
      { speakerId: '<SPEAKER_ID_2>' },
    ],
    language: 'en',
    mode: 'debate',
  }),
});
const data = await response.json();
console.log(data);
import os
import requests

response = requests.post(
    'https://api.marswave.ai/openapi/v1/podcast/episodes',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
    json={
        'query': 'Should remote work become the default model?',
        'speakers': [
            {'speakerId': '<SPEAKER_ID_1>'},
            {'speakerId': '<SPEAKER_ID_2>'},
        ],
        'language': 'en',
        'mode': 'debate',
    }
)
data = response.json()
print(data)

참고 소스 사용하기

특정 소재에 기반한 에피소드를 만들려면 sources를 전달하세요. 각 항목은 가져올 URL이거나 원본 텍스트입니다:

curl -X POST "https://api.marswave.ai/openapi/v1/podcast/episodes" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "Summarize and discuss the core ideas in this article.",
    "sources": [
      {
        "type": "url",
        "content": "https://blog.samaltman.com/reflections"
      }
    ],
    "speakers": [
      {"speakerId": "<SPEAKER_ID_1>"},
      {"speakerId": "<SPEAKER_ID_2>"}
    ],
    "language": "en",
    "mode": "deep"
  }'
const response = await fetch('https://api.marswave.ai/openapi/v1/podcast/episodes', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: 'Summarize and discuss the core ideas in this article.',
    sources: [
      { type: 'url', content: 'https://blog.samaltman.com/reflections' },
    ],
    speakers: [
      { speakerId: '<SPEAKER_ID_1>' },
      { speakerId: '<SPEAKER_ID_2>' },
    ],
    language: 'en',
    mode: 'deep',
  }),
});
const data = await response.json();
console.log(data);
import os
import requests

response = requests.post(
    'https://api.marswave.ai/openapi/v1/podcast/episodes',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
    json={
        'query': 'Summarize and discuss the core ideas in this article.',
        'sources': [
            {'type': 'url', 'content': 'https://blog.samaltman.com/reflections'}
        ],
        'speakers': [
            {'speakerId': '<SPEAKER_ID_1>'},
            {'speakerId': '<SPEAKER_ID_2>'},
        ],
        'language': 'en',
        'mode': 'deep',
    }
)
data = response.json()
print(data)

응답

{
  "code": 0,
  "message": "",
  "data": {
    "episodeId": "665f1c2a9b3e4d0012a8c7e1"
  }
}

episodeId는 이후 모든 호출에서 사용하는 핸들입니다. 저장해 두고 상태를 폴링하세요.


에피소드 상태 조회

GET /v1/podcast/episodes/{episodeId}

현재 상태를 가져오고, 완료된 뒤에는 생성된 결과물을 가져옵니다. 에피소드를 생성한 다음 생성이 끝날 때까지 이 엔드포인트를 폴링하세요.

curl -X GET "https://api.marswave.ai/openapi/v1/podcast/episodes/{episodeId}" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY"
const response = await fetch(
  `https://api.marswave.ai/openapi/v1/podcast/episodes/${episodeId}`,
  { headers: { 'Authorization': `Bearer ${process.env.LISTENHUB_API_KEY}` } }
);
const data = await response.json();
console.log('Status:', data.data.processStatus);
console.log('Audio URL:', data.data.audioUrl);
import os
import requests

response = requests.get(
    f'https://api.marswave.ai/openapi/v1/podcast/episodes/{episode_id}',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'}
)
data = response.json()
print('Status:', data['data']['processStatus'])
print('Audio URL:', data['data']['audioUrl'])

응답 필드

필드타입설명
episodeIdstring에피소드 식별자.
createdAtnumber생성 타임스탬프(epoch 밀리초).
processStatusstring전체 작업 상태: pending, success, fail.
contentStatusstring단계별 상태로, 2단계 워크플로에서 제공됩니다: text-success, text-fail, audio-success, audio-fail. 한 번에 생성할 때는 없습니다.
failCodenumber실패 사유 코드. 실패가 없으면 0.
messagestring사람이 읽을 수 있는 상태 상세.
completedTimenumber완료 타임스탬프(epoch 밀리초).
creditsnumber현재까지 소모된 크레딧.
titlestring생성된 에피소드 제목.
outlinestring생성된 아웃라인.
coverstring커버 이미지 URL.
audioUrlstring최종 오디오 파일 URL(MP3).
audioStreamUrlstring스트리밍 오디오 URL(HLS .m3u8).
subtitlesUrlstring자막 파일 URL(SRT).
sourceProcessResultobject처리된 소스 소재: content와 인용 목록인 references 배열.
scriptsarray대사 단위 스크립트. 각 항목은 { "speakerId", "speakerName", "content" }.

생성이 완료된 상태(processStatus: "success")의 응답:

{
  "code": 0,
  "message": "",
  "data": {
    "episodeId": "665f1c2a9b3e4d0012a8c7e1",
    "processStatus": "success",
    "failCode": 0,
    "credits": 27,
    "title": "The Story of AI: How the Intelligent Age Was Born",
    "audioUrl": "https://assets.listenhub.ai/listenhub-public-prod/podcast/665f1c2a9b3e4d0012a8c7e1.mp3",
    "audioStreamUrl": "https://assets.listenhub.ai/listenhub-public-prod/podcast/665f1c2a9b3e4d0012a8c7e1.m3u8",
    "scripts": [
      {
        "speakerId": "<SPEAKER_ID_1>",
        "speakerName": "Ethan",
        "content": "These days it feels like AI news surrounds us everywhere."
      }
    ]
  }
}

팟캐스트 생성은 보통 1~4분 걸립니다. 권장 폴링 방식은 첫 요청 전에 60초 기다린 뒤 10초 간격으로 폴링하는 것입니다. 실패하면 processStatusfail이 되고 failCode가 사유를 나타냅니다.

스크립트와 아웃라인 스트리밍(SSE)

GET /v1/podcast/episodes/{episodeId}/text-stream?event={script|outline}

스크립트나 아웃라인이 생성되는 동안, 폴링 대신 Server-Sent Events 스트림을 구독해 텍스트를 점진적으로 받을 수 있습니다. event 쿼리 파라미터로 어떤 스트림을 받을지 선택합니다:

  • outline — 작성되는 중의 아웃라인.
  • script — 작성되는 중의 대사 단위 스크립트.
curl -N "https://api.marswave.ai/openapi/v1/podcast/episodes/{episodeId}/text-stream?event=script" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY"

응답은 text/event-stream입니다. 하나의 JSON body로 파싱하지 말고 스트림으로 읽으세요. 실시간 진행 상황 UI에 적합합니다. 최종 결과물(오디오, 자막)을 읽으려면 생성이 완료된 뒤 상태 엔드포인트를 사용하세요.


2단계 생성

생성을 두 번의 독립적인 호출로 나눕니다. 스크립트를 먼저 만들고, 확인하거나 편집한 뒤 오디오를 렌더링합니다. 오디오로 확정하기 전에 사람이나 자동화된 검토 단계가 필요할 때, 또는 텍스트 생성 크레딧과 오디오 생성 크레딧을 따로 관리하고 싶을 때 적합한 패턴입니다.

이 흐름에는 두 개의 엔드포인트가 있습니다:

  1. POST /v1/podcast/episodes/text-content — 스크립트만 생성합니다.
  2. POST /v1/podcast/episodes/{episodeId}/audio — (필요하면 편집한) 스크립트로 오디오를 렌더링합니다.

text-content에서는 language필수이며, 각 화자의 언어가 language 값과 일치해야 합니다. 일치하지 않으면 Speaker language mismatch 오류가 반환됩니다.

스크립트 생성

POST /v1/podcast/episodes/text-content는 한 번에 생성할 때와 동일한 query, sources, speakers, mode 필드를 받지만, 여기서는 language가 필수입니다. 오디오 없이 스크립트만 생성합니다:

curl -X POST "https://api.marswave.ai/openapi/v1/podcast/episodes/text-content" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "Discuss the current state of quantum computing.",
    "speakers": [
      {"speakerId": "<SPEAKER_ID_1>"},
      {"speakerId": "<SPEAKER_ID_2>"}
    ],
    "language": "en",
    "mode": "deep"
  }'
const response = await fetch('https://api.marswave.ai/openapi/v1/podcast/episodes/text-content', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: 'Discuss the current state of quantum computing.',
    speakers: [
      { speakerId: '<SPEAKER_ID_1>' },
      { speakerId: '<SPEAKER_ID_2>' },
    ],
    language: 'en',
    mode: 'deep',
  }),
});
const data = await response.json();
const episodeId = data.data.episodeId;
console.log('Episode ID:', episodeId);
import os
import requests

response = requests.post(
    'https://api.marswave.ai/openapi/v1/podcast/episodes/text-content',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
    json={
        'query': 'Discuss the current state of quantum computing.',
        'speakers': [
            {'speakerId': '<SPEAKER_ID_1>'},
            {'speakerId': '<SPEAKER_ID_2>'},
        ],
        'language': 'en',
        'mode': 'deep',
    }
)
data = response.json()
episode_id = data['data']['episodeId']
print('Episode ID:', episode_id)

응답:

{
  "code": 0,
  "message": "",
  "data": {
    "episodeId": "665f1c2a9b3e4d0012a8c7e1",
    "message": "Text content generation started. Audio generation can be triggered later."
  }
}

스크립트 완성 대기

contentStatustext-success가 될 때까지 GET /v1/podcast/episodes/{episodeId}를 폴링합니다. 2단계 흐름에서는 어떤 단계가 끝났는지 알려주는 필드가 contentStatus이므로, processStatus 대신 이 값을 확인하세요.

curl -X GET "https://api.marswave.ai/openapi/v1/podcast/episodes/{episodeId}" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY"
const result = await fetch(`https://api.marswave.ai/openapi/v1/podcast/episodes/${episodeId}`, {
  headers: { 'Authorization': `Bearer ${process.env.LISTENHUB_API_KEY}` },
});
const status = await result.json();
console.log('Content status:', status.data.contentStatus);
import os
import requests

result = requests.get(
    f'https://api.marswave.ai/openapi/v1/podcast/episodes/{episode_id}',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'}
)
status = result.json()
print('Content status:', status['data']['contentStatus'])

스크립트가 준비된 응답(contentStatus: "text-success"):

{
  "code": 0,
  "message": "",
  "data": {
    "episodeId": "665f1c2a9b3e4d0012a8c7e1",
    "processStatus": "success",
    "contentStatus": "text-success",
    "credits": 15,
    "title": "Quantum Computing: Present and Future",
    "outline": "...",
    "scripts": [
      {
        "speakerId": "<SPEAKER_ID_1>",
        "speakerName": "Ethan",
        "content": "Welcome to this discussion on quantum computing..."
      },
      {
        "speakerId": "<SPEAKER_ID_2>",
        "speakerName": "Sophia",
        "content": "Quantum computing is exciting and rapidly evolving..."
      }
    ]
  }
}

실시간 진행 상황 UI가 필요하다면 폴링 대신 /v1/podcast/episodes/{episodeId}/text-stream?event=script SSE 스트림을 구독하세요.

(선택) 스크립트 편집

이전 응답의 scripts 배열을 가져와 필요한 만큼 각 대사의 내용을 다시 씁니다.

변경할 수 있는 것은 content뿐입니다. 각 대사의 speakerId는 반환된 값 그대로 유지하고, 스크립트에 등장하는 서로 다른 화자는 1~2명으로 유지하세요. 이는 API의 강제 제약입니다.

편집한 배열을 다음 단계의 요청 본문으로 전달합니다.

오디오 렌더링

POST /v1/podcast/episodes/{episodeId}/audio를 호출합니다. 원본 스크립트를 그대로 렌더링하려면 빈 본문을 보내고, 편집한 버전을 렌더링하려면 scripts 배열(각 항목은 { "content", "speakerId" })을 전달하세요:

# Render the original script
curl -X POST "https://api.marswave.ai/openapi/v1/podcast/episodes/{episodeId}/audio" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json"

# Render an edited script
curl -X POST "https://api.marswave.ai/openapi/v1/podcast/episodes/{episodeId}/audio" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "scripts": [
      {
        "content": "Welcome to this episode. Today we go deeper into quantum computing...",
        "speakerId": "<SPEAKER_ID_1>"
      },
      {
        "content": "This field has moved quickly from theory to practical experiments...",
        "speakerId": "<SPEAKER_ID_2>"
      }
    ]
  }'
// Render the original script
await fetch(`https://api.marswave.ai/openapi/v1/podcast/episodes/${episodeId}/audio`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
});

// Render an edited script
await fetch(`https://api.marswave.ai/openapi/v1/podcast/episodes/${episodeId}/audio`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    scripts: [
      { content: 'Welcome to this episode. Today we go deeper into quantum computing...', speakerId: '<SPEAKER_ID_1>' },
      { content: 'This field has moved quickly from theory to practical experiments...', speakerId: '<SPEAKER_ID_2>' },
    ],
  }),
});
import os
import requests

# Render the original script
requests.post(
    f'https://api.marswave.ai/openapi/v1/podcast/episodes/{episode_id}/audio',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'}
)

# Render an edited script
requests.post(
    f'https://api.marswave.ai/openapi/v1/podcast/episodes/{episode_id}/audio',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
    json={
        'scripts': [
            {'content': 'Welcome to this episode. Today we go deeper into quantum computing...', 'speakerId': '<SPEAKER_ID_1>'},
            {'content': 'This field has moved quickly from theory to practical experiments...', 'speakerId': '<SPEAKER_ID_2>'},
        ]
    }
)

응답:

{
  "code": 0,
  "message": "",
  "data": {
    "success": true,
    "message": "Audio generation started",
    "episodeId": "665f1c2a9b3e4d0012a8c7e1",
    "status": "submit"
  }
}

오디오 완성 대기

contentStatusaudio-success가 될 때까지 GET /v1/podcast/episodes/{episodeId} 폴링을 계속합니다. 그러면 오디오, 스트리밍, 자막 URL이 채워집니다:

{
  "code": 0,
  "message": "",
  "data": {
    "episodeId": "665f1c2a9b3e4d0012a8c7e1",
    "processStatus": "success",
    "contentStatus": "audio-success",
    "credits": 42,
    "title": "Quantum Computing: Present and Future",
    "audioUrl": "https://assets.listenhub.ai/podcast/665f1c2a9b3e4d0012a8c7e1.mp3",
    "audioStreamUrl": "https://assets.listenhub.ai/podcast/665f1c2a9b3e4d0012a8c7e1.m3u8",
    "subtitlesUrl": "https://assets.listenhub.ai/podcast/665f1c2a9b3e4d0012a8c7e1.srt",
    "scripts": [ ]
  }
}

contentStatus 레퍼런스

의미다음 단계
text-success스크립트 생성 완료오디오 렌더링
text-fail스크립트 생성 실패에피소드 다시 생성
audio-success오디오 생성 완료완료
audio-fail오디오 생성 실패오디오 렌더링 재시도

크레딧: 1단계는 텍스트 생성 크레딧을, 2단계는 오디오 생성 크레딧을 소모합니다. credits 필드는 두 단계에 걸쳐 누적되며 실제 청구액을 반영합니다. 실시간 잔액은 GET /v1/user/subscription으로 확인하고, 크레딧과 기능의 대응 관계는 요금을 참고하세요.


관련 문서

이 페이지의 내용