Payments
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.
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
/productsIdempotency-Key requiredcurl -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 }
}'// 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));{
"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
/products?search=blend&channel=IN_STORE&page=1&limit=50/products/{id}/products/{id}Idempotency-Key requiredcurl "https://sandbox-api.baxpay.com/v1/products?search=blend&channel=BOTH" \
-H "Authorization: Bearer $BAXPAY_API_KEY"{
"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.
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 }'{ "success": true, "data": { "id": "0d6fcf24-…", "unitAmount": "13" } }Selling catalog lines
Put catalog lines on a payment link so stock and reporting follow the sale. The
link's amount is still what the customer pays:
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
/products/{id}/labelcurl https://sandbox-api.baxpay.com/v1/products/0d6fcf24-…/label \
-H "Authorization: Bearer $BAXPAY_API_KEY"{
"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
/products/export/catalog/products/export/intrastat?from=2026-01-01&to=2026-03-31/products/export/push-erpIdempotency-Key requiredcurl https://sandbox-api.baxpay.com/v1/products/export/catalog \
-H "Authorization: Bearer $BAXPAY_API_KEY"{
"success": true,
"data": {
"format": "baxpay.catalog.v1",
"products": [{ "id": "…", "name": "…", "unitAmount": 1.4, "currency": "EUR", "active": true, "inventories": [] }]
}
}- Catalog: stable
baxpay.catalog.v1JSON 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, signed. See 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, with the
connection's callbackSecret.
Default payload (baxpay.catalog.v1)
{
"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.
Related
- Accounting & ERP sync — OAuth connect, sync, import, webhook setup
- E-invoicing — invoice lines can reference
productIdfor master data - Connections & credentials — POS tills and pumps (
bxc_…) - Payment links:
itemsties a charge to catalog lines