Catalog
Discover gift cards, eSIMs, top-ups, and digital services with workspace-scoped filters, denominations, and product details.
The catalog contains the digital goods available to your workspace. All catalog endpoints require an API key with the products.read scope. Products, counts, filter choices, and images are scoped to the workspace derived from that key.
Discovery endpoints
| Endpoint | Use |
|---|---|
GET /api/v1/categories | The public categories your catalog fills, with product counts — build navigation from it. |
GET /api/v1/products | List and search products with filters and pagination, or read a named selection by id. |
GET /api/v1/products/filters | Discover filter choices and matching product counts before pagination. |
GET /api/v1/products/{productId} | Read a product, its available variants, and gallery. |
GET /api/v1/catalog-product-images/{publicReference} | Fetch an image using the returned public reference. |
Use the categories and filters endpoints to populate navigation and selectors, list matching products, then read a selected product before ordering. There is no need to download every product just to build country, product-type, category, or currency selectors — and once you have chosen the products you sell, refresh exactly those with ids.
Public categories
A public category is a Xegora-defined shelf with a stable key for filtering and a title for display. Categories are Xegora's own everywhere on this API — on a product, in the filter counts and as a filter. The keys are, in display order: marketplaces, travel, food, gaming, wallets, fuel, transport, electronics, fashion, privacy, department-stores, multi-brand, entertainment, home, health-beauty, sports, gifts, kids, pets, crypto, and telecom. A product can sit on more than one category, or on none. Keep clients tolerant of new keys.
Categories
curl --get "https://myapi.xegora.com/api/v1/categories" \
-H "X-Api-Key: $XEGORA_API_KEY" \
--data-urlencode "productType=gift-card"{
"totalProducts": 1260,
"items": [
{ "key": "marketplaces", "title": "Online Marketplaces", "productCount": 84 },
{ "key": "gaming", "title": "Games & Gaming", "productCount": 212 }
]
}items are the public categories that hold at least one listable product, in display order, each with its title and how many products it holds. A product can sit on more than one category, so the counts do not add up to totalProducts, which counts the listable products themselves. productType is optional and narrows both. Feed a key back as publicCategory on the product list.
List and search products
curl --get "https://myapi.xegora.com/api/v1/products" \
-H "X-Api-Key: $XEGORA_API_KEY" \
--data-urlencode "productType=gift-card" \
--data-urlencode "countryCode=US" \
--data-urlencode "currency=USD" \
--data-urlencode "minimumValue=25" \
--data-urlencode "maximumValue=100" \
--data-urlencode "offset=0" \
--data-urlencode "limit=50"| Parameter | Meaning |
|---|---|
query | Free-text search; up to 200 characters. |
countryCode | Two-letter country code, for example US or DE. Input is trimmed and uppercased; at most 16 characters before trimming. |
publicCategory | One of the public category keys, for example travel; up to 40 characters, trimmed and case-insensitive. An unknown key returns an empty page. |
category | The older name of publicCategory; it takes only the same public category keys (any case). Anything else — a key that is not a public category, a product type such as gift-card (use productType), or popular (use sort=popular) — is 400 invalid_request. Send category or publicCategory; if you send both, they must name the same category or the request is 400. |
productType | gift-card, esim, phone-top-up, or digital-service; case-insensitive, up to 30 characters before trimming. |
currency | Three-letter denomination currency, for example USD; at most 16 characters before trimming. This is the currency of face values, not a price-conversion request. |
isFeatured | true for featured products, false for non-featured products; omit for both. |
minimumValue / maximumValue | Optional inclusive face-value bounds. Each must be positive, at most 99999999999.99999999, and have at most 8 fractional digits. The minimum cannot exceed the maximum. currency is required when either bound is supplied. |
sort | recommended (default): featured first, catalog sort order, then product id. id-asc: ascending product id. popular: featured first, then how often the product is bought and reviewed, then catalog sort order, name and product id. az: name, then product id. newest: most recently published first, then product id. Case-insensitive; at most 24 characters before trimming. |
ids | Comma-separated product ids, 1–100 of them: read exactly these products — the way to refresh the selection you sell. Every other filter still applies; a product that is not listable now is simply absent. A malformed or empty id is 400. |
amountType | fixed: products you can buy in a listed denomination (variants). custom: products you can buy for an amount of your choosing (customAmount). A product offering both matches both. |
offset | 0–100000; default 0. |
limit | 1–100; default 50 — or, with ids, the number of ids, so a named selection comes back on one page. |
Filters combine with AND. A value-window match means an available variant denomination, a default fixed denomination, or the product's published range overlaps the window. It does not guarantee that every amount in the window can be bought. Range increments and availability are checked when creating the quote. Value filters compare face values; they do not filter by checkout price.
The response is a page envelope:
{ "items": [], "offset": 0, "limit": 50, "hasMore": false }When hasMore is true, request the next page at offset + items.length with the same filters and sort. Every sort mode has a deterministic id tie-breaker. Offset pagination is not a snapshot: additions, removals, availability changes, or merchandising changes between requests can move page boundaries. Deduplicate by product id when walking multiple pages.
An unknown publicCategory and valid filters with no matches return an empty page. An invalid two-letter country code within the length bound also returns an empty page. A category that is not a public category key, unsupported product types or sort values, malformed currency, invalid value bounds, missing currency for value filters, out-of-range pagination, and oversized or control-character-containing filters return 400 with code: "invalid_request".
Discover filter choices and counts
curl --get "https://myapi.xegora.com/api/v1/products/filters" \
-H "X-Api-Key: $XEGORA_API_KEY" \
--data-urlencode "productType=gift-card" \
--data-urlencode "countryCode=US"{
"totalCount": 12,
"productTypes": [{ "value": "gift-card", "count": 12 }],
"countryCodes": [{ "value": "US", "count": 12 }],
"categories": [
{ "value": "gaming", "count": 7 },
{ "value": "department-stores", "count": 5 }
],
"currencies": [{ "value": "USD", "count": 12 }],
"isTruncated": false,
"publicCategories": [
{ "key": "gaming", "title": "Games & Gaming", "count": 7 },
{ "key": "department-stores", "title": "Department Stores", "count": 5 }
]
}This endpoint accepts the same filters as the product list — including ids and amountType — excluding offset, limit, and sort. With no filters it describes the workspace's currently listable catalog. totalCount counts all matching products, not variants, before pagination, even when filter choices are truncated. Each facet entry counts products matching that filter value within the filtered result set.
Counts are conjunctive: every supplied filter applies to every facet. For example, after setting countryCode=US, the country facet describes the US result set; it does not enumerate other countries that would match if that filter were removed. To populate alternative country choices, omit countryCode in that facet request. Blank or unavailable public metadata is omitted; do not sum facet counts to calculate totalCount.
categories and publicCategories describe the same thing in two shapes: the public categories the matching products sit on, in display order, with a product counted once per category. categories gives each as { value, count } like the other facets, publicCategories as { key, title, count }. A product can sit on several categories, so their counts can overlap. Feed a key back as publicCategory (or category).
productTypes, countryCodes and currencies each contain at most 200 entries, ordered by value. isTruncated: true means that at least one facet exceeded the bounded candidate set, so the returned filter choices may be incomplete. Refine the request with a product type, country, or search query and fetch filters again. There is no facet-pagination cursor. A truncated array may contain fewer than 200 public entries because unavailable metadata is omitted.
An empty result returns totalCount: 0, empty facet arrays, and isTruncated: false. The total, individual facets, and a subsequent product list are separate live reads, not one transaction snapshot; they may differ if catalog availability changes while requests are being served.
Read one product
GET /api/v1/products/{productId}{
"id": "0198d72d-99d6-75a6-9f12-971050ba7a5f",
"type": "gift-card",
"name": "Everyday Digital Gift Card",
"countryCode": "US",
"currency": "USD",
"minimumValue": null,
"maximumValue": null,
"valueStep": null,
"imageUrl": "/api/v1/catalog-product-images/img_5f4e3d2c1b0a99887766554433221100",
"isAvailable": true,
"variants": [
{
"id": "0198d72d-a15b-7cbf-ab69-b95410e98636",
"displayValue": "$50",
"price": 51.50,
"currency": "USD",
"faceValue": 50.00,
"attributes": null
}
],
"description": "A digital product.",
"terms": "Terms apply.",
"images": [
{
"id": "0198d9a6-40f8-7680-bbf8-054f5aff56ef",
"publicReference": "img_5f4e3d2c1b0a99887766554433221100",
"url": "/api/v1/catalog-product-images/img_5f4e3d2c1b0a99887766554433221100",
"altText": "Product image",
"width": 1200,
"height": 800,
"sortOrder": 0,
"isPrimary": true
}
],
"sortOrder": 0,
"isFeatured": false,
"category": "department-stores",
"sellCurrency": "USD",
"faceValue": null,
"priceFrom": 51.50,
"priceTo": 51.50,
"customAmount": {
"minimum": 5,
"maximum": 500,
"step": null,
"currency": "USD",
"indicativeRate": 1.03,
"indicativeFlat": 0
},
"publicCategories": [{ "key": "department-stores", "title": "Department Stores" }],
"facts": [
{ "key": "redeem-in", "countryCode": "US", "currency": null, "minimum": null, "maximum": null, "step": null, "count": null, "codes": null },
{ "key": "card-currency", "countryCode": null, "currency": "USD", "minimum": null, "maximum": null, "step": null, "count": null, "codes": null },
{ "key": "denominations", "countryCode": null, "currency": "USD", "minimum": 50.00, "maximum": 50.00, "step": null, "count": 1, "codes": null },
{ "key": "custom-amount", "countryCode": null, "currency": "USD", "minimum": 5, "maximum": 500, "step": null, "count": null, "codes": null }
],
"attributes": null,
"guide": [
{ "step": 1, "title": "Pay", "body": "The order is paid from your wallet." },
{ "step": 2, "title": "Receive the code", "body": "The code is ready to reveal as soon as the order is fulfilled." }
],
"brandCopy": {
"title": "Everyday Digital Gift Card",
"summary": "A gift card for everyday shopping online.",
"about": "Spend it on anything in the store.\n\nIt never expires.",
"howToRedeem": ["Open the store's website.", "Enter the code at checkout."],
"goodToKnow": ["Valid only in the United States."],
"faq": [{ "question": "Can the balance be split?", "answer": "Yes, across several purchases." }]
},
"siblings": [
{ "id": "0198d72d-99d6-75a6-9f12-971050ba7a60", "name": "Everyday Digital Gift Card (DE)", "countryCode": "DE", "currency": "EUR", "priceFrom": 21.40, "sellCurrency": "USD" }
],
"cashbackPercentage": null
}The public type values are gift-card, esim, phone-top-up, and digital-service. Keep clients tolerant of future product types. For phone top-ups, supply purchaseInput.phoneNumber when placing the order; see Orders.
Optional members are written as null when they do not apply; they are not omitted. Use these fields when choosing what to buy:
variants: choose an available variant by its opaqueidand quote withvariantId, leavingvalueabsent or null.faceValueis its numeric denomination when known, expressed in the variant'scurrency. Do not parsedisplayValueto calculate a denomination.minimumValue,maximumValue,valueStep: published range metadata. For a range purchase, quote with a positivevalueand novariantId. For a published minimum 5, maximum 100, and step 5, candidate values are 5, 10, 15, and so on. A quote still validates the exact amount against current availability and currency precision. A product can expose both variants and range metadata.faceValueon the product: an optional default fixed denomination in the product'scurrency. For a purchase without a variant, this default may be used whenvalueis omitted. Prefer explicitly supplying the intended value.currency: the product denomination currency, used for range values and face-value discovery. It can differ from the currency debited from your wallet.sellCurrency: the workspace offer's settlement currency. Use it ascurrencyin a quote or order. AUSD-denominated gift card may, for example, havesellCurrency: "EUR"and be charged to your EUR wallet.price: the variant's indicative price insellCurrency. It is computed the way a quote is, from the same current inputs, but it is not a quote: it can benullwhen no price can be shown (that means no indicative price, not a free product), and it can differ from the total a quote returns moments later. A face value is never a substitute for a sell price. Obtain the binding total through Quotes.priceFrom,priceTo: the lowest and highest indicative variantprice, insellCurrency, ornullwhen nothing could be priced. Convenient for listings; never binding.customAmount:nullunless the product can be quoted for an amount of your choosing.minimum,maximumand the optionalstepare in the facecurrency; an amount costs approximatelyindicativeRateper face unit plusindicativeFlat, eachnullwhen no price can be shown. Two components because part of a price does not grow with the amount, so a single multiplier would be right at one amount only. A quote validates the exact amount, its increment and its currency precision, and prices it; do not compute the figure yourself for anything you show as a total.category: the public category key the product is first filed under — one of itspublicCategories— ornullwhen it sits on none.publicCategories: every public category the product sits on, each with thekeyaccepted by thepublicCategoryfilter and a displaytitle.facts: typed facts for a product page, each with akey; the members a fact does not use (countryCode,currency,minimum,maximum,step,count,codes) arenull. Gift cards carryredeem-in,card-currency,denominationsand, when applicable,custom-amountandavailable-in; eSIMs carrycoverage(ISO 3166-1 alpha-2codes),plans(minimum/maximumin MB) andvalidity(minimum/maximumin days); top-ups carrycountry,top-up-currencyandamounts. A product whose options are bundles rather than amounts (a data bundle, a ticket) carriesbundleswith only acountin place ofdenominations/amounts— its variants have nofaceValue, so never print one. Keep clients tolerant of new keys; render only the keys you know.attributes: for eSIMs,{ "kind": "esim", "coverage": [...], "dataMinMb", "dataMaxMb", "unlimited", "validityMinDays", "validityMaxDays" }; each eSIM variant may carryattributesof{ "kind": "esim-plan", "dataMb", "unlimited", "validityDays" }.nullfor other kinds.guide: how the item reaches the buyer, as ordered steps (step,title,body). Present on a single-product read only;nullon lists.brandCopy: Xegora's own, reviewed copy about the brand —title, a one-linesummary,about(paragraphs separated by a blank line),howToRedeemandgoodToKnowas ordered sentences, andfaq. Plain text, ready to show your buyers. Present on a single-product read when copy has been published;nullotherwise.siblings: the same brand in other countries or editions (id,name,countryCode,currency, and an indicativepriceFromin the sibling's ownsellCurrency, which need not be the viewed product's) — read one byidfor its denominations. Present on a single-product read only;nullon lists.cashbackPercentage: what your workspace earns back on a sale of this product, as a percentage of what you pay — your own share, already worked out.nullwhen the product carries no cashback, which is the usual case. After an order for it is fulfilled, the share is credited to your wallet (acashback_creditline on the statement), and taken back (cashback_reversal) if the order is refunded later.isAvailable: advisory availability. Listings and filter counts exclude unavailable offers. A previously known product can still resolve withisAvailable: false; an inaccessible, removed, or no-longer-resolvable product returns404withcode: "not_found". Quoting and ordering recheck eligibility.
Treat product ids, variant ids, and image references as opaque. Use only returned public fields and URLs; no internal identifiers, costs, routing, or storage credentials are part of this contract.
The language a product reads in
Ask with either header; X-Language takes one code and wins when both are sent, and Accept-Language is read as the
standard allows, so a browser's fa-IR,fa;q=0.9,en;q=0.8 resolves to Persian:
Accept-Language: fa
X-Language: fa
Three parts of a product carry translated prose, each answered independently:
| Field | Answered in your language |
|---|---|
description, terms | Yes — together, as one block, with one exception: terms that cannot be translated (a dash or a web address, terms the brand wrote in its own language, or words public copy never carries) stay as the brand wrote them beside the translated description. |
brandCopy (title, summary, about, howToRedeem, goodToKnow, faq) | Yes — the whole block together. |
guide steps | Yes — at once. The steps are Xegora's own and are written ahead of time in every language Xegora speaks. A step written for one particular product reads as it was written. |
facts | No words to translate — each fact is a key plus typed values (a country code, a currency, numbers), so you word it yourself and it reads correctly in any language. |
name, variantLabel, siblings[].name | No. Product and option names are the brand's own and are never rewritten. |
category title, publicCategories[].title | No. Always English; the key beside it is the stable slug to word yourself. |
description/terms and brandCopy are answered in the asked-for language only when Xegora has a published
translation of that block for those exact words; otherwise that block comes back in English (apart from terms kept
as written, above). It is all-or-nothing within a block, so a brandCopy never
mixes two languages.
Asking for a language Xegora has not translated yet returns English, and asks for the translation. The first
request for a product in a new language is served immediately in English and records that somebody wanted it; once the
platform has translated and published it, later requests answer in that language. So do not treat an English answer as
permanent — ask again later. Languages Xegora does not offer at all always answer in English.
Only prose changes. Ids, product and variant identifiers, currency codes, countryCode, statuses, publicCategory
keys, every amount and every other part of the contract are identical whatever language you ask for. A client cannot be
broken by asking in another language, so a translated catalogue is safe to cache per language.
The same two headers also decide the language of a refusal — its title, detail and message — on every
endpoint of this API, not only the catalog ones. See Errors.
Product images
Image URLs are relative, same-origin paths. Resolve a returned URL against your API origin and request it with the same X-Api-Key header. Do not hotlink authenticated API images from end-user browsers; serve them through your own backend so the API key stays private.
For gallery references, the endpoint is:
GET /api/v1/catalog-product-images/{publicReference}A reference is img_ followed by 32 lowercase hex characters; anything else is 404. Successful image responses include ETag and Cache-Control: private, max-age=300. Send the returned ETag in If-None-Match to revalidate; an unchanged image returns 304 Not Modified without a body. Access is checked again before serving or revalidating the image. Keep caches workspace-scoped even when public image references coincide. When image storage is briefly unavailable the answer is 503 media_unavailable with Retry-After.
Image requests are counted against a budget of their own — 300 per minute per workspace — so drawing product pictures never spends the 120 requests per minute your orders need.
Keeping a local catalog fresh
For a local mirror, walk GET /products?sort=id-asc&limit=100 with unchanged filters, upsert by product id, and deduplicate across pages. Perform periodic full refreshes at a cadence appropriate to your application. There is no incremental-change cursor, snapshot token, or catalog-change webhook in this contract.
If you sell a curated selection rather than the whole catalog, refresh it with GET /products?ids=… — up to 100 products per call — for current availability and indicative prices, and fall back to the full walk only to discover new products.
Treat missing products as candidates for unavailability; do not infer a definitive deletion from one live paginated walk. Recheck a selected product and request a fresh quote at checkout. Preserve historical product ids for existing orders, and refresh public filter choices separately. Only the quote's returned total and expiry determine the binding purchase price.
Updated 13 days ago