POST /v1/offers/decide
유일한 V1 오퍼 결정 엔드포인트입니다. 표기된 오퍼 또는 성공적인 no-fill을 반환합니다.
현재 턴에 표기된 추적 오퍼를 반환할지 결정합니다.
인증
Bearer serving key가 필요합니다. 설정 토큰은 거절됩니다. 앱은 키에서 확인됩니다.
속도 제한
클라이언트 IP당 분당 60회. HTTP 429와 `{ ok: false, error: "rate_limited" }`.
요청 헤더
| 헤더 | 값 |
|---|---|
| Authorization | Bearer $KNOBASE_API_KEY |
| Content-Type | application/json |
요청 본문
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| message | string (max 4000) | 아니오 | Current user turn only. Processed transiently and not persisted. Do not send a transcript. |
| session.token | string (max 128) | 아니오 | Ephemeral host session token for frequency. Not a user id. |
| session.digest.intent_type | "purchase" | "compare" | "browse" | "low" | "unknown" | 아니오 | Typed intent. No free text. |
| session.digest.category_ids | string[] (max 20, each max 64) | 아니오 | Allowlisted category identifiers only. |
| session.digest.entity_ids | string[] (max 20, each max 64) | 아니오 | Allowlisted entity identifiers only. |
| session.digest.excluded_entity_ids | string[] (max 20, each max 64) | 아니오 | Allowlisted exclusions. Unknown ids are dropped. |
| session.digest.budget_bucket | "unknown" | "low" | "mid" | "high" | 아니오 | Coarse budget band. No currency or amounts. |
| session.digest.safety | "clear" | "unknown" | "blocked" | 아니오 | `unknown` or `blocked` results in safe no-fill. |
| session.digest.generated_at | string (max 40) | 아니오 | ISO timestamp. Digests older than 30 minutes are ignored. |
| locale | string (2–16) | 아니오 | Serving is live for `ko*` and `en*` locales only. |
| country | string (2–8) | 아니오 | ISO-style country hint. Not a precise location. |
| audience.age_band | "under_13" | "13_17" | "18_plus" | "unknown" | 아니오 | Never infer age. Serving requires `18_plus`. |
| audience.contextual_offer_consent | boolean | 아니오 | Serving requires `true`. Omitted or false is no-fill. |
| native_surface.clickable | boolean | 예 | Self-attestation that an existing separate clickable surface is available. Not production trust. |
| native_surface.disclosure_capable | boolean | 예 | Self-attestation that disclosure and advertiser identity can be shown. False is CAPABILITY_UNMET. |
제약
- message는 현재 사용자 턴만입니다. 전체 기록을 보내지 마세요.
- 신원, 이메일, 전화, 광고 ID, 정밀 위치, 건강, 금융, 생체, 교차 앱 식별 데이터를 보내지 마세요.
- 현재 메시지는 일시 처리되며 저장되지 않습니다.
- typed digest는 선택이며 자유 텍스트가 없습니다.
- 동의, 나이, 안전이 불명이면 안전한 no-fill입니다.
- serving은 audience.age_band = 18_plus와 contextual_offer_consent = true가 필요합니다.
- native_surface.clickable과 disclosure_capable는 필수 자가 증명 불리언입니다. 하나라도 false이면 CAPABILITY_UNMET입니다.
- 알 수 없는 최상위 키는 검증 실패입니다. envelope는 strict입니다.
- 타임아웃, 네트워크 오류, 잘못된 응답, serve:false는 no-render로 다루세요.
본문 어디서든 거절되는 키: email, phone, tel, user_id, userid, userId, name, full_name, fullname, lat, lng, latitude, longitude, address, ip, conversation, conversation_history, messages, raw_message, app_id, placement, display_style, surface_id, capabilities.
요청 예제
curl -sS -X POST "https://knobase.com/v1/offers/decide" \
-H "Authorization: Bearer $KNOBASE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"message":"시니어 반려견에게 줄 부드러운 사료를 찾고 있어요","locale":"ko","country":"KR","audience":{"age_band":"18_plus","contextual_offer_consent":true},"native_surface":{"clickable":true,"disclosure_capable":true}}'오퍼 반환 응답
HTTP 200. disclosure, advertiser, link는 필수 상업 무결성 필드입니다. title, description, image_url, cta는 선택 힌트이며 특정 UI를 만들라는 지시가 아닙니다. offer_id는 이벤트의 offer_snapshot_id로 쓰는 스냅샷 id입니다.
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| serve | true | 예 | An eligible offer may be used on an existing surface. |
| request_id | string | 예 | Request identifier. |
| decision_id | string | 예 | Decision identifier (`dec_…`). |
| offer_id | string | 예 | Offer snapshot id (`snp_…`). Send this as `offer_snapshot_id` on events. |
| expires_at | string | null | 예 | ISO expiry when present. |
| disclosure | string | 예 | Required sponsorship label. Must be visible. |
| advertiser | string | 예 | Required advertiser identity. Must be visible. |
| link | string | 예 | Knobase tracked URL. Never replace with a merchant URL. |
| title | string | 아니오 | Optional presentation hint. Not a UI instruction. |
| description | string | 아니오 | Optional presentation hint. Not a UI instruction. |
| image_url | string | 아니오 | Optional presentation hint. Not a UI instruction. |
| cta | string | 아니오 | Optional presentation hint. Not a UI instruction. |
| reason_codes | string[] | 예 | Includes `MATCHED` when an offer is served. |
{
"serve": true,
"request_id": "req_01",
"decision_id": "dec_01",
"offer_id": "snp_01EXAMPLEOFFER",
"expires_at": "2026-09-10T12:30:00.000Z",
"disclosure": "광고",
"advertiser": "Example Pet",
"link": "https://knobase.com/r/tok_example",
"title": "시니어 반려견을 위한 부드러운 식사 옵션",
"description": "제품 정보와 원재료를 확인해보세요.",
"cta": "제품 보기",
"reason_codes": [
"MATCHED"
]
}No-fill 응답
serve:false인 HTTP 200은 성공입니다. 유기적 답변을 유지하세요. 사용자를 막는 재시도는 하지 마세요.
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| serve | false | 예 | Do not render an offer. |
| request_id | string | 예 | Request identifier. |
| decision_id | string | 예 | Decision identifier (`dec_…`). |
| reason_codes | string[] | 예 | Why no offer was returned. |
| retry_after_seconds | number | 아니오 | Present on some no-fill outcomes, including timeout. |
{
"serve": false,
"request_id": "req_02",
"decision_id": "dec_02",
"reason_codes": [
"NO_ELIGIBLE_OFFER"
],
"retry_after_seconds": 0
}사유 코드
| 코드 | 의미 |
|---|---|
| POLICY_BLOCK | Audience, safety, or policy did not allow a serve. |
| LOW_INTENT | Intent was too weak to serve. |
| NO_ELIGIBLE_OFFER | No matching eligible offer. |
| BUDGET_EXHAUSTED | Campaign or account budget is exhausted. |
| TIMEOUT | Handler budget exceeded. Fail open. |
| LOCALE_NOT_LIVE | Locale is not in the live serving set. |
| FREQUENCY_CAPPED | Session frequency cap reached. |
| MATCHED | An eligible offer was returned. |
| INVALID_ENVELOPE | Request failed schema or unknown-field validation. |
| FORBIDDEN_IDENTITY_FIELD | A forbidden identity or placement field was present. |
| CAPABILITY_UNMET | native_surface.clickable or disclosure_capable was false. |
오류
| HTTP | 본문 | 상황 |
|---|---|---|
| 401 | { "ok": false, "error": "…" } | 없거나 잘못된 키, 폐기된 키 |
| 400 | { "ok": false, "error": "INVALID_ENVELOPE" | "FORBIDDEN_IDENTITY_FIELD" } | 잘못된 또는 금지된 본문 |
| 429 | { "ok": false, "error": "rate_limited" } | IP당 분당 60회 초과 |
| 200 | serve:false, TIMEOUT | 처리 예산 초과. fail-open |