Skip to main content
Catalog Ingestion lets you bring products into Galactic Core from an outside source. When a merchant’s own system — an ERP, a PIM, another storefront, or a spreadsheet export — is the source of truth for the catalog, you push that data to Tybrite instead of entering products by hand. Send a batch as JSON, XML, or CSV; Tybrite matches each row to your existing catalog by SKU, updates what already exists, and creates what doesn’t.
Dashboard: You can register a feed URL for scheduled sync from GC Admin, or push products directly via the API and SDK. Find your signing secret under Integrations → API keys.
Secret key + request signature required. POST /v1/ingest/products is the one write endpoint where both a secret key (tybrite_sk_*) and a signed request are mandatory. A publishable key returns 403; a missing or invalid signature returns 401.

When it applies

Ingestion is the path for a catalog that lives somewhere else and is mirrored into Galactic Core:
  • A merchant runs their inventory in an ERP/PIM and wants it reflected in their Tybrite store.
  • You’re migrating a catalog from another platform.
  • A nightly export from a back-office system should keep the storefront in sync.
Ingestion is a merchant-side write for keeping a catalog up to date, which is why it sits outside the otherwise storefront-facing API and carries stricter authentication than any other write.

Batch size

A request carries at most 12 products. A larger batch is rejected with 400 and nothing is written, rather than applying part of the batch and leaving the rest to fail item by item. For a large catalog, send a sequence of batches, or configure a scheduled sync, which fetches and imports a whole catalog on a timer and has no such limit.

Selling on a marketplace

Nothing changes. A merchant whose store is part of a marketplace pushes their catalog exactly as any other merchant does, with their own store’s secret key, and their products appear on the marketplace storefront alongside everyone else’s once the store is approved. The marketplace listing is a read over the same catalog — approved stores, aggregated — so there is no separate marketplace feed, no marketplace ingestion endpoint, and nothing extra to configure. An operator key is refused with 403 — ingestion is store-scoped, because a catalog belongs to the merchant who owns it rather than to the marketplace that lists it.

Quick start

Ingestion is a write, so initialize the SDK with a secret key and sign each request. Below, one standalone product and one multi-variant product (its rows share a product_group):
The SDK signs the request for you when it’s configured with your secret key. If you call the endpoint directly (curl, another language, a serverless handler), you must add the signing headers yourself — see Signing a request.

How matching by SKU works

Every row carries a sku — your unique stock-keeping unit. That SKU is the identity Galactic Core matches on, scoped to your store:
  • SKU already exists → the product, its variant, and its stock are updated in place.
  • SKU is new → a product (and variant) is created.
To import only brand-new products and leave existing ones untouched, pass strategy: 'create_only'. Matching SKUs are then skipped (counted in rows_skipped) rather than updated:
The default is upsert_by_sku (update existing, create new).

Variants

Rows that share the same non-empty product_group become variants of a single product — each row still needs its own unique sku. A row with no product_group is a standalone, single-variant product. The first row in a group supplies the product-level fields (name, description, category); each row contributes its own variant_name, price, and stock.

Formats

Declare the format with the Content-Type header or the ?format= query parameter. All three normalize to the same result. The SDK sends JSON; the XML and CSV shapes below are for when you push the raw body yourself.
CSV follows standard quoting rules, so commas and newlines inside quoted fields are handled. Field names are matched leniently (e.g. Product Name maps to name).

Product fields

Field names are matched leniently on case, spaces, underscores and hyphens, so Product Name, product_name and productname all reach name. Several fields accept alternative spellings a source system is likely to emit: title for name, qty or quantity for stock, cost for cost_price, handle or parent for product_group, and image_url or image_link for image.

Cost price and margin reporting

cost_price is what margin is calculated from. A product imported without one books no cost against its sales, so its margin reads as the full selling price and every report that rolls margin up reads as complete while resting on that gap. Where the source system holds a unit cost, carrying it in the feed is worth the column; where it does not, the merchant can fill it in afterwards and the figures correct themselves from that point.

Catalog identity and channel fields

A row also carries the identifiers a sales channel matches a listing against and the attributes it lists alongside them. These are read on both the push API and a scheduled sync.
gtin and barcode are separate fields. barcode is whatever the merchant scans in-store, which may be an internal label with no meaning outside the store; gtin is the manufacturer’s number a sales channel matches the listing against. A feed carrying only one of them populates both, since in practice that single value serves both purposes.

Your own columns

A column that is not one of the fields above is matched against the custom fields defined on products for your store, by the field’s label — the name shown in the dashboard, which is what you would have called the column.
Here Fabric and Country of origin land on the product under fields with those labels. A column matching no defined field is ignored, so extra columns in a source export are safe to leave in. On a scheduled sync, a column matching no field can define one instead of being ignored. The definition takes its label from the column header, so warranty_months reads back as “Warranty Months”, and its type from the value carried — a date, a number, a true/false, or text where none of those fit. A source that keeps a column the store does not model yet therefore carries it across without the merchant having to anticipate it. An auto-defined field is merchant-facing until published. It is visible in the dashboard and in the store’s own exports, and it reaches no storefront and appears in no API response until the merchant publishes it — a feed may model a field, but what a shopper sees stays the merchant’s decision. A single run defines at most 25 fields, so a malformed file, a shifted header row or a wide export cannot fill the field list, and each one records that a feed created it.
Defining a field from a column is a per-source setting on scheduled sync, on by default. With it off, and on the push API, a column is recorded only where a field with that label already exists. Fields are defined in the dashboard under Settings → Custom fields.

Response shape

Rows are validated and applied independently — one bad row never fails the whole batch. The response always carries a summary of counts and an errors array describing every rejected row:
status is success when nothing failed, or partial when some rows failed but others imported. If every row fails validation, the request returns 422 and nothing is written.

Summary fields

Per-row errors

Each entry in errors pinpoints exactly what to fix:

Signing a request

The SDK signs for you when initialized with a secret key. If you push the raw body yourself, add three headers. The signature is a base64 HMAC-SHA256 of `${timestamp}.${rawBody}` using your store’s signing secret (the same scheme as orders and payments) — sign the exact bytes you send.
The signature covers the raw body exactly as sent. Re-serializing the JSON (whitespace or key ordering changes) produces a different byte sequence, and verification fails.

Test without a key

Two public, no-key endpoints validate a feed format ahead of any integration work. Both are rate-limited to 60 requests/hour per IP. A valid sample feed to model your own on (?format=json|xml|csv):
The sample includes a single-variant product and a multi-variant product, so you can see exactly how product_group expresses variants. A validation run that writes nothing. It returns the same summary and errors a real import would, plus a normalized_preview showing how Galactic Core interpreted each row:
From the SDK:

Scheduled sync

Instead of pushing, you can have Galactic Core pull a feed for you. Register a feed URL in your dashboard, choose a cadence, and Galactic Core fetches and syncs the catalog on that schedule — matching by SKU exactly like the push API.

Which side owns a field

A catalog that syncs on a timer is edited from two directions: the feed carries what moves in the source system, and the merchant curates the listing in their dashboard. Each feed source records who owns each field, which decides what a re-sync does with it. Ownership belongs to the registered source, so it applies to scheduled sync alone. A push carries no source, and the values in the batch are applied as sent. A feed-owned field is rewritten from the feed on every run. A merchant-owned field is written only while its value still matches what the feed last wrote for it; once the merchant changes it, the feed stops touching that field on that product and leaves the edit standing. Other fields on the same product, and the same field on every other product, are unaffected. The defaults put each field with the side that is usually authoritative for it: Price, cost, stock and the identifiers move in the source system, so the feed keeps them and a scheduled sync goes on correcting them. Names, descriptions, taxonomy, imagery and channel metadata are where a merchant’s own judgement lives, so an edit there survives the next run rather than being reverted by it. The assignment is per source and per field: a merchant who wants the feed to keep restoring product names, or to stop overwriting price, moves that field to the other side in their dashboard.
The first run after a source starts recording ownership has no earlier feed value to compare against, so it writes every field it carries and records them as its baseline. Merchant ownership takes effect from the run after that, which is the first one able to tell an edit from a value nobody has touched.
After each scheduled run, a feed.sync.completed webhook fires with the run’s summary, so your systems can react to the result without polling. Subscribe to it the same way as any other event:
See the Webhooks guide for delivery, signing, and idempotency.

Publish your own catalog (outbound feed)

Ingestion works both ways: a store can also expose its own catalog as a public feed for other systems to pull from — another store’s scheduled sync, a partner, a marketplace, or your own tooling. It is enabled in the dashboard under Integrations → Catalog sync → Publish my catalog, which produces a public URL in two formats, readable with no API key:
{store} is your store id or short store code.
On a store belonging to an operated platform, the host is that platform’s own. The dashboard shows the URL to copy, and where the store belongs to an agency it reads https://api.theirplatform.com/v1/feeds/… rather than the address above — a feed URL is pasted into Google Merchant Center or a partner’s system, where it outlives the screen it came from. The path, the formats and the token all behave identically. See Running your own platform.
The feed contains only storefront-safe fields (never cost or margins) and includes each product’s rich media gallery and its published specifications:
The feed shape matches what the ingestion endpoints accept, so a store-to-store sync needs no field mapping: one store’s scheduled sync can point straight at another store’s products.json. A token added in the dashboard and appended as ?token=… keeps a feed semi-private.

Response codes


SDK reference

See the IngestionService SDK reference for full method signatures, or the API Reference for the raw endpoints.