> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brandgate.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Send products, prices and customers

> Keep BrandGate in step with your ERP: products, price lists, customers and delivery status.

Use these endpoints when your ERP is the master for products, prices and customers. They all work the same way, so you learn the pattern once.

> [!NOTE]
>
> New to the API? Read [Sync an ERP](/guides/erp-sync) first for the order loop. This page is about sending data **to** BrandGate.

## How it works

* **You choose the id.** Every object carries your own `external_id` (the id in your ERP). BrandGate remembers it per object type, so sending the same `external_id` again updates the same record. Two integrations on one workspace never collide: ids are kept per API key label (`integration`).
* **Send state, not events.** Repeating a request changes nothing (`"result": "unchanged"`). You can resend a whole batch after a crash.
* **Fields you leave out are not touched.** Send `null` to clear a field that can be cleared.
* **Each item stands alone.** A batch returns one result per item (`created`, `updated`, `unchanged` or `error`). A bad item never blocks the others, and business errors come back with HTTP `200` and an `error.code` on the item.
* **Money** is a decimal string, excl. VAT, at most two decimals, never negative (`"129.50"`).
* **Optional** `source_updated_at` (ISO time of the change in your ERP): BrandGate refuses an older change with `stale_write`, so late retries cannot overwrite newer data.

You need the scopes `products:write`, `pricing:write`, `customers:write` and `orders:write`. A key created from your ERP's tile under **Settings → Integrations** has them (plus `inventory`, `invoices:read` and webhook scopes).

## Who owns what

| You (the ERP) own | BrandGate owns |
| - | - |
| Product master data and base price, price lists and their prices, customer master data and which price list a customer has, delivery status and tracking | Stock (unless you use `POST /v1/inventory`), images, translations, order rules (minimum, maximum, steps), orders, invoices, payments, customer logins |

Do not send fields a person edits in BrandGate; it is last write wins per field.

## Products

```bash theme={null}
curl https://api.brandgate.dev/v1/products \
  -H "Authorization: Bearer $BRANDGATE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: products-2026-10-21-0001" \
  -d '{"items":[
    {"external_id":"1001","sku":"PAINT-5L-WHITE","name":"Wall paint white 5 L","unit_price":"349.00","vat_rate":"25","category":"Paint"}
  ]}'
```

Up to 200 products per request. The first time an `external_id` is seen, an existing product with the same `sku` is adopted; otherwise a product is created (`name` and `unit_price` are required then). Changing `sku` or `name` later updates the same product (`sku_conflict` if another product has that sku). Stock is set with the inventory endpoint.

## Price lists and customer prices

A price list is a named set of prices that you assign to customers. For a per-customer price, create one list for that customer.

```bash theme={null}
curl https://api.brandgate.dev/v1/price_lists \
  -H "Authorization: Bearer $BRANDGATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"external_id":"PL-PAINTERS","name":"Painters","items":[
    {"product_external_id":"1001","unit_price":"299.00"},
    {"sku":"PAINT-5L-BLACK","unit_price":"319.00"}
  ]}'
```

* One list per request, up to 1000 prices. Name each product by `product_external_id` or `sku`.
* By default listed products are added or updated. With `"replace": true` products you do **not** list are removed from the list (the response reports `removed`).
* Products that cannot be found come back in `unmatched_products`; the rest is saved.
* A customer pays the list price for a product when their list has it, otherwise the product's base price. Volume prices still apply when lower. BrandGate never recalculates old orders.
* `status: "inactive"` retires a list. Renaming a list keeps every customer assigned to it.

## Customers

```bash theme={null}
curl https://api.brandgate.dev/v1/customers \
  -H "Authorization: Bearer $BRANDGATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"items":[
    {"external_id":"K-1001","customer_number":"1001","company_name":"Målarmästarn AB","email":"order@malarmastarn.example",
     "billing_address":{"line1":"Storgatan 1","postal_code":"211 22","city":"Malmö","country":"SE"},
     "payment_terms_days":30,"price_list":"PL-PAINTERS"}
  ]}'
```

Up to 100 customers per request. `price_list` is the `external_id` of a price list you sent (send `null` to return to standard prices). A new customer is created as **pending**: they log in through BrandGate's invitation, and you never handle passwords.

A customer is **never merged by email**. If the email or `customer_number` already belongs to a BrandGate customer, that item answers `customer_exists` with `existing_id`. To link it, repeat the item with `"id": "<existing_id>"` once.

### Resellers that already exist

Most brands already have resellers in BrandGate before an ERP is connected, so **the first customer sync will mostly answer `customer_exists`**. That is expected, not a failure. For each one, check that it is the same company, then send the item again with `"id": "<existing_id>"` once. From then on `external_id` finds it. Run this first sync with a small batch and review the answers before sending everything.

### Resellers created in BrandGate

A reseller who registers in BrandGate has no ERP customer number yet. Their orders show `customer.id` and `customer.name` only; read `GET /v1/customers/{id}` for the contact and address details. Create the customer in your ERP, then link it back by sending `{"external_id": "<your id>", "id": "<customer.id>", "customer_number": "<your number>"}` to `POST /v1/customers`. Do this before you acknowledge the order, so later orders find the customer by number.

## Delivery status

```bash theme={null}
curl https://api.brandgate.dev/v1/orders/$ORDER_ID/status \
  -H "Authorization: Bearer $BRANDGATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"shipped","tracking_reference":"JJSE0123456789","tracking_url":"https://tracking.example/JJSE0123456789"}'
```

Orders move forward one step: `processing` → `shipped` → `delivered`. Repeating the same status is safe and may add tracking. Only `shipped` and `delivered` are accepted values (anything else is `400 invalid_request`). Cancelled orders, going back or skipping a step answer `409 status_not_allowed`. The whole order moves at once: partial shipments are not supported yet, so send `shipped` when the last line leaves. The customer is notified exactly as when the seller marks the order shipped in BrandGate.

## Try it without touching real data

Ask the seller to create a test workspace in BrandGate and generate a key there, the same way as for production. Everything on this page works the same.

## Checklist

* Send a stable `external_id` per object and never reuse one.
* Send changes often and the full set once a night (it is safe).
* Use `Idempotency-Key` on writes you may retry ([Idempotency](/idempotency)).
* Expect `plan_limit_reached` when the company's BrandGate plan caps products or customers: the seller must upgrade (or remove records) before the rest of a large first sync goes through.
* Read the `results` of every batch and log the items with `error`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.