Skip to main content

Headless Store Integration

This page is for developers who run a custom storefront on the Tebex Headless API and want to sell the packages INTRACT syncs into their store. If you use the standard Tebex-hosted webstore, you don't need any of this. INTRACT packages show up there like any other package.

Once a server owner links their store to INTRACT, every customizable asset is created in Tebex automatically, inside a dedicated category. Each package carries extra intract_* fields with the asset's category, gender and GTA V clothing slot. You can build filters and subcategories from those fields without parsing package names.

What changes for your storefront

  1. List packages through categories, not /packages.
  2. Read the intract_* fields to group and filter.
  3. Add INTRACT packages to the basket by ident, not by numeric id.
  4. Allow images from intract.com.

The sections below cover each one.

Fetching INTRACT packages

INTRACT packages are only returned through the categories endpoint. GET /packages does not include them.

RequestReturns INTRACT packages?
GET /api/accounts/{token}/packagesNo
GET /api/accounts/{token}/categories?includePackages=1Yes, inside the INTRACT category
GET /api/accounts/{token}/categories/{categoryId}?includePackages=1Yes, that category only
GET /api/accounts/{token}/packages/{ident}Yes, one package
GET /api/accounts/{token}/packages/{id} (numeric)No: 400 Invalid Package ID

Don't hardcode the category id or name. A package is an INTRACT package when it has an intract_asset_id.

The Headless API ignores filter query parameters on these endpoints. Fetch the category once (and cache it), then filter on your side.

Package fields

INTRACT packages carry the regular Tebex fields (id, ident, name, image, total_price, …) plus the following:

{
  "id": 1049684195727704,
  "ident": "ext_3485288_019f390b-bc31-72e0-b140-83e592e66673",
  "name": "Sweater (M)",
  "image": "https://intract.com/storage/projects/.../preview.gif",
  "total_price": 0,
  "currency": "USD",

  "intract_asset_id": "019f390b-bc31-72e0-b140-83e592e66673",
  "intract_category": "clothes",
  "intract_sub_category": {
    "pack_name": "STORE DEMO - ALL MALE",
    "pack_thumbnail": "https://intract.com/storage/projects/.../pack.png",
    "is_contest": false,
    "pedestrian": "mp_m_freemode_01",
    "gender": "male",
    "component": "jbib",
    "prop": null
  },
  "intract_pack_name": "STORE DEMO - ALL MALE",
  "intract_pack_thumbnail": "https://intract.com/storage/projects/.../pack.png"
}
FieldValuesNotes
identext_{category}_{asset}Use it for the basket and for single-package lookups
intract_asset_idstringThe asset id on INTRACT
intract_categoryclothes, vehicles, weapons, bundles, otherTop-level category
intract_sub_categoryobject or nullnull for bundles
…gendermale, female, unisex, nullClothing only
…pedestrianmp_m_freemode_01, mp_f_freemode_01, unisexClothing only
…componentGTA V component slotnull when the item is a prop
…propGTA V prop slotnull when the item is a component
…pack_namestringThe INTRACT pack the asset belongs to
…is_contestbooleanWhether the asset comes from a contest

A clothing item has either a component or a prop, never both. Package names end in (M) or (F), but the name doesn't tell you the item type, so use the slot instead.

Clothing slots

component and prop are GTA V clothing slots, not product types. A hoodie, a shirt and a jacket are all jbib. Map the slots to the labels you want to show:

SlotKindSuggested label
jbibcomponentTops
accscomponentUndershirts
lowrcomponentBottoms
feetcomponentShoes
berdcomponentMasks
handcomponentBags
upprcomponentArms / Gloves
teefcomponentAccessories
taskcomponentArmor
declcomponentDecals
haircomponentHair
headcomponentHead
p_headpropHats
p_eyespropGlasses
p_earspropEarrings
p_lwristpropWatches
p_rwristpropBracelets

If you need finer groups (for example Hoodies vs. Jackets inside Tops), match keywords in the package name on top of the slot, and fall back to the slot label when no keyword matches.

Snippet: build categories and filters

Plain JavaScript, no dependencies. Run it on your server so the webstore token isn't exposed in the browser.

const API = `https://headless.tebex.io/api/accounts/${process.env.TEBEX_WEBSTORE_TOKEN}`;

const SLOT_LABELS = {
  jbib: "Tops", accs: "Undershirts", lowr: "Bottoms", feet: "Shoes",
  berd: "Masks", hand: "Bags", uppr: "Arms", teef: "Accessories",
  task: "Armor", decl: "Decals", hair: "Hair", head: "Head",
  p_head: "Hats", p_eyes: "Glasses", p_ears: "Earrings",
  p_lwrist: "Watches", p_rwrist: "Bracelets",
};

const CATEGORY_LABELS = {
  clothes: "Clothing", vehicles: "Vehicles", weapons: "Weapons",
  bundles: "Bundles", other: "Other",
};

// Fetch every INTRACT package and flatten it into what a storefront needs.
export async function getIntractItems() {
  const res = await fetch(`${API}/categories?includePackages=1`);
  if (!res.ok) throw new Error(`Tebex categories: ${res.status}`);
  const { data } = await res.json();

  return data
    .flatMap((category) => category.packages)
    .filter((pkg) => pkg.intract_asset_id)
    .map((pkg) => {
      const sub = pkg.intract_sub_category;
      const slot = sub?.component ?? sub?.prop ?? null;
      return {
        id: pkg.id,
        ident: pkg.ident, // send this to the basket
        name: pkg.name.replace(/\s*\((M|F)\)\s*$/, ""),
        image: pkg.image,
        price: pkg.total_price,
        currency: pkg.currency,
        category: pkg.intract_category,
        categoryLabel: CATEGORY_LABELS[pkg.intract_category] ?? pkg.intract_category,
        gender: sub?.gender ?? null,
        slot,
        subcategory: slot ? SLOT_LABELS[slot] ?? slot : null,
        pack: sub?.pack_name ?? null,
      };
    });
}

// Category -> subcategories with counts, built from what is actually in the store.
export function buildMenu(items) {
  const menu = {};
  for (const item of items) {
    const cat = (menu[item.categoryLabel] ??= {});
    const key = item.subcategory ?? "All";
    cat[key] = (cat[key] ?? 0) + 1;
  }
  return menu;
}

// Filter by any combination of fields.
export function filterItems(items, { category, gender, slot, pack } = {}) {
  return items.filter((item) =>
    (!category || item.category === category) &&
    (!gender || item.gender === gender || item.gender === "unisex") &&
    (!slot || item.slot === slot) &&
    (!pack || item.pack === pack)
  );
}

Example usage:

const items = await getIntractItems();

buildMenu(items);
// { Clothing: { Tops: 60, Bottoms: 63, Masks: 23, Hats: 14, Decals: 2, Armor: 2 } }

filterItems(items, { gender: "male", slot: "jbib" }); // male tops
filterItems(items, { gender: "female", slot: "p_head" }); // female hats

Adding INTRACT packages to a basket

Tebex rejects the numeric id of an INTRACT package when you add it to a basket. Send its ident as package_id instead. Regular packages keep working with the numeric id.

POST https://headless.tebex.io/api/baskets/{basketIdent}/packages
Content-Type: application/json

{ "package_id": "ext_3485288_019f390b-bc31-72e0-b140-83e592e66673", "quantity": 1 }
package_id sentResult
1049684195727704 (numeric id)400 The selected id is invalid
"019f390b-…" (INTRACT asset id)400 The provided package does not belong to the same account
"ext_3485288_019f390b-…" (ident)Added

If your cart stores numeric ids, resolve them to the ident on your server just before you add them to the basket:

function basketPackageId(pkg) {
  return pkg.ident?.startsWith("ext_") ? pkg.ident : pkg.id;
}

FiveM baskets still need the player to log in before any package can be added, the same as with regular packages.

Basket caveats

  • Removing: POST /baskets/{ident}/packages/remove doesn't accept INTRACT packages, by ident or by numeric id. Let players edit their cart on your side and create the Tebex basket only at checkout.
  • Duplicates: Tebex lets you add the same INTRACT package twice even though user_limit is 1. De-duplicate in your cart.
  • Basket contents: in the basket response, INTRACT packages come back with id: null. Don't rely on that id to match basket lines to your products.

Images

Package images and pack thumbnails are hosted on intract.com, and most previews are animated GIFs.

  • Allow https://intract.com in your image CSP (img-src).
  • On Next.js, add it to next.config:
images: {
  remotePatterns: [{ protocol: "https", hostname: "intract.com" }],
},
  • Next.js doesn't resize animated GIFs, and previews can be several MB each. Lazy-load them in product grids, or show the pack thumbnail in listings and the animated preview on the product page.

Checklist

  • List packages with /categories?includePackages=1.
  • Detect INTRACT packages by intract_asset_id.
  • Build categories from intract_category and subcategories from component / prop.
  • Filter by gender (including unisex) instead of parsing names.
  • Send the ident as package_id when adding INTRACT packages to a basket.
  • Don't rely on basket removal or the basket id for INTRACT packages.
  • Allow intract.com images.