Skip to main content

POST /api/v1/recommend

Returns ranked offer recommendations for a customer, filtered by channel, placement, qualification rules, contact policies, and guardrails. Each response includes an interactionId to link outcomes recorded via the Respond API.

Request Body

{
  "customerId": "CUST001",
  "channel": "email",
  "placement": "hero_banner",
  "limit": 5,
  "sessionId": "sess-abc-123",
  "context": {
    "device": "mobile",
    "location": "US-NY",
    "pageUrl": "https://example.com/offers"
  },
  "segments": ["high_value", "early_adopter"],
  "attributes": {
    "tier": "gold",
    "region": "northeast",
    "propensityScores": { "credit_card": 0.82 }
  },
  "locale": "en-US",
  "currency": "USD",
  "excludeOffers": ["offer_xyz"],
  "excludeCreatives": ["creative_abc"],
  "decisionFlowKey": "default-flow",
  "debug": false
}

Field Reference

FieldTypeRequiredDescription
customerIdstringYesUnique customer identifier. If omitted or set to "anonymous", a stable anonymous ID is derived from sessionId or the request IP + user agent
channelstringNoFilter creatives by channel type (e.g., "email", "sms", "web")
channelIdstringNoFilter by a specific channel ID instead of channel type
placementstringNoFilter creatives by placement slot type (e.g., "hero_banner", "sidebar")
limitintegerNoMaximum number of results to return. Default: 5, max: 50
sessionIdstringNoSession identifier for tracking. Must be alphanumeric (UUID format recommended, max 64 chars). Used as the anonymous ID seed when customerId is not provided
contextobjectNoReal-time context (device, location, pageUrl). Passed through to interaction history
segmentsstring[]NoCustomer segment tags used for qualification rule evaluation
attributesobjectNoCustomer attributes for personalization and qualification. Can include propensityScores as a nested object
localestringNoLocale code for content selection (e.g., "en-US")
currencystringNoCurrency code (e.g., "USD")
placementsarrayNoMulti-placement request. Each entry: { placementId: string, limit?: number }. When provided, the response uses the grouped format
deduplicatebooleanNoWhen true with multi-placement requests, each placement excludes offers already returned by prior placements (sequential resolution)
excludeOffersstring[]NoOffer IDs to exclude from results
excludeCreativesstring[]NoCreative IDs to exclude from results
decisionFlowKeystringNoRoute the request through a specific Decision Flow pipeline. If omitted, the system auto-resolves via flow routing rules
debugbooleanNoWhen true, includes detailed debugTrace in the response with qualification reasons, contact policy reasons, and feature contributions

V1 Response (flat decisions array)

Returned when the request does not use multi-placement and the Decision Flow does not produce grouped output.
{
  "interactionId": "550e8400-e29b-41d4-a716-446655440000",
  "customerId": "CUST001",
  "sessionId": "sess-abc-123",
  "decisionFlowKey": "default-flow",
  "decisionFlowVersion": 3,
  "experimentVariant": "champion",
  "timestamp": "2026-03-16T14:30:00.000Z",
  "channel": "email",
  "placement": "hero_banner",
  "policyVersion": "a1b2c3d4e5f67890",
  "locale": "en-US",
  "currency": "USD",
  "count": 3,
  "decisions": [
    {
      "rank": 1,
      "score": 0.872,
      "creativeId": "creative_001",
      "creativeName": "Platinum Card - Email Hero",
      "offerId": "offer_001",
      "offerName": "Platinum Card",
      "category": "Credit Cards",
      "subCategory": "Premium",
      "channelType": "email",
      "channelName": "Marketing Email",
      "placement": "hero_banner",
      "templateType": "email_html",
      "content": {
        "subject": "Exclusive offer for you",
        "body": "<html>...</html>"
      },
      "personalization": {
        "personalized_rate": "3.99",
        "customer_name": "{{customer_name}}"
      },
      "abTestVariant": null,
      "weight": 100,
      "priority": 90,
      "constraints": {},
      "expiresAt": "2026-06-30T00:00:00.000Z",
      "metadata": {},
      "scoreExplanation": {
        "method": "priority_weighted",
        "priority": 90,
        "weight": 100,
        "fitMultiplier": 1.0,
        "finalScore": 0.872
      }
    }
  ]
}

V2 Response (grouped placements)

Returned when the Decision Flow pipeline produces grouped output (Composable Pipeline with a Group node), or when placements is provided in the request. Multi-placement request response:
{
  "placements": {
    "hero_banner": {
      "offers": [
        {
          "rank": 1,
          "score": 0.92,
          "offerId": "offer_001",
          "creative": { ... },
          "offer": { ... }
        }
      ],
      "count": 1
    },
    "sidebar": {
      "offers": [
        {
          "rank": 1,
          "score": 0.78,
          "offerId": "offer_002",
          "creative": { ... },
          "offer": { ... }
        }
      ],
      "count": 1
    }
  },
  "customerId": "CUST001",
  "requestId": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2026-03-16T14:30:00.000Z"
}
Decision flow grouped response:
{
  "interactionId": "550e8400-e29b-41d4-a716-446655440000",
  "customerId": "CUST001",
  "timestamp": "2026-03-16T14:30:00.000Z",
  "placements": {
    "hero_banner": {
      "offers": [ ... ],
      "count": 2
    }
  },
  "meta": {
    "decisionFlowKey": "default-flow",
    "decisionFlowVersion": 3,
    "experimentVariant": "champion",
    "totalCandidates": 47
  }
}

Debug Trace

When debug=true, the response includes a debugTrace object:
{
  "debugTrace": {
    "totalCandidates": 47,
    "afterQualification": 32,
    "afterContactPolicy": 28,
    "topScores": [
      { "offerId": "offer_001", "score": 0.872 }
    ],
    "policyVersion": "a1b2c3d4e5f67890",
    "qualificationReasons": [
      { "offerId": "offer_015", "reason": "segment_required: missing 'premium'", "policyId": "qr_001" }
    ],
    "contactPolicyReasons": [
      { "offerId": "offer_008", "creativeId": "creative_020", "reason": "frequency_cap exceeded", "policyId": "cp_003" }
    ],
    "featureContributions": [
      {
        "offerId": "offer_001",
        "offerName": "Platinum Card",
        "explanation": { "modelType": "scorecard", "totalScore": 0.872, "contributions": [] }
      }
    ]
  }
}

GET /api/v1/recommend

A query-parameter variant for simple integrations that do not need the full request body.
GET /api/v1/recommend?customerId=CUST001&channel=email&placement=hero_banner&limit=5&debug=true
ParameterTypeDefaultDescription
customerIdstring"anonymous"Customer identifier
channelstringFilter by channel type
placementstringFilter by placement slot
limitinteger5Max results (max 50)
debugstring"false"Set to "true" for debug trace
The GET response uses the same V1 flat format as the POST endpoint (without interactionId, sessionId, locale, or currency fields).

Error Responses

StatusCause
400Invalid JSON body, missing customerId, or invalid sessionId format
401Missing or invalid X-Tenant-Id / API key
415Content-Type header is not application/json (POST only)
429Rate limit exceeded (1,000 requests per 60s per tenant)
500Internal server error

Example: cURL

curl -X POST https://playground.kaireonai.com/api/v1/recommend \
  -H "Content-Type: application/json" \
  -H "X-Tenant-Id: my-tenant" \
  -H "Authorization: Bearer sk_live_abc123" \
  -d '{
    "customerId": "CUST001",
    "channel": "web",
    "placement": "hero_banner",
    "limit": 3,
    "segments": ["high_value"],
    "attributes": {
      "tier": "gold"
    }
  }'
See also: Decision Flows | Composable Pipeline | API Tutorial