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 theX-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.
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 thePOST /offers/searchcall the Web SDK makes on your behalf, read the stored set only to fill an attribute the request itself omitted.POST /offers/catalogandPOST /offers/messagedo not read it. - Storing an attribute never makes it optional on a request.
/offers/feedstill requirescountryCodeandlanguagein 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
userIdcreates 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.
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
Onlyage, 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.
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:
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
400or401is 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.
Related
- Hosted Offer Page: the page these attributes target, and how to move off URL parameters.
POST /offers/feed: the ranked selection that reads the stored set for attributes its own body omits.- API Reference overview: auth and the claim model.