Select offers for one end user

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.

Recent Requests
Log in to see full request history
TimeStatusUser Agent
Retrieving recent requests…
LoadingLoading…

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 the x-thanks-impression-id response header still use the Thanks name. You don't need to change your integration.

Before you start

  • An API key. Send it as the Authorization header on every request to https://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

  1. 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 in position order, and do not reorder, filter, or pick from the list. An off status means show nothing.
  2. Render. As each offer's card comes on screen, call GET on that offer's href. You get back the copy, images, terms, and clickUrl. A 410 means show nothing in that slot.
  3. Click. When the user taps the card, open clickUrl in 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's User-Agent string unchanged. Native apps whose User-Agent does not describe the device send device.type instead.
  • 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, and termsHtml, whenever the offer has them.
  • Show copy exactly as returned. It is plain text: do not rewrite, translate, or render it as HTML.
  • termsHtml is HTML. Render it unchanged, on the card or one tap away.
  • Use image.mobile on phone-width layouts and image.desktop on 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. count goes up to 100. Kodo can set a lower cap for an experience; a count above 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 5xx or a 429, once, with a short backoff. Never retry any other 4xx. 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 its Retry-After header 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 same requestId and exactly the same body, and you get the first attempt's result instead of a second selection. The same requestId with a different body is a 409.
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 traceId in the selection. We store it with the impression and echo it in the response.
  • Send X-Request-Id or traceparent on your requests; every response echoes them.
  • When you contact us, quote these ids with the x-thanks-impression-id response header.
Path Params
string
required
Body Params
integer
required
1 to 100

How many offers to select, up to 100. Thanks can set a lower cap for an experience; a count above it is clamped, never rejected. The response may hold fewer offers than count when fewer qualify.

string
length ≤ 1024

The end user's User-Agent, verbatim. Required unless device.type is sent. Your own HTTP client's User-Agent header is ignored.

device
object

Overrides for native apps, whose User-Agent is not a browser one.

location
object
required

Where the end user is.

date-time
required

The end user's local time, ISO 8601 with its UTC offset. Selection reads the hour and weekday from it.

string
length between 1 and 35

The end user's language tag. Without it, campaigns that require a language are not served.

string
^[\x20-\x7E]{1,128}$

Your own id for this session, echoed in the response and reported back to you. At most 128 printable ASCII characters and no personal information. Never used for selection.

user
object

Who the end user is, when you know. Used for offers that depend on past behaviour and to attribute clicks to one person.

Responses

Language
Credentials
Header
LoadingLoading…
Response
Click Try It! to start a request and see the response here! Or choose an example:
application/json