TasteRay Emotional API

Recommendations for any vertical, each one returned with a written explanation of why it fits. One HTTP endpoint, no model training, no user profiles stored on our side.

On this page

This reference is for developers integrating the API into a product. It assumes you can make an HTTPS request and read JSON; it does not assume any machine-learning background. It covers every public endpoint, parameter, response field, and error code.

It does not cover pricing or what the product is for — see the TasteRay home page for that — and you don't need to read it to try the API. To send a request without writing any code, open the playground, which runs real requests without a key.

The essentials, before anything else:

SettingValue
Base URLValuehttps://api.tasteray.com
AuthenticationValueX-API-Key header
Content typeValueapplication/json
Latest releaseValue0.7.0 — see Changelog

Before you begin#

You need three things:

  • An API key. Get one at api.tasteray.com/generate-key. It takes an email address and nothing else.
  • A server to call from. An API key grants access to your whole quota, so keep it on a server you control. Never ship it in a browser bundle or a mobile app.
  • Something to describe. The API needs a vertical (movies, restaurants, headphones — any noun works) and either a list of preferences or a paragraph describing the person.

The API is stateless. It stores no user profiles and no sessions, so every request carries the full context it needs. That makes requests independent and safe to retry, and it means you decide what user data leaves your system.

Make your first request#

This procedure returns three movie recommendations. It takes about a minute.

  1. Get an API key at api.tasteray.com/generate-key. Copy it — the key is shown once and cannot be retrieved later.

  2. Store the key in your shell:

    Shell
    export TASTERAY_API_KEY=API_KEY

    Replace API_KEY with the key you copied. Keys begin with reco_live_.

  3. Send a recommendation request:

    Shell
    curl -X POST https://api.tasteray.com/v1/recommend \
      -H "Content-Type: application/json" \
      -H "X-API-Key: $TASTERAY_API_KEY" \
      -d '{
        "vertical": "movies",
        "context": {
          "preferences": ["sci-fi", "slow-burn", "practical effects"]
        },
        "options": { "count": 3 }
      }'
  4. Confirm that the response has status 200 and a recommendations array with three entries. Each entry carries an item, an explanation with a why_match sentence, and a confidence score between 0 and 1.

A request that uses web grounding — the default — takes several seconds, because the model searches the web before it answers. See Grounding and speed for the trade-off between latency and freshness.

What's next:

Authentication#

Every endpoint except GET /v1/health requires an API key in the X-API-Key header:

HTTP
POST /v1/recommend HTTP/1.1
Host: api.tasteray.com
Content-Type: application/json
X-API-Key: API_KEY

Replace API_KEY with your key. Self-serve keys are live keys: the prefix reco_live_ followed by 48 hexadecimal characters.

Keys are hashed with SHA-256 before storage, so the plaintext key exists only in your copy of it. A key that is unknown or revoked returns 401 with the code INVALID_API_KEY; a request with no header at all returns 401 with the code MISSING_API_KEY.

The API sends Access-Control-Allow-Origin: *, so a browser can technically call it directly. Don't. A key in client-side code is a key you have published. Proxy the call through your own backend instead.

Rate limits#

Each key has a per-minute request limit, enforced with a sliding window, and a monthly request allowance.

The following tiers are available:

TierRequests per minuteRequests per monthHow to get it
FreeRequests per minute5Requests per month1,000How to get itSelf-serve at /generate-key
BasicRequests per minute50Requests per month10,000How to get itContact hello@tasteray.com
ProRequests per minute200Requests per month100,000How to get itContact hello@tasteray.com
EnterpriseRequests per minuteCustomRequests per monthCustomHow to get itContact hello@tasteray.com

Every response from /v1/recommend and /v1/explain carries the current window's budget in three headers:

HeaderMeaning
X-RateLimit-LimitMeaningRequests allowed per minute on this key
X-RateLimit-RemainingMeaningRequests left in the current window
X-RateLimit-ResetMeaningWhen the window resets, as a Unix timestamp in milliseconds

Note: X-RateLimit-Reset is in milliseconds, not seconds. Divide by 1,000 before passing it to a function that expects Unix seconds.

When a key exceeds its per-minute limit, the API returns 429 with a Retry-After header giving the seconds until the window resets:

JSON
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded for your tier",
    "details": "Limit: 5 requests/minute",
    "suggestion": "Wait before retrying or upgrade to a higher tier"
  },
  "meta": {
    "request_id": "req_1772370000000_a1b2c3d4e",
    "timestamp": "2026-08-31T12:00:00.000Z"
  }
}

Honor Retry-After when it's present, and back off exponentially otherwise: wait min(base_delay * 2^attempt, max_delay) between attempts. See Retry failed requests for a worked example.

Endpoints#

The API has four endpoints. Two generate text with a model and are the slow, expensive ones; two are ordinary reads.

MethodPathPurposeAuth
POSTPath/v1/recommendPurposeGenerate or rank recommendationsAuthRequired
POSTPath/v1/explainPurposeExplain one item in depthAuthRequired
GETPath/v1/usagePurposeRead this key's usage and tierAuthRequired
GETPath/v1/healthPurposeCheck service statusAuthNone

POST /v1/recommend#

Generates recommendations for a vertical, or ranks a list of items you supply. Returns each result with a written explanation and a confidence score.

Request body

JSON
{
  "vertical": "movies",
  "context": {
    "preferences": ["sci-fi", "slow-burn"],
    "profile": "Watches one film a week, usually late at night, alone.",
    "constraints": { "runtime_max": 150, "platforms": ["Netflix"] },
    "history": [
      { "item": "Arrival", "rating": "loved", "metadata": { "year": 2016 } }
    ],
    "history_text": "Loved Arrival and Annihilation, bounced off Tenet."
  },
  "items": [
    { "id": "tt0133093", "name": "The Matrix", "description": "1999 action sci-fi" }
  ],
  "options": {
    "count": 3,
    "explanation_depth": "detailed",
    "include_alternatives": true,
    "language": "en_US",
    "grounding": true,
    "fast": false
  }
}

Parameters

Only vertical and context are required, and context needs either preferences or profile.

ParameterTypeRequiredDescription
verticalTypestringRequiredYesDescriptionWhat kind of thing to recommend. Any non-empty string: movies, restaurants, standing desks. See Verticals.
context.preferencesTypestring[]RequiredConditionalDescriptionWhat the person likes, one trait per entry. Required unless context.profile is set.
context.profileTypestringRequiredConditionalDescriptionThe same information as free text, for cases where you have a paragraph rather than a list. Required unless context.preferences is set.
context.constraintsTypeobjectRequiredNoDescriptionHard limits as key-value pairs: budget, location, runtime, platform. Any keys you like.
context.historyTypeobject[]RequiredNoDescriptionUp to 50 items the person already saw, each with item and rating strings and optional metadata.
context.history_textTypestringRequiredNoDescriptionThe same history as free text.
itemsTypeobject[]RequiredNoDescriptionA list to rank instead of generating from scratch. Each entry needs id and name; description and metadata are optional. See Rank items you already have.
options.countTypenumberRequiredNoDescriptionHow many recommendations to return, from 1 to 10. Defaults to 3.
options.explanation_depthTypestringRequiredNoDescriptionbrief or detailed. Defaults to detailed.
options.include_alternativesTypebooleanRequiredNoDescriptionWhether explanations may name alternatives. Defaults to true.
options.languageTypestringRequiredNoDescriptionLocale for the generated text, such as es_ES or ja_JP. Defaults to en_US. See Response language.
options.groundingTypebooleanRequiredNoDescriptionWhether the model may search the web before answering. Defaults to true, or to false when you supply items.
options.fastTypebooleanRequiredNoDescriptionWhether to use the low-latency model. Defaults to false. Turning it on disables grounding. See Grounding and speed.
options.researchTypebooleanRequiredNoDescriptionDeprecated. An older name for grounding, honored only when grounding is absent. Use grounding instead.

Both history and history_text can be sent together, and so can preferences and profile. When you send both forms, the model reads both.

Response

Returns 200 with a recommendations array and a meta object.

JSON
{
  "recommendations": [
    {
      "item": {
        "name": "Arrival",
        "vertical": "movies",
        "type": "film",
        "metadata": {
          "creator": "Denis Villeneuve",
          "year": 2016,
          "genre": ["Sci-Fi", "Drama"],
          "runtime_minutes": 116,
          "platform_availability": ["Max"],
          "rating": 7.9
        }
      },
      "explanation": {
        "why_match": "A first-contact film that spends its running time on grief and language rather than on spectacle.",
        "key_factors": [
          "Slow-burn structure — the tension is intellectual, not kinetic.",
          "Practical sets and restrained effects work throughout.",
          "116 minutes, inside your runtime limit."
        ],
        "potential_concerns": [
          "The non-linear structure rewards attention; it is a poor second-screen film."
        ]
      },
      "confidence": 0.92,
      "metadata": {
        "taste_match_score": 92,
        "mood_match": "reflective, late-night"
      }
    }
  ],
  "meta": {
    "request_id": "req_1772370000000_a1b2c3d4e",
    "timestamp": "2026-08-31T12:00:00.000Z",
    "processing_time_ms": 4213,
    "prompt_version": "v2.1",
    "total_results": 3,
    "tool_usage": {
      "total_tool_calls": 1,
      "tools_used": ["webSearch"],
      "tools_called": [
        {
          "tool_name": "webSearch",
          "arguments": { "query": "slow-burn sci-fi films practical effects" },
          "timestamp": "2026-08-31T12:00:01.402Z"
        }
      ]
    }
  }
}

For the meaning of each field, see Response objects.

POST /v1/explain#

Explains why one specific item does or does not suit a person. Use it on an item the user tapped, or on a result from /v1/recommend when the reader wants more than a sentence.

Request body

JSON
{
  "vertical": "headphones",
  "item": {
    "name": "Sony WH-1000XM5",
    "type": "over-ear wireless",
    "metadata": { "price": 399 }
  },
  "context": {
    "user_preferences": ["noise cancelling", "long battery", "comfortable for 8 hours"],
    "profile": "Commutes 90 minutes a day and works in an open-plan office.",
    "constraints": { "budget_max": 400 }
  },
  "options": {
    "depth": "detailed",
    "include_alternatives": true,
    "language": "en_US",
    "grounding": true,
    "fast": false
  }
}

Parameters

ParameterTypeRequiredDescription
verticalTypestringRequiredYesDescriptionWhat kind of thing the item is.
item.nameTypestringRequiredYesDescriptionThe item to explain.
item.typeTypestringRequiredYesDescriptionIts subtype, such as film, bistro, or over-ear wireless.
item.metadataTypeobjectRequiredNoDescriptionAnything you already know about the item: price, year, location.
context.user_preferencesTypestring[]RequiredConditionalDescriptionWhat the person likes. Required unless context.profile is set. Note the name differs from /v1/recommend, which uses context.preferences.
context.profileTypestringRequiredConditionalDescriptionThe same information as free text. Required unless context.user_preferences is set.
context.constraintsTypeobjectRequiredNoDescriptionHard limits as key-value pairs.
options.depthTypestringRequiredNoDescriptionbrief or detailed. Defaults to detailed. Note the name differs from /v1/recommend, which uses options.explanation_depth.
options.include_alternativesTypebooleanRequiredNoDescriptionWhether to return an alternatives array. Defaults to false.
options.languageTypestringRequiredNoDescriptionLocale for the generated text. Defaults to en_US.
options.groundingTypebooleanRequiredNoDescriptionWhether the model may search the web. Defaults to true.
options.fastTypebooleanRequiredNoDescriptionWhether to use the low-latency model. Defaults to false.
options.researchTypebooleanRequiredNoDescriptionDeprecated. An older name for grounding. Use grounding instead.

Note: Two parameter names differ from /v1/recommend, and mixing them up is the easiest mistake to make here: this endpoint takes context.user_preferences, not context.preferences, and options.depth, not options.explanation_depth.

Response

Returns 200 with a single explanation object, a confidence score, and meta.

JSON
{
  "explanation": {
    "summary": "The right call for an open-plan office, with one caveat about fit.",
    "detailed_reasoning": {
      "taste_alignment": [
        "Industry-leading cancellation on the low-frequency hum that open-plan offices produce.",
        "30 hours of battery covers a full week of your commute between charges."
      ],
      "constraint_satisfaction": [
        "$399 sits inside your $400 ceiling."
      ],
      "unique_factors": [
        "The clamping force is unusually light, which is what makes eight-hour wear plausible."
      ]
    },
    "potential_concerns": [
      "Over-ear cups get warm over a long session in a warm room."
    ],
    "alternatives": [
      {
        "name": "Bose QuietComfort Ultra",
        "reason": "Slightly better on voices, which matters more than hum in some offices.",
        "key_difference": "Softer cancellation of low frequencies, stronger on speech."
      }
    ]
  },
  "confidence": 0.95,
  "meta": {
    "request_id": "req_1772370300000_f5g6h7i8j",
    "timestamp": "2026-08-31T12:05:00.000Z",
    "processing_time_ms": 3187
  }
}

alternatives is present only when you set options.include_alternatives to true. meta.prompt_version and meta.total_results do not apply to this endpoint and are absent.

GET /v1/usage#

Returns the usage record and tier for the key in the request header. It reads state and calls no model, so it's fast and doesn't consume the model budget.

Shell
curl https://api.tasteray.com/v1/usage \
  -H "X-API-Key: $TASTERAY_API_KEY"

Returns 200:

JSON
{
  "period": "current_month",
  "start_date": "2026-08-01T00:00:00.000Z",
  "end_date": "2026-08-31T00:00:00.000Z",
  "usage": {
    "requests_made": 2450,
    "requests_limit": 10000,
    "requests_remaining": 7550,
    "percentage_used": 24.5,
    "reset_date": "2026-09-01T00:00:00.000Z"
  },
  "breakdown_by_vertical": { "movies": 1200, "restaurants": 800 },
  "breakdown_by_day": [
    { "date": "2026-08-01", "requests": 120 },
    { "date": "2026-08-02", "requests": 135 }
  ],
  "tier": { "name": "Basic", "rate_limit": "50 requests/minute" }
}

The per-minute limit in tier.rate_limit is the one the API enforces on every request; see Rate limits. For a live count of the current minute's budget, read the X-RateLimit-* headers on a /v1/recommend or /v1/explain response rather than polling this endpoint.

GET /v1/health#

Reports service status. It needs no API key, so you can wire it into an uptime monitor.

Shell
curl https://api.tasteray.com/v1/health

Returns 200 when the service is healthy or degraded, and 503 when it's unhealthy:

JSON
{
  "status": "healthy",
  "version": "1.0.0",
  "timestamp": "2026-08-31T12:00:00.000Z",
  "checks": {
    "ai_service": "ok",
    "fast_ai_service": "ok",
    "analytics": "ok",
    "cache": "ok"
  }
}

The response fields are:

FieldValuesMeaning
statusValueshealthy, degraded, unhealthyMeaningOverall verdict. unhealthy is the only one that returns 503.
versionValuesstringMeaningThe running service version.
checks.ai_serviceValuesok, degraded, errorMeaningThe model that serves normal requests.
checks.fast_ai_serviceValuesok, degraded, errorMeaningThe model that serves fast: true requests. degraded means fast falls back to the normal model.
checks.analyticsValuesok, degraded, errorMeaningPrompt management and tracing.
checks.cacheValuesok, degraded, errorMeaningThe key-value store holding keys and rate-limit counters.

Alert on status, not on individual checks. A degraded fast_ai_service still serves every request correctly, but without the low-latency path.


Request context#

context is where you describe the person. Both endpoints accept the same two shapes for that description, and you can mix them.

Structured or free text#

Send a list when you hold tags, facets, or checkbox answers:

JSON
{ "preferences": ["Italian", "romantic", "no reservations needed"] }

Send a paragraph when you hold something a person wrote or said — an onboarding answer, a chat transcript, a survey box:

JSON
{ "profile": "Just moved to Seattle. Cooks most nights but wants somewhere unfussy for a first date. Hates loud rooms." }

At least one of the two is required. Send both when you have both, and the model reads both. The same choice applies to history: history takes structured entries, history_text takes a paragraph.

Free text is what lets you skip the transformation layer most recommendation systems demand. If your product already stores a written profile, pass it through.

Constraints#

constraints is an open object. Use whatever keys describe a hard limit, and prefer values a machine could check:

JSON
{
  "budget_max": 300,
  "location": "Seattle, WA",
  "runtime_max": 150,
  "dietary_restrictions": ["gluten-free"]
}

Specific beats vague. {"budget_max": 300} constrains the answer; {"budget": "not too expensive"} mostly doesn't.

History#

history is an array of up to 50 items. Each entry needs item and rating strings, plus optional metadata:

JSON
{
  "history": [
    { "item": "Inception", "rating": "loved", "metadata": { "year": 2010 } },
    { "item": "Tenet", "rating": "3/10", "metadata": { "finished": false } }
  ]
}

rating is free-form on purpose — loved, 5 stars, 8/10, and bounced off it all work, because whatever scale your product already uses can go in unchanged. A request with more than 50 history entries returns 400.

Recent and significant beats exhaustive. Ten items the person felt strongly about steer the answer better than fifty they merely clicked.

Example contexts by vertical#

The following contexts show the shape in four domains.

Movies:

JSON
{
  "preferences": ["sci-fi", "thriller", "thought-provoking"],
  "constraints": { "runtime_max": 150, "release_year_min": 2010, "platforms": ["Netflix", "Max"] },
  "history": [{ "item": "Inception", "rating": "loved", "metadata": { "year": 2010 } }]
}

Restaurants:

JSON
{
  "preferences": ["Italian", "romantic", "authentic"],
  "constraints": { "location": "Seattle, WA", "price_range": "$$$", "dietary_restrictions": ["gluten-free options"] },
  "history": [{ "item": "Canlis", "rating": "excellent", "metadata": { "occasion": "anniversary" } }]
}

Products:

JSON
{
  "preferences": ["wireless", "noise-cancelling", "long battery"],
  "constraints": { "category": "headphones", "budget_max": 300, "brand_preference": ["Sony", "Bose"] },
  "history": [{ "item": "Sony WH-1000XM4", "rating": "9/10", "metadata": { "owned_for": "2 years" } }]
}

Travel:

JSON
{
  "preferences": ["beaches", "adventure", "culture"],
  "constraints": { "budget_total": 3000, "duration_days": 14, "region": "Southeast Asia", "season": "winter" },
  "history": [{ "item": "Bali, Indonesia", "rating": "loved", "metadata": { "visited": "2023" } }]
}

Response objects#

Both generating endpoints return the same building blocks. Fields whose value is null are removed before the response is sent, so a field you don't see was not populated.

Recommendation#

Each entry in the recommendations array has this shape:

FieldTypeDescription
itemTypeobjectDescriptionThe recommended thing. See Item.
explanationTypeobjectDescriptionWhy it fits. See Explanation.
confidenceTypenumberDescriptionHow sure the model is, from 0.0 to 1.0.
metadataTypeobjectDescriptionRanking signals. See Recommendation metadata. Absent when empty.

Item#

FieldTypeDescription
nameTypestringDescriptionThe item's name.
verticalTypestringDescriptionThe vertical it belongs to, echoing your request.
typeTypestringDescriptionIts subtype, such as film, bistro, or over-ear wireless.
metadataTypeobjectDescriptionFacts about the item, drawn from a fixed set of fields. Absent when empty.

item.metadata is a closed set: the model fills in the fields that apply to the vertical and leaves the rest out. Do not expect keys outside this list.

FieldTypeApplies to
ratingTypenumberApplies toAny vertical
locationTypestringApplies toAny vertical
verified_availableTypebooleanApplies toAny vertical
verified_as_ofTypestringApplies toAny vertical
creatorTypestringApplies toFilms, TV, music, books, games — director, author, artist
yearTypenumberApplies toFilms, TV, music, books, games
genreTypestring[]Applies toFilms, TV, music, books, games
runtime_minutesTypenumberApplies toFilms, TV
platform_availabilityTypestring[]Applies toFilms, TV, music
review_sourcesTypestring[]Applies toFilms, TV, music, books, games
cuisineTypestringApplies toRestaurants
price_rangeTypestringApplies toRestaurants
distance_milesTypenumberApplies toRestaurants
dietary_optionsTypestring[]Applies toRestaurants
atmosphereTypestringApplies toRestaurants
brandTypestringApplies toProducts
priceTypenumberApplies toProducts
key_featuresTypestring[]Applies toProducts
reviews_countTypenumberApplies toProducts
price_as_ofTypestringApplies toProducts
categoryTypestringApplies toProducts
price_per_nightTypenumberApplies toHotels
star_ratingTypenumberApplies toHotels
amenitiesTypestring[]Applies toHotels
room_typesTypestring[]Applies toHotels
distance_to_centerTypestringApplies toHotels
websiteTypestringApplies toHotels
phoneTypestringApplies toHotels
verified_operatingTypebooleanApplies toHotels
best_time_to_visitTypestringApplies toDestinations
estimated_budgetTypenumberApplies toDestinations
duration_recommendedTypestringApplies toDestinations
activitiesTypestring[]Applies toDestinations
climateTypestringApplies toDestinations
difficulty_levelTypestringApplies toDestinations

The verified_* and *_as_of fields carry the model's own claim about freshness, and they appear when web grounding is on. Treat them as a hint, not as a guarantee: confirm price and availability against your own source before you show them as fact.

Explanation#

/v1/recommend returns a short explanation on every recommendation:

FieldTypeDescription
why_matchTypestringDescriptionOne or two sentences on the fit. Good as a card subtitle.
key_factorsTypestring[]DescriptionThe specific reasons, one per entry. Good as a bulleted detail panel.
potential_concernsTypestring[]DescriptionHonest caveats. May be null.

/v1/explain returns a longer object instead:

FieldTypeDescription
summaryTypestringDescriptionThe verdict in one or two sentences.
detailed_reasoning.taste_alignmentTypestring[]DescriptionWhere the item matches what the person likes.
detailed_reasoning.constraint_satisfactionTypestring[]DescriptionHow it meets the hard limits.
detailed_reasoning.unique_factorsTypestring[]DescriptionWhat sets it apart from near-identical options.
potential_concernsTypestring[]DescriptionHonest caveats.
alternativesTypeobject[]DescriptionOther options, each with name, reason, and key_difference. Present only when options.include_alternatives is true.

Recommendation metadata#

recommendations[].metadata carries ranking signals, drawn from a fixed set:

FieldTypeDescription
taste_match_scoreTypenumberDescriptionFit as a 0–100 score. The primary ranking signal.
mood_matchTypestringDescriptionThe mood the item suits, such as reflective, late-night.
emotional_appealTypestringDescriptionThe feeling the item is reaching for.
reasoningTypestringDescriptionA compressed note on the ranking decision.

Meta#

Every successful response carries meta:

FieldTypeDescription
request_idTypestringDescriptionUnique per request, in the form req_{timestamp}_{random}. Log it and quote it in support requests.
timestampTypestringDescriptionISO 8601 time the response was built.
processing_time_msTypenumberDescriptionServer-side duration, excluding network time.
prompt_versionTypestringDescriptionThe prompt template version. /v1/recommend only.
total_resultsTypenumberDescriptionHow many recommendations came back. /v1/recommend only.
tool_usageTypeobjectDescriptionWhat the model searched for. Present only when it used a tool. See Tool usage.

Tool usage#

When grounding is on and the model chooses to search, meta.tool_usage reports what it looked up:

JSON
{
  "tool_usage": {
    "total_tool_calls": 2,
    "tools_used": ["webSearch"],
    "tools_called": [
      {
        "tool_name": "webSearch",
        "arguments": { "query": "best Italian restaurants Seattle 2026" },
        "timestamp": "2026-08-31T12:00:01.402Z"
      },
      {
        "tool_name": "webSearch",
        "arguments": { "query": "gluten-free Italian Capitol Hill Seattle" },
        "timestamp": "2026-08-31T12:00:03.918Z"
      }
    ]
  }
}
FieldTypeDescription
total_tool_callsTypenumberDescriptionHow many calls the model made across all steps.
tools_usedTypestring[]DescriptionThe distinct tool names used. webSearch is the only tool today.
tools_calledTypeobject[]DescriptionOne entry per call, with tool_name, arguments, and timestamp.

The only argument the model supplies is query; result count and search mode are fixed server-side. The model decides for itself whether to search at all, so a grounded request that answers from the model's own knowledge returns no tool_usage field. That's expected, not a failure.

The whole object is absent when options.grounding is false, when options.fast is true, and when the model chose not to search.


Verticals#

vertical is a free string. The API is not restricted to a fixed catalog: whatever noun you send, the model treats as the category to recommend within. movies, sourdough starters, and B2B analytics vendors are all valid.

These verticals are the well-worn paths, grouped by the kind of thing they describe:

GroupVerticals
EntertainmentVerticalsmovies, tv-series, books, music, podcasts, games, mobile-apps
Food and diningVerticalsrestaurants, cafes, bars, recipes, meal-kits
TravelVerticalshotels, flights, destinations, activities, tours, cruises
ProductsVerticalselectronics, clothing, furniture, home-goods, beauty, sports
ServicesVerticalscourses, jobs, real-estate, healthcare, finance, fitness
EventsVerticalsconcerts, conferences, sports-events, theater, festivals

What to put in context, by group#

The vertical changes which preferences and constraints carry weight. The following pairings produce the sharpest results:

GroupPreferences that workConstraints that work
EntertainmentPreferences that workGenre, theme, mood, pacingConstraints that workRuntime, release year, platform, content rating
Food and diningPreferences that workCuisine, atmosphere, dietary focusConstraints that workLocation, price range, dietary restrictions, party size
TravelPreferences that workActivity type, atmosphere, cultural interestsConstraints that workBudget, duration, region, season, accessibility
ProductsPreferences that workFeatures, style, brand affinityConstraints that workBudget, category, specifications, compatibility
ServicesPreferences that workSkills to learn, career goals, specializationsConstraints that workBudget, time commitment, location, qualifications

Options in depth#

Grounding and speed#

Grounding lets the model search the web before it answers. It's on by default for both generating endpoints, except when you send items to /v1/recommend — ranking a list you supplied needs no fresh facts.

Grounding is the right default when the answer depends on the world as it is now: what's showing this week, what a restaurant charges, whether a product is still sold. Turn it off when your catalog is the whole truth, or when you need the response faster.

fast switches to a lower-latency model. The trade is real and worth stating plainly:

BehaviorDefault"fast": true
Web groundingDefaultAvailable`"fast": true`Not available
meta.tool_usageDefaultPresent when the model searches`"fast": true`Never present
LatencyDefaultHigher — a web search runs before the answer`"fast": true`Lower — no search, smaller model
Best forDefaultA considered answer with current facts`"fast": true`Typeahead, previews, bulk re-ranking

Setting fast to true and grounding to true in the same request is not an error, but grounding is ignored — the fast path has no web access.

Response language#

options.language takes a locale code and sets the language of every generated string: why_match, key_factors, summary, and the rest.

Shell
curl -X POST https://api.tasteray.com/v1/recommend \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $TASTERAY_API_KEY" \
  -d '{
    "vertical": "travel",
    "context": { "preferences": ["temples", "nature", "hot springs"] },
    "options": { "count": 5, "language": "ja_JP" }
  }'

Any valid locale code works — en_US, es_ES, fr_FR, de_DE, ja_JP, zh_CN, pt_BR, it_IT. Error messages stay in English regardless, so you can log and match on them without a locale-aware parser.


Recipes#

Rank items you already have#

Send your catalog in items and the API ranks that list instead of inventing one. Every returned item comes from what you sent, which keeps recommendations inside your inventory.

options.count caps at 10 on this endpoint as on any other, so rank in batches:

JavaScript
// categoryProducts: [{ id, name, description }, ...]
async function rankCategory(categoryProducts, user) {
  const batch = categoryProducts.slice(0, 10);

  // count must be 1 to 10, so an empty category never reaches the API.
  if (batch.length === 0) return [];

  const res = await fetch('https://api.tasteray.com/v1/recommend', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': process.env.TASTERAY_API_KEY,
    },
    body: JSON.stringify({
      vertical: 'products',
      items: batch,
      context: { preferences: user.preferences },
      options: { count: batch.length },
    }),
  });

  const { recommendations } = await res.json();
  return recommendations;
}

options.count must be between 1 and 10, so passing a raw categoryProducts.length returns 400 for an empty category and for any catalog past ten. To personalize a longer list, loop over batches of ten, or rank the first ten and leave the tail in its original order. Grounding also defaults to false whenever you send items, since a list you supplied needs no fresh facts.

Build a "For You" surface#

A "For You" feed is one /v1/recommend call whose result you cache per user and re-rank client-side.

JavaScript
async function buildFeed(user) {
  const res = await fetch('https://api.tasteray.com/v1/recommend', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': process.env.TASTERAY_API_KEY,
    },
    body: JSON.stringify({
      vertical: 'movies',
      context: {
        preferences: user.preferences,
        history: user.recentWatches.slice(0, 20),
      },
      options: { count: 10, explanation_depth: 'brief' },
    }),
  });

  const { recommendations } = await res.json();

  // taste_match_score is 0-100 and confidence is 0.0-1.0, so the fallback is
  // scaled before comparing. Mixing the two ranges would sort every
  // recommendation without a score below every one that has it.
  const rank = (r) => r.metadata?.taste_match_score ?? r.confidence * 100;

  return [...recommendations].sort((a, b) => rank(b) - rank(a));
}

metadata is omitted when it's empty, so taste_match_score can be absent. confidence is always present, which makes it the fallback — scale it by 100 first, or the items you fell back on all sink to the bottom of the feed.

These fields map onto the parts of a feed card:

FieldUse it for
metadata.taste_match_scoreUse it forThe ranking signal, 0–100
confidenceUse it forA tiebreaker, 0.0–1.0
explanation.why_matchUse it forThe one-line reason under the title
explanation.key_factorsUse it forThe expanded "why this" panel
metadata.mood_matchUse it forGrouping a row by mood

Cache the result against the user and their preference set, and invalidate it when their preferences or history change. Recommendations are generated fresh on every call, so an uncached feed spends a request — and several seconds — on every page view.

Retry failed requests#

Retry on 429 and on 5xx. Never retry a 4xx other than 429: the request is malformed, and sending it again produces the same error.

JavaScript
async function requestWithRetry(url, body, maxAttempts = 3) {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const res = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': process.env.TASTERAY_API_KEY,
      },
      body: JSON.stringify(body),
    });

    if (res.ok) return res.json();

    if (res.status === 429) {
      const retryAfter = Number(res.headers.get('Retry-After') ?? 60);
      await new Promise((r) => setTimeout(r, retryAfter * 1000));
      continue;
    }

    if (res.status >= 500) {
      await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
      continue;
    }

    throw new Error(`Request failed: ${(await res.json()).error.message}`);
  }

  throw new Error('Exhausted retries');
}

Call the API from Python#

Python
import os
import requests

response = requests.post(
    "https://api.tasteray.com/v1/recommend",
    headers={
        "Content-Type": "application/json",
        "X-API-Key": os.environ["TASTERAY_API_KEY"],
    },
    json={
        "vertical": "restaurants",
        "context": {
            "preferences": ["Italian", "romantic", "authentic"],
            "constraints": {"location": "Seattle, WA", "price_range": "$$$"},
        },
        "options": {"count": 3},
    },
    timeout=60,
)
response.raise_for_status()

for rec in response.json()["recommendations"]:
    print(rec["item"]["name"])
    print(f"  {rec['explanation']['why_match']}")

Set a generous client timeout. A grounded request routinely takes longer than the default timeout of most HTTP clients, and a client that gives up at five seconds reports failures the API never had.


Errors#

Every error uses the same envelope, whatever the status code:

JSON
{
  "error": {
    "code": "INVALID_REQUEST",
    "message": "Invalid count option",
    "details": "The options.count must be a number between 1 and 10",
    "suggestion": "Set count to a value between 1 and 10"
  },
  "meta": {
    "request_id": "req_1772370000000_a1b2c3d4e",
    "timestamp": "2026-08-31T12:00:00.000Z"
  }
}

error.code is the stable field — branch on it. error.message names what went wrong, error.details says which rule was broken, and error.suggestion says what to change; all three are written for a human reading a log, and their wording can change. meta.request_id identifies the request in our traces, so include it in support mail.

Error codes#

StatusCodeCauseWhat to do
400CodeINVALID_REQUESTCauseThe body isn't valid JSON, a required field is missing, or a value is out of range. error.details names the field.What to doFix the field named in details. Don't retry unchanged.
401CodeMISSING_API_KEYCauseNo X-API-Key header.What to doAdd the header.
401CodeINVALID_API_KEYCauseThe key is unknown or revoked.What to doCheck the key, or mint a new one at /generate-key.
404CodeNOT_FOUNDCauseThe path doesn't exist.What to doCheck the path against Endpoints.
429CodeRATE_LIMIT_EXCEEDEDCauseThe key passed its per-minute limit.What to doWait for Retry-After seconds, then retry.
500CodeINTERNAL_ERRORCauseSomething failed on our side, including a model that failed after its fallback.What to doRetry with exponential backoff. If it persists, send us the request_id.

INVALID_REQUEST covers every validation failure, so the code alone doesn't tell you which field is wrong — error.details does. These are the checks that produce it:

  • vertical is missing, empty, or not a string.
  • context is missing, or has neither preferences nor profile.
  • context.history isn't an array, holds more than 50 entries, or has an entry missing item or rating.
  • options.count is outside 1 to 10.
  • options.explanation_depth isn't brief or detailed.
  • options.language isn't a non-empty string.
  • options.grounding, options.research, or options.fast isn't a boolean.
  • On /v1/explain: item.name or item.type is missing, or context has neither user_preferences nor profile.

Best practices#

Shape the request#

Three to seven preferences is the productive range. Past that, additional traits dilute rather than sharpen, because every trait competes for the same answer. Constraints behave the opposite way: add every one that's genuinely hard, since each removes wrong answers outright.

Match count to the surface. One to three suits a decision moment, three to five a standard result set, five to ten a browsing feed. Asking for ten when you show three spends latency on results nobody reads.

Handle latency#

A grounded request takes several seconds, because it makes real web searches before answering. Design for that rather than around it: generate the recommendation off the request path where you can, cache the result per user, and show a skeleton rather than a spinner when you can't. Where a sub-second answer matters more than current facts, use "fast": true.

Protect the key#

Keep the key on your server, read it from an environment variable or secret manager, and rotate it if it ever reaches a log or a repository. A key is bearer credential for your whole quota — anyone holding it can spend it.

Watch the budget#

Read X-RateLimit-Remaining on responses and throttle before you hit zero, which costs a round trip and a Retry-After wait. Call GET /v1/usage for the monthly picture rather than the per-request one.

Log the request ID#

Store meta.request_id alongside your own request logs. It's the one value that lets us find a specific generation in our traces, and it turns a support thread into a lookup.


Agent skills#

TasteRay publishes two skills on the Agent Skills open standard. Install them in a coding agent to use elicitation and recommendations from your editor.

Install the elicitation skill, which walks a user through a conversation that uncovers mood, constraints, and past favorites before any request is made:

Shell
npx skills add tasteray/skills/elicitation

Install the recommendations skill, which calls this API and formats the results with their match scores and explanations:

Shell
npx skills add tasteray/skills/recommendations

Install both:

Shell
npx skills add tasteray/skills

The skills run in Claude Code, Cursor, Copilot, Windsurf, Cline, Aider, and Roo Code. The source is at github.com/tasteray/skills.

How it works#

The API runs on Cloudflare Workers, so requests are served from the edge location nearest the caller and there's no cold start to absorb.

A request passes through five stages:

  1. Authentication. The key is hashed and looked up; the plaintext is never stored.
  2. Rate limiting. A sliding one-minute window counts requests against the key's tier.
  3. Validation. The body is checked field by field, and a failure returns 400 before any model is called.
  4. Generation. A prompt template is fetched by version, compiled with your context, and sent to the model, which may call web search. A failure falls back to a second model.
  5. Response. The model's output is validated against a JSON schema, null fields are stripped, and traces are flushed.

State lives in a key-value store: hashed keys, rate-limit counters on a two-minute expiry, and usage records. The store is eventually consistent, with roughly 60 seconds of propagation, so a burst spread across two edge locations can occasionally clear a per-minute limit it would have hit from one.

Support#


Changelog#

The format follows Keep a Changelog. Entries that change the API surface are marked; the rest are website and tooling work.

[0.7.0] — 2026-02-02#

Changed

  • Rebuilt the landing page around the playground as the activation surface.
  • Moved body type to Hanken Grotesk and display type to Epilogue.
  • Fixed a mobile overlap between the header and the hero.

[0.6.0] — 2026-01-12#

Added

  • Product analytics on the landing page, playground, and key generator.

[0.5.0] — 2025-12-02#

Added

  • API: context.profile and context.history_text, which accept free-form text in place of the structured preferences and history arrays.

Changed

  • API: Renamed the health check services to ai_service, fast_ai_service, analytics, and cache.
  • Renamed the product from "TasteRay External API" to "TasteRay Emotional API".
  • Unified the dark theme across every page.

[0.4.1] — 2025-11-24#

Added

  • API: options.fast, which routes a request to a lower-latency model.
  • API: Automatic fallback to a second model when the first one fails.

[0.4.0] — 2025-11-16#

Added

  • API: Server-side control over when the model may call web search.

[0.3.0] — 2025-11-15#

Added

  • API: Web search grounding, with the searches reported in meta.tool_usage.
  • API: options.language, which sets the language of every generated string.
  • API: Schema-validated structured output on both generating endpoints.
  • Endpoint tabs in the playground.

[0.2.0] — 2025-11-07#

Added

  • The interactive playground.
  • Self-serve API key generation.

Changed

  • Switched the package manager from npm to Bun.

[0.1.0] — 2025-11-05#

Added

  • First release: POST /v1/recommend, POST /v1/explain, GET /v1/usage, and GET /v1/health.
  • API key authentication and sliding-window rate limiting.
  • Versioned prompt management and request tracing.
↑ Back to top