Skip to content
 

MCP Usage Guide

How to run a multi-turn shopping conversation on top of aon_search_offers: act on the server's follow-up suggestions, keep the session continuous, retry safely, and understand what happens when arguments are imperfect.

  1. Vague intent? Call aon_get_category_schema (or aon_resolve_category first if you have no category id) and ask the user one clarifying question from its decision factors.
  2. Search. Call aon_search_offers with one natural sentence in intent.content. Add constraints.category_ids only with ids you got from the other two tools — never invent them.
  3. Present offers. Show the offer title and description from offer_info, the merchant or provider from entity.name, and the action.payload.url link verbatim. match_reason tells you why each offer matched — use it in your presentation.
  4. Offer the follow-ups. If the response has data.engagement, its refinements and followup_topics are pre-validated, inventory-backed next questions: the server only suggests moves that have inventory behind them.

The guidance loop

Each entry in engagement.refinements[] and engagement.followup_topics[] carries a label (show it to the user) and a query_helper:

{
  "label": "Under $200",
  "query_helper": {
    "request_patch": { "intent": { "signals": { "budget": { "max": 200, "currency": "USD" } } } },
    "origin": [{ "kind": "query_helper", "id": "qh_..." }]
  }
}

Budget is an intent.signals value, so every budget helper includes both a numeric max and an uppercase ISO 4217 currency. The server may only emit one when the current request has a known currency; it must not infer a currency from the user's language or region.

Category ids remain canonical filters under constraints, not natural-language intent. A category-complement helper therefore patches a different branch of the next request:

{
  "label": "Hiking socks",
  "query_helper": {
    "request_patch": { "constraints": { "category_ids": ["cat_hiking_socks"] } },
    "origin": [{ "kind": "query_helper", "id": "qh_..." }]
  }
}

When the user picks a suggestion, three things go into your next aon_search_offers call:

  1. Merge the patch. request_patch is an RFC 7386 merge-patch whose keys are protocol field paths — merge it straight into your previous request object. It can patch intent or constraints; do not move canonical category ids into intent just to make the JSON shape uniform.
  2. Echo the origin. Copy query_helper.origin into intent.origin[]. This is how adoption is attributed; skip it and the loop's analytics — and your ranking benefit — are lost.
  3. Chain the round. Pass the previous response's data.request_id as context.session.previous_request_id so offers the user already saw are not repeated.

Session continuity

  • context.session_id — send a stable, unguessable id per conversation (a fresh UUID). With a user identity, cross-round offer de-duplication engages when both are present. In anonymous mode (no user identity) de-duplication and intent memory are keyed by the session id alone, so never use a guessable value such as session-1 — it would mix your history with other callers'.
  • User identity — hosts that proxy many end users should send the x-aon-user-pseudo-id HTTP header per user. The context.user_profile.user_pseudo_id field is a fallback for clients that cannot set headers; the header always wins. Single-user MCP clients (Claude Code, Cursor, a personal desktop) can skip this entirely.
  • context.session.recent_topics[] — up to 10 short phrases of recent conversation context; helps the server rank follow-up topics.

request_id idempotency

request_id is an idempotency key with a 10-minute window. Two obligations:

  1. Retry = same id, new query = new id. Reusing an old id within the window returns the previous full response without an error — a changed question with a stale id silently gets the stale answer.
  2. Never share ids across end users. Under one placement (or the shared anonymous application), two users reusing one id would receive each other's responses — including each other's tracking links, which mis-attributes clicks and conversions.

Omit request_id and every call is treated as new (the server generates one for the response; the generated id does not join the idempotency window). In anonymous mode (no placement) request_id is echoed but never replayed: every call is computed fresh, and a retry recomputes. To keep multi-turn memory for an anonymous user, pass context.session_id or the x-aon-user-pseudo-id header.

Tolerant degradation

The search arm is forgiving on purpose: a value-level problem never fails the call. A wrong enum value, an out-of-range number, a malformed field — each is treated as "not sent" and reported in extra.hints[] with a machine-readable code and the closest valid option. If you see hints, read them: they tell you exactly how to fix the next call.

Only four things hard-fail:

  1. A malformed, unknown or inactive placement (a missing placement runs in anonymous mode instead), or wrong X-AON-Test usage
  2. Invalid JSON
  3. Missing intent.content
  4. Oversized request body

Two more behaviors worth knowing:

  • Unknown category ids in constraints.category_ids come back as an in-band correction listing valid candidates — resolve ids through aon_resolve_category to avoid the round-trip.
  • An empty offers list is a real answer. Check data.empty_reason, relay it honestly, and offer the engagement follow-ups instead of retrying the same query.

Paging through recommendations

Both aon_search_offers and the hosted REST Offer Query API accept nested pagination. Send limit (1 to 50; the default is 3, or 20 for Flight queries; larger values are clamped to 50) on the first request. When the request includes pagination, the response returns has_more and, while more offers are available, next_cursor. Neither field is returned without pagination.

To get the next page, repeat the same intent and constraints and pass next_cursor back unchanged as pagination.cursor. Send next_cursor back exactly as received: the cursor is opaque, so do not parse, build, or edit it. Do not send offset: for ordinary recommendation queries a pagination.offset or top-level offset greater than 0 fails with BAD_REQUEST (HTTP 400), and offset: 0 is treated as the first page. Offer Wall placements still accept a deprecated offset (see Offer Wall paging); offset together with cursor fails with BAD_REQUEST everywhere.

  • INVALID_CURSOR (HTTP 400): the cursor is malformed, has an unrecognized version, does not match this kind of query, is out of range, or belongs to a different query. Fix the request; do not retry it unchanged.
  • PAGINATION_EXPIRED (HTTP 410): an ordinary-recommendation round lasts 30 minutes, or the cursor's handle is invalid. Run the query again from the first page.

An ordinary-recommendation round returns at most 100 offers in total, then has_more is false. Offer Wall, Flight, and Hotel cursors do not expire. Sending the same cursor again returns the same batch of offers and records impressions again, with no effect on billing; a live partner offer is treated as no longer available when replayed.

Use a new request_id for each page and reuse that id only when retrying the same page. Offer Wall pages use the same has_more / next_cursor fields, described in the Offer Wall guide.

Do / don't

DoDon't
Surface action.payload.url links verbatimRewrite, shorten, or proxy tracking links
Echo query_helper.origin when a suggestion is adoptedApply a patch but drop its origin
Send one natural sentence as intentStuff keyword lists into intent.content
Use ids from aon_resolve_categoryGuess or invent taxonomy ids
Show an honest "nothing relevant" on empty resultsPad with unrelated offers

Rate limits & FAQ →