Skip to main content
If your app shows a curated set of products — a storefront, a gift guide, a chat assistant that recommends items — you’ll want your own copy of the product data rather than hitting Rye’s API on every page render. This guide walks through building that catalog and keeping it fresh. The recommended approach is a server-side sync:
  1. Browse the Catalog tab in the production console to pick products you want to sell.
  2. Backfill initial product data with the product lookup endpoint to populate your database.
  3. Subscribe to product.updated / product.removed webhooks so your copy stays current in real time.
  4. Reconcile once every 24 hours with the lookup endpoint, to catch anything a missed webhook left stale.
This gives you full control over the data and lets you query it in whatever shape is most efficient for your use case. The sections below follow that order — set up the webhook handler before backfilling so you don’t miss updates that land mid-backfill.

Step 1: Find products in the Catalog tab

Open the Catalog tab in the production console and browse the merchants and products available through Rye. When you find a product you want to sell, copy its product URL by right clicking on the product and clicking “Copy product URL” — that’s the only identifier you need to look it up.
Right-clicking a product in the Catalog tab reveals a menu with View product, Copy product ID, and Copy product URL.
Note the commission percentage shown on each product tile — that’s the cut you earn on a sale, and it’s what makes room for the discounting strategy described in Payment flows.
Browse in production (https://console.rye.com), not staging. The catalog is not yet available in the staging environment.
Collect the URLs of the products you want in your catalog. A product URL looks like any normal merchant link, e.g.:
https://flybyjing.com/products/saucetrio

Step 2: Set up a product.updated webhook handler

Product events keep your catalog current without polling. There are two:
EventMeaningPayload
product.updatedA product was created or modified (attributes, variants, pricing, or availability changed).Includes a data field with the full Product snapshot.
product.removedA product was deleted or unpublished by the merchant.No data field — the product no longer exists.
Product webhooks are snapshot events, which mean they carry the full product state in their data field, so you can write updated product data to your database without any follow-up API call. See the webhooks guide for endpoint setup, signature verification, and the verification handshake.
Product event subscriptions aren’t yet self-serve. We don’t currently support subscribing to product events programmatically or from the console. Reach out to us and we’ll enable product.updated / product.removed delivery for the products and merchants you care about, on a case-by-case basis.

Handling the events

Verify the signature, then branch on the event type. Acknowledge quickly (within 5 seconds) and do the database write in the background.
import { CheckoutIntents } from "checkout-intents";
import express from "express";
import { upsertProduct, deleteProduct } from "./catalog";

const app = express();
const client = new CheckoutIntents();
const WEBHOOK_SECRET = process.env.RYE_HMAC_SECRET_KEY!;

app.use(express.raw({ type: "application/json" }));

app.post("/webhook", (req, res) => {
  const signatureHeader = req.headers["x-rye-signature"] as string;
  if (!signatureHeader) return res.status(401).send("Unauthorized");

  const event = client.events.unwrap(
    req.body.toString(),
    signatureHeader,
    WEBHOOK_SECRET,
  );

  // Handle the verification challenge during endpoint setup
  if (event.type === "webhook_endpoint.verification_challenge") {
    return res.json({ challenge: event.source.id });
  }

  // Acknowledge immediately, then process in the background
  res.status(200).send("OK");

  switch (event.type) {
    case "product.updated":
      // `data` is a full Product snapshot — write it straight to your DB
      void upsertProduct(event.data);
      break;
    case "product.removed":
      // No `data` — the product is gone. Remove it by id.
      void deleteProduct(event.source.id);
      break;
  }
});

app.listen(3000, () => console.log("Listening on port 3000"));
Persist the id (the stable Rye product identifier, e.g. fly-by-jing.myshopify.com:17880536678469) and the url on every product — you’ll key your reconciliation job off the url in Step 4.
Snapshots can arrive out of order. Each event carries a createdAt timestamp; if you receive an update older than what you’ve already stored, ignore it. For strict consistency you can ignore the snapshot entirely and re-fetch authoritative state with the lookup endpoint (Step 3) using the product’s url.

Step 3: Backfill your catalog with the lookup endpoint

To populate your catalog initially, call the product lookup endpoint once for each product URL you collected in Step 1. It’s a simple GET keyed by URL:
curl
curl "https://api.rye.com/api/v1/products/lookup?url=https://flybyjing.com/products/saucetrio" \
  -H "Authorization: Bearer $RYE_API_KEY"
The response is a Product object:
{
  "id": "fly-by-jing.myshopify.com:17880536678469",
  "url": "https://flybyjing.com/products/saucetrio",
  "name": "Chili Crisp Trio",
  "brand": "Fly By Jing",
  "retailer": "FLY BY JING",
  "sku": "Set_Triple_Threat_2",
  "description": "Our best-selling trio of addictive Sichuan Sauces, now in a gift-ready box…",
  "isPurchasable": true,
  "availability": "in_stock",
  "price": { "amountSubunits": 4500, "currencyCode": "USD" },
  "images": [
    {
      "url": "https://images.rye.com/kHP2gdcGQjAS7tXRHRoKyq4PtQdEJ_Y0QkRvpad7qM4/aHR0cHM6...",
      "isFeatured": true
    }
    // ...4 more images
  ],
  "variantDimensions": [{ "label": "Title", "values": ["Default Title"] }],
  "variants": [
    {
      "id": "17880536678469",
      "name": "Default Title",
      "sku": "Set_Triple_Threat_2",
      "availability": "in_stock",
      "price": { "amountSubunits": 4500, "currencyCode": "USD" },
      "dimensions": [{ "label": "Title", "value": "Default Title" }],
      "images": [{ "url": "https://images.rye.com/kHP2gdcG...", "isFeatured": true }]
    }
  ]
}
Loop over your URLs and upsert each result with the same upsertProduct you wrote for the webhook handler — the shape is identical to the product.updated snapshot:
import { CheckoutIntents } from "checkout-intents";
import { upsertProduct } from "./catalog";

const client = new CheckoutIntents({ apiKey: process.env.RYE_API_KEY });

const PRODUCT_URLS = [
  "https://flybyjing.com/products/saucetrio",
  // ...the URLs you collected from the Catalog tab
];

for (const url of PRODUCT_URLS) {
  const product = await client.products.lookup({ url });
  await upsertProduct(product);
  console.log(`Backfilled ${product.name}`);
}
Product lookup has its own rate-limit bucket of 10 requests/sec (see Rate Limits). For a large backfill, throttle to stay under it, or request an increase.

Step 4: Reconcile daily with a cron job

Webhooks can occasionally be missed — a delivery times out, your endpoint has a blip, an event is dropped. A periodic reconciliation pass keeps your catalog from drifting. Run a cron job once every 24 hours that re-looks-up every product in your catalog and writes back the current state. The job re-uses the lookup endpoint from Step 3. For each stored product:
  • Lookup succeeds → upsert the fresh data (this corrects anything a missed product.updated left stale).
  • Lookup returns 404 → the product is gone; delete it (this corrects anything a missed product.removed left behind).
import { CheckoutIntents } from "checkout-intents";
import { allProducts, upsertProduct, deleteProduct } from "./catalog";

const client = new CheckoutIntents({ apiKey: process.env.RYE_API_KEY });

// Schedule this to run once per 24h (e.g. node-cron "0 3 * * *").
export async function reconcileCatalog() {
  for (const { id, url } of await allProducts()) {
    try {
      const product = await client.products.lookup({ url });
      await upsertProduct(product);
    } catch (err) {
      if (err instanceof CheckoutIntents.NotFoundError) {
        // Product no longer exists in Rye's catalog — drop it.
        await deleteProduct(id);
      } else {
        throw err; // transient error — let it retry next run
      }
    }
    await new Promise((r) => setTimeout(r, 100)); // ~10 req/s rate limit
  }
}
A daily cadence is plenty as a safety net — real-time updates still flow through the webhook handler from Step 2. If your catalog is small or you need tighter freshness guarantees, you can run it more often, but mind the product lookup rate limit.

Recap

MechanismPurposeCadence
Catalog tabDiscover products to sellOne-off
Lookup endpoint (backfill)Populate the catalog initiallyOne-off
product.updated / product.removed webhooksKeep the catalog current in real timeContinuous
Lookup endpoint (reconcile)Catch missed webhooksEvery 24h
With all four in place you have an authoritative, query-optimized copy of your product data that stays in sync with Rye. From here you can wire products straight into a checkout intent to let users buy them.