Skip to main content
This release introduces breaking changes to the public API. Existing integrations must be updated before the deprecation date. Read each section carefully and check all items in the migration checklist.
No API versioning. This is not a new API version running alongside the previous one — the new contract replaces the old one directly. There is no /v2/ prefix or compatibility shim. Any integration still using the old field names will break as soon as the release is deployed.

Migration checklist

Before deploying your updated integration, confirm each item below.
1

Update external identifiers in read responses

Read identifiers.crm / .erp / .accounting instead of id_crm, id_erp, id_accounting on customers, contacts, addresses, catalogs and payment terms. Read identifiers instead of external_ids on quotes.
2

Update external identifiers in write requests

Send grouped fields as nested JSON (identifiers, shipping, inventory, settings, accounting). Bracket-flat keys (identifiers[crm], shipping[weight]…) are rejected with a 400.
3

Update product-parent catalog field

Read catalogs from catalogs[].id instead of catalog_ids on product-parents.
4

Update price linked resources

Read linked resources on prices from the embedded objects product, pricebook, currency, default_tax, billing_frequency. Read prices[].pricebook on products.
5

Check integration token permissions

Ensure your integration token has the product_pricing:write right if it writes to catalogs, prices or products. Writes that previously passed will now return a 403.
6

Update custom fields format

Read and write custom fields as custom_fields: { "<api_name>": value }. Use custom_fields instead of custom_properties on quotes.

Summary


1. External identifiers: identifiers replaces flat fields

1.1 Read responses (GET)

The fields id_crm, id_erp, id_accounting are removed from all responses.
They are replaced by an identifiers object with three keys.
Affected endpoints
Products (/products, /product-parents/{id}/products) and prices already returned identifiers as an object — nothing changes for them on read. They are however affected on write (§1.2, §1.3). Product-parents have no identifiers: they are carried by the product.

1.2 Write requests (POST / PATCH body)

The fields id_crm, id_erp, id_accounting are no longer accepted in request bodies.
Use the nested JSON object identifiers — same shape as in read responses.
The following are rejected with a 400:
  • Bracket-flat keys ("identifiers[crm]": "...")
  • The fields id_crm, id_erp, id_accounting and their aliases (crm_id, erp_id, accounting_id)
CSV import columns are not affected — they remain flat (identifiers[crm]).

1.3 Other grouped fields (products)

The same rule applies to all grouped fields on products (POST /products, PATCH /products/{id}, POST /product-parents, POST /product-parents/{id}/products): settings, shipping, inventory, accounting, identifiers.

1.4 Migration snippet

1.5 Quotes: external_ids → identifiers

GET /quote/{id} previously used external_ids. The field is now identifiers, with the same keys crm, erp, accounting, everywhere in the quote:
  • business_unit, customer, owner and each line in pricing_table
  • Global discounts (discounts.global_discounts[])
  • Nested JSON: line discounts, taxes, payment_terms, payment_methods
Empty values are now null instead of "", consistent with other endpoints.

2. Product-parents: catalog_ids → catalogs

Read

The catalog_ids field (UUID array) is replaced by catalogs (array of objects).

Write

Unchanged — continue sending catalog_ids in POST / PATCH request bodies.

Migration snippet

Affected endpoints: GET /product-parents, GET /product-parents/{id}, responses of POST /product-parents and PATCH /product-parents/{id}.

Products and variants

On /products and /products/{id}, the catalogs field already existed but only contained id and api_name. It now also includes name.

prices[] on products

On /products, /products/{id} and /product-parents/{id}/products, each prices[] entry now takes the same shape as a price from /prices (see §3): pricebook becomes an object, billing_frequency and key are added. pricebook_id and pricebook_api_name are removed.
A recurring product has one entry per frequency. billing_frequency ({ id, period, unit }) and key (e.g. …standard.month.1) allow you to distinguish them.

3. Prices: embedded linked resources

The five raw UUID FK fields are replaced by embedded objects containing id and the main descriptive fields.
Write is unchanged — continue sending pricebook_id, currency_id, etc. in POST / PATCH request bodies.

3a. product_variant_id → product

3b. pricebook_id → pricebook

3c. currency_id → currency

3d. default_tax_id → default_tax

If no tax is associated: "default_tax": null.

3e. billing_frequency_id → billing_frequency

If no frequency is associated: "billing_frequency": null.
Billing frequency has no api_name. Identify it by unit + period.

3f. New key field (semantic price key)

Each price exposes a calculated (read-only, not stored) semantic key, returned immediately after id.
key is not an api_name. It changes if the product or pricebook is renamed, and is not accepted on write.
Affected endpoints: GET /prices, GET /prices/{id}, response of PATCH /prices/{id}.

4. Token rights enforced on writes

Write endpoints in the catalog now check the token’s role rights, as /customers, /contacts, /addresses and /payment-terms already did. A token without the required right receives a 403 — writes that previously passed will now fail.
Action required: Ensure your integration token is attached to a role with product_pricing:write (system roles admin and manager). Roles sale and view_only remain read-only. GET requests are not affected.

5. Non-breaking additions

No existing fields are modified. No action required.

6. Custom fields: custom_fields object indexed by api_name

Custom fields are no longer an array of objects but an object whose keys are the fields’ api_name and values are the field values themselves. Same shape on read and write. Values are typed according to the field type: text → string, number → number, timestamptz → ISO 8601 string.

Read

  • GET /customers/{id} and responses of POST /customers / PATCH /customers/{id}: all workspace customer custom fields are present, null if not set.
  • GET /quote/{id}: custom_properties is renamed custom_fields on the quote, on customer, and on each line item (pricing_table.sections[].line_items[] and …phases[].line_items[]). Values are no longer coerced to strings ("12" → 12).
  • Per-value flags (is_active, is_mandatory, is_editable_by_sale, is_display_for_customer) and the definition id are no longer exposed. They remain manageable in the application.

Write (POST /customers, PATCH /customers/{id})

  • custom_fields is an object { "<api_name>": value }. The legacy array format is rejected (400).
  • PATCH follows JSON Merge Patch (RFC 7396): only the keys sent are written, null clears a value, absent keys are unchanged.
  • Unknown key, wrong type, or value outside constraints (min, max, maxLength) → 400, nothing is written. Errors reference the field as custom_fields[<api_name>].

General rules


Full reference table