Market catalog --- external read API

Última actualización: May 6, 2026Sección: General

Market catalog --- external read API

Full API reference: https://api.mailoo.app/docs/v1

Use a Market integration to manage a minimal product catalog in the Mailoo dashboard and expose read-only JSON to your storefront or BFF. Every external route requires ``X-API-Key``; there is no anonymous access.

For dashboard CSV bulk editing (export -> edit -> upload -> preview/apply), see market-catalog-csv{.interpreted-text role="doc"}.

Overview

  • Auth: Header ``X-API-Key`` on every request.
  • Scope: RESTRICTED keys need ``market.external-read`` in allowedOperations (FULL keys also work).
  • Routing: GET {baseUrl}/api/v1/market/{projectUid}/integrations/{integrationId}/… --- same path shape as dashboard resources under /projects/{uid}/integrations/{id}/market/…, without the projects prefix.
  • Ownership: The API key must belong to the project owner; projectUid and integrationId must refer to an ACTIVE integration of type MARKET.
  • Visibility: Products have status DRAFT/PUBLISHED/ARCHIVED. External API list/detail/variants return only products with status PUBLISHED. Dashboard APIs can still access all statuses for owners.
  • Product fields (dashboard): canonical product and variant fields are stored in attributes; optional per-language overlays live in locales.<code> (name and attributes on each entry). name is a Base type attribute on product records; sku is a required Base type attribute marked as variant axis (stored on variants, used by effective-price lookup).
  • Product JSON (external list/detail/variants only): responses do not include a locales object. attributes on the product and on each variant are already merged for a single language: base values from the dashboard record plus the overlay for the resolved locale (per-locale name maps onto the name attribute key). Resolved locale = optional query locale=<code> if present and valid (e.g. en, de, pt-br); otherwise ``defaultCatalogLocale`` on the MARKET integration's config (set in the dashboard Edit integration screen); if unset, ``en``. Invalid locale query returns 400.
  • Product fields (external): after merge, the same rules as dashboard apply to the flattened attributes (including name / sku semantics above).
  • ID format: MARKET resource IDs in request params and response payloads accept and return both cuid and cuid2 values (for example tag/product/price-list/supplier IDs).
  • Price kinds: See Price kinds and effective price below. For which list price to use, integrators should configure and send the price kind key (query ``kind`` on ``GET .../effective-price``). The price kind id (CUID) is internal (and appears in some JSON); it is not the main external identifier for that choice.
  • Integrator root flag: Tag objects include useAsRoot. Integrators can treat tags with useAsRoot = true as top-level catalog roots (multiple tags allowed), even when parentTagId is not null.

Tag icons: Tag objects may include iconMediaId. To fetch image bytes, use the Images integration with a key that allows ``image.external-read`` (see images{.interpreted-text role="doc"}).

Mailoo dashboard rendering note: useAsRoot is intended for external integrator menu logic. Mailoo's own dashboard tag tree rendering and ordering remain unchanged and still follow the existing hierarchy rules.

BFF environment variables

Align with the Connection panel on the integration page (same names as below). Typical server-side variables:

  • MAILOO_MARKET_API --- API base URL (e.g. https://api.mailoo.app or your dev host), no trailing slash.
  • MAILOO_MARKET_API_KEY --- API key with ``market.external-read`` (or FULL).
  • MAILOO_MARKET_PROJECT_UID --- Project UID.
  • MAILOO_MARKET_INTEGRATION_ID --- Market integration id (CUID).

Endpoints (all GET, all require X-API-Key)

Replace {baseUrl}, {projectUid}, {integrationId}, and path parameters as needed.

  • GET {baseUrl}/api/v1/market/{projectUid}/integrations/{integrationId}/types --- Product types and attributes.
  • GET …/products --- All published products (variants and tag links included; merged locale, no locales). Optional query ``locale`` (see Overview).
  • GET …/products/{productId} --- One product (same contract).
  • GET …/products/{productId}/variants --- Variants only; each row has merged attributes and no locales. Optional ``locale``.
  • GET …/products/{productId}/price-range --- For one published product: effective unit price per variant (minAmount / maxAmount are equal for a single price kind) plus optional product-wide row and an aggregate min/max across those numbers. Query: ``kind`` or ``priceKindId`` (same rules as ``effective-price``), optional ``measurementUnitId``, ``at`` (ISO datetime), optional ``currency`` (ISO 4217, three letters). Currency resolution: if ``currency`` is omitted, the response currency is taken from the first matching price line when walking active lists in chronological order (``effectiveAt`` ascending) and lines in ``id`` order; only lines in that currency are used for merges (other currencies are ignored). If ``currency`` is set, only lines in that currency participate; the response ``currency`` field echoes the query even when there are no matching lines (then amounts are null).
  • GET …/tags and GET …/tags/{tagId} --- Catalog tags.
  • GET …/price-lists and GET …/price-lists/{priceListId} --- Internal price lists.
  • GET …/effective-price --- Resolve unit price for a product and a price kind. Use query ``kind=<priceKindKey>`` (see Price kinds and effective price). Optional: productId / variantId / measurementUnitId / at (ISO datetime). (Query ``priceKindId`` is accepted for internal or BFF callers; external catalog code should use ``kind``.)
  • GET …/suppliers --- Suppliers.
  • GET …/suppliers/{supplierId}/products --- Supplier products.
  • GET …/suppliers/{supplierId}/price-lists and GET …/suppliers/{supplierId}/price-lists/{supplierPriceListId} --- Supplier price lists.
  • GET …/units --- Measurement units.
  • GET …/packagings --- Packagings.
  • GET …/packaging-equivalences --- Packaging equivalences.

Responses use { success: true, data: … }. For external product list/detail/variants, data matches the merged-locale shape above (no locales). Authenticated dashboard GET /api/v1/projects/…/market/products… still returns the full editor shape including locales.

Price kinds and effective price

  • In the database, a price kind has a stable string ``key`` (unique per MARKET integration, e.g. base) and an internal ``id`` (CUID). The key is what you should configure in env / CMS ("use base for list prices").
  • Resolving a price from external or storefront code: call ``GET {baseUrl}/api/v1/market/{projectUid}/integrations/{integrationId}/effective-price`` with ``kind=<key>`` and ``productId=`` (and optional ``variantId``, ``measurementUnitId``, ``at``). This is the supported contract: ``kind`` is the price kind key, not the CUID. All active price lists with effectiveAt <= at are merged in chronological order (oldest first, then newer lists overlay): for a given ``variantId``, the newest variant-specific line for that SKU wins; if no list after that SKU line redefines that SKU, older SKU lines remain in effect. If there is no SKU line at all, the newest product-wide line (``variantId`` null) applies. When ``variantId`` is omitted, only product-wide rows are considered (newest wins).
  • ``priceKindId`` in the query string is only for callers that already store CUIDs (Mailoo tools, BFFs mirroring internal ids). Do not treat the query parameter priceKindId as the main external API for "which list price to use" --- use ``kind`` instead.
  • Responses may include ``priceKindId`` and ``priceKindKey`` (and similar on price list lines). You may read ``priceKindKey``; ``priceKindId`` in responses is for correlation and is not something public frontend JS needs to hard-code.
  • Price list lifecycle (read paths): GET …/price-lists and list payloads include ``archivedAt`` and ``endsAt`` when set. ``GET .../effective-price``, ``GET .../products/{productId}/price-range``, and order checkout pricing only consider lists that are not archived and, if ``endsAt`` is set, only when the query ``at`` instant is strictly before ``endsAt``. Dashboard callers may PATCH / DELETE internal price lists under ``/api/v1/projects/.../market/price-lists/{priceListId}`` (Bearer); delete is rejected with 409 when any line's dimensions appear on an existing order line for that integration.

Product attribute value types (/types)

GET …/types returns type attributes with:

  • valueType: STRING, NUMBER, BOOLEAN, ENUM, SELECT, MD_TEXT
  • options: string[] | null

SELECT is the strict list-based type. For SELECT, options is a non-empty list of allowed values, and product/variant attribute values must match one of them exactly.

``MD_TEXT`` and images: Any product or variant attribute with valueType: MD_TEXT may embed Mailoo-hosted images using markdown image syntax (for example ![alt](url)). On write, the dashboard normalizes Mailoo image URLs inside those fields; product and variant JSON from GET responses also normalizes nested strings so integrators see the canonical public embed shape (…/api/v1/images/public/content?id=<mediaId>). To fetch image bytes from that URL, use the Images API with a key that allows ``image.external-read`` (see images{.interpreted-text role="doc"}).

``MD_TEXT`` and HTML (external catalog GET only): On external routes GET …/products, GET …/products/{productId}, and GET …/products/{productId}/variants (with X-API-Key and a published product), the API adds a parallel string field <attributeKey>Html next to each Markdown value for attributes declared as MD_TEXT on any of the product's linked types, on the merged product attributes and each variant's merged attributes (there is no separate locales tree in the response). Use the original key for the Markdown source (like blog content) and …Html for server-rendered HTML (like blog htmlContent). Dashboard GET /projects/…/market/… responses omit these *Html fields to avoid extra work when editing.

Product gallery and listing image: Each product may have an ordered ``images`` array (objects with id, mediaId, sortOrder, and canonical ``url``) plus ``previewMediaId`` and ``previewImageUrl``. The preview is the image recommended for catalog cards / product lists; if ``previewMediaId`` is null, integrators may fall back to the first gallery image. Gallery media ids are validated on create/update (READY media, scoped to the owner's USER / PROJECT / INTEGRATION context for that market). Optional on-the-fly sizing: append ``w``, ``h``, ``fit``, ``format``, ``q`` to the public content URL as documented in images{.interpreted-text role="doc"}.

Example attribute definition:

{
  "key": "display_type",
  "name": "Display Type",
  "valueType": "SELECT",
  "options": ["LCD", "OLED", "micro-OLED", "QD-LCD"]
}

Breaking-change note: integrations that previously treated ENUM as a free-form string should switch to SELECT when a strict options list is required.

Dashboard: create products from JSON

In the MARKET dashboard ([Products]{.title-ref} section), you can prepare product payloads as JSON:

  • Copy template --- copies current product draft + available schema hints:
  • schemaVersion
  • product (typeIds, attributes, tagIds)
  • template.attributeSchema (keys, value types, required flags, SELECT options)
  • template.availableTags (all currently available catalog tags)
  • Paste product data --- switches the form to a JSON input mode where you paste payload manually (no browser clipboard read prompt is required).
  • Create Product (in JSON mode) validates the payload on server and then creates the product if validation succeeds.

For multi-row updates, prefer CSV upload/download: market-catalog-csv{.interpreted-text role="doc"}.

Expected JSON payload

{
  "schemaVersion": 1,
  "product": {
    "typeIds": ["<typeId-1>", "<typeId-2>"],
    "attributes": {
      "name": "Vision Pro X Ultra",
      "display-type": "micro-OLED",
      "short_description": "Flagship model.",
      "full_description": "## Vision Pro X Ultra"
    },
    "tagIds": ["<tagId-1>"]
  }
}

Validation behavior

Server-side validation is the single source of truth (no duplicated domain rules on client):

  • Dashboard calls POST /api/v1/projects/{uid}/integrations/{id}/market/products/validate before create.
  • The endpoint reuses the same validation rules as product creation:
  • unknown attribute keys are rejected
  • required attributes must be present
  • value types must match (NUMBER/BOOLEAN/STRING/MD_TEXT/ENUM/SELECT)
  • SELECT values must be one of declared options
  • tag ids must belong to the integration
  • If validation fails, product is not created and the API error message is shown.

Email checkout (orders)

The catalog read API above does not include a shopping cart: the integrator storefront keeps cart state (for example localStorage) and submits a snapshot when the buyer checks out.

  • Create order (server / BFF): POST {baseUrl}/api/v1/market/{projectUid}/integrations/{integrationId}/orders with header ``X-API-Key`` and scope ``market.order.submit`` (RESTRICTED keys) or a FULL key. Request body: buyer ``customerEmail``, ``items[]`` (each line: ``productId``, ``quantity`` as a decimal string, and either ``priceKindKey`` (integrator-friendly key, e.g. base) or ``priceKindId`` (CUID) --- not both, optional ``variantId`` / ``measurementUnitId``). Mailoo resolves unit prices from the same price lists as ``GET .../effective-price``; the response includes ``accessToken`` and ``accessExpiresAt`` (temporary read-only access to the order).
  • Read order (browser / any client): GET {baseUrl}/api/v1/market/public/orders/{orderId}?token= or header ``X-Market-Order-Token`` --- no API key. Returns a snapshot of the order while the token is valid (default TTL 72 hours, overridable via ``MARKET_ORDER_ACCESS_TOKEN_TTL_HOURS`` on the API host). Missing/invalid/expired tokens return 404 (same message as wrong id).
  • Owner dashboard (JWT): list/get/patch orders under /api/v1/projects/{uid}/integrations/{id}/market/orders/… (Bearer), as in ../../../api/endpoints{.interpreted-text role="doc"}.

End-to-end flow, BFF patterns, and operational notes: market-order-email-checkout{.interpreted-text role="doc"}.

Further reading

  • API keys and scopes: ../../../api/authentication{.interpreted-text role="doc"}
  • Dashboard CSV workflow: market-catalog-csv{.interpreted-text role="doc"}