Products API
Products API
GET /api/integrations/v2/{connection_id}/products
The catalogue, keyset-paged on (updated_at, id); a price change moves updated_at.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
connection_id |
path | string (uuid) | yes | Your custom integration’s id; the connection whose secret signs the request. |
updated_since |
query | string (date-time), nullable | no | Only rows changed at or after this instant (inclusive). |
cursor |
query | string, nullable | no | The next_cursor of the previous page, as given. |
limit |
query | integer | no | Page size; at most 250 rows are answered whatever is asked. Default 50. |
Headers
| Header | Required | Description |
|---|---|---|
X-Yedran-Timestamp |
yes | Unix seconds; within five minutes of the server’s clock. |
X-Yedran-Signature |
yes | Lowercase hex HMAC-SHA256, keyed with the integration’s API secret, over METHOD\npath\ntimestamp\nsha256_hex(body). |
Yedran-Version |
no | Optional: the contract version, 2026-10 today; absent is 2026-10. Any other value is 400 unsupported_version. Every answer echoes the version served. |
Response 200
Schema ProductPageV2.
| Field | Type | Required | Description |
|---|---|---|---|
data |
array of ProductV2 | yes | |
next_cursor |
string, nullable | yes |
Example
curl -X GET "https://app.yedran.com/api/integrations/v2/{connection_id}/products?limit=1" \ -H "X-Yedran-Timestamp: $TIMESTAMP" \ -H "X-Yedran-Signature: $SIGNATURE" \ -H "Yedran-Version: 2026-10"Response 200:
{ "data": [ { "id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301", "external_id": "string", "sku": "string", "name": "string", "description": "string", "kind": "string", "active": false, "pricing_method": "fixed", "package_size": 1, "price": { "currency": "string", "unit_amount_minor": 1, "tiers": [ {} ] }, "updated_at": "2026-10-02T09:00:00Z", "archived_at": "2026-10-02T09:00:00Z", "template_id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301", "options": [ { "name": "string", "value": "string" } ] } ], "next_cursor": "string"}Errors
| Code | Meaning |
|---|---|
| 400 | Bad Request |
| 401 | Unauthorized |
| 422 | Unprocessable Entity |
GET /api/integrations/v2/{connection_id}/products/{product_id}
One product by our id.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
connection_id |
path | string (uuid) | yes | Your custom integration’s id; the connection whose secret signs the request. |
product_id |
path | string (uuid) | yes |
Headers
| Header | Required | Description |
|---|---|---|
X-Yedran-Timestamp |
yes | Unix seconds; within five minutes of the server’s clock. |
X-Yedran-Signature |
yes | Lowercase hex HMAC-SHA256, keyed with the integration’s API secret, over METHOD\npath\ntimestamp\nsha256_hex(body). |
Yedran-Version |
no | Optional: the contract version, 2026-10 today; absent is 2026-10. Any other value is 400 unsupported_version. Every answer echoes the version served. |
Response 200
Schema ProductV2.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | |
external_id |
string, nullable | yes | |
sku |
string | yes | |
name |
string | yes | |
description |
string, nullable | yes | |
kind |
string | yes | |
active |
boolean | yes | |
pricing_method |
one of fixed, volume, graduated, package |
yes | |
package_size |
integer, nullable | yes | |
price |
IntegrationPrice, nullable | yes | |
updated_at |
string (date-time) | yes | |
archived_at |
string (date-time), nullable | yes | |
template_id |
string (uuid) | yes | |
options |
array of OptionValueV2 | yes |
Example
curl -X GET "https://app.yedran.com/api/integrations/v2/{connection_id}/products/3f2504e0-4f89-11d3-9a0c-0305e82c3301" \ -H "X-Yedran-Timestamp: $TIMESTAMP" \ -H "X-Yedran-Signature: $SIGNATURE" \ -H "Yedran-Version: 2026-10"Response 200:
{ "id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301", "external_id": "string", "sku": "string", "name": "string", "description": "string", "kind": "string", "active": false, "pricing_method": "fixed", "package_size": 1, "price": { "currency": "string", "unit_amount_minor": 1, "tiers": [ { "min_quantity": 1, "unit_amount_minor": 1, "discount_pct": "string" } ] }, "updated_at": "2026-10-02T09:00:00Z", "archived_at": "2026-10-02T09:00:00Z", "template_id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301", "options": [ { "name": "string", "value": "string" } ]}Errors
| Code | Meaning |
|---|---|
| 400 | Bad Request |
| 401 | Unauthorized |
| 404 | Not Found |
| 422 | Unprocessable Entity |
PUT /api/integrations/v2/{connection_id}/products/by-external-id/{external_id}
Create or replace the product you call external_id, through the inbound product.upserted path.
Requires the catalogue.write grant (403 no_catalogue_authority) and products enabled inbound in the integration’s sync settings (403 not_enabled). Prices are never touched. 409 external_id_conflict when the id or the sku is bound to another record; 409 stale_write when source_updated_at is not newer than the version applied.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
connection_id |
path | string (uuid) | yes | Your custom integration’s id; the connection whose secret signs the request. |
external_id |
path | string | yes | Your id for this product. |
Headers
| Header | Required | Description |
|---|---|---|
X-Yedran-Timestamp |
yes | Unix seconds; within five minutes of the server’s clock. |
X-Yedran-Signature |
yes | Lowercase hex HMAC-SHA256, keyed with the integration’s API secret, over METHOD\npath\ntimestamp\nsha256_hex(body). |
Yedran-Version |
no | Optional: the contract version, 2026-10 today; absent is 2026-10. Any other value is 400 unsupported_version. Every answer echoes the version served. |
Idempotency-Key |
yes | Required on every write: a unique key per logical request, kept 24 hours. |
Request body
Schema ProductUpsertV2.
| Field | Type | Required | Description |
|---|---|---|---|
sku |
string | yes | |
name |
string | yes | |
description |
string, nullable | no | |
kind |
one of good, service |
yes | |
active |
boolean | no | |
external_version |
string, nullable | no | |
source_updated_at |
string (date-time), nullable | no | |
template_external_id |
string, nullable | no | |
template_name |
string, nullable | no | |
options |
array of NamedOption | no |
Response 200
Schema ProductV2.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | |
external_id |
string, nullable | yes | |
sku |
string | yes | |
name |
string | yes | |
description |
string, nullable | yes | |
kind |
string | yes | |
active |
boolean | yes | |
pricing_method |
one of fixed, volume, graduated, package |
yes | |
package_size |
integer, nullable | yes | |
price |
IntegrationPrice, nullable | yes | |
updated_at |
string (date-time) | yes | |
archived_at |
string (date-time), nullable | yes | |
template_id |
string (uuid) | yes | |
options |
array of OptionValueV2 | yes |
Example
curl -X PUT "https://app.yedran.com/api/integrations/v2/{connection_id}/products/by-external-id/string" \ -H "X-Yedran-Timestamp: $TIMESTAMP" \ -H "X-Yedran-Signature: $SIGNATURE" \ -H "Yedran-Version: 2026-10" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"sku": "string", "name": "string", "kind": "good"}'Response 200:
{ "id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301", "external_id": "string", "sku": "string", "name": "string", "description": "string", "kind": "string", "active": false, "pricing_method": "fixed", "package_size": 1, "price": { "currency": "string", "unit_amount_minor": 1, "tiers": [ { "min_quantity": 1, "unit_amount_minor": 1, "discount_pct": "string" } ] }, "updated_at": "2026-10-02T09:00:00Z", "archived_at": "2026-10-02T09:00:00Z", "template_id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301", "options": [ { "name": "string", "value": "string" } ]}Errors
| Code | Meaning |
|---|---|
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 409 | Conflict |
| 422 | Unprocessable Entity |
Schemas
ProductPageV2
A page of the catalogue, (updated_at, id) ascending.
| Field | Type | Required | Description |
|---|---|---|---|
data |
array of ProductV2 | yes | |
next_cursor |
string, nullable | yes |
ProductV2
One catalogue row, as the v2 API shows it.
archived_at is the product’s last change while it is inactive and null while it is active: no separate archive stamp is stored, and an inactive product’s updated_at is when it was last touched as archived. price is its price in the workspace’s default active list, null when it has none (listed, not sellable) — v1’s price.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | |
external_id |
string, nullable | yes | |
sku |
string | yes | |
name |
string | yes | |
description |
string, nullable | yes | |
kind |
string | yes | |
active |
boolean | yes | |
pricing_method |
one of fixed, volume, graduated, package |
yes | |
package_size |
integer, nullable | yes | |
price |
IntegrationPrice, nullable | yes | |
updated_at |
string (date-time) | yes | |
archived_at |
string (date-time), nullable | yes | |
template_id |
string (uuid) | yes | |
options |
array of OptionValueV2 | yes |
ProductUpsertV2
PUT /products/by-external-id/{external_id} — the whole catalogue row, keyed by the caller’s id.
Applied as an inbound product.upserted carrying this external_id, so it obeys that event’s rules: every field replaces the stored one, prices are never touched (PUT /v1/…/products/{sku}/price sets them), and source_updated_at — the caller’s own time for this version, now when omitted — must be newer than the last one applied or the write is 409 stale_write. external_version is stored on the binding as sent.
| Field | Type | Required | Description |
|---|---|---|---|
sku |
string | yes | |
name |
string | yes | |
description |
string, nullable | no | |
kind |
one of good, service |
yes | |
active |
boolean | no | |
external_version |
string, nullable | no | |
source_updated_at |
string (date-time), nullable | no | |
template_external_id |
string, nullable | no | |
template_name |
string, nullable | no | |
options |
array of NamedOption | no |
IntegrationPrice
What one product costs in the active list.
unit_amount_minor is the LOWEST QUANTITY BREAK — the amount of the tier with the smallest min_quantity, whatever that quantity is, and not a promise about quantity one. See _price_of, which fills it: a list whose lowest break is 5 has a price, and reading its first tier as “the tier for one” would report that product as unpriced.
It coincides with the price of one whenever the ladder has a base tier. IntegrationPriceWrite requires one, so every ladder a connected system publishes does; the console’s tier-by-tier writer does not, so a ladder typed there may start at 5. Then this field carries that break’s amount while quote(quantity=1) refuses with SkuNotPriced, and the quote is the authority — a line’s real number always comes from it, which applies the break and the discount.
currency is the LIST’s, because an entry has none: erp_price_lists carries one currency for the whole list (CT-03).
| Field | Type | Required | Description |
|---|---|---|---|
currency |
string | yes | |
unit_amount_minor |
integer | yes | |
tiers |
array of IntegrationPriceTier | yes |
OptionValueV2
One variant’s value of one option, by text: {"name": "Color", "value": "Red"}.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | |
value |
string | yes |
NamedOption
An option value as a connected system names it: {name, value}, no ids.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | |
value |
string | yes |
IntegrationPriceTier
One quantity break, as a connected system sees it.
| Field | Type | Required | Description |
|---|---|---|---|
min_quantity |
integer | yes | |
unit_amount_minor |
integer | yes | |
discount_pct |
string, nullable | yes |