Recommendations for product import
Last updatedImporting product catalog usually involves multitude of products and can take a lot of time and make a lot of queries and mutations. For this reason it makes a lot of sense to use batch mutations or combine multiple into one request.
Remember that Rate Limits are also applied to number of requests made so limiting request count works in favor of the integration.
External ID strategy (ID conversions)#
Using externalId simplifies the workflow, as you don’t have to fetch ID of created entities and you can fit many related mutations in a single request:
- Assign an
externalIdwhen creating new objects (Product, ProductVariant, SizeChart, etc.). - Reference already-created objects by
externalIdinstead of numericid. - Keep a deterministic naming scheme (e.g.,
prd_<ERP_ID>,var_<ERP_ID>,sizechart_<NAME>).
Remember to maintain up-to-date ID mapping between your system and Centra whenever IDs change. Mutations setIdConversions / unsetIdConversions exist specifically for that purpose.
Read more in the ID conversions guide.
Caveats#
- Calling the same mutation (eg.
createProductVariant) multiple times with the sameexternalIdwill create duplicates and re-assign the ID conversion. - Make sure to first create all required size charts, brands, collections, folders and categories (if you want to assign products to any of them) in a separate call so that all required related entities exist, before you call a combined mutation. Albeit it is not required to do so, it is recommended because usually size charts and other related entities are not as numerous as products and therefore creating them in advance will save you requests and mutations (which count toward rate limits).
Combined mutations#
Combined mutations let you create and wire multiple catalog entities in one GraphQL request (e.g., Product → Variants (+ Sizes) → Display). This saves network round trips while keeping each mutation explicit and observable. Use them when you need to:
- Import many SKUs quickly with consistent linking via external IDs.
- Guarantee referential integrity between just-created objects (e.g., add variants to the product you just created).
- Reduce orchestration complexity in your importer while retaining granular error reporting.
Combined mutations are not atomic operations. Treat them as an ordered sequence of independent operations executed in one call.
If you did not create the required size charts, collections, folders, categories, pricelists or brands beforehand, mutations referencing this data will fail.
Keep in mind that you must verify if and which mutations failed and resending them again when reported problems are solved.
You can also combine multiple queries in one request to determine which of the objects need to be created.
Complete example#
This example assumes that the product you are importing contains many variants (3+) which is best handled using 4 step approach. In case of different data structure where single product consists of 1 or 2 variants alternative approach with only 3 requests might be better. Consider the table for comparison.
Comparison of product import strategies
| Standard approach (4 steps) | Alternative approach (3 steps) |
|---|---|
| 1. Query for prerequisite data | |
| 2. Create prerequisite data – number of mutations varies depending on data count, but remains the same for both methods. | |
3. Create product with variants using: a. createProduct – 1 mutation per products, creates all variants) | 3. Create product, variants, displays and prices using externalId, without need of setting the ID conversions manually: acreateProduct – 1 mutation per product; b. createVariant – 1 mutation per variant; c. createDisplay – 1 mutation per display (same); d. setPrices – 1 mutation (same) |
4. Store externalId for variants, create displays, set prices using: a. setIdConversions – 1 mutation for all variants; b. createDisplay – 1 mutation per display; c. setPrices – 1 mutation |
In this approach you are not able to provide externalId for variants created using createProduct. For this reason you have to provide the ID conversions yourself in 4th request before continuing the import. You need 1 more request to accomplish that.
The 3-step approach allows you to fit all mutations in one request. You can set externalId using createProductVariant mutation, but each additional variant costs you one mutation toward rate limit.
As you can see both approaches result in different number of requests and different number of mutations. If your data contains many variants per product consider the 4-step import strategy. When there is only 1-2 variants per product it might be advisable to save on number of requests with 3-step strategy. Consider both examples below.
Step 1: query related entities#
Assume you have multiple products to add to catalog, together with variants, sizes and displays. As a first step gather all brads, pricelists and size charts that would be needed and verify that they exist using an Integration API query:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
query {
existingBrands: brands(
where: { externalId: [
"brand-101",
"brand-102"
] }
) { externalId }
existingSizeCharts: sizeCharts(
where: { externalId: [
"shoes-men",
"shoes-women"
] }
) { externalId }
existingPricelists: pricelists(
where: { externalId: [
"prices-usd"
] }
) { externalId }
}
Note that we are querying the entities using externalId. This makes the whole workflow much easier and straightforward. You do not need to know ID Centra has assigned to your entities (and if it has Centra ID at all). You can just “talk to the API” using your own ID space.
Also note that we do not even return Centra ID for the entities. The query above will tell you which of the objects already exist in our database so you can easily reason which of them need to be created before you proceed.
Step 2: create missing entities#
Based on the results decide if new entities need to be inserted before the products can be imported. Mutations to create missing entities:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
mutation {
# Create a brand if not found by the query
brand1: createBrand(input: {
name: "Fikołki"
uri: "fikolki"
externalId: "external-101"
addToStores: [ { id: 1 } ]
}) {
brand { id }
userErrors { path, message }
userWarnings { path, message }
}
# Create a size chart if not found by the query
sizeChart1: createSizeChart(
input: {
name: "Shoes (men)"
horizontalLabels: ["40", "41", "42", "43", "44"]
externalId: "shoes-men"
}
) {
sizeChart { id }
userErrors { path, message }
userWarnings { path, message }
}
# Create another size chart if not found by the query
sizeChart2: createSizeChart(
input: {
name: "Shoes (women)"
horizontalLabels: ["33", "34", "35", "36", "37"]
externalId: "shoes-women"
}
) {
sizeChart { id }
userErrors { path, message }
userWarnings { path, message }
}
# Create a pricelist if not found by the query
priceList1: createPricelist(
input: {
name:"Pricelist USD",
status: ACTIVE
currencyIsoCode: "USD"
store: { id: 1 }
}
){
pricelist { id }
userErrors { message, path }
userWarnings { path, message }
}
}
Also keep in mind that we are assigning externalId to newly created entities. This will allow us referencing the entities disregarding IDs assigned by Centra.
Categories and folders are optional, but if your use case requires you can create them using similar flow. Keep in mind that categories do not have an externalId (albeit it might change in the future).
Step 3: import products with variants#
In case of numerous variant for single product use single mutation to create all the entities.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
mutation {
prod1: createProduct(
input: {
name: "Flip-flops"
status: ACTIVE
externalId: "external-1234"
productNumber: "EXT-1234"
brand: { externalId: "brand-101"}
addProductVariants: [
{
name: "Black"
status: ACTIVE
sizeChart: { externalId: "shoes-men" }
productSizes: [
{ size: { name: "41" } },
{ size: { name: "42" } }
]
},
{
name: "Grey"
status: ACTIVE
sizeChart: { externalId: "shoes-men" }
productSizes: [
{ size: { name: "41" } },
{ size: { name: "42" } }
]
},
# Additional variants here...
{
name: "Pink"
status: ACTIVE
sizeChart: { externalId: "shoes-women" }
productSizes: [
{ size: { name: "37" } },
{ size: { name: "35" } }
]
}
]
}
) {
product {
id
name
# Keep note of the IDs of created variants.
variants { id, name }
}
userErrors { path, message }
userWarnings { path, message }
}
}
Step 4: set ID conversions, then import displays & prices#
Once the product with variants is created save the IDs of created variants and use them to fill the conversion table. You might include the rest of the mutations in the same request using whichever ID you prefer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
mutation {
idConversions: setIdConversions(
input: [
{
externalId: "external-1234-BLK"
# Use variant IDs obtained from createProuct mutation
internalId: 1446
objectType: ProductVariant
}
{
externalId: "external-1234-GRE"
internalId: 1447
objectType: ProductVariant
}
{
externalId: "external-1234-PNK"
internalId: 1448
objectType: ProductVariant
}
]
) {
entries {
externalId
internalId
objectType
}
userErrors { path, message }
userWarnings { path, message }
}
display: createDisplay(
input: {
name: "Super Flip-flops"
status: ACTIVE
store: { id: 1 }
product: { externalId: "external-1234" }
uri: "super-flip-flops-22"
addCategories: [
{ id: 1 }
]
canonicalCategory: { id: 1 }
addProductVariants: [
{ productVariant: { externalId: "external-1234-BLK" } }
{ productVariant: { externalId: "external-1234-GRE" } }
]
}
) {
display { id, name }
userErrors { path, message }
userWarnings { path, message }
}
# Create additional displays if needed.
prod1prices: setPrices(
input: {
pricelist: { externalId: "prices-usd" }
productPrices: [
{
product: { externalId: "external-1234" }
price: { value: 100, currencyIsoCode: "USD" }
}
# Include additional prices if needed
]
}
) {
userErrors { path, message }
userWarnings { path, message }
}
}
Alternative 3-step example#
Preparation steps look exactly the same so we will not present them.
Step 3: import products, variants, sizes & displays#
Having completed steps 1 & 2 from the previous chapter, you can create all the necessary objects in one request, leveraging the externalId approach discussed above.
Remember you can also set the product prices in the same request. You are free to decide between setPrices or setPricesBatch mutations, depending how many pricelists you have to fill in – both work similarly. For the purpose of this example we will use the single pricelist mutation.
The example below is missing collection or folder setup for simplicity, but adding those should be pretty straightforward.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
mutation {
prod1: createProduct(
input: {
name: "Flip-flops"
status: ACTIVE
externalId: "prod-1234"
productNumber: "EXT-1234"
brand: { externalId: "external-101"}
}
){
product { id, name }
userErrors { path, message }
userWarnings { path, message }
}
var_1: createProductVariant(
input: {
# Using externalId allows combining multiple mutations in a single request
# because it is not necessary to retrieve the Centra ID of created entity.
product: { externalId: "prod-1234" }
name: "Black"
status: ACTIVE
sizeChart: { externalId: "shoes-men" }
externalId: "prod-1234-BLK"
productSizes: [
{ size: { name: "41" } },
{ size: { name: "42" } }
]
}
) {
productVariant { id, name, product {id, name} }
userErrors { message, path }
userWarnings { path, message }
}
var_2: createProductVariant(
input: {
product: { externalId: "prod-1234" }
name: "Grey"
status: ACTIVE
sizeChart: { externalId: "shoes-men" }
externalId: "prod-1234-GRE"
productSizes: [
{ size: { name: "41" } },
{ size: { name: "42" } }
]
}
) {
productVariant { id, name, product {id, name} }
userErrors { message, path }
userWarnings { path, message }
}
var_3: createProductVariant(
input: {
product: { externalId: "prod-1234" }
name: "Pink"
status: ACTIVE
sizeChart: { externalId: "shoes-women" }
externalId: "prod-1234-PNK"
productSizes: [
{ size: { name: "37" } },
{ size: { name: "38" } }
]
}
) {
productVariant { id, name, product {id, name} }
userErrors { message, path }
userWarnings { path, message }
}
display1: createDisplay(
input: {
name: "Super Flip-flops"
status: ACTIVE
store: { id: 1 }
product: { externalId: "prod-1234" }
uri: "super-flip-flops"
addCategories: [
{ id: 1 }
]
canonicalCategory: { id: 1 }
addProductVariants: [
{ productVariant: { externalId: "prod-1234-GRE" } },
{ productVariant: { externalId: "prod-1234-BLK" } }
]
}
) {
display {
id
name
productVariants { id, name }
product { id, name }
}
userErrors { message, path }
userWarnings { path, message }
}
# The rest of displays follows...
prod1prices: setPrices(
input: {
pricelist: { externalId: "prices-usd" }
productPrices: [
{
product: { externalId: "external-1234" }
price: { value: 100, currencyIsoCode: "USD"}
}
]
}
) {
userErrors { message, path }
userWarnings { path, message }
}
}
Using presented approach allows for creation of multiple fully configured products using a sequence of just 3 Integration API requests.
Keep in mind that you should not include excessive number of products in one request. Start with 1 full product import per request, and then adjust to stay within the rate limits. Eg. if the products have only single variants, then you might increate product count per request.
Find the value that works best for you, your QA server should be perfect for that kind of testing.
Additional recommendations#
Avoid over-fetching#
When importing products you usually do not need to fetch data back from API. GraphQL allows developers to define what entity properties will be returned so it is recommended to keep the fetched fields to the minimum. The minimal required returned set is the error listing: userErrors { path message }. The rest might be omitted, which will save on query complexity points and rate limits.
Error handling#
Remember to check for mutation errors when importing data. Failing to do so might result in incomplete import. It is recommended to pause the integration and resolve observed issues before moving on to next batch of products. Please note that in case of batch mutations you should also verify userWarnings as they might include failed operations (refer to batch mutations documentation for details).
Mutation aliasing#
Use meaningful aliases for mutations so you are able to detect which product failed to import and resolve the raised issues.
Monitor rate limits#
Respect the rate limits and back off when you get a 429 HTTP response. Refer to rate-limits documentation for details.
FAQ#
Why should I optimize my catalog import mutations?
The short answer is: to adhere to best practices whenever necessary. But the more in-depth answer should also take into consideration the overall usage patterns and specific use case you are addressing.
In case of single, non-frequent product update – it does not make any significant difference if you follow the recommendations. But when importing a numerous product catalog you might run into rate limits or performance issues when not following the best practices. Each separate request counts toward rate limits, as well as each separate mutation. So batching the updates helps to stay within the limits and to perform in a timely manner, which is especially important for syncing large catalogs.
How many products should I import one request?
Request sizing is more of an art than science. Answer depends on how many variants, pricelists, markets, stores etc. there are. As a rule of the thumb start with importing 1 product, along with related entities per request and adjust if you see this falls safely within the limits.
What is the difference between batch mutation and combined mutations?
Batch mutation is a single mutation that performs updates on multiple items. It is designed that way, potentially optimized under the hood to perform better with loads of data. Detailed explanation and examples of such mutations can be found in the batch mutations docs.
A combined mutations is a single GraphQL request that contains more than one mutation. This approach executes each of the mutations independently so the result of one of the mutations does not affect the execution of another.
Can I set external IDs when creating objects?
Yes, and it is the recommended way to create objects. It simplifies the flow of integration.
My combined call partially failed. Do I have to redo everything?
No. Resend only the failed mutations, after resolving the issue that caused an error. In case of server errors (HTTP 5xx) it is advisable to query which entities were actually saved before retrying. Remember that multiple mutations combined in one request are independent. The ones that did not raise any errors succeeded and do not need re-running. They should not be run again, because that will result in duplicated data and cause more trouble.
What if I need to update existing products?
Use updateProduct instead of createProduct. In this use case you should make sure that product with given externalId already exists. Failing to do so might result in error in updateProduct mutation. On the other hand calling createProduct more than once will result in a duplicate, even if the same externalId is passed.
What if the required SizeChart or Brand doesn’t exist yet?
Query them first and create missing entities before the combined product import call.
Is there a transaction rollback if one mutation fails?
No. Combined mutations are not atomic transactions. Handle rollbacks in your importer logic.
How can I verify import results efficiently?
Use queries filtered by externalId to confirm that Products, Variants, and Displays exist, and check their statuses.
How do I monitor rate limits during import?
Inspect HTTP response status code and headers (Retry-After) and implement a backoff policy. You might use rateLimits query to monitor you usage and remaining quota to estimate limit utilization of your requests. Refer to rate-limits documentation for details.