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
- List packages through categories, not
/packages. - Read the
intract_*fields to group and filter. - Add INTRACT packages to the basket by
ident, not by numericid. - 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.
| Request | Returns INTRACT packages? |
|---|---|
GET /api/accounts/{token}/packages | No |
GET /api/accounts/{token}/categories?includePackages=1 | Yes, inside the INTRACT category |
GET /api/accounts/{token}/categories/{categoryId}?includePackages=1 | Yes, 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"
}
| Field | Values | Notes |
|---|---|---|
ident | ext_{category}_{asset} | Use it for the basket and for single-package lookups |
intract_asset_id | string | The asset id on INTRACT |
intract_category | clothes, vehicles, weapons, bundles, other | Top-level category |
intract_sub_category | object or null | null for bundles |
…gender | male, female, unisex, null | Clothing only |
…pedestrian | mp_m_freemode_01, mp_f_freemode_01, unisex | Clothing only |
…component | GTA V component slot | null when the item is a prop |
…prop | GTA V prop slot | null when the item is a component |
…pack_name | string | The INTRACT pack the asset belongs to |
…is_contest | boolean | Whether 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:
| Slot | Kind | Suggested label |
|---|---|---|
jbib | component | Tops |
accs | component | Undershirts |
lowr | component | Bottoms |
feet | component | Shoes |
berd | component | Masks |
hand | component | Bags |
uppr | component | Arms / Gloves |
teef | component | Accessories |
task | component | Armor |
decl | component | Decals |
hair | component | Hair |
head | component | Head |
p_head | prop | Hats |
p_eyes | prop | Glasses |
p_ears | prop | Earrings |
p_lwrist | prop | Watches |
p_rwrist | prop | Bracelets |
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 sent | Result |
|---|---|
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/removedoesn'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_limitis 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.comin 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_categoryand subcategories fromcomponent/prop. - Filter by
gender(includingunisex) instead of parsing names. - Send the
identaspackage_idwhen adding INTRACT packages to a basket. - Don't rely on basket removal or the basket
idfor INTRACT packages. - Allow
intract.comimages.