Skip to content
 

Implement Offer Fetch with Protocol v1.0

POST  {offer_fetch_url}   AON -> Partner

Offer Fetch is the AON -> Partner supply-side contract. AON sends signed POST requests to the exact URL you configure in Partner Portal, and your backend returns eligible offers in the OfferProvider response envelope. Partner Portal manages account setup and operational access for this flow.

Continue here after an approved Partner has valid credentials and an exact Offer Fetch URL ready to register in Partner Portal. The endpoint must be under your team's control and able to receive signed AON requests.

Authoritative spec

This page is a partner-friendly implementation guide. The normative specification lives in the public protocol repository: OfferProvider API spec. When summary and spec disagree, the spec wins.

Canonical OfferProvider supply contract

This guide covers the current v1.0 Partner supply carrier. Return stable source_offer_id; AON resolves its own offer_id, creates offer_instance_id, and authors any match_reason for the later public Query response. Service access and operations are configured separately from this protocol contract.

Protocol v1.0 wire contract

Send and echo the exact negotiation value 1.0. Do not rename, add, remove, or reinterpret fields from the canonical v1.0 OfferProvider schema. This transport version is separate from each Offer's required document-model marker, which remains version: "3.0". Do not replace it with "1.0" or omit it. Requiredness, validation, signing, retry, test mode, and postback behavior are defined by the linked v1.0 specifications.

Implementation checklist

  • Configure your Offer Fetch URL through an enabled Partner account and follow the Portal setup requirements for your integration.
  • Verify X-AON-Timestamp, X-AON-Nonce, and X-AON-Signature before parsing business logic.
  • Parse the OfferProvider request body with request_id, context, and intent.
  • Match offers from your catalog and return the v1.0 Partner supply envelope with request_id, protocol_version, language, and offers.
  • For each returned offer, populate offer_info.category.id with an AON Taxonomy v1 id from the Category Taxonomy.
  • Keep each Offer's required document-model marker at version: "3.0"; the OfferProvider transport selector and body echo remain 1.0.
  • When you have canonical flight or hotel facts, add the optional closed offer_info.details profile described in Travel offer details.
  • Treat X-AON-Test: true as non-production traffic and suppress side effects.
  • Keep protocol and schema questions anchored to the GitHub sources; the spec wins.

What this page is and is not

This page covers everything you need to stand up an Offer Fetch endpoint and pass AON's onboarding compliance check:

  • How AON calls you: method, URL, headers, and signing.
  • What the request body contains.
  • The 6-step path your handler should walk.
  • The response envelope for success and errors.
  • Test mode semantics.

This page does not restate the full Partner Offer Schema or invent new OfferProvider fields. For normative field definitions, follow the protocol and schema links in Authoritative protocol references.

AON calling convention

AON sends every offer fetch as a signed POST to the complete URL you register in Partner Portal as your Offer Fetch URL.

POST  {offer_fetch_url}

PropertyValue
MethodPOST
URLThe exact URL you saved as Offer Fetch URL; AON does not append a path
Content-Typeapplication/json
BodyOfferProvider request body; see Request body schema

The URL you save in Partner Portal is the URL AON calls. If you want the protocol's /v1/offers/query path on your side, register https://api.example.com/aon/v1/offers/query directly.

Required headers

HeaderValue
X-AON-KeyThe appkey AON issued to your Partner account
X-AON-TimestampUnix epoch seconds as an ASCII decimal integer
X-AON-NonceUnique-per-request random string; UUIDv4 recommended
X-AON-SignatureLowercase hex HMAC-SHA256(secret, signing_string)
AON-Protocol-VersionExact value 1.0; omitted or unsupported values fail closed
X-AON-Testtrue when AON is sending a test request

Signing string

The signing string is exactly:

POST\n
{path}\n
{raw_request_body_bytes}\n
{X-AON-Timestamp}\n
{X-AON-Nonce}

Notes:

  • Use the raw UTF-8 bytes AON sent on the wire. Do not re-serialize, re-order keys, or normalize whitespace before hashing.
  • Compare signatures in constant time to avoid timing side-channels.

Sample verify implementations

Minimal verify(secret, signing_string, received_signature_hex) -> bool functions in five languages, each using only the standard library. These snippets cover signature verification only; a complete handler also needs the timestamp window check and nonce anti-replay.

For full reproducible test vectors and a generator script, see hmac-signing-cases.md in the protocol examples repo.

// Node.js (built-in `crypto`)
const crypto = require('crypto');
 
const HEX_64 = /^[0-9a-f]{64}$/i;
 
function verify(secret, signingString, receivedSigHex) {
  if (!HEX_64.test(receivedSigHex)) return false;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(signingString, 'utf8')
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(receivedSigHex, 'hex'),
  );
}
# Python 3.7+ (built-in `hmac` + `hashlib`)
import hmac
import hashlib
 
def verify(secret: bytes, signing_string: bytes, received_sig_hex: str) -> bool:
    expected = hmac.new(secret, signing_string, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, received_sig_hex.lower())
// Go 1.18+ (built-in `crypto/hmac` + `crypto/sha256`)
package partner
 
import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
)
 
func Verify(secret, signingString []byte, receivedSigHex string) bool {
	mac := hmac.New(sha256.New, secret)
	mac.Write(signingString)
	expected := mac.Sum(nil)
	received, err := hex.DecodeString(receivedSigHex)
	if err != nil {
		return false
	}
	return hmac.Equal(expected, received)
}
// Java 8+ (built-in `javax.crypto.Mac` + `java.security.MessageDigest`)
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
 
public static boolean verify(byte[] secret, byte[] signingString, String receivedSigHex)
        throws Exception {
    byte[] received = hexDecode(receivedSigHex);
    if (received == null) return false;
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret, "HmacSHA256"));
    byte[] expected = mac.doFinal(signingString);
    return MessageDigest.isEqual(expected, received);
}
 
private static byte[] hexDecode(String s) {
    if (s == null || s.length() != 64) return null;
    byte[] out = new byte[32];
    for (int i = 0; i < 64; i += 2) {
        int hi = Character.digit(s.charAt(i), 16);
        int lo = Character.digit(s.charAt(i + 1), 16);
        if (hi < 0 || lo < 0) return null;
        out[i / 2] = (byte) ((hi << 4) + lo);
    }
    return out;
}
<?php
// PHP 7.2+ (built-in `hash_hmac` + `hash_equals`)
function aon_verify(string $secret, string $signingString, string $receivedSigHex): bool {
    $expected = hash_hmac('sha256', $signingString, $secret);
    return hash_equals($expected, strtolower($receivedSigHex));
}

Each snippet uses its language's constant-time HMAC compare primitive: crypto.timingSafeEqual, hmac.compare_digest, hmac.Equal, MessageDigest.isEqual, or hash_equals.

Timestamp window

Reject any request where |server_now - X-AON-Timestamp| > 300 seconds with 401 UNAUTHORIZED and message "timestamp outside allowed skew". Do this before verifying the signature so tampered timestamps do not waste HMAC compute.

The 5-minute skew is part of the protocol contract for onboarding compliance.

Request body schema

The body follows offer-provider-request.json. The AON -> Partner channel requires request_id, context, and intent on every dispatch.

Supply-side constraints

The OfferProvider request uses constraints, matching the public Query root field name. AON Taxonomy v1 dispatches constraints.category_ids.

When constraints.category_ids is present:

RuleMeaning
Multiple idsOR logic. Return offers that match any requested id.
Parent idsA parent id matches its whole subtree, so travel_tourism includes deeper travel ids.
CaseIds are lowercase and case-sensitive. Do not send display names.
othersothers is a standard Level 1 id. It does not stand in for invalid or unknown ids.

Top-level

Parameter
request_id
string (uuid)Required

AON-generated request correlation id. UUIDv7 is recommended. Partners may echo it in logs or `X-AON-Request-Id`.

context
objectRequired

Bounded platform, session, and conversation context. Long-term user profiles are not part of the v1.0 wire contract.

intent
objectRequired

Current-turn multimodal intent with required provenance and at least one entry in intent.content[].

constraints
objectOptional

Partner-facing supply-side constraints. Current dispatches expose category_ids.

force_offer
booleanOptional

Request a fallback recommendation when normal matching returns no offer.

response_options
objectOptional

Public projection controls. `thinking_mode` defaults to true; when false, AON omits `match_reason` from its later Query response. Partner supply never returns that field.

timestamp
stringOptional

RFC 3339 dispatch time; informational, not the signing timestamp.

test_mode
booleanOptional

Body mirror of X-AON-Test. Header wins on disagreement.

Example payload

{
  "request_id": "01984dc5-3b32-7c1a-9e8b-2f1a7b4d8c11",
  "timestamp": "2026-05-04T08:30:00Z",
  "test_mode": false,
  "context": {
    "platform": { "name": "PartnerDemo", "version": "2.0.0", "channel": "web" },
    "session": {
      "previous_request_id": "01984dc5-3b32-7c1a-9e8b-2f1a7b4d8c10",
      "recent_topics": ["Tokyo hotels"]
    }
  },
  "intent": {
    "content": [
      { "type": "input_text", "text": "weekend hotel deals in Tokyo under $200" }
    ],
    "provenance": "user_expressed",
    "signals": {
      "budget": { "max": 200, "currency": "USD" },
      "timeframe": "this_week"
    }
  },
  "constraints": {
    "category_ids": ["travel_tourism"]
  },
  "force_offer": false,
  "response_options": { "thinking_mode": true }
}

Pin intent.content[] and intent.provenance first. Forward only the bounded context and structured intent.signals fields defined by the v1.0 schema. A budget without an explicit ISO 4217 currency must not be turned into a numeric budget constraint.

For the full field-by-field semantics, see offer-provider-request.json.

Handler flow

The minimum conformant handler walks 6 steps in this order:

1. Verify X-AON-Timestamp window -> reject 401 on skew
2. Verify X-AON-Signature HMAC -> reject 401 on mismatch
3. Check X-AON-Nonce against a short-TTL replay store
4. Parse JSON body to OfferProvider request
5. Match offers from your catalog
6. Assemble the response envelope and return 200

Step-by-step:

  1. Verify timestamp. If |now - ts| > 300s, return 401 UNAUTHORIZED with message: "timestamp outside allowed skew".
  2. Verify signature. Compute HMAC_SHA256(secret, signing_string) over the raw bytes you received, hex-encode lowercase, and constant-time compare.
  3. Check nonce. Keep a short-TTL set of (appkey, nonce) for the last 5 minutes. Duplicate nonces should return 401 UNAUTHORIZED.
  4. Parse the body. Return 400 BAD_REQUEST on malformed JSON or missing REQUIRED fields such as request_id, context, intent.provenance, or intent.content[].
  5. Match offers. Apply constraints.* first, then rank against intent.content[]. Empty results are normal; return 200 with offers: [], not 404.
  6. Return the envelope. Make sure each offer satisfies every REQUIRED field in the Partner Offer Schema.

When X-AON-Test: true, still walk steps 1-6, but skip tracking, billing, fulfillment, inventory decrement, conversion accounting, and other side effects.

Success response envelope

Return the response header AON-Protocol-Version: 1.0. Where the response type defines a body echo, return the exact protocol_version: "1.0" value.

{
  "request_id": "01984dc5-3b32-7c1a-9e8b-2f1a7b4d8c11",
  "protocol_version": "1.0",
  "language": "en-US",
  "offers": []
}
FieldRequired?Notes
request_idREQUIREDEchoes the incoming request_id so the agent -> AON -> Partner chain shares one correlation id.
protocol_versionREQUIREDExact value 1.0 for this response profile.
languageREQUIREDLanguage of Partner-authored user-facing Offer content under the current language profile.
offers[]REQUIREDArray of Partner supply Offers from offer-partner-schema.json. May be empty.

Do not add pagination or diagnostic top-level fields to the current success envelope. The v1.0 response schema carries request_id, protocol_version, language, and offers; future pagination metadata will arrive through a schema revision if AON needs it on the Partner channel.

Each offers[] element must satisfy the REQUIRED fields in offer-partner-schema.json: source_offer_id, version, offer_info, entity, action, and goals. Do not return AON-owned offer_id, offer_instance_id, or match_reason; AON adds those fields after source identity resolution and eligibility evaluation. Each goal declares its public event and gross Partner-to-AON commission basis. Inside offer_info, title, category, and description are required; offer_type is an optional fulfillment hint. Every offer must carry offer_info.category.id. Do not emit legacy bid or internal-only aliases in a v1.0 response.

offer_info.category is an object, not a flat category_id string: set its id to the best-fit AON Taxonomy v1 id for the offer. A complete minimal offer that satisfies every REQUIRED field looks like this:

{
  "source_offer_id": "hotel-deluxe-king-001",
  "version": "3.0",
  "offer_info": {
    "title": "The Manhattan Grand - Deluxe King Room",
    "offer_type": "online_service",
    "category": { "id": "travel_tourism.accommodations" },
    "description": "Luxury midtown hotel with rooftop pool and breakfast."
  },
  "entity": {
    "id": "ent_manhattan_grand",
    "name": "The Manhattan Grand Hotel"
  },
  "action": {
    "type": "open_url",
    "name": "Reserve a room",
    "payload": { "url": "https://www.manhattangrand.example/book/deluxe-king" }
  },
  "goals": [
    {
      "event": "conversion",
      "pricing": { "model": "cpa", "amount": "42.00", "currency": "USD" }
    }
  ]
}

Travel offer details

Every Partner Offer still requires the universal Offer shell, including source_offer_id, version: "3.0", offer_info.title, offer_info.category.id, offer_info.description, entity, action, and goals. A registered profile adds structured domain facts; it does not replace those fields.

Use offer_info.details only when your source can satisfy a complete registered shape. Generic Offers remain valid and omit offer_info.details. Do not send custom profile names, null placeholders, partial profile objects, or facts your source does not know.

Browse all registered Supply Offer Profiles for searchable fields, requirement rules, source links, and complete fixture-derived examples.

source_offer_id remains opaque and stable within the identity namespace configured for your integration. AON resolves (Partner, identity namespace, source_offer_id) to one globally unique canonical offer_id. Do not rotate the source value between requests for the same inventory item.

Choose a real, active offer_info.category.id from AON Taxonomy v1. Level 1 ids remain valid when you cannot confidently choose a deeper child; when you can, prefer the most specific id. See the Category Taxonomy for the full list.

Error envelope

Errors follow AON's project-wide ApiResponse contract:

{
  "code": "BAD_REQUEST",
  "message": "intent.content must contain at least one item",
  "data": {},
  "extra": {}
}

code is a machine-readable string; message is a human-readable summary. Always include data: {} and extra: {} even when empty.

HTTPcodeWhen
400BAD_REQUESTMalformed body or missing REQUIRED fields.
401UNAUTHORIZEDMissing/invalid auth header, signature mismatch, expired timestamp, or replayed nonce.
403FORBIDDENValid appkey but the Partner account is suspended or not permitted for this query.
429RATE_LIMITEDFrequency cap exceeded. Include a Retry-After header when you can.
500INTERNAL_ERRORUnexpected failure on your side.

OfferProvider error envelope deliberately uses ApiResponse ({code, message, data, extra}), not the agent-facing {error: {...}} shape. Success responses do not include code; code only appears in the error envelope.

Test mode

AON signals a test request in two places that must carry identical semantics:

  • Header: X-AON-Test: true, which is authoritative.
  • Body: test_mode: true.

When either signal is true, your handler must:

  • Return shape-compatible offers so AON's onboarding validator can inspect the response.
  • Suppress all tracking, billing, fulfillment, inventory decrement, and conversion side-effects.
  • Tag your own logs to distinguish test from production traffic.

Recommended pattern: branch once at the top of the handler on req.headers["X-AON-Test"] === "true", set a test_mode boolean, and short-circuit side-effecting writes for the rest of the request.

Offer Fetch completion

This step is complete when the exact Offer Fetch URL is registered. A signed fetch test succeeds with the v1.0 OfferProvider envelope. Test mode has no production side effects. Continue to Postbacks & Attribution to complete the Provider click and postback test; a successful fetch test alone is not a live integration claim.

Authoritative protocol references

When you need the complete normative spec, auth signing math, full schema, test vectors, conformance tests, or design rationale, go to the public source:

When this partner-facing summary and the protocol spec disagree, the spec wins. File protocol-level issues against the protocol repo rather than treating this docs page as the source of truth.

Continue to Postbacks & Attribution