Skip to content
Knobase

POST /v1/offers/decide

The only V1 offer decision endpoint. Returns a labeled offer or a successful no-fill.

Decide whether the current turn may receive a labeled, tracked offer.

Authentication

Bearer serving key required. Setup tokens are rejected. The app is inferred from the key.

Rate limit

60 requests per minute per client IP. HTTP 429 with `{ ok: false, error: "rate_limited" }`.

Request headers

HeaderValue
AuthorizationBearer $KNOBASE_API_KEY
Content-Typeapplication/json

Request body

FieldTypeRequiredDescription
messagestring (max 4000)NoCurrent user turn only. Processed transiently and not persisted. Do not send a transcript.
session.tokenstring (max 128)NoEphemeral host session token for frequency. Not a user id.
session.digest.intent_type"purchase" | "compare" | "browse" | "low" | "unknown"NoTyped intent. No free text.
session.digest.category_idsstring[] (max 20, each max 64)NoAllowlisted category identifiers only.
session.digest.entity_idsstring[] (max 20, each max 64)NoAllowlisted entity identifiers only.
session.digest.excluded_entity_idsstring[] (max 20, each max 64)NoAllowlisted exclusions. Unknown ids are dropped.
session.digest.budget_bucket"unknown" | "low" | "mid" | "high"NoCoarse budget band. No currency or amounts.
session.digest.safety"clear" | "unknown" | "blocked"No`unknown` or `blocked` results in safe no-fill.
session.digest.generated_atstring (max 40)NoISO timestamp. Digests older than 30 minutes are ignored.
localestring (2–16)NoServing is live for `ko*` and `en*` locales only.
countrystring (2–8)NoISO-style country hint. Not a precise location.
audience.age_band"under_13" | "13_17" | "18_plus" | "unknown"NoNever infer age. Serving requires `18_plus`.
audience.contextual_offer_consentbooleanNoServing requires `true`. Omitted or false is no-fill.
native_surface.clickablebooleanYesSelf-attestation that an existing separate clickable surface is available. Not production trust.
native_surface.disclosure_capablebooleanYesSelf-attestation that disclosure and advertiser identity can be shown. False is CAPABILITY_UNMET.

Constraints

  • message is the current user turn only. Do not send a raw full transcript.
  • No personal identity, email, phone, advertising ID, precise location, health, financial, biometric, or cross-app identity data.
  • The current message is processed transiently and is not persisted.
  • Typed digest is optional and has no free text.
  • Unknown consent, age, or safety results in safe no-fill.
  • Serving requires audience.age_band = 18_plus and audience.contextual_offer_consent = true.
  • native_surface.clickable and native_surface.disclosure_capable are required self-attestation booleans. Either false is CAPABILITY_UNMET.
  • Unknown top-level keys fail validation. The envelope is strict.
  • Treat timeout, network errors, malformed responses, and serve:false as no-render.

These keys are rejected anywhere in the body: 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.

Request examples

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}}'

Served-offer response

HTTP 200. disclosure, advertiser, and link are required commercial-integrity fields. title, description, image_url, and cta are optional presentation hints and do not instruct you to create a visual component. offer_id is the snapshot id used as offer_snapshot_id on events.

FieldTypeRequiredDescription
servetrueYesAn eligible offer may be used on an existing surface.
request_idstringYesRequest identifier.
decision_idstringYesDecision identifier (`dec_…`).
offer_idstringYesOffer snapshot id (`snp_…`). Send this as `offer_snapshot_id` on events.
expires_atstring | nullYesISO expiry when present.
disclosurestringYesRequired sponsorship label. Must be visible.
advertiserstringYesRequired advertiser identity. Must be visible.
linkstringYesKnobase tracked URL. Never replace with a merchant URL.
titlestringNoOptional presentation hint. Not a UI instruction.
descriptionstringNoOptional presentation hint. Not a UI instruction.
image_urlstringNoOptional presentation hint. Not a UI instruction.
ctastringNoOptional presentation hint. Not a UI instruction.
reason_codesstring[]YesIncludes `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 response

HTTP 200 with serve:false is success. Keep the organic answer. Do not retry in a way that blocks the user.

FieldTypeRequiredDescription
servefalseYesDo not render an offer.
request_idstringYesRequest identifier.
decision_idstringYesDecision identifier (`dec_…`).
reason_codesstring[]YesWhy no offer was returned.
retry_after_secondsnumberNoPresent 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
}

Reason codes

CodeMeaning
POLICY_BLOCKAudience, safety, or policy did not allow a serve.
LOW_INTENTIntent was too weak to serve.
NO_ELIGIBLE_OFFERNo matching eligible offer.
BUDGET_EXHAUSTEDCampaign or account budget is exhausted.
TIMEOUTHandler budget exceeded. Fail open.
LOCALE_NOT_LIVELocale is not in the live serving set.
FREQUENCY_CAPPEDSession frequency cap reached.
MATCHEDAn eligible offer was returned.
INVALID_ENVELOPERequest failed schema or unknown-field validation.
FORBIDDEN_IDENTITY_FIELDA forbidden identity or placement field was present.
CAPABILITY_UNMETnative_surface.clickable or disclosure_capable was false.

Errors

HTTPBodyWhen
401{ "ok": false, "error": "…" }Missing, malformed, invalid, or revoked key
400{ "ok": false, "error": "INVALID_ENVELOPE" | "FORBIDDEN_IDENTITY_FIELD", "reason_codes": […] }Invalid or forbidden body
429{ "ok": false, "error": "rate_limited" }More than 60 requests / minute / IP
200serve:false, reason_codes includes TIMEOUTHandler budget exceeded; fail open

Docs version v1 · implemented publisher API