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 fieldsid_crm, id_erp, id_accounting are removed from all responses.They are replaced by an
identifiers object with three keys.
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 fieldsid_crm, id_erp, id_accounting are no longer accepted in request bodies.Use the nested JSON object
identifiers — same shape as in read responses.
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,ownerand each line inpricing_table- Global discounts (
discounts.global_discounts[]) - Nested JSON: line discounts, taxes,
payment_terms,payment_methods
null instead of "", consistent with other endpoints.
2. Product-parents: catalog_ids → catalogs
Read
Thecatalog_ids field (UUID array) is replaced by catalogs (array of objects).
Write
Unchanged — continue sendingcatalog_ids in POST / PATCH request bodies.
Migration snippet
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 containingid 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
"default_tax": null.
3e. billing_frequency_id → billing_frequency
"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.
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.
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 ofPOST /customers/PATCH /customers/{id}: all workspace customer custom fields are present,nullif not set.GET /quote/{id}:custom_propertiesis renamedcustom_fieldson the quote, oncustomer, 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 definitionidare no longer exposed. They remain manageable in the application.
Write (POST /customers, PATCH /customers/{id})
custom_fieldsis an object{ "<api_name>": value }. The legacy array format is rejected (400).PATCHfollows JSON Merge Patch (RFC 7396): only the keys sent are written,nullclears 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 ascustom_fields[<api_name>].

