# Products & catalog

Create and sync a merchant product catalog for checkout, invoicing, and accounting — with barcodes,
HS codes, per-store inventory, and ERP-ready exports.

Products is gated twice: the platform must have **`growth.products`** live, and the merchant must
opt in under **Settings → Products**. Until both are true, catalog endpoints return `403`.

> **Authentication**
> All endpoints on this page use your developer API keys — `bx_test_…` or `bx_live_…` — as a
> Bearer token. The key pins both the merchant and the mode. See
> [Sandbox & test mode](https://developers.baxpay.com/sandbox).

## Enable products

From the Business dashboard: **Settings → Products → Enable**. Multi-store inventory is optional
(**Settings → Multi-store**). Merchants can also toggle via the dashboard JWT API
`PUT /business/products/feature`.

## Create a product

- `POST /products` — Idempotency-Key required

```bash
curl -X POST https://sandbox-api.baxpay.com/v1/products \
  -H "Authorization: Bearer $BAXPAY_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "House blend — 250 g",
    "sku": "HB-250",
    "barcode": "3760123456789",
    "hsCode": "090121",
    "unitAmount": 12.5,
    "currency": "EUR",
    "taxRatePercent": 5.5,
    "unitOfMeasure": "EA",
    "channel": "BOTH",
    "trackInventory": true,
    "inventory": { "storeIds": "all", "quantityOnHand": 40 }
  }'
```

```js
// No stock tracking: only a name and a price are needed.
const res = await fetch('https://sandbox-api.baxpay.com/v1/products', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.BAXPAY_API_KEY}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ name: 'Croissant', unitAmount: 1.4, taxRatePercent: 5.5, channel: 'IN_STORE' }),
});
const body = await res.json();
if (!res.ok) throw new Error(JSON.stringify(body));
console.log(JSON.stringify(body, null, 2));
```

```json
{
  "success": true,
  "data": {
    "id": "0d6fcf24-…",
    "name": "House blend — 250 g",
    "sku": "HB-250",
    "unitAmount": "12.5",
    "currency": "EUR",
    "channel": "BOTH",
    "active": true,
    "trackInventory": true,
    "imageUrl": null,
    "livemode": false
  }
}
```

`unitAmount` is the price **including VAT**, in decimal euros, and comes back as a decimal string.
`channel` sets where it's sold: `ONLINE`, `IN_STORE` or `BOTH`.

**Stock.** With `trackInventory: true`, seed the opening stock with `inventory`: `quantityOnHand`, and
`storeIds` as `"all"` or a list of location ids. Without it the product counts as always in stock.

**Images.** A product image is optional through the API. Square images are uploaded and attached in
the Business dashboard, which then requires one.

## List and update

- `GET /products` — ?search=blend&channel=IN_STORE&page=1&limit=50
- `GET /products/{id}`
- `PATCH /products/{id}` — Idempotency-Key required

```bash
curl "https://sandbox-api.baxpay.com/v1/products?search=blend&channel=BOTH" \
  -H "Authorization: Bearer $BAXPAY_API_KEY"
```

```json
{
  "success": true,
  "data": { "data": [{ "id": "0d6fcf24-…", "name": "House blend — 250 g", "active": true }], "total": 1, "page": 1, "limit": 50 }
}
```

The list only returns active products. Archived ones are hidden.

```bash
curl -X PATCH https://sandbox-api.baxpay.com/v1/products/0d6fcf24-… \
  -H "Authorization: Bearer $BAXPAY_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "unitAmount": 13 }'
```

```json
{ "success": true, "data": { "id": "0d6fcf24-…", "unitAmount": "13" } }
```

## Selling catalog lines

Put catalog lines on a [payment link](https://developers.baxpay.com/payment-links) so stock and reporting follow the sale. The
link's `amount` is still what the customer pays:

```bash
curl -X POST https://sandbox-api.baxpay.com/v1/payment-links \
  -H "Authorization: Bearer $BAXPAY_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "2 × House blend",
    "amount": 26,
    "businessWide": true,
    "items": [{ "productId": "0d6fcf24-…", "quantity": 2 }]
  }'
```

## Shelf labels

- `GET /products/{id}/label`

```bash
curl https://sandbox-api.baxpay.com/v1/products/0d6fcf24-…/label \
  -H "Authorization: Bearer $BAXPAY_API_KEY"
```

```json
{
  "success": true,
  "data": {
    "format": "baxpay.label.v1",
    "productId": "0d6fcf24-…",
    "name": "House blend — 250 g",
    "barcodeValue": "3760123456789",
    "unitAmount": 13,
    "currency": "EUR",
    "suggestedSymbology": "EAN"
  }
}
```

A print-ready payload (symbology hint, price, SKU) for shelf-edge printers.

## Exports

- `GET /products/export/catalog`
- `GET /products/export/intrastat` — ?from=2026-01-01&to=2026-03-31
- `POST /products/export/push-erp` — Idempotency-Key required

```bash
curl https://sandbox-api.baxpay.com/v1/products/export/catalog \
  -H "Authorization: Bearer $BAXPAY_API_KEY"
```

```json
{
  "success": true,
  "data": {
    "format": "baxpay.catalog.v1",
    "products": [{ "id": "…", "name": "…", "unitAmount": 1.4, "currency": "EUR", "active": true, "inventories": [] }]
  }
}
```

- **Catalog**: stable `baxpay.catalog.v1` JSON for accounting connectors.
- **Intrastat**: HS code + unit-of-measure rows, with sold quantities for the period.
- **Push to ERP**: sends the catalog to every active [accounting link](https://developers.baxpay.com/accounting), signed.
  See [ERP catalog sync](#erp-catalog-sync) below.

## Bulk import

Bulk onboarding uses the dashboard JWT endpoint `POST /business/products/import` (≤500 rows).
Map CSV columns in the dashboard **Products → Import** tab.

## ERP catalog sync

When you push the catalog (dashboard **Products → Export → Push to ERP**, or
`POST /products/export/push-erp` with your API key), BaxPay delivers a snapshot to every **ACTIVE**
accounting link with a **catalog callback URL** (configure under **Developers → Accounting**).

### Request (BaxPay → your endpoint)

| Header | Value |
|--------|-------|
| `Content-Type` | `application/json` |
| `X-BaxPay-Event` | `catalog.sync` |
| `X-BaxPay-Livemode` | `true` or `false` |
| `X-BaxPay-Format` | `baxpay.catalog.v1`, `pennylane.products.v1`, `quickbooks.items.v1`, or `sage.products.v1` |
| `X-BaxPay-Vendor` | `generic`, `pennylane`, `quickbooks`, or `sage` |
| `X-BaxPay-Signature-V2` | `t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">` keyed with the connection `callbackSecret`: **verify this one** |
| `X-BaxPay-Timestamp` | The same `t` |
| `X-BaxPay-Event-Id` | Unique per push; use it to deduplicate |
| `X-BaxPay-Signature` | Legacy: HMAC-SHA256 hex of the raw body only (no replay protection) |

Verify `X-BaxPay-Signature-V2` exactly like a [webhook](https://developers.baxpay.com/webhooks#verifying-signatures), with the
connection's `callbackSecret`.

### Default payload (`baxpay.catalog.v1`)

```json
{
  "format": "baxpay.catalog.v1",
  "generatedAt": "2026-07-20T09:00:00.000Z",
  "livemode": true,
  "merchant": { "id": "…", "name": "Acme SA", "vatNumber": "FR12345678901" },
  "products": [
    {
      "id": "0d6fcf24-…",
      "name": "House blend — 250 g",
      "sku": "HB-250",
      "barcode": "3760123456789",
      "hsCode": "090121",
      "unitOfMeasure": "EA",
      "unitAmount": 12.5,
      "currency": "EUR",
      "taxRatePercent": 5.5,
      "channel": "BOTH",
      "active": true,
      "trackInventory": true,
      "inventories": [
        {
          "storeId": "5a8c0e3b-…",
          "storeName": "Main boutique",
          "storeCode": "MAIN",
          "quantityOnHand": 42,
          "quantityReserved": 0,
          "reorderPoint": 10
        }
      ],
      "updatedAt": "2026-07-19T14:22:00.000Z"
    }
  ]
}
```

Vendor-specific shapes (Pennylane, QuickBooks, Sage) are selected when provisioning the accounting
link. For **direct OAuth sync** (no middleware), connect from the Business dashboard under
**Developers → Accounting** — see [Accounting & ERP sync](https://developers.baxpay.com/accounting).

> **Respond quickly**
> BaxPay waits up to **10 seconds** for your callback. Return `2xx` to acknowledge receipt; process
> asynchronously if import takes longer.

## Related

- [Accounting & ERP sync](https://developers.baxpay.com/accounting) — OAuth connect, sync, import, webhook setup
- [E-invoicing](https://developers.baxpay.com/einvoicing) — invoice lines can reference `productId` for master data
- [Connections & credentials](https://developers.baxpay.com/connections) — POS tills and pumps (`bxc_…`)
- [Payment links](https://developers.baxpay.com/payment-links): `items` ties a charge to catalog lines
