본문으로 건너뛰기

광고

광고 레지스트리는 광고 소재를 한곳에서 관리하는 저장소입니다. 소재를 한 번 등록하면(파일 업로드 또는 URL / VAST) ad_id를 받습니다. 이후 광고 매칭과 브랜드 세이프티 검수에는 작업마다 파일을 다시 올리지 않고 이 ID를 전달합니다.

등록 방식​

입구는 두 가지이고 결과는 같습니다. id(ad_id)와 status를 가진 AdRecord입니다.

경로엔드포인트동작응답의 status
파일 업로드POST /api/v1/ads/upload(멀티파트)동기 방식으로 저장ready
URL / VASTPOST /api/v1/ads(JSON)즉시 202를 반환하고, 백그라운드 리졸버가 미디어를 내려받아 검증resolving 이후 ready 또는 failed
upload (file) -> ready
register (url/vast) -> resolving -> ready (storage_path, duration_ms filled)
-> failed (error carries the reason)

모든 소재에 적용되는 규칙입니다.

  • 길이 제한: 120초. 이보다 길거나, 분석할 수 없거나, 비어 있는 미디어는 거부합니다.
  • 원본 그대로 저장. 레지스트리는 트랜스코딩하지 않습니다. 코덱 정규화는 분석 단계에서 처리합니다.
  • 팀 범위. 목록과 조회는 조직 범위로 한정됩니다. 존재하지 않는 ad_id와 다른 팀의 ad_id는 모두 404를 반환합니다.

파일 업로드(동기)​

POST /api/v1/ads/upload는 multipart/form-data를 받습니다. 응답은 이미 status: ready이므로 ad_id를 바로 사용할 수 있습니다.

filefilebodyrequired

광고 영상 파일(mp4 / mov / ...)이며 최대 120초입니다.

titlestringbody

선택 입력인 표시용 제목입니다.

brand_hintsstringbody

쉼표 또는 줄바꿈으로 구분한 브랜드 힌트입니다.

curl -X POST https://video.pyler.ai/api/v1/ads/upload \
-H "Authorization: Bearer $VUF_API_KEY" \
-F "file=@./ads/summer_campaign.mp4" \
-F "title=Summer campaign" \
-F "brand_hints=acme"

응답 — 202 Accepted:

{
"id": "11111111-1111-1111-1111-111111111111",
"title": "Summer campaign",
"storage_path": "s3://…",
"duration_ms": 15000,
"brand_hints": ["acme"],
"source_type": "file",
"status": "ready",
"error": null,
"uploaded_at": "2026-07-01T09:00:00Z"
}
노트

업로드 라우트는 레코드가 이미 ready여도 202를 반환합니다. 소재를 사용할 수 있는지 확인하려면 HTTP 코드가 아니라 status를 확인하세요.

URL로 등록(비동기)​

POST /api/v1/ads는 JSON 본문을 받습니다. 즉시 status: resolving과 함께 202를 반환합니다. 이후 백그라운드 리졸버가 미디어를 내려받아 검증한 뒤 레코드를 ready(storage_path, duration_ms 포함) 또는 failed(error 포함)로 바꿉니다. 진행 상황은 GET /api/v1/ads/{ad_id}를 폴링해 확인하세요.

source_typestringbodyrequired

url, vast_url, vast_xml 중 하나입니다. 여기서는 file을 받지 않습니다. 파일은 POST /api/v1/ads/upload로 업로드하세요.

source_urlstringbodyrequired

미디어 URL, VAST 태그 URL, 또는 (vast_xml인 경우) VAST XML 원문입니다.

titlestringbody

선택 입력인 표시용 제목입니다.

brand_hintsstring[]body

문자열 배열로 된 브랜드 힌트입니다.

curl -X POST https://video.pyler.ai/api/v1/ads \
-H "Authorization: Bearer $VUF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source_type": "url",
"source_url": "https://cdn.example.com/ad.mp4",
"title": "Summer campaign",
"brand_hints": ["acme"]
}'

응답 — 202 Accepted:

{
"id": "22222222-2222-2222-2222-222222222222",
"title": "Summer campaign",
"storage_path": null,
"duration_ms": null,
"brand_hints": ["acme"],
"source_type": "url",
"status": "resolving",
"error": null,
"uploaded_at": "2026-07-01T09:00:00Z"
}

VAST 입력​

POST /api/v1/ads는 VAST도 받습니다. VAST 태그 URL이면 source_type: "vast_url", XML 원문을 source_url에 넣으면 source_type: "vast_xml"입니다. 리졸버는 Inline·Wrapper 광고를 미디어 파일까지 풀어낸 뒤, 일반 URL 등록과 같은 다운로드 → 분석 → 저장 경로를 따릅니다. 응답 규약도 같습니다. 같은 AdRecord를 반환하고, 같은 방식으로 resolving → ready / failed를 폴링합니다.

  • 지원 범위: VAST 2.0 / 3.0 / 4.0, Inline과 Wrapper(VASTAdTagURI를 재귀적으로 따라감, 깊이 5 이하).
  • MediaFile 선택: 해상도가 가장 높은 video/mp4 MediaFile을 고릅니다. VPAID 후보는 건너뜁니다.
  • 제출 시점 검사: 202로 접수하기 전에 VAST를 동기로 해석하고 MediaFile에 접근할 수 있는지 확인합니다. 실패가 확실한 입력은 즉시 422로 거부하며, code는 vast_invalid(VMAP, VPAID 전용, MediaFile 없음, Wrapper 깊이 초과, 잘못된 XML, 너무 큰 응답), url_unreachable(MediaFile이 403 / 404 / 410 응답) 또는 blocked_target(내부 또는 사설 주소) 중 하나입니다.
  • 일시적 오류는 접수합니다. 태그나 MediaFile에서 429, 5xx, 타임아웃, 네트워크 오류가 나도 요청을 거부하지 않습니다. 202로 접수하고 백그라운드 리졸버에 맡깁니다. 그래도 실패하면 레코드는 error와 함께 failed가 됩니다.
curl -X POST https://video.pyler.ai/api/v1/ads \
-H "Authorization: Bearer $VUF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"source_type": "vast_url", "source_url": "https://ads.example.com/vast.xml"}'
# -> 202 resolving; poll GET /api/v1/ads/{ad_id} until ready or failed

소재 목록과 조회​

  • GET /api/v1/ads는 팀의 소재를 최신순으로 { "ads": [AdRecord, ...], "total": <int> } 형태로 반환합니다. 쿼리 파라미터는 limit(1–200, 기본값 50)과 offset(기본값 0)입니다.
  • GET /api/v1/ads/{ad_id}는 AdRecord 하나를 반환합니다. 비동기 등록의 폴링 엔드포인트이기도 합니다.
curl "https://video.pyler.ai/api/v1/ads?limit=50&offset=0" \
-H "Authorization: Bearer $VUF_API_KEY"

curl https://video.pyler.ai/api/v1/ads/$AD_ID \
-H "Authorization: Bearer $VUF_API_KEY"

AdRecord 필드​

idstring

ad_id입니다. 매칭과 검수에서 이 값을 참조합니다.

titlestring | null

표시용 제목입니다.

storage_pathstring | null

저장된 파일의 내부 객체 키입니다. resolving 동안에는 null입니다. API 호출에는 필요하지 않습니다. 소재를 재생하려면 GET /api/v1/ads/{ad_id}/stream을 사용하세요.

duration_msinteger | null

소재 길이(밀리초)입니다. resolving 동안에는 null입니다.

brand_hintsstring[]

등록 시 전달한 브랜드 힌트입니다.

source_typestring

소재가 레지스트리에 들어온 방식입니다. file, url, vast_url, vast_xml 중 하나입니다.

statusstring

resolving, ready, failed 중 하나입니다.

errorstring | null

status가 failed일 때의 실패 사유입니다.

uploaded_atstring

ISO 8601 타임스탬프입니다.

org_idstring | null

소유 조직입니다.

thumbnail_urlstring | null

소재 썸네일입니다(있는 경우).

분석 결과​

GET /api/v1/ads/{ad_id}/analysis는 매칭을 실행하지 않고도 소재가 무엇을 판매하는지, 누구를 타기팅하는지, 브랜드 세이프티 축에서 어떻게 읽히는지 반환합니다. 값은 POST /api/v1/ads/match의 ad_analysis 이벤트에 담기는 값과 같습니다.

분석은 보통 등록 시점에 실행되므로, ready인 소재는 바로 200을 반환합니다. 아직 실행되지 않았다면 엔드포인트는 { "ad_id": "...", "status": "pending" }과 함께 202를 반환하고 분석을 시작합니다. 200이 올 때까지 같은 URL을 폴링하세요. 30초 소재는 약 30초가 걸리고, 더 긴 소재는 길이에 비례해 더 걸립니다. 폴링해도 분석이 중복으로 시작되지 않습니다.

curl https://video.pyler.ai/api/v1/ads/$AD_ID/analysis \
-H "Authorization: Bearer $VUF_API_KEY"
{
"ad_id": "11111111-1111-1111-1111-111111111111",
"status": "ready",
"summary": "Premium smartphone feature spot with a carrier-exclusive bundle.",
"primary_brand": "nimbus mobile",
"primary_product": "smartphone",
"brands": ["Nimbus Mobile", "Orbit"],
"products": ["smartphone", "poster", "water bottle"],
"secondary_brands": ["Orbit"],
"secondary_products": ["water bottle"],
"genre": [],
"iab_content_leaf": ["Smartphones"],
"iab_ad_product_leaf": ["Smartphones"],
"iab_audience_interest_leaf": ["Technology"],
"iab_audience_purchase_intent_leaf": ["smartphone"],
"garm_video": [],
"ad_profile": {
"ad_type": "brand_awareness",
"pacing": "medium",
"tone_register": ["aspirational", "informative"],
"cta_type": "brand_recall",
"regulatory_class": "none"
},
"transcript_excerpt": "...",
"audio_events": ["speech"],
"music_moods": ["upbeat"]
}
필드타입참고
statusstringready(분석 있음) 또는 pending(202, 폴링 필요).
primary_brand / primary_productstring | null광고가 실제로 홍보하는 단 하나의 대상입니다.
brands / productsstring[]화면에 나오는 모든 것입니다. 광고하는 노트북 옆의 커피잔은 primary_product가 아니라 여기에 들어갑니다.
secondary_brands / secondary_productsstring[]광고에 나오지만 판매 대상은 아닌 것입니다.
iab_content_leaf, iab_ad_product_leaf, iab_audience_interest_leaf, iab_audience_purchase_intent_leafstring[]IAB 네 가지 축입니다. 콘텐츠, 광고 상품, 오디언스 관심사, 구매 의도.
garm_videoGarmEntry[]소재에서 탐지된 GARM 브랜드 세이프티 카테고리입니다. 항목마다 category, level(high / medium / low), reasoning, time_ranges가 있습니다.
ad_profileobject | nullad_type, pacing, tone_register, cta_type, regulatory_class.
transcript_excerpt, audio_events, music_moods오디오에서 도출한 신호입니다. 오디오 트랙이 실제로 분석되었는지 확인할 때 유용합니다.

장면 단위 캡션은 이 엔드포인트에서 제공하지 않습니다. IAB·GARM 어휘는 분류 체계와 판정을 참고하세요.

HTTP조건
200분석 결과가 있습니다.
202아직 결과가 없고 방금 분석을 시작했습니다. 같은 URL을 폴링하세요.
404존재하지 않는 ad_id이거나 다른 팀의 소재입니다.
409소재가 ready가 아니거나 분석할 미디어가 없습니다(길이 알 수 없음). 폴링해도 성공하지 않습니다.
5xx분석 조회 자체가 실패했습니다. 202로 가리지 않으므로 계속 폴링하지 마세요.

소재 스트리밍​

GET /api/v1/ads/{ad_id}/stream은 저장된 파일의 사전 서명 URL로 리디렉션(307)합니다. 소재가 ready가 아니면 스트리밍할 저장 파일이 아직 없으므로 409를 반환합니다.

첫 요청에는 Authorization 헤더가 필요합니다. 브라우저가 헤더를 붙일 수 없으므로 HTML <video> 태그에 이 URL을 바로 넣을 수 없습니다. 서버에서 해석한 뒤 사전 서명된 Location을 플레이어에 넘기세요.

r = requests.get(
f"{BASE}/ads/{ad_id}/stream",
headers=HEADERS,
allow_redirects=False,
timeout=30,
)
if r.status_code == 307:
video_src = r.headers["Location"] # valid for about one hour
노트

리디렉션을 따라가는 클라이언트(curl -L, 기본값 allow_redirects=True인 requests)는 자동으로 사전 서명 URL에 도달합니다. 사전 서명 URL 자체는 헤더가 필요 없고 약 1시간 후 만료됩니다. 데이터 처리를 참고하세요.

ad_id 재사용​

status: ready인 등록 소재는 다음 기능에서 참조할 수 있습니다.

기능엔드포인트ad_id를 받나요?
광고 매칭POST /api/v1/ads/match예. JSON 본문으로 받습니다.
광고 차단(브랜드 세이프티)POST /api/v1.1/ad-blocking예 — { "ad_id": "...", "rules": "..." }. v1 멀티파트 라우트 POST /api/v1/ad-blocking은 그대로입니다.

ready가 아닌 ad_id를 참조하면 409로 거부합니다. 존재하지 않거나 다른 팀의 ID는 404를 반환합니다. 등록된 ad_id를 입력으로 받는 라우트는 /api/v1.1 버전 루트 아래에 있습니다. 두 루트 모두 같은 API 키를 사용합니다.

# 1) Register by URL -> ad_id
AD_ID=$(curl -s -X POST https://video.pyler.ai/api/v1/ads \
-H "Authorization: Bearer $VUF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"source_type": "url", "source_url": "https://cdn.example.com/ad.mp4"}' | jq -r .id)

# 2) Poll GET /api/v1/ads/$AD_ID until status is ready

# 3) Screen the registered creative (v1.1)
curl -X POST https://video.pyler.ai/api/v1.1/ad-blocking \
-H "Authorization: Bearer $VUF_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"ad_id\": \"$AD_ID\"}"

판정 형식은 브랜드 세이프티를 참고하세요.

레시피: 광고 매칭(NDJSON 스트리밍)​

POST /api/v1/ads/match는 ad_id를 받아, 다단계 검색 + 재순위화 파이프라인이 실행되는 동안 색인된 코퍼스에서 가장 잘 맞는 영상을 스트리밍합니다. 그래서 파이프라인이 끝나기 전에도 부분 결과를 쓸 수 있습니다.

경고

응답은 application/x-ndjson입니다. 하나의 JSON 문서가 아니라 한 줄에 JSON 객체 하나입니다. 줄 단위로 파싱하고 본문 전체를 버퍼링하지 마세요.

ad_idstringbodyrequired

등록된 소재(status: ready)의 UUID입니다.

brand_hintsstring[]body

매칭에 반영할 수동 브랜드 키워드입니다(대소문자 구분 없음). 엔드 카드의 작은 브랜드처럼 캡션 모델이 로고를 놓쳤을 때 사용하세요.

per_query_kintegerbodydefault: 100

검색 레인별 ANN 예산입니다(2–400).

max_candidatesintegerbodydefault: 100

최종 재순위화의 후보 풀 크기입니다(5–400). 클수록 꼼꼼하지만 느립니다.

max_per_videointegerbodydefault: 10

영상당 최대 매치 수입니다(1–50).

  1. 소재 등록

    위 방법으로 업로드하거나 등록하고 status: ready가 될 때까지 기다립니다. ad_id를 보관하세요.

  2. 스트림 열기

    응답을 버퍼링하지 않는 클라이언트를 사용하세요(curl -N, stream=True를 설정한 requests).

    curl -N -X POST https://video.pyler.ai/api/v1/ads/match \
    -H "Authorization: Bearer $VUF_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{
    \"ad_id\": \"$AD_ID\",
    \"brand_hints\": [\"Acme Snacks\", \"Crunchy Sticks\"],
    \"max_candidates\": 100,
    \"max_per_video\": 5
    }"
  3. `type` 필드로 분기

    모든 줄은 type 필드가 있는 JSON 객체입니다. 다음 순서로 도착합니다.

    type페이로드시점
    analyzingstage소재를 가져와 분석하는 동안의 진행 표시입니다.
    ad_analysisGET /api/v1/ads/{ad_id}/analysis와 같은 필드소재 분석이 끝난 뒤 한 번.
    agent_steptool, query, ok, note쿼리 계획 단계마다.
    metafacets, tags, brand_context, candidates재순위화 전에 한 번 — 후보 장면입니다.
    resultvideo_id, video_title, scene_id, scene_index, score(0–10), reason, timestamp_ms, timestamp_sec_in_clip, scene_start_ms, scene_end_ms, clip_url재순위화가 끝나는 대로 장면마다 하나씩. 정렬되어 있지 않으므로 score로 직접 정렬하세요.
    done—스트림이 끝났습니다.
    errormessage실패했습니다. 스트림이 종료됩니다.

    timestamp_ms는 영상 안의 절대 위치(정수 밀리초)이고, timestamp_sec_in_clip은 해당 장면 클립 안의 오프셋입니다. 사용하는 타입만 처리하고 모르는 타입은 무시하세요. 이벤트별 페이로드는 OpenAPI 스키마에 고정되어 있지 않으므로, 알 수 없는 type 값은 향후 추가될 수 있는 값으로 다루세요.

    {"type": "analyzing", "stage": "fetch_ad"}
    {"type": "ad_analysis", "summary": "...", "iab": [...], "brands": [...], ...}
    {"type": "meta", "facets": {...}, "tags": [...], "candidates": [...]}
    {"type": "result", "video_id": "...", "scene_id": "...", "score": 8, "reason": "...", "timestamp_ms": 65000, ...}
    {"type": "done"}
팁

스트리밍 응답은 줄을 계속 보내는 동안 열려 있습니다. 그래서 오래 걸리는 매칭도 조용한 요청을 끊는 유휴 제한 시간에 걸리지 않습니다. 클라이언트의 읽기 타임아웃은 넉넉하게 설정하세요(위 예시의 timeout=(10, 600)). 사용량과 한도: 타임아웃과 재시도를 참고하세요.

오류​

모든 오류는 공통 형식 { "detail": "...", "code": "..." }을 사용합니다. 전체 목록은 오류를 참고하세요.

HTTP조건
202등록 또는 업로드가 접수되었습니다. 비동기 등록은 폴링이 필요합니다.
401 / 403키가 없거나 유효하지 않거나, 공개 API 루트 밖에서 키를 사용했습니다.
404존재하지 않는 ad_id이거나 다른 팀의 리소스입니다.
409소재가 ready가 아닙니다(분석, 스트림, v1.1 ad-blocking).
422검증 실패입니다. 라우트가 받지 않는 source_type이거나, VAST가 제출 시점 검사(vast_invalid, url_unreachable, blocked_target)를 통과하지 못했습니다.
500상위 서비스 오류입니다. 대체로 재시도해도 안전합니다.