Call when you are about to render the experience for one end user. Selects up to count offers; nothing is recorded until an offer is viewed. GET each offer href when its card is on screen. Retry only a 5xx or 429; a retry selects again and gets a new impressionId.
| Time | Status | User Agent | |
|---|---|---|---|
Retrieving recent requests… | |||
Integrating the Offers API
The Offers API lets your server fetch ranked Kodo Ad Network offers for one end user and render them in your own surface. This page explains the order of the calls and the rules to follow. Every field and error is described in the API reference.
The API host (
api.thanks.is) and thex-thanks-impression-idresponse header still use the Thanks name. You don't need to change your integration.
Before you start
- An API key. Send it as the
Authorizationheader on every request tohttps://api.thanks.is. Keep it on your server and never ship it to a browser or app. - An experience id for each surface where you show offers. This is the same Partner ID (
plat…) used by the Kodo Ad Network SDK, and it identifies one experience on your site or app. If you run surfaces for several brands, each brand has its own experience and its own API key. Always call with the experience the user is on, because it decides the country, the revenue share, and which advertisers are blocked.
Call the API in this order
- Select. When you are about to show the experience to a user, call
POST /api/v1/experiences/{experienceId}/impressions. You get back a ranked list of offers. Show them inpositionorder, and do not reorder, filter, or pick from the list. Anoffstatus means show nothing. - Render. As each offer's card comes on screen, call
GETon that offer'shref. You get back the copy, images, terms, andclickUrl. A410means show nothing in that slot. - Click. When the user taps the card, open
clickUrlin the user's browser. Never call it from your server, and never replace it with the advertiser's own link.
Each call counts something different. The POST records that a user was offered a list. The first GET of an offer counts as that offer being seen, so call it only when the card is on screen, never ahead of time. The click counts only when the user opens clickUrl.
Why we ask for the user's device, location, and local time
Many offers carry restrictions set by the advertiser: only on mobile, only on iOS, only in certain states or postal codes, or only on certain weekdays or at certain times of day. A browser integration gives us these details automatically. Your server's request comes from your data center, so you send them for the user.
We pick offers using what you send, so a missing value means the user gets fewer offers than they qualify for. To make that visible, these are required, and a request without them gets a 400 naming the field:
userAgent, the user'sUser-Agentstring unchanged. Native apps whoseUser-Agentdoes not describe the device senddevice.typeinstead.location.regionCode, the user's state or region. The country comes from the experience.localTime, the user's local time with its UTC offset. Without the offset we would read your server's time, not the user's.
location.postalCode and locale are optional, but offers targeted by postal code or by language are not shown without them. Send the best data you have: a delivery or account address is better than an approximate location.
Rendering rules
- A card shows
title, the advertiser's name and logo, the image,cta,disclaimer, andtermsHtml, whenever the offer has them. - Show copy exactly as returned. It is plain text: do not rewrite, translate, or render it as HTML.
termsHtmlis HTML. Render it unchanged, on the card or one tap away.- Use
image.mobileon phone-width layouts andimage.desktopon wider ones, falling back to the other when one is missing.
Rules for every integration
- One selection belongs to one user. Never cache a selection or share it between users. Clicks are attributed per selection, so a shared one breaks attribution.
- Select only when you are about to render, not ahead of time or in the background.
- Ask for as many offers as you can show.
countgoes up to 100. Kodo can set a lower cap for an experience; acountabove it is reduced, not rejected. - You decide how often a user sees the experience. Kodo does not limit it.
Retries and rate limits
- Retry only a
5xxor a429, once, with a short backoff. Never retry any other4xx. If the retry fails too, show nothing rather than making the user wait. - Requests are rate-limited for each API key and experience, by default 200 requests every 10 seconds per experience. Over the limit is a
429: wait the number of seconds in itsRetry-Afterheader before you retry. Contact your Account Manager if you expect more traffic. - Create a new
requestId, such as a UUID, for each selection. Retry with the samerequestIdand exactly the same body, and you get the first attempt's result instead of a second selection. The samerequestIdwith a different body is a409.
const body = { requestId: crypto.randomUUID(), count: 6 /* user details */ };
await retry(() => post(`/api/v1/experiences/${experienceId}/impressions`, body));Reporting a problem
- Send your own session or page id as
traceIdin the selection. We store it with the impression and echo it in the response. - Send
X-Request-Idortraceparenton your requests; every response echoes them. - When you contact us, quote these ids with the
x-thanks-impression-idresponse header.
