Skip to main content

Products

These endpoints create, update, and delete products in Corso that do not exist on the platform Corso is connected to. Your live catalog is synced automatically, so these endpoints exist only for catalog data that sync cannot reach.

When to Use These Endpoints

Use them for products a customer may still file a warranty claim or registration against, but that Corso has no other way to learn about:

  • Retired products that were removed from your platform before your Corso integration went live.
  • Legacy products that only ever existed in a system you have since migrated off.
  • Unconnected channels, such as a marketplace, wholesale channel, or storefront that Corso is not connected to.
danger

Do not create products here that exist on your connected platform. Those are synced automatically, and creating them again produces duplicate entries in returns, exchanges, and registrations.

Scope and Limits

Every product created here is recorded as not existing on your platform, and that flag scopes every operation. GET, PUT, and DELETE resolve a product by its Corso ID and that flag together, so a product synced from your platform is not addressable through this API at all. Such a request returns a 404, indistinguishable from a product that does not exist. To change a product that lives on your platform, change it there and let it sync.

Two further limits apply to the products you create here:

  • They are excluded from exchange destinations. Their identifiers do not resolve against your platform's API, so Corso will not offer them as something a customer can exchange into. They work as the item being registered or claimed against, not the item being received.
  • They carry no inventory. There is no quantity field, because Corso has no location-level stock data for them. Treat them as catalog records, not sellable stock.

A product must also be active to be usable anywhere in Corso. The archived and draft statuses are accepted and stored, but a product in either state is treated as unavailable; use DELETE to retire one.

Product Handles

Every product you create here is identified by its handle, which has two consequences worth knowing before a bulk backfill:

Creates are idempotent. Sending the same handle twice resolves to the same product and updates it in place. The response is a 201 Created the first time and a 200 OK after that, so a backfill that fails halfway can be replayed from the start without deduplicating anything first.

Handles cannot collide with your platform's products. A handle only has to be unique among the products you create through this API. If vintage-tee also exists in your Shopify catalog, that is fine, because platform-synced products are identified by your platform's own product ID rather than by the handle.

The handle is fixed at creation and seeds the product's identity, so a different handle is a different product. Use the productId Corso returns for every subsequent read, update, and delete.

Creating a Product

curl --request POST \
--url 'https://api.corso.com/v1/products' \
--header 'authorization: Bearer eyJhbyfQ...adQssw5c' \
--header 'content-type: application/json' \
--data '{
"handle": "vintage-tee-2019",
"title": "Vintage Tee (2019)",
"vendor": "Acme",
"productType": "Shirts",
"tags": ["archive", "2019"],
"variants": [
{
"sku": "VT-2019-S",
"price": "29.99",
"options": [{ "name": "Size", "value": "Small" }]
},
{
"sku": "VT-2019-L",
"price": "29.99",
"options": [{ "name": "Size", "value": "Large" }]
}
]
}'

A few field-level rules apply to every write:

FieldRule
price, compareAtPrice, unitCostPlain decimal strings up to 99999999.99, with no currency symbol and no thousands separators. "$29.99" and "1,299.00" are rejected with a 400. Amounts are interpreted in your store's currency.
imageUrlAn http or https URL. Corso stores it as given and does not re-host the image, so it must stay publicly reachable for the image to keep rendering in customer-facing flows.
tagsAn individual tag cannot contain a comma.
gramsA whole number of grams.
storeIdRequired only when your access token grants access to more than one store, since there is no store in the request path. A storeId your token does not cover returns a 404.

Variant Identity

A variant is identified by its combination of option values. As long as those values do not change, the variant keeps its variantId across every write. The order you send the options in does not affect that identity, but every variant on a product must use the same set of option names, and no two variants may repeat the same combination of values.

The order is preserved, though. It sets each option's position on the product and the order Corso composes the variant's display name in, so a variant sent as Size then Color reads "Small / Red" rather than "Red / Small". Send your options in the order you want them read.

Each variant must have either at least one option or a sku. Without one of the two, Corso would have to fall back to the variant's position in the array, and reordering your request would silently re-key your variants. Requests with such a variant are rejected with a 400.

For a product with a single, unvaried variant, send an empty options array:

{
"handle": "retired-mug",
"title": "Retired Mug",
"variants": [{ "sku": "MUG-001", "price": "12.00", "options": [] }]
}

Updating a Product

Both POST (by handle) and PUT /products/{productId} carry the product's full representation. Any field you omit is cleared, and any variant you previously stored but omit is marked as removed.

So to drop the Large variant from the example above, send the Small variant along with every field you still want to keep:

curl --request PUT \
--url 'https://api.corso.com/v1/products/902113' \
--header 'authorization: Bearer eyJhbyfQ...adQssw5c' \
--header 'content-type: application/json' \
--data '{
"title": "Vintage Tee (2019)",
"vendor": "Acme",
"productType": "Shirts",
"tags": ["archive", "2019"],
"variants": [
{
"sku": "VT-2019-S",
"price": "29.99",
"options": [{ "name": "Size", "value": "Small" }]
}
]
}'

Send the request again with both variants and the Large one comes back, keeping its original variantId.

Deleting and Restoring

DELETE /products/{productId} marks the product and all of its variants as removed. The product stops appearing in returns, exchanges, and search results, but its history is retained, so existing orders and claims that reference it stay intact.

Deletes are idempotent, so deleting an already-deleted product also returns a 204. A deleted product is still retrievable with GET, with deletedOn set, so you can confirm the delete went through. Its variants array comes back empty, because the response only ever lists live variants.

To restore a product, create it again with the same handle. Because the handle resolves to the same product, the original productId comes back:

curl --request DELETE \
--url 'https://api.corso.com/v1/products/902113' \
--header 'authorization: Bearer eyJhbyfQ...adQssw5c'
# 204 No Content

curl --request POST \
--url 'https://api.corso.com/v1/products' \
--header 'authorization: Bearer eyJhbyfQ...adQssw5c' \
--header 'content-type: application/json' \
--data '{ "handle": "vintage-tee-2019", "title": "Vintage Tee (2019)", "variants": [ ... ] }'
# 200 OK, productId 902113

Permissions

These endpoints require the following scopes on your access token:

ScopeEndpoints
read:productsGET /products/{productId}
write:productsPOST, PUT, and DELETE

See Authenticating for how to request a token.