Skip to main content
Stores a set of targeting attributes against one user so later offer requests are targeted on them without the values traveling in a link. Your hosted offer page URL then carries no attributes at all, just apiKey, userId, and whichever presentation parameters you use.
Call it before you open the offer page for a user, or ahead of your POST /offers/feed, and again whenever what you know about that user changes. To attach attributes to a single request instead of storing them, put them in that request’s own attributes object: a request attribute always wins over the stored one.

Authentication

Pass your publishable key in the X-API-Key header (pk_live_* for production, pk_test_* for development). This is the same guard POST /offers/feed uses, so no new credential is involved. Multi-app projects also send X-Platform.
Store against the app that serves the offers. The set is keyed on the app your key resolves plus userId, so a set stored against one app in a multi-app project is invisible to another. The hosted offer page runs the Web SDK, which sends X-Platform: web, so a multi-app project storing attributes for that page sends X-Platform: web here too.

How the stored set is used

  • Each call replaces the last one. The write is idempotent on the app plus userId, and the newest set is the whole truth. Attributes are never merged into what was stored before, so send the complete set every time.
  • There is no expiry. The last set stays authoritative until a newer one arrives, and a data-subject erasure request removes it.
  • A request attribute always wins. POST /offers/feed, and the POST /offers/search call the Web SDK makes on your behalf, read the stored set only to fill an attribute the request itself omitted. POST /offers/catalog and POST /offers/message do not read it.
  • Storing an attribute never makes it optional on a request. /offers/feed still requires countryCode and language in its own body, and rejects a request that omits them before the stored set is read. What the store fills on that endpoint is everything else.
  • The user does not have to exist yet. The first call for a userId creates the user, so there is no ordering requirement against any other endpoint.

Request

Structure

Field Reference

Sending an age

age is an integer count of whole years. It is accepted from every app, whatever that app’s audience declaration says, because it is what narrows which offers a user may be shown rather than something that identifies them.
If you hold only a range, send its lowest end. "13-17" is sent as 13, never 15 and never 17. Sending the middle of a range defeats the check silently: flooring can never mask a minor, and a midpoint can.
An age under 18 does not switch offers off. That user is still served. What changes is that the offers come from age-appropriate inventory only, ranked by the app-level bandit, with no per-user personalization. The response reports minor: true so you can see which branch a user took. The instant you sent the set is recorded, and the age acted on is counted forward from it. A user stored at 17 becomes an adult on schedule rather than staying 17 forever. Send age, never a date of birth. dateOfBirth is refused here: an age carries the same targeting value with far less identifying power.

What is stored for a user under 18

Only age, countryCode, and language are kept. Everything else you sent is discarded, custom included, along with city, state, region, and every subscription and billing field. Those three survive because of what they do. age narrows which offers the user may be shown, countryCode decides where an offer may legally appear, and language picks the creative locale that renders. None of the three chooses an advertiser. Everything else exists to personalize the pick, and personalization is withheld for this user, so storing it would keep data that can never be acted on.
This is silent, and it is not an error. The call returns 200, and the only sign is minor: true in the response. Send subscriptionTier for a 16-year-old and you get success, no warning, and no subscriptionTier in the stored set afterwards.
It is decided per request, from the age that request carried, so this is not a separate integration to build. The same set sent for an adult is stored whole. Once a user is 18, re-send their set and it is stored in full.

Attributes that need an adults-only app

email, firstName, lastName, mobile, phoneNumber, postcode, postalCode, latitude, longitude, and gender are accepted only from an app whose audience is declared adults_only, meaning it is rated 18+ in its store listing. Every other status is refused with a 400, including the default status an app carries before anyone has declared its audience. The refusal names the keys it rejected and stores nothing at all, not even the attributes that would have been allowed:
Re-send without those keys, or have the app’s audience declaration corrected in the dashboard first. Neither postalCode nor postcode reaches offer selection today, so an app that cannot send them loses no targeting by leaving them out.

Attributes that are refused

Response

Structure

Field Reference

Example

Status codes

Retry guidance

  • Retrying is safe, but only useful for some failures. The call is an idempotent replace, so sending the same set twice leaves exactly the state one call would have. This is unlike the offer-selection endpoints, where each call mints fresh links and analytics rows. A 400 or 401 is deterministic, so repeating it produces the same refusal: fix the request instead.
  • Use exponential backoff with jitter (for example 200ms, 1s, 5s, max 3 retries) for transient failures (503 / network).
  • After exhausting retries, open the offer page anyway. A user with no stored set is served from what the request itself carries, so the worst case is less targeted, never nothing.