---
name: crossup
description: Integrate CrossUp's recommendation engine into a store with @crossup/storefront or the API, and verify each step with evidence. Use when asked to integrate CrossUp, display recommendations, send signals or debug an integration.
---
# CrossUp — integrate and verify
Documentation: https://docs.crossup.ai/llms.txt · Error reference: https://docs.crossup.ai/en/api/errores.md
## Non-negotiable rules
1. The store comes from the key. No request carries `store_id`.
2. `results: []` is a valid response; `reason_codes` says why. Nothing is displayed.
3. An absent field is unknown, not zero. `sellable` is a string; `title` is an object per language.
4. Never break the page: use `tryGetRecommendations`, never `await` on `track.*`.
5. On `429`, wait `Retry-After`. Never retry in a loop.
6. Don't make up endpoints, fields or values. If the docs don't say it, ask.
## Path
1. **Data.** Ask for the `spk_…` key and the `connection_id` (`cn_…`). Never commit the key to a file that isn't public configuration; it is public by design, but it still goes in environment variables.
2. **Verify the key.** `GET {base}/v1/catalog/items/{item_id}` with `Authorization: Bearer`. A `200` with `item.item_id` confirms key and connection. `401` → key; `403 forbidden_connection` → another store's connection; `404` → item from another store or nonexistent.
3. **Install.** `npm install @crossup/storefront`. One instance per page: `createStorefront({ apiKey, connectionId })`.
4. **Request.** `tryGetRecommendations({ surface, placementKey, contextItemId?, cartItems? })`. `pdp` and `cart` need `contextItemId`; `carousel`, `in-checkout` and `success-page` need `cartItems`; `checkout` accepts either.
5. **Display.** Iterate `results[]`; skip the ones without `product`; use `product.item.title[lang]` with a fallback, `variants[].price.amount` as a string, `availability[].sellable` as text (never as a boolean). Sanitize `item.description` if it is HTML. The design is the store's: don't bring CrossUp components.
6. **Signals.** `track.impression(decision, { position })` when the recommendation enters the screen (IntersectionObserver, once). `track.click(decision, result)` on open. `track.addToCart` with the added quantity. `track.purchase(decision, { orderExternalId, items })` on the thank-you page.
7. **Evidence.** Show a `decision_id` from a test decision and a `signal_id` with `status: "accepted"`. Without both, the integration is not finished.
## When something fails
- `results: []` with `module_not_configured`: the moment is not enabled in the admin. It is not a code bug.
- `results: []` with `no_offer` or `brain_unavailable`: display nothing. The store keeps selling.
- `rejected` with `unknown_decision` on a signal: a decision that doesn't belong to this connection was sent.
- `429 rate_limited`: find the loop (one request per render, one request per card).