뉴스카드 생성 API
링크 하나를 보내면 인스타그램용 카드 이미지(1080×1350)와 캡션을 만들어 돌려줍니다. 브라우저 없이 서버가 직접 렌더링하므로 텔레그램 봇·자동화 에이전트 등 어디서든 호출할 수 있습니다.
| Base URL | https://algoajae.info |
|---|---|
| 인증 | X-API-Key 헤더 |
| 응답 | JSON (이미지는 URL로 제공) |
| 이미지 규격 | 1080 × 1350 PNG (인스타 4:5) |
빠른 시작
curl -X POST https://algoajae.info/api/cards \
-H "X-API-Key: $CARD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://n.news.naver.com/mnews/article/654/0000192269"}'
{
"cardId": "7dc12ecf2080",
"imageUrl": "https://algoajae.info/api/cards/7dc12ecf2080/image.png?v=0",
"selected": 0,
"line1": "레버리지 ETF 규제",
"line2": "거래대금 30% 급감했다",
"variants": [
{ "line1": "레버리지 ETF 규제", "line2": "거래대금 30% 급감했다", "angle": "숫자강조" },
{ "line1": "31일부터 예탁금 3배", "line2": "투자자들 발걸음 멈췄다", "angle": "반전" }
// ... 총 10개
],
"caption": "오는 31일부터 레버리지 ETF 규제가 강화됩니다.\n기본예탁금이...",
"hashtags": ["레버리지ETF", "투자규제", "금융시장"],
"hashtagText": "#레버리지ETF #투자규제 #금융시장",
"source": { "adapter": "naver-news", "title": "...", "chars": 1265 },
"cost": { "usd": 0.0583, "krw": 77 },
"expiresAt": "2026-07-28T12:00:00.000Z"
}
imageUrl은 공개 URL입니다. 텔레그램 sendPhoto에 그대로 넘기면 됩니다 — 파일 업로드가 필요 없습니다.인증 · 비용 부담
모든 요청에 X-API-Key 헤더가 필요합니다. 사이트 로그인 토큰(Authorization: Bearer <JWT>)으로도 호출할 수 있습니다.
X-API-Key: your-api-key-here비용은 누구에게 청구되나
OpenAI·Apify 키는 아래 순서로 결정되며, 그 키의 소유자에게 사용료가 청구됩니다.
| 순서 | 출처 | 청구 대상 |
|---|---|---|
| 1 | 요청 헤더 X-OpenAI-Key / X-Apify-Token | 헤더에 담은 키의 주인 |
| 2 | 회원이 내 정보에 저장한 키 (JWT로 호출 시) | 그 회원 |
| 3 | 서버 환경변수 | 사이트 운영자 |
API 키 발급받기
- 만료되지 않습니다. 로그인 토큰(JWT)은 30일이면 끊기지만 이 키는 폐기할 때까지 유효합니다
- 사용료는 본인이 내 정보에 저장한 OpenAI 키로 청구됩니다 — 다른 회원과 완전히 분리됩니다
- 발급 직후 한 번만 보여줍니다. 서버에는 해시만 저장되어 다시 확인할 수 없습니다
- 분실하면 재발급하세요. 이전 키는 즉시 무효가 됩니다
- 유출이 의심되면 폐기 버튼으로 즉시 차단할 수 있습니다
발급 전에 내 정보 → OpenAI API Key를 먼저 저장해 두세요. 없으면 카드 생성이 막힙니다.
GET .../image.png)만 인증이 없습니다. URL을 아는 사람은 볼 수 있으므로 공개해도 되는 내용만 담으세요.카드 만들기
요청 본문
| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
url | string | — | 기사·게시글 링크. text와 둘 중 하나 필수 |
text | string | — | 링크 대신 본문을 직접 전달 (사이트가 차단될 때) |
noAI | boolean | false | true면 AI 배경 생략 — 6배 저렴, 10배 빠름 |
imageUrl | string | — | 이 이미지를 배경으로 사용 (AI 생성 안 함) |
imageBase64 | string | — | 배경 이미지를 base64로 직접 전달 |
variant | number | 0 | 처음부터 N번 헤드라인으로 렌더 |
size | string | 1088x1360 | 배경 해상도. 1728x2160도 가능 |
quality | string | medium | low / medium / high |
async | boolean | false | true면 즉시 작업ID 반환 |
webhookUrl | string | — | async일 때 완료 결과를 POST로 전송 |
style | object | — | 디자인 설정 |
copyPrompt | string | — | 카피 지침 덮어쓰기 |
imageStyle | string | — | 이미지 스타일 추가 |
tone | string | photoreal | photoreal / illust |
동기 모드 (기본)
응답이 올 때까지 기다립니다. AI 배경 포함 시 약 60~90초가 걸리므로 클라이언트 타임아웃을 넉넉히 잡으세요.
curl -X POST https://algoajae.info/api/cards \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"url": "https://...", "noAI": true}' # 약 6초비동기 모드
curl -X POST https://algoajae.info/api/cards \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"url": "https://...", "async": true, "webhookUrl": "https://my-bot/hook"}'즉시 202로 응답합니다:
{
"jobId": "j_601a22b8cb71",
"status": "queued",
"statusUrl": "https://algoajae.info/api/cards/j_601a22b8cb71"
}완료되면 webhookUrl로 결과가 POST됩니다. 웹훅을 쓰지 않으면 statusUrl을 폴링하세요.
진행 단계: fetching → composing → generating_image → rendering → done
상태 · 정보 조회
cardId와 jobId 모두 조회할 수 있습니다.
curl https://algoajae.info/api/cards/j_601a22b8cb71 -H "X-API-Key: $KEY"
# 진행 중
{ "jobId": "j_601a22b8cb71", "status": "running", "stage": "generating_image" }완료 시에는 카드 정보 전체가 함께 반환됩니다.
헤드라인 교체
10개 후보 중 다른 것으로 다시 렌더합니다. 배경을 재사용하므로 추가 비용이 없고 즉시 완료됩니다.
curl -X POST https://algoajae.info/api/cards/7dc12ecf2080/variant \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"index": 4}'응답 형식은 POST /api/cards와 같으며 imageUrl이 새 헤드라인 버전을 가리킵니다.
배경 붙이기 · 교체
noAI: true로 카피만 먼저 뽑아 헤드라인을 확인한 뒤, 마음에 들 때만 배경을 만드는 흐름에 씁니다.
카피는 이미 있으므로 카피 비용이 다시 들지 않습니다.
# ① 카피만 — 6초, 17원
POST /api/cards
{ "url": "https://...", "noAI": true }
# ② 헤드라인이 마음에 들면 배경 추가 — 48초, +65원
POST /api/cards/<cardId>/background
{} # AI로 생성
{ "imageUrl": "https://..." } # 또는 내 이미지로
{ "imageBase64": "..." } # 또는 직접 업로드
{ "prompt": "다른 장면 묘사" } # 프롬프트를 바꿔 재생성imageUrl에 &r=1 같은 버전이 붙습니다.
같은 URL이면 텔레그램·브라우저가 옛 이미지를 캐시해서 보여주기 때문입니다. 반환된 URL을 그대로 쓰세요.이미지
GET /api/cards/7dc12ecf2080/image.png # 현재 선택된 헤드라인
GET /api/cards/7dc12ecf2080/image.png?v=4 # 4번 후보 버전지원하는 링크
| 종류 | 상태 | 비고 |
|---|---|---|
| 네이버 뉴스 | ✅ | 본문 정확히 추출 |
| 네이버 블로그 | ✅ | iframe 자동 추적 |
| 일반 언론사 | ✅ | 조선·한겨레 등 |
| 커뮤니티 | ⚠️ | 디시·클리앙·에펨 등 대체로 가능. 일부는 서버 IP 차단 |
| 인스타그램 | ✅ | Apify 경유 (별도 과금) |
| 네이버 카페 | ❌ | 로그인 필요 |
차단된 사이트는 text 필드에 본문을 직접 넣어 호출하면 됩니다.
{ "text": "여기에 기사 본문 전체를 붙여넣기..." }비용
cost 필드에 실제 사용한 토큰 기준 금액이 담깁니다.
| 모드 | 소요 시간 | 비용 |
|---|---|---|
noAI: true (카피만) | 약 6초 | 약 13원 |
| AI 배경 포함 | 약 60~90초 | 약 77원 |
| 헤드라인 교체 | 즉시 | 0원 |
인스타그램 링크는 Apify 크레딧이 건당 약 $0.0027 추가로 소모됩니다.
noAI: true + imageUrl 조합이 가장 빠르고 저렴합니다.디자인 설정
style 객체로 카드 모양을 조정합니다.
style → 회원이 웹에서 계정에 저장한 디자인 → 서버 기본값웹 화면(뉴스카드 → 디자인)에서 값을 맞춘 뒤 "이 디자인을 내 계정에 저장"을 누르면 API로 만들 때도 같은 디자인이 나옵니다. 저장하지 않으면 API는 서버 기본값을 씁니다.
{
"url": "https://...",
"style": {
"hw": 900, "hmax": 76, "lh": 1.25, "tr": -0.03,
"pad": 72, "bottom": 168, "badgeTop": 178,
"grad": 0.52, "topgrad": 0.22,
"shadow": 14, "align": "center",
"sub": "자세한 내용은 본문 참고",
"badge": "이해를 돕기 위한 AI 이미지"
}
}| 키 | 기본값 | 설명 |
|---|---|---|
hw | 900 | 헤드라인 굵기 (700 / 800 / 900) |
hmax | 76 | 헤드라인 최대 크기(px). 길면 자동 축소 |
lh | 1.25 | 줄 간격 배수 |
tr | -0.03 | 자간 (em) |
pad | 72 | 좌우 여백(px) |
bottom | 168 | 하단 여백(px). 135 미만이면 인스타 1:1 화면에서 잘림 |
badgeTop | 178 | 배지 높이(px). 135 미만이면 잘림 |
badgeSide | 40 | 배지 가장자리 여백(px) |
badgeSize | 20 | 배지 글자 크기(px) |
badgeAlign | right | 배지 정렬 — left / center / right |
badgeOpacity | 0.88 | 배지 불투명도 (0~1) |
subSize | 26 | 서브 문구 크기(px) |
subGap | 58 | 서브 문구와 헤드라인 간격(px) |
subOpacity | 0.72 | 서브 문구 불투명도 (0~1) |
grad | 0.52 | 하단 그라데이션 높이 (0~1) |
topgrad | 0.22 | 상단 그라데이션 높이 (0~1) |
shadow | 14 | 글자 그림자 세기 |
align | center | center / left |
sub | 자세한 내용은 본문 참고 | 하단 서브 문구. ""면 표시 안 함 |
badge | 이해를 돕기 위한 AI 이미지 | 우상단 배지. ""면 표시 안 함 |
폰트는 Paperlogy로 고정입니다.
프롬프트 커스터마이즈
둘 다 생략하면 기본 지침으로 동작합니다.
copyPrompt — 카피 지침 덮어쓰기
문체·톤·길이 규칙을 직접 지정합니다. 출력 형식(후보 10개, 캡션·해시태그 분리)은 스키마로 강제되므로 깨지지 않습니다.
{ "copyPrompt": "20대 여성 독자 대상, 친근한 반말체. 헤드라인은 질문형 위주로." }imageStyle — 이미지 스타일 추가
생성된 이미지 프롬프트 뒤에 덧붙습니다. 매번 공통 적용할 스타일에 적합합니다.
{ "imageStyle": "always Korean people in their 20s-30s, warm color grading, subtle film grain" }오류
{ "error": "인스타그램은 Apify 토큰이 필요합니다.", "needsApify": true }| 코드 | 의미 | 대처 |
|---|---|---|
400 | 잘못된 요청 / 키 없음 | 본문·설정 확인 |
401 | API 키 오류 | X-API-Key 확인 |
404 | 카드 없음 또는 만료 | 6시간 경과했는지 확인 |
422 | 본문 추출 실패 | text로 본문 직접 전달 |
429 | 속도 제한 | 잠시 후 재시도 |
502 | 외부 API 오류 | 재시도 |
429가 발생할 수 있습니다.텔레그램 봇 예제
링크를 받아 카드를 만들고, 헤드라인 후보를 버튼으로 보여주는 최소 구현입니다.
const API = 'https://algoajae.info';
const KEY = process.env.CARD_API_KEY;
// 1) 링크 수신 → 카드 생성
async function onLink(chatId, url) {
await tg('sendMessage', { chat_id: chatId, text: '카드 만드는 중… (약 1분)' });
const r = await fetch(`${API}/api/cards`, {
method: 'POST',
headers: { 'X-API-Key': KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ url }),
});
const card = await r.json();
if (!r.ok) return tg('sendMessage', { chat_id: chatId, text: '실패: ' + card.error });
// 2) 이미지 + 캡션 전송 (imageUrl을 그대로 넘기면 됨)
await tg('sendPhoto', {
chat_id: chatId,
photo: card.imageUrl,
caption: `${card.caption}\n\n${card.hashtagText}\n\n비용 ${card.cost.krw}원`,
});
// 3) 헤드라인 후보를 버튼으로
await tg('sendMessage', {
chat_id: chatId,
text: '다른 헤드라인으로 바꿀까요? (무료)',
reply_markup: {
inline_keyboard: card.variants.map((v, i) => [{
text: `${v.line1} / ${v.line2}`,
callback_data: `v:${card.cardId}:${i}`,
}]),
},
});
}
// 4) 버튼 클릭 → 해당 후보로 교체 (추가 비용 없음)
async function onPick(chatId, cardId, index) {
const r = await fetch(`${API}/api/cards/${cardId}/variant`, {
method: 'POST',
headers: { 'X-API-Key': KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ index: Number(index) }),
});
const card = await r.json();
await tg('sendPhoto', { chat_id: chatId, photo: card.imageUrl });
}
function tg(method, body) {
return fetch(`https://api.telegram.org/bot${process.env.TG_TOKEN}/${method}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(r => r.json());
}60초 대기가 부담스럽다면
await fetch(`${API}/api/cards`, {
method: 'POST',
headers: { 'X-API-Key': KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
url,
async: true,
webhookUrl: `https://my-bot.example.com/card-done?chat=${chatId}`,
}),
});
// → 즉시 202. 완료되면 webhookUrl로 카드 정보가 POST됨더 빠르고 저렴하게
{ "url": "https://...", "noAI": true, "imageUrl": "https://my-cdn/photo.jpg" }
// 약 6초, 13원