Docs
Build with Recapi
Three steps to your first recommendations, with every call in JavaScript, Python, PHP, Java, Go, Swift and Ruby. Your dashboard's quick start shows the same snippets with your own keys.
- 1
Get your keys
Ask for early access, then copy a public key (
rk_pub_…) for browsers and a secret key (rk_sec_…) for your server. - 2
Send what people do
Each view, add to cart or purchase, with its item and a level from 1 to 10.
- 3
Show the lists
Ask for a list where you want it on the page, and see what it brings in your dashboard.
Pick your library
- jsJavaScriptAny website with one script tag, or
npm install @recapi/client - pyPythonDjango, Flask, FastAPI:
pip install recapi - phpPHPWordPress, WooCommerce, Laravel:
composer require recapi/client, one file - javaJavaSpring, Kotlin and Android apps: on Maven Central, no dependency
- goGoAny Go server: the sources, standard library only
- swiftSwiftiOS and macOS apps, Vapor servers: a Swift package, Foundation only
- rbRubyRails, Solidus, Sinatra:
bundle add recapi, no dependency - csC#ASP.NET Core and any .NET 8 server:
dotnet add package Recapi - ShopifyTwo snippets to paste, shown in your dashboard
Use a library rather than calling the API yourself. It keeps each user's history and sends it with every request, so lists follow the latest click without a lookup. It batches and retries events, falls back to popular items when Recapi is slow, and credits clicks and purchases to the list that showed the item. Recommendations without it are slower and less relevant, and your dashboard counts no clicks.
Install
Each library is on its language's package registry, with no extra dependency. Your Recapi API also serves each one itself, for servers that can't reach the registry: your dashboard's quick start shows how.
<script type="module"> import { createClient } from "https://api.recapi.io/sdk/0.6.1/index.js"; </script> // or, with a bundler (Vite, webpack…): npm install @recapi/client import { createClient } from "@recapi/client";
# Python 3.11 or later, from PyPI
pip install recapi# PHP 8.2 or later, with curl: one file, from Packagist
composer require recapi/client// Java 11 or later, Kotlin, Android 7 or later: Maven Central, no dependency dependencies { implementation("io.recapi:recapi:0.2.3") }
# Go 1.24 or later: the module recapi.io/go, no dependency
go get recapi.io/go// Swift 6, iOS 15 or macOS 12 or later: a Swift package, Foundation only .package(url: "https://github.com/Blushedface/recapi-swift.git", from: "0.1.4") // and in your target: .product(name: "Recapi", package: "recapi-swift")
# Ruby 3.1 or later, Rails, Solidus: the gem, standard library only
bundle add recapi# .NET 8 or later, ASP.NET Core: the NuGet package, standard library only
dotnet add package RecapiCreate a client
In a browser, use a public key. On your server, use a secret key: it never leaves your server. Create one client per page or per process.
const recapi = createClient({ apiKey: "rk_pub_…", baseUrl: "https://api.recapi.io" });
import recapi client = recapi.Client("rk_sec_…", "https://api.recapi.io") # Several processes or servers? Share each user's state between them: # recapi.Client(…, store=redis.Redis()), or Django's cache. # In async handlers (FastAPI), use recapi.AsyncClient with the same arguments.
require __DIR__ . '/vendor/autoload.php'; // SessionStore keeps each visitor's history in their PHP session. session_start(); $recapi = new Recapi\Client('rk_sec_…', 'https://api.recapi.io', store: new Recapi\SessionStore());
import io.recapi.*; Client recapi = new Client("rk_sec_…", "https://api.recapi.io"); // An Android app takes a public key (rk_pub_…), and calls recommend() off the main thread. // It keeps each visitor's history across launches: Client.builder(…).store(new FileStore(dir)).build() // Several servers? Share each user's state: Client.builder(…).store(yourRedisStore).build()
import recapi "recapi.io/go" client, err := recapi.New("rk_sec_…", "https://api.recapi.io", recapi.Options{}) defer client.Close() // sends the queued events at exit // Several servers? Share each user's state: recapi.Options{Store: yourRedisStore}
import Recapi let recapi = try RecapiClient(apiKey: "rk_sec_…", baseURL: "https://api.recapi.io") // An iOS app takes a public key (rk_pub_…). At exit, await recapi.close() sends the queued events. // It keeps each visitor's history across launches: RecapiClient(…, store: FileStore()) // Several servers? Share each user's state: RecapiClient(…, store: yourRedisStore)
require "recapi" RECAPI = Recapi::Client.new("rk_sec_…", "https://api.recapi.io", store: Rails.cache) # One client per process: it sends the queued events at exit.
using Recapi; builder.Services.AddSingleton(_ => new RecapiClient("rk_sec_…", "https://api.recapi.io")); // One client per process, disposed at shutdown: it sends the queued events then. // Several servers? Share each user's state: new RecapiOptions { Store = yourRedisStore }
Describe your items
Optional, and worth it: tags let you ask for a list from one category, and help new items get shown. Send up to 1,000 items per call, from your server. On Shopify, upload your products export in the dashboard instead.
// Items need a secret key, so they are sent from your server: // see the Python, PHP, Java, Go, Swift, Ruby or C# tab.
client.upsert_items([
{"item_id": "item_4471", "tags": ["sports"], "title": "Cup final highlights"},
])$recapi->upsertItems([
['item_id' => 'item_4471', 'tags' => ['sports'], 'title' => 'Cup final highlights'],
]);recapi.upsertItems(List.of(
Map.of("item_id", "item_4471", "tags", List.of("sports"), "title", "Cup final highlights")));n, err := client.UpsertItems(ctx, []recapi.CatalogItem{
{ItemID: "item_4471", Tags: []string{"sports"}, Title: "Cup final highlights"},
})let n = try await recapi.upsertItems([ CatalogItem(itemID: "item_4471", tags: ["sports"], title: "Cup final highlights"), ])
RECAPI.upsert_items([
{ item_id: "item_4471", tags: ["sports"], title: "Cup final highlights" }
])var n = await recapi.UpsertItemsAsync(new[] { new CatalogItem { ItemId = "item_4471", Tags = new[] { "sports" }, Title = "Cup final highlights" }, });
Send what people do
Each time someone interacts with an item, send the item and a level. The call returns at once: the library queues the event and sends it in the background.
Levels go from -1 to 10: the bigger the interaction, the higher the level.
- -1: not interested, such as a "not interested" button. The item is not shown to them again until they interact with it. Never use it for skips.
- 0: shown in a list, nothing more.
- 1 to 2: light interest, short of opening the item, such as a quick view or a hover.
- 3: clicked or opened, including a click on a recommendation.
- 4 to 9: a deeper interaction, higher as it matters more: 5 for an add to cart or an article read to the end.
- 10: the biggest interaction, such as a purchase.
On an item from a recommendation, any level from 1 counts as a click on that list, and from 5 as a conversion (you can change that level in your dashboard's settings). So send 0, not 1, for items that are only shown.
recapi.track("item_4471", 3); // a click recapi.track("item_4471", 10); // a purchase recapi.track("item_9120", -1); // not interested // Short-lived jobs and serverless handlers: send the queue before returning // (Cloudflare Workers: ctx.waitUntil(recapi.flush()), Next.js: after(() => recapi.flush())). await recapi.flush();
client.track("item_4471", 3, user_id="u_812") # a click client.track("item_4471", 10, user_id="u_812") # a purchase client.track("item_9120", -1, user_id="u_812") # not interested # Visitors who are not logged in: a random id kept in their session. client.track("item_4471", 3, anon_id=session["recapi_anon"]) # Short-lived jobs and serverless handlers: send the queue before returning. client.flush()
$recapi->track('item_4471', 3, userId: 'u_812'); // a click $recapi->track('item_4471', 10, userId: 'u_812'); // a purchase $recapi->track('item_9120', -1, userId: 'u_812'); // not interested // Visitors who are not logged in: a random id kept in their session. $_SESSION['recapi_anon'] ??= bin2hex(random_bytes(16)); $recapi->track('item_4471', 3, anonId: $_SESSION['recapi_anon']);
recapi.track(new Event("item_4471", 3).userId("u_812")); // a click recapi.track(new Event("item_4471", 10).userId("u_812")); // a purchase recapi.track(new Event("item_9120", -1).userId("u_812")); // not interested // Visitors who are not logged in: a random id kept in their session. recapi.track(new Event("item_4471", 3).anonId(visitorId)); // Short-lived jobs, or an Android activity's onStop (off the main thread): send the queue. recapi.flush();
client.Track(ctx, recapi.Event{ItemID: "item_4471", Level: 3, UserID: "u_812"}) // a click
client.Track(ctx, recapi.Event{ItemID: "item_4471", Level: 10, UserID: "u_812"}) // a purchase
client.Track(ctx, recapi.Event{ItemID: "item_9120", Level: -1, UserID: "u_812"}) // not interested
// Visitors who are not logged in: a random id kept in their session.
client.Track(ctx, recapi.Event{ItemID: "item_4471", Level: 3, AnonID: visitorID})
// Short-lived jobs and serverless handlers: send the queue before returning.
client.Flush()try recapi.track(itemID: "item_4471", level: 3, userID: "u_812") // a click try recapi.track(itemID: "item_4471", level: 10, userID: "u_812") // a purchase try recapi.track(itemID: "item_9120", level: -1, userID: "u_812") // not interested // Visitors who are not logged in: a random id kept in their session. try recapi.track(itemID: "item_4471", level: 3, anonID: visitorID) // Short-lived jobs, or an app going to the background: send the queue. try await recapi.flush()
RECAPI.track("item_4471", 3, user_id: "u_812") # a click RECAPI.track("item_4471", 10, user_id: "u_812") # a purchase RECAPI.track("item_9120", -1, user_id: "u_812") # not interested # Visitors who are not logged in: a random id kept in their session. RECAPI.track("item_4471", 3, anon_id: session[:recapi_anon]) # Short-lived jobs: send the queue before returning. RECAPI.flush
await recapi.TrackAsync(new RecapiEvent { ItemId = "item_4471", Level = 3, UserId = "u_812" }); // a click await recapi.TrackAsync(new RecapiEvent { ItemId = "item_4471", Level = 10, UserId = "u_812" }); // a purchase await recapi.TrackAsync(new RecapiEvent { ItemId = "item_9120", Level = -1, UserId = "u_812" }); // not interested // Visitors who are not logged in: a random id kept in their session. await recapi.TrackAsync(new RecapiEvent { ItemId = "item_4471", Level = 3, AnonId = visitorId }); // Short-lived jobs: send the queue before returning. await recapi.FlushAsync();
Show recommendations
Ask for a list where you show it. If Recapi is slow or failing, you get popular items instead (fallback is true), and an empty list if it is down: hide the slot then. No error handling needed. Clicks on the items are credited to the list by themselves.
const { items } = await recapi.recommend({ placement: "product_page", count: 8, tagsAny: ["sports"] }); render(items); // [{ itemId, score, slot }] // On pages without a user: const popular = await recapi.popular({ count: 8 });
result = client.recommend(user_id="u_812", placement="product_page", count=8, tags_any=["sports"]) item_ids = [item.item_id for item in result.items] # On pages without a user: popular = client.popular(count=8)
$result = $recapi->recommend(userId: 'u_812', placement: 'product_page', count: 8, tagsAny: ['sports']); $itemIds = array_map(fn ($item) => $item->itemId, $result->items); // On pages without a user: $popular = $recapi->popular(count: 8);
Recommendations result = recapi.recommend(new RecommendRequest() .userId("u_812").placement("product_page").count(8).tagsAny(List.of("sports"))); for (Item item : result.items()) render(item.itemId()); // On pages without a user: List<Item> popular = recapi.popular(8);
result, err := client.Recommend(ctx, recapi.Request{
UserID: "u_812", Placement: "product_page", Count: 8, TagsAny: []string{"sports"}})
for _, item := range result.Items {
render(item.ItemID)
}
// On pages without a user:
popular, err := client.Popular(ctx, recapi.PopularRequest{Count: 8})let result = try await recapi.recommend(RecommendRequest( userID: "u_812", count: 8, placement: "product_page", tagsAny: ["sports"])) for item in result.items { render(item.itemID) } // On pages without a user: let popular = try await recapi.popular(count: 8)
result = RECAPI.recommend(user_id: "u_812", placement: "product_page", count: 8, tags_any: ["sports"]) item_ids = result.items.map(&:item_id) # On pages without a user: popular = RECAPI.popular(count: 8)
var result = await recapi.RecommendAsync(new RecommendRequest { UserId = "u_812", Placement = "product_page", Count = 8, TagsAny = new[] { "sports" } }); foreach (var item in result.Items) Render(item.ItemId); // On pages without a user: var popular = await recapi.PopularAsync(new PopularRequest { Count = 8 });
A page cache (a CDN, WP Rocket, Django's cache_page) would show one visitor's list to everyone. Render the list in the browser with the JavaScript library, or from a part of the page the cache leaves out.
Log in and log out
When a visitor logs in, tell Recapi who they are: what they did before logging in keeps counting.
recapi.identify("u_812"); // at login recapi.reset(); // at logout
client.identify(session["recapi_anon"], "u_812") # at login
$recapi->identify($_SESSION['recapi_anon'], 'u_812'); // at login
recapi.identify(visitorId, "u_812"); // at login
client.Identify(ctx, visitorID, "u_812") // at login
try recapi.identify(anonID: visitorID, userID: "u_812") // at login
RECAPI.identify(session[:recapi_anon], "u_812") # at login
await recapi.IdentifyAsync(visitorId, "u_812"); // at login
Privacy
In the browser, connect the library to your cookie banner: without consent it stores nothing on the device. To erase a user's data, make one call from your server.
const recapi = createClient({ apiKey, baseUrl, consent: cmp.hasConsent("personalisation") }); cmp.onChange((granted) => recapi.setConsent(granted)); // Erasing a user needs a secret key: see the Python, PHP, Java, Go, Swift, Ruby or C# tab.
client.delete_user("u_812")$recapi->deleteUser('u_812');recapi.deleteUser("u_812");err := client.DeleteUser(ctx, "u_812")try await recapi.deleteUser("u_812")
RECAPI.delete_user("u_812")await recapi.DeleteUserAsync("u_812");
Shopify
No code to write: paste two snippets, one in your theme and one as a custom pixel, then upload your products and orders exports. Your dashboard's quick start shows both snippets with your keys, step by step.