In this article you’ll get an understanding how an ERP system (or similar) could be integrated with Centra. Keep in mind that this guide is not the absolute truth, but a recommendation on how it could be handled in the specific scenario that product data and inventory is sent to Centra, and that orders are read from Centra and that they’re updated in Centra by the ERP upon fulfillment.
This article goes through how to integrate to Centra step-by-step. If an integration of this kind is to be made for a client that has been running their e-commerce through Centra for some time, a different approach might need to be taken.
Integration API introduction#
All mutations of the API are documented in Integration API Docs.
Communication#
Whether it’s stock updates, product updates or fetching orders, the integration is always initiating the communication. Centra APIs are passive and do not trigger any updates or send information to external systems. Centra, however, has the option to send webhooks for Certain events. If that is of interest, more information is available here. You might also be interested in using the GraphQL API events system.
Prerequisites#
An Integration API token with proper access rights. These are set up in Centra AMS.
ID Conversion#
To make an integration as easy as possible to build and maintain, the Integration API enables interactions with Centra based on IDs in any external integration. This means the integration only has to be aware of its own IDs and not of Centra's IDs. This is very similar to how ID conversion works in SOAP API and other modules, however, we extended it to be manageable through the Integration API directly as part of the Centra core. It also allows to support any custom integrations that are not Centra modules.
You can read and learn how to work with ID Conversion here.
Events system#
Integration GraphQL API exposes a stream of events to the interested parties. When anything in Centra is changed, there will be events you can listen to. This will allow you to:
know what changed since your last synchronization, so that you can fetch only changed data selectively,
avoid periodical polling for new data of each type.
For example, when a new order is created by Checkout API or in any other place, there will be an event of “object type” Order
and “change type” CREATED
, and your integration will see it as soon as the events are fetched by the new query events.
You can read all about the Events system here.
Special characters and checkout fields limitations#
Depending on your ERP, there might be some limitations when it comes to the allowed types of characters, or to the allowed number of characters per field. Centra Admin backend will accept any characters set, and the fields lengths are designed to not be a limiting factor. If your ERP has known limits, e.g. allowing only 20 characters each for the customer's first and last name, you should make sure your front end partner is aware of this and ask them to introduce those limits in the checkout fields. Similarly, if your ERP only accepts ASCII characters, you would probably want to strip them in checkout, so that (for example) "Michael Ständer" is sent as "Michael Stander" into Centra.
Product data#
The first step in this kind of integration, whether the product data is coming directly from the ERP or a PIM system, is to create the products in Centra. This needs to be done in order to run tests for orders, stock etc. with the correct data.
Before products can be added, size charts must be created in Centra. Sizes for products is a core feature in Centra and must be used, whether the products to be sold come in different sizes or not. One reason for this is that stock is stored on the size level. If the products sold only come in a single size, simply create a size chart with a single size that can be used for all products. One such size chart is already there by default in every Centra instance.
A size chart contains a name, and sizes. Size charts can be named in any way and the sizes within a size chart can also be named in any way. It could be a classic size chart named XS-XL which contains the sizes XS, S, M, L, and XL. It could also be a multi-dimensional size chart for pants that can hold both length and width.
It’s also worth noting that you can choose to only activate some of the size chart sizes for specific products. For example, if you have two products: one selling in sizes between XXS-XXL, and another product which only has sizes S/M/L, they can use the same size chart - one with sizes XXS, XS, S, M, L, XL, XXL. Then you can configure the second product to only activate sizes S/M/L, and keep stock for those. Other sizes can remain unused, or can be activated at a later time, in case you would start selling different sizes in the future.
Once the size charts are in place, products can be added to Centra. Let’s take a look at how a simple size chart can be created. Remember to store the IDs that are returned upon creation, these are needed when selecting the size chart and its sizes to be used for a product. There are no limitations on how many size charts can be used. However, there’s no need to create multiple size charts with the XS-XL range, one is enough. It doesn’t matter if it’s for men or women. Measurements are added on product level, with the help of Measurement charts.
Creating size charts#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mutation addSizeChart {
createSizeChart(
input: {
name: "One Size"
horizontalLabels: ["One Size"]
}
) {
sizeChart {
id
name
sizes {id name}
horizontalLabels
verticalLabels
dividerSymbol
}
userErrors {
message
path
}
}
}
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
{
"data": {
"createSizeChart": {
"userErrors": [],
"sizeChart": {
"id": 1,
"name": "One Size",
"sizes": [
{
"id": 1,
"name": "One Size"
}
],
"horizontalLabels": [
"One Size"
],
"verticalLabels": null,
"dividerSymbol": "x"
}
}
},
"extensions": {
"complexity": 121,
"permissionsUsed": [
"SizeChart:write",
"SizeChart:read"
]
}
}
And here’s another example, a size chart that we could use for pants.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mutation addSizeChart {
createSizeChart(input: {
name: "Pants sizes"
horizontalLabels: ["24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "36", "38"]
verticalLabels: ["26", "28", "30", "32", "34", "36"]
}) {
sizeChart {
id
sizes {
id
name }
}
userErrors {
message
path
}
}
}
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
{
"data": {
"createSizeChart": {
"sizeChart": {
"id": 43,
"sizes": [
{
"id": 513,
"name": "24x26"
},
{
"id": 514,
"name": "24x28"
},
{
"id": 515,
"name": "24x30"
},
{
"id": 516,
"name": "24x32"
},
{
"id": 517,
"name": "24x34"
},
{
"id": 518,
"name": "24x36"
},
{
"id": 519,
"name": "25x26"
},
{
"id": 520,
"name": "25x28"
},
{
"id": 521,
"name": "25x30"
},
{
"id": 522,
"name": "25x32"
},
{
"id": 523,
"name": "25x34"
},
{
"id": 524,
"name": "25x36"
},
{
"id": 525,
"name": "26x26"
},
{
"id": 526,
"name": "26x28"
},
{
"id": 527,
"name": "26x30"
},
{
"id": 528,
"name": "26x32"
},
{
"id": 529,
"name": "26x34"
},
{
"id": 530,
"name": "26x36"
},
{
"id": 531,
"name": "27x26"
},
{
"id": 532,
"name": "27x28"
},
{
"id": 533,
"name": "27x30"
},
{
"id": 534,
"name": "27x32"
},
{
"id": 535,
"name": "27x34"
},
{
"id": 536,
"name": "27x36"
},
{
"id": 537,
"name": "28x26"
},
{
"id": 538,
"name": "28x28"
},
{
"id": 539,
"name": "28x30"
},
{
"id": 540,
"name": "28x32"
},
{
"id": 541,
"name": "28x34"
},
{
"id": 542,
"name": "28x36"
},
{
"id": 543,
"name": "29x26"
},
{
"id": 544,
"name": "29x28"
},
{
"id": 545,
"name": "29x30"
},
{
"id": 546,
"name": "29x32"
},
{
"id": 547,
"name": "29x34"
},
{
"id": 548,
"name": "29x36"
},
{
"id": 549,
"name": "30x26"
},
{
"id": 550,
"name": "30x28"
},
{
"id": 551,
"name": "30x30"
},
{
"id": 552,
"name": "30x32"
},
{
"id": 553,
"name": "30x34"
},
{
"id": 554,
"name": "30x36"
},
{
"id": 555,
"name": "31x26"
},
{
"id": 556,
"name": "31x28"
},
{
"id": 557,
"name": "31x30"
},
{
"id": 558,
"name": "31x32"
},
{
"id": 559,
"name": "31x34"
},
{
"id": 560,
"name": "31x36"
},
{
"id": 561,
"name": "32x26"
},
{
"id": 562,
"name": "32x28"
},
{
"id": 563,
"name": "32x30"
},
{
"id": 564,
"name": "32x32"
},
{
"id": 565,
"name": "32x34"
},
{
"id": 566,
"name": "32x36"
},
{
"id": 567,
"name": "33x26"
},
{
"id": 568,
"name": "33x28"
},
{
"id": 569,
"name": "33x30"
},
{
"id": 570,
"name": "33x32"
},
{
"id": 571,
"name": "33x34"
},
{
"id": 572,
"name": "33x36"
},
{
"id": 573,
"name": "34x26"
},
{
"id": 574,
"name": "34x28"
},
{
"id": 575,
"name": "34x30"
},
{
"id": 576,
"name": "34x32"
},
{
"id": 577,
"name": "34x34"
},
{
"id": 578,
"name": "34x36"
},
{
"id": 579,
"name": "36x26"
},
{
"id": 580,
"name": "36x28"
},
{
"id": 581,
"name": "36x30"
},
{
"id": 582,
"name": "36x32"
},
{
"id": 583,
"name": "36x34"
},
{
"id": 584,
"name": "36x36"
},
{
"id": 585,
"name": "38x26"
},
{
"id": 586,
"name": "38x28"
},
{
"id": 587,
"name": "38x30"
},
{
"id": 588,
"name": "38x32"
},
{
"id": 589,
"name": "38x34"
},
{
"id": 590,
"name": "38x36"
}
]
},
"userErrors": []
}
},
"extensions": {
"complexity": 122,
"permissionsUsed": [
"SizeChart:write",
"SizeChart:read"
],
"appVersion": "v0.47.7"
}
}
With size charts and their sizes in place, everything that is needed to create the first product is available.
Create the first product#
Centra’s light-weight PIM can contain a lot of product data with the help of “custom attributes”. In this example, only the basic attributes will be taken into account. Each product in Centra contains three different “sections”. First, there’s the general section where the basic attributes are available, such as Product number, Product name, Collection, Country of Origin, and more. This section needs to be created before the next subset of data can be created in Centra.
Here’s an example of how a mutation to create the first part (General) of a product can look like. Remember: the id
you get back needs to be used in order to update the product at a later stage. Store it somewhere secure.
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
mutation addProduct {
createProduct(input: {
name: "First Product"
status: ACTIVE
productNumber: "Prod123"
brand: { id: 1 }
collection: { id: 2 }
folder: { id: 1 }
countryOfOrigin: { code: "DE" }
harmonizedCommodityCode: "HCC123"
harmonizedCommodityCodeDescription: "Harm Code Description"
}) {
product {
id
name
status
productNumber
brand { name }
collection { name }
folder { name }
harmonizedCommodityCode
harmonizedCommodityCodeDescription
}
userErrors {
message
path
}
}
}
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
{
"data": {
"createProduct": {
"product": {
"id": 1,
"name": "First Product",
"status": "ACTIVE",
"productNumber": "Prod123",
"brand": {
"name": "Base Brand"
},
"collection": {
"name": "AW21"
},
"folder": {
"name": "Shop"
},
"harmonizedCommodityCode": "HCC123",
"harmonizedCommodityCodeDescription": "Harm Code Description"
},
"userErrors": []
}
},
"extensions": {
"complexity": 121,
"permissionsUsed": [
"Product:write",
"Product:read",
"Product.Brand:read",
"Product.Collection:read",
"Product.Folder:read"
]
}
}
The first part of the product is now created, and it’s also possible to view it in Centra. It cannot be sold until we configure the rest of its sections. This is also where a new concept will be introduced: Variants.
Much like size charts and their sizes, variants are a core feature in Centra. Each product has at least one variant. Just as with size charts and sizes, if the product setup doesn’t really have variants, one variant will still need to be created. It’s also at the variant level where the size chart is selected. It's recommended to have no more than 10-20 variants per product. If there's need for more, please consult with Centra to find a good structure for the product data.
A variant has basic attributes and custom attributes. Just as the product’s general section. The most used basic attributes of the variant are Variant number and Variant name. Variant names many times will be the color name of the product, or even material in cases where a product comes in multiple materials rather than colors.
Let’s take a look at the example on how to create a variant. Notice that there’s a product id that needs to be sent with the query. This is the id
provided from the creation of the base product. A size chart id
must also be provided in order to tell Centra, which size chart should be assigned to this variant. Just as with the product, this id
was returned when the size chart was created.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mutation createVariant {
createProductVariant(input: {
product: { id: 1 }
name: "First Product"
status: ACTIVE
variantNumber: "Var456"
internalName: "vrnt2"
unitCost: { # MonetaryValueInput
value: 60
currencyIsoCode: "EUR"
}
sizeChart: { id: 2 }
}) {
productVariant {
id
}
userErrors {
message
path
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"data": {
"createProductVariant": {
"productVariant": {
"id": 2
},
"userErrors": []
}
},
"extensions": {
"complexity": 121,
"permissionsUsed": [
"ProductVariant:write",
"ProductVariant:read"
]
}
}
Much like with the product, an ID
for the variant will be returned. This is needed to update the variant and activate the sizes. Remember to store it.
Creating variants together with a product#
You can include variant creation in the createProduct
mutation directly in order to minimize the number of calls made. Have a look at this example:
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
mutation addProductWithVariants {
createProduct(
input: {
name: "First Product"
status: ACTIVE
productNumber: "Prod123"
addProductVariants: [
{
name: "First ProductVariant"
status: ACTIVE
variantNumber: "Var123"
internalName: "vrnt"
sizeChart: { id: 1 }
}
]
}
) {
product {
id
name
status
productNumber
variants {
name
status
variantNumber
internalName
sizeChart {
id
}
}
}
userErrors {
message
path
}
}
}
Create product sizes#
The last step to be made before the full core set of product data is ready, is to activate the desired sizes in the size chart selected on each variant of the product. To elaborate: when selecting a size chart for the variant, it doesn’t automatically get all sizes activated. The reason for this is that a size chart can contain more sizes than what the specific variant will be available in.
When activating the sizes for the variant, it’s also possible to add some data per size like EAN/UPC and size number. Size number can be a part of the SKU (when combined with product and variant SKUs), or it can contain the full SKU (similar to an EAN), depending on the needs of the integration. In the example you can see that the id
of the variant will be used. However, a size can also be identified by a name, because it's unique within a size chart.
Much like with the product and variant data. An ID
will be returned, which you should store as well. This will be needed if you need to update the EAN, as an example.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mutation createOneSize {
createProductSize(
input: {
productVariant: { id: 1 },
size: { id: 1 },
EAN: "EAN000111",
sizeNumber: "111"
}
) {
productSize {
id
EAN
sizeNumber
}
userErrors {
message
path
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"data": {
"createProductSize": {
"productSize": {
"id": 279,
"GTIN": "EAN000111",
"sizeNumber": "111"
},
"userErrors": []
}
},
"extensions": {
"complexity": 121,
"permissionsUsed": [
"ProductVariant:write",
"ProductSize:read"
]
}
}
Creating variants with activated sizes#
During initial configuration, it's likely you will need to activate multiple variant sizes. Using the createProductVariant
mutation with productSizes
input, you can do it with a single mutation:
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
mutation createVariantWithSizes {
createProductVariant(input: {
product: { id: 1 }
name: "Blue shirt"
status: INACTIVE
variantNumber: "876"
sizeChart: { id: 3 }
productSizes: [
{size: {name: "S"}, EAN: "5678902338761"},
{size: {name: "M"}, EAN: "5678902338762"},
{size: {name: "L"}, EAN: "5678902338763"},
]
}) {
productVariant {
id
productSizes {
...basicProductSizeFields
}
}
userErrors {
message
path
}
userWarnings {
message
path
}
}
}
Bundles#
Bundles consist of multiple sections, of which each can be selected from a pre-selected list of variants. The price of the bundle can be predefined, or calculated dynamically based on the prices of variants selected in the bundle. In the more complex, flexible bundles, the number of the variants in each section can differ as well.
A bundle is a virtual product, which only has one internal Variant by design. This variant also needs to be activated on a Display, just like any other Variant. Stock of each Bundle is calculated based on the contained section variants' stock amounts.
It’s possible to create bundles with the help of the Integration API. When creating the bundle, you’ll need to use the ID for each variant you want to include.
The fixed bundle type will work straight away in a client’s storefront as it will appear as any other product. Where the flexible bundle type will require new functionality in the storefront in order to work due to its more complex structure.
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
mutation createFixedBundle {
createBundle(
input: {
product: {
name: "Test bundle product"
status: ACTIVE
productNumber: "123"
brand: {id: 1}
collection: {id: 1}
countryOfOrigin: {id: 65}
folder: {id: 1}
harmonizedCommodityCode: "code"
harmonizedCommodityCodeDescription: "description"
}
type: FIXED
priceType: STATIC
sizeChart: {id: 2}
sizes: [
{name: "S"},
{name: "M"}
]
addBundleSections: [
{
quantity: 1
productVariants: [
{id: 1449}
]
},
{
quantity: 2
productVariants: [
{id: 1450}
]
}
]
}
) {
userErrors {message path}
bundle {
...bundleFields
}
}
}
Warehouses and inventory#
All of the core data needed for a product has now been added. Inventory can now be added to the product, let’s take a look on how to handle that.
Centra has an advanced way of allocating inventory to orders named “Allocation rules”, but before that at least one warehouse needs to be created. It’s possible to create as many warehouses as needed in Centra. One common setup is to have a single, or multiple, warehouses for fulfillment of D2C online orders and a warehouse for each brick and mortar store available - in order to display what’s available in stores or even to fulfill the orders from a store.
Warehouses might already be set up in Centra when the integration work starts, but it’s easy to create a warehouse if needed. See an example below. Just as with the product data, remember to store the ID you get back. This will be needed when inventory levels are sent to Centra.
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
mutation createWarehouse {
createWarehouse(
input: {
name: "Default warehouse"
beforeWarehouse: { id: 1 }
hideFromStockView: true
threshold: 1
stockMasterPolicy: EXTERNAL
stockOwnershipPolicy: THIRD_PARTY
allocationPolicy: CHECK_FIRST
warehouseLocation: { country: { id: 2 }, stateOrProvince: "nevada" }
}
) {
userErrors {
message
path
}
warehouse {
id
name
isHiddenFromStockView
threshold
stockMaster
stockOwnership
allocationPolicy
isConsignation
brickAndMortar {id name}
country {id name}
state {id name}
zipCode
}
}
}
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
{
"data": {
"createWarehouse": {
"userErrors": [],
"warehouse": {
"id": 22,
"name": "Default warehouse",
"isHiddenFromStockView": true,
"threshold": 1,
"stockMaster": "EXTERNAL",
"stockOwnership": "THIRD_PARTY",
"allocationPolicy": "CHECK_FIRST",
"isConsignation": false,
"brickAndMortar": null,
"country": {
"id": 2,
"name": "United States"
},
"state": {
"id": 29,
"name": "Nevada"
},
"zipCode": null
}
}
},
"extensions": {
"complexity": 117,
"permissionsUsed": [
"Warehouse:write",
"Warehouse:read",
"BrickAndMortar:read"
],
"appVersion": "v0.47.7"
}
}
With a warehouse in place, it’s easy to send inventory levels to Centra. Keep in mind that Centra keeps track of inventory, there’s no need to send stock updates when an order read from Centra has been fulfilled. It’s only necessary to sync the stock when there’s other types of stock movements made in the external system, like inbound deliveries, internal warehouse transfers etc.
There are two ways to set stock in Centra. You can either set the absolute physical inventory balance (what’s actually sitting on the warehouse shelf, including inventory reserved by unfulfilled orders) or send in stock changes. Take a look at the examples below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
mutation addStock {
changeStock (
input: {
intoWarehouse: {id: 3}
description: "New stock"
productVariants: [
{
productVariant: {id: 2}
unitCost: {
value: 41
currencyIsoCode: "EUR"
}
sizeChanges: {
size: {name: "S"}
deliveredQuantity: 2
}
}
]
}
) {
stockChange {id}
userErrors {message}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"data": {
"changeStock": {
"stockChange": {
"id": 4284
},
"userErrors": []
}
},
"extensions": {
"complexity": 121,
"permissionsUsed": [
"StockChange:write",
"WarehouseDelivery:read"
]
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
mutation removeStock {
changeStock (
input: {
outFromWarehouse: { id: 1 }
description: "Remove stock"
productVariants: [
{
productVariant: {id: 1}
unitCost: {
value: 41
currencyIsoCode: "EUR"
}
sizeChanges: {
size: {id: 1} # One Size
deliveredQuantity: 5
}
}
]
}
) {
stockChange {id}
userErrors {message}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"data": {
"changeStock": {
"stockChange": {
"id": 4284
},
"userErrors": []
}
},
"extensions": {
"complexity": 121,
"permissionsUsed": [
"StockChange:write",
"WarehouseDelivery:read"
]
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mutation absoluteStock {
setStock(input: {
warehouse: {id: 1}
description: "A typical stock update"
stockQuantityType: PHYSICAL
productVariants: [
{
productVariant: {id: 1445}
sizeStockLevels: [
{size: {name: "XL"}, quantity: 10},
]
}
]
}) {
userErrors {
message
path
}
stockChanges {
id
}
}
}
Pricelists and Price Alterations#
With products and inventory in place, there’s some more data needed to be fully able to start selling a product. One of these are prices. Prices in Centra are set with a price list. A price list in Centra can be in any currency. Multiple price lists with the same currency are also supported.
Prices can be set on variant level, but not size. By default, all variants of the same product share the same price, but you can choose to enable individual variant prices for some or all variants. When creating a price list, there are multiple data points that can be added, but not all of them are mandatory during the creation phase, since some oftentimes are set by the client in Centra. Let’s take a look at what you must send in and an example after that.
Name: the name of the pricelist. Could be as simple as the currency of the pricelist or something with a longer description.
Store: This is the ID of the store that the price list should be added to.
Currency: set with
currencyIsoCode
. Self-explanatory, the currency of the pricelist.
Other fields like Default shipping option, adding countries, can also be done upon creation or by updating the pricelist. But as stated earlier, these are oftentimes set by the client directly in the Centra admin, however the example contains them. Countries are only added for pricelists in a DTC store. In Wholesale, there’s also an option to add RRP prices to the pricelist.
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
mutation CreatePricelist {
createPricelist(input: {
name: "Test pricelist SEK"
status: INACTIVE
store: {
id: 1
}
currencyIsoCode: "SEK"
defaultShippingOption: {
id: 1
},
addCountries: [
{
code: "SE"
}
]
}) {
pricelist {
id
name
status
store { id name type }
currency { code }
assignedToCountries { id name }
defaultShippingOption { id name }
}
userErrors { message path }
}
}
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
{
"data": {
"createPricelist": {
"pricelist": {
"id": 61,
"name": "Test pricelist SEK",
"status": "INACTIVE",
"store": {
"id": 1,
"name": "Retail",
"type": "DIRECT_TO_CONSUMER"
},
"currency": {
"code": "SEK"
},
"assignedToCountries": [
{
"id": 6,
"name": "Sweden"
}
],
"defaultShippingOption": {
"id": 1,
"name": "SEK"
}
},
"userErrors": []
}
},
"extensions": {
"complexity": 125,
"permissionsUsed": [
"Pricelist:write",
"Pricelist:read",
"Store:read",
"Country:read",
"ShippingOption:read"
],
"appVersion": "v0.47.7"
}
}
When the price list is created, it’s now time to add the actual prices to the products. As usual, use the id for the pricelist that was returned upon its creation. Here’s an example of how setting the prices work:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mutation {
setPrices(
input: {
pricelist: { id: 23 }
productPrices:[
{
product: {id: 2}
price: { value: 290, currencyIsoCode: "EUR" }
}
]
}
)
{
pricelist {
id
}
userErrors { message path }
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"data": {
"setPrices": {
"pricelist": {
"id": 23
},
"userErrors": []
}
},
"extensions": {
"complexity": 112,
"permissionsUsed": [
"Price:write",
"Pricelist:read"
],
"appVersion": "v0.47.7"
}
}
Price Alterations
For Wholesale, Centra offers functionality called “Price alteration”. This is a function used to have a different price point for products. This is many times used for carry over products which will see a price increase in a new collection. You can only have one active price alteration object per Wholesale store.
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
{
"data": {
"createPriceAlteration": {
"priceAlteration": {
"id": 4,
"name": "First price alteration",
"startDate": "2024-02-01",
"status": "INACTIVE",
"store": {
"id": 2
},
"deliveryWindows": []
},
"userErrors": []
}
},
"extensions": {
"complexity": 133,
"permissionsUsed": [
"Price:write",
"Price:read",
"Store:read",
"DeliveryWindow:read"
],
"appVersion": "v0.47.7"
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mutation CreatePriceAlteration {
createPriceAlteration(
input: {
name: "First price alteration"
status: INACTIVE
store: {
id: 2
}
startDate: "2024-02-01T00:00:00+0000"
}
) {
priceAlteration {
id
name
startDate
status
store { id }
deliveryWindows { id }
}
userErrors { message path }
}
}
When the Price Alteration is created, the specific prices for the products can also be added. Take a look at the example below to get an understanding on how to handle that.
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
mutation SetAlteredPrices {
setAlteredPrices(input: {
priceAlteration: {
id: 1
},
pricelist: {
id: 21
},
productPrices: [
{
product: {
id: 1
}
price: {
value: 1000
currencyIsoCode: "SEK"
}
}
]
}){
priceAlteration {
id
}
pricelist {
id
}
products {
id name
}
userErrors {
message path
}
}
}
Set Price & Alterations batchsetPrices
and setAlteredPrices
allow to change prices only for a single pricelist, which can introduce unnecessary overhead if prices for multiple pricelists have to be updated. New mutations setPricesBatch
and setAlteredPricesBatch
have been introduced to overcome that. Their input is basically the same as for their single pricelist counterpart, but an array. Below is an example of therse mutations in action:
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
mutation setPricesBatch {
setPricesBatch(input: [
{
pricelist: {id: 19}
productPrices: {
product: {id: 1}
price: {value: 2 currencyIsoCode: "USD"}
overrideIndividualVariantPrices: true
#individualVariantPrices
}
}
{
pricelist: {externalId: "19"}
productPrices: [
{
product: {id: 1}
price: {value: 100 currencyIsoCode: "EUR"}
#recommendedRetailPrice: {value: 10 currencyIsoCode: "USD"}
overrideIndividualVariantPrices: true
#individualVariantPrices
}
{
product: {id: 2}
price: {value: 200 currencyIsoCode: "EUR"}
#recommendedRetailPrice: {value: 10 currencyIsoCode: "USD"}
overrideIndividualVariantPrices: true
#individualVariantPrices
}
]
}
]) {
userErrors {message path}
userWarnings {message path}
pricelists {
id name
prices {
product {id}
productVariant {id}
price {value}
recommendedRetailPrice {value}
}
}
products {id name}
}
}
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
mutation SetAlteredPricesBatch {
setAlteredPricesBatch(input: [
{
priceAlteration: {
id: 1
}
pricelist: {
id: 65
},
productPrices: [
{
product: {
id: 1293
}
price: {
value: 10
currencyIsoCode: "EUR"
}
overrideIndividualVariantPrices: true
}
{
product: {
id: 67
}
price: {
value: 11
currencyIsoCode: "EUR"
}
overrideIndividualVariantPrices: true
}
]
}
{
priceAlteration: {
id: 1
}
pricelist: {
id: 66
},
productPrices: [
{
product: {
id: 1293
}
price: {
value: 12
currencyIsoCode: "GBP"
}
overrideIndividualVariantPrices: true
}
{
product: {
id: 67
}
price: {
value: 13
currencyIsoCode: "GBP"
}
overrideIndividualVariantPrices: true
}
]
}
]) {
userErrors {
message path
}
userWarnings {message path}
pricelists {
id
}
products {
id name
}
priceAlterations {
id
name
prices(where: {pricelistId: [65, 66] productId: [1293, 67]}) {
pricelist {
id
}
product {
id
}
price {value}
}
}
}
}
Product displays#
There’s one last thing that needs to be done in order to get products that are fully sellable. They’re called Displays and their function is to connect products to specific categories, assign product images, select for which Market they should be available for, and more. Depending on the project scope, Displays might be handled directly in Centra by the client and that’s why it’s saved for last. However, they can also be created directly through an integration. To make it less abstract, we can call the Display the product presentation layer. Just like in the real world you are not just sending your customers to browse your warehouse, but instead display your products in a way that will entice a sale, Displays in Centra can be used to control how your backend products will look like in your Stores. The beauty of displays is that you can configure one Display per Store, which means you can sell the same products from different stores using different categories, media and metadata, depending on requirements.
Displays are connected to a specific Store and if a client has multiple stores in Centra and wants to sell the product in all stores, a display must be created for each store. Commonly a DTC and Wholesale store.
A Display has a lot of parameters, most of which are optional. Take a look at the full set below
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
input DisplayCreateInput {
# basic required fields
store: StoreInput!
product: ProductInput!
name: String!
status: Status!
# basic optional fields
uri: String (If not provided, it will be auto-generated based on the display name)
minimumOrderQuantity: Int
orderQuantityDenominator: Int
description: String
shortDescription: String
metaTitle: String
metaDescription: String
metaKeywords: String
comment: String
tags: [String!]
# relations to other types
canonicalCategory: CategoryInput
addCategories: [CategoryInput!]
addMarkets: [MarketInput!]
addProductMedia: [ProductMediaAddInput!]
addProductVariants: [ProductVariantAddInput!]
taxGroup: TaxGroupInput
}
Let’s take a look at how to create a basic Display with one activated Variant.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mutation create {
createDisplay(input: {
name: "New display!"
status: ACTIVE
store: {id: 1}
product: {id: 2}
addProductVariants: [
{productVariant: {id: 1545}}
]
}) {
userErrors {
message path
}
display {
id
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"data": {
"createDisplay": {
"userErrors": [],
"display": {
"id": 2276
}
}
},
"extensions": {
"complexity": 112,
"permissionsUsed": [
"Display:write",
"Display:read"
],
"appVersion": "v0.47.7"
}
}
With this query, a display named “New Display!” will be created in the store with ID 1 for the product with ID 2. With the mutation we also added one of the product's variants to the display. You can only add variants of the same product to a Display.
The ID which was returned when creating the Display should be saved, if you need to update the Display at a later stage, like adding a second variant to the display. See an example of that below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
mutation addVariantsToDisplay {
updateDisplay(
id: 2276
input: [ addProductVariants: { productVariant: { id: 1446 } } ]
) {
userErrors {
message
path
}
display {
id
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"data": {
"updateDisplay": {
"userErrors": [],
"display": {
"id": 2276
}
}
},
"extensions": {
"complexity": 112,
"permissionsUsed": [
"Display:write",
"Display:read"
],
"appVersion": "v0.47.7"
}
}
Wholesale#
With products and price lists in place, products can now be sold in a DTC store (when displays have been created). But for Wholesale we also need Accounts to be created along with Delivery Windows. Let’s go through the setup of an Account in Wholesale and after that the Delivery Window functionality.
Account creation
With payment and shipping terms in place, it’s about time to create the first Wholesale Account in Centra. An Account in Centra will hold all of the necessary information like Invoice address, Shipping Address (multiple ship-to addresses available through “Address book”), discounts, and buyers. Note: in order for an Account to be fully functional, at least one Buyer needs to be added to the account. It is the Buyers who can access Centra B2B Showroom and place orders for their parent Account.
There’s quite a lot of data that can be added to an Account. Either during its creation or by updating it later. Please see a list below with a short explanation on what these different data sets are and if they’re mandatory or not.
name
: The name of the Account.status
: Status, as in active, inactive or canceled.internalComment
: An internal comment field for the Centra client. Optional.otherComment
: Other comments will be visible for the Account. Optional.websiteUrl
: A field for a URL. Optional.creditLimit
: Limit on how much the Account can shop for before settling their outstanding invoices. Optional.discounts
: Discount. General percentage based discount specific for this Account. Can also be set for specific delivery windows. Optional.applyCreditLimit
: If credit limit should be enforced. Optional.blockIfUnpaidInvoices
: Used to block new orders if there are unpaid invoices. Optional.hasBrandsRestriction
: Use to set which brands the Account will be able to see. Optional.isInternal
: Flag an account as an “Internal Account”. Optional. Oftentimes used to place internal orders.carrierInformation
: Fields to store preferred carrier for fulfillment for the Account. Optional.market
: Which market the Account should belong to.pricelist
: The pricelist that the Account will use.allocationRule
: Used to set another than the default Allocation rule. Optional.paymentTerms
: Set the Payment term for the account.shippingTerms
: Set the Shipping term for the account.salesRepresentative
: Set the sales rep for the account. Optional.taxClass
: Set tax class for account. Optional.documentTemplate
: Used to set the Document template that will be used for the Account. Optional.addBrands
: Add new brands that the Account will be able to access. Only used if brands are to be used as a restriction. Optional.addVisibleForAgents
: Add which agents that should have access to the Account. Optional.accountAddress
: Fields for Account address. The account address is the registered address of the company.shippingAddress
: Fields for default Shipping address.billingAddress
: Fields for the billing address (invoice address)
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
mutation createAccount {
createAccount(input: {
status: INACTIVE
name: "Test Account"
market: {id: 2}
pricelist: {id: 21}
discounts: {
generalDiscount: {
discountPercent: 14.3
isVisible: true
}
# addDeliveryWindowDiscounts: [
# {deliveryWindow: {id: 1} discountPercent: 12.3}
# {deliveryWindow: {id: 2} discountPercent: 23.4}
# ]
}
}) {
userErrors {
message path
}
account {
id
discountPercent
isDiscountVisible
deliveryWindowDiscounts {
deliveryWindow {
id
}
discountPercent
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"data": {
"createAccount": {
"userErrors": [],
"account": {
"id": 132,
"discountPercent": 14.3,
"isDiscountVisible": true,
"deliveryWindowDiscounts": []
}
}
},
"extensions": {
"complexity": 142,
"permissionsUsed": [
"Account:write",
"Account:read"
],
"appVersion": "v0.47.7"
}
}
Adding a buyer
With the Account created, a Buyer for the specific account can be added. As mentioned earlier, at least one Buyer needs to be created for each account.
The mutation for creating a Buyer has below fields:
account
: This is the ID of the account where the buyer should be createdstore
: The ID of the store used. It’s not common to run multiple Wholesale stores in one Centra instance, but it can happen.status
: Status of the buyer.websiteUrl
: Field to store a URLreceiveAutoEmails
: Boolean to define whether or not the Buyer should receive emails (order confirmation, shipping confirmation etc)receiveNewsletters
: Boolean to define whether ot not the Buyer should be flagged to accept newslettersbillingAddress
: Address fields for the buyer. Not commonly used.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
mutation createBuyer {
createBuyer(input: {
store: {id: 2}
status: ACTIVE
account: {id: 5}
websiteUrl: "Example URL"
receiveAutoEmails: true
billingAddress: {
email: "jon.snow@centra.com"
}
}) {
userErrors { message path }
buyer {
id
status
account { id name }
receiveAutoEmails
websiteUrl
billingAddress {
email
}
}
}
}
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
{
"data": {
"createBuyer": {
"userErrors": [],
"buyer": {
"id": 1902,
"status": "ACTIVE",
"account": {
"id": 5,
"name": "Centra 5"
},
"receiveAutoEmails": true,
"websiteUrl": "Example URL",
"billingAddress": {
"email": "jon.snow@centra.com"
}
}
}
},
"extensions": {
"complexity": 114,
"permissionsUsed": [
"Buyer:write",
"Buyer:read",
"Account:read",
"Buyer.billingAddress:read"
],
"appVersion": "v0.47.7"
}
}
Delivery windows
Next up are Delivery Windows. Delivery Windows control how goods are sold in Centra Wholesale, and must be created in order to enable sales. Delivery Windows can be configured with different Variant Delivery Types which defines how goods availability is displayed for a Buyer. Let’s take a look at them to get an understanding which to use.
Preorder
: The preorder type is a common type used to enable pre orders before inventory is available. For fashion a common use case is to take orders for a new collection. The amount of units available can still be limited with this type.Stock
: The stock type will only show what’s actually available to order right now, based on allocation rules for the Account used by a Buyer. In combination with allowing backorders, it’s still possible to place orders.Link
: Not as commonly used as Preorder and Stock. Link works in conjunction with Centra’s Supplier module, allowing one to sell units still not reserved on incoming Supplier orders.Stock/Link
: A combination of Stock and Link.
When creating a Delivery Window,
preorder
andatOnce
is also to be included, independently from the Variant Delivery Type. Usepre order
for Delivery Windows with the Preorder Variant Delivery Type, with this a date range for accepting orders can be set. UseatOnce
for Stock type, which is sold - at once.At least one Market needs to be added to the Delivery Window, use the ID of a Market previously created or fetch Markets to find their IDs.
In below example a Preorder Delivery Window is created
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
mutation createPreorderDelWin {
createDeliveryWindow(input: {
name: "First PreOrder DelWin"
status: ACTIVE
deliveryDatesVisible: true
selectableByBuyers: true
selectedByDefault: false
defaultVariantDeliveryType: STOCK
allocationRule: { id: 2 }
markets: [{ id: 2 }]
# preorder specific fields
preorder: {
deliveryDateRange: { from: "2022-08-01", to: "2022-10-31" }
salesDateRange: { from: "2022-08-01", to: "2022-10-31" }
allocationOrder: FIFO
}
}) {
userErrors {
message
path
}
deliveryWindow {
id
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"data": {
"createDeliveryWindow": {
"userErrors": [],
"deliveryWindow": {
"id": 26
}
}
},
"extensions": {
"complexity": 113,
"permissionsUsed": [
"DeliveryWindow:write",
"DeliveryWindow:read"
],
"appVersion": "v0.47.7"
}
}
With a Delivery Window in place, it’s time to add products to it. When it comes to Delivery Windows, it’s a product’s variant(s) that will be added to the delivery window. Use the ID for the variant(s) which was returned during its creation, or find the desired variant(s) info based on their name or number.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mutation setVariants {
setDeliveryWindowVariants(input: {
deliveryWindow: { id: 3 }
variants: [
{
productVariant: { id: 1844 }
type: STOCK
},
{
productVariant: { id: 1845 }
type: STOCK
}
# Up to 100 variants can be specified in a single mutation
]
}) {
userErrors {
message
path
}
deliveryWindow {
id
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"data": {
"setDeliveryWindowVariants": {
"userErrors": [],
"deliveryWindow": {
"id": 3,
}
},
"extensions": {
"complexity": 193,
"permissionsUsed": [
"DeliveryWindow:write",
"DeliveryWindow:read",
"Market:read",
"Campaign:read",
"AllocationRule:read",
"Product:read"
],
"appVersion": "v0.26.0"
}
}
Vouchers#
Vouchers in Centra can help you add discounts and manage custom promotions in your store. Click here to learn more.
You can either create a voucher separately and then create actions for it, or you can manage that in one call.
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
mutation addVoucherWithActions {
createVoucher(
input: {
name: "Awesome code discount"
status: ACTIVE
startAt: "2024-04-01 11:22"
stopAt: "2026-04-01 11:22"
method: CODE
code: "uniqueCode"
url: null
priority: 9
maxUsages: 100
reuse: true
type: DISCOUNT
entryPointStrategy: APPLY_ALL_IF_ALL_APPLY
conversionHTML: "Some HTML"
store: { id: 1 }
addMarkets: [{ id: 1 }]
actions: [
{
entrypointAction: { id: null } # meaning this action is an entry point
addCriteria: [{ always: true }]
result: {
selectedProducts: {
applyOn: ORDER
discount: { percentOff: 12.00 }
}
}
continueStrategy: SELECTED_PRODUCTS
}
{
entrypointAction: { id: null }
addCriteria: [{ always: true }]
result: { freeShipping: { shippingOptions: [{ id: 1 }] } }
continueStrategy: ALL_PRODUCTS
}
]
}
) {
userErrors {
message
path
}
voucher {
actions {
isEntryPoint
criteria {
type
}
result {
type
}
continueStrategy
}
id
name
status
startAt
stopAt
method
code
url
priority
maxUsages
combineWithOtherVouchers
reuse
type
entryPointStrategy
conversionHTML
store {
id
}
markets {
id
name
}
}
}
}
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
{
"data": {
"createVoucher": {
"userErrors": [],
"voucher": {
"actions": [
{
"isEntryPoint": true,
"criteria": [
{
"type": "ALWAYS"
}
],
"result": {
"type": "CHANGE_SELECTED_PRODUCTS_PRICE"
},
"continueStrategy": "SELECTED_PRODUCTS"
},
{
"isEntryPoint": true,
"criteria": [
{
"type": "ALWAYS"
}
],
"result": {
"type": "SET_FREE_SHIPPING"
},
"continueStrategy": "ALL_PRODUCTS"
}
],
"id": 6,
"name": "Awesome code discount",
"status": "ACTIVE",
"startAt": "2024-04-01T11:22:00+02:00",
"stopAt": "2026-04-01T11:22:00+02:00",
"method": "CODE",
"code": "uniqueCode",
"url": null,
"priority": 9,
"maxUsages": 100,
"combineWithOtherVouchers": true,
"reuse": true,
"type": "DISCOUNT",
"entryPointStrategy": "APPLY_ALL_IF_ALL_APPLY",
"conversionHTML": "Some HTML",
"store": {
"id": 1
},
"markets": [
{
"id": 1,
"name": "A"
}
]
}
}
},
"extensions": {
"complexity": 243,
"permissionsUsed": [
"Voucher:write",
"Voucher:read",
"Store:read",
"Market:read"
]
}
}
Order processing#
With products in place, orders should start pouring in. Let’s go through how to read orders and how to work with fulfillment in Centra.
Before we do that, let’s talk about the standard order flow in Centra. It’s described in detail here:
https://docs.centra.com/overview/orderflow#order-flow
Order statuses#
There are 6 possible order statuses in Centra:
Pending
: This order has just been authorized and inserted into Centra. It needs to be confirmed before it can be processed further. You can skip this status by enabling the Store SettingAutoconfirm orders
, which will automatically put all new orders in Confirmed status. In the past customers used Pending to manually review all new orders, which is only feasible if you have a relatively low number of them every day.Confirmed
: This order has been confirmed, either via API, in AMS backend, or the store setting. Once confirmed, an order confirmation email can be sent to the shopper. Now you are ready to start creating shipments.Processing
: This order has at least one shipment, and at least one of those shipments is not completed. You can also enable Store SettingDirect to Shipment
, which will automatically create GTG shipments for you, one per order. Each shipment in Centra has 3 sub-statuses:Good-to-go
: Set this boolean once your warehouse staff have collected the order. Good-to-go means all shipment items are packed in a parcel, with address labels attached, and any other documents that might be required. But we are not shipping this parcel just yet.Paid
: This denotes that the shipment has been paid for - the sum of shipped order items and shipping cost have been captured. If you are capturing payments outside of Centra, instead of capturing you can simply mark the shipment as Paid, without triggering a capture. Remember, in some countries it's against regulations to capture money for an un-shipped package, so make sure you know exactly what you’re doing, and when.Shipped
: Once the shipment is good-to-go and paid for, nothing stops you from shipping it - meaning handing it over to the delivery person and obtaining its tracking number. You can add the tracking information at the moment you complete your shipment. Remember, Centra doesn’t track parcel delivery, so from our perspective the shipment is completed as soon as the parcel is out the door and on its way to the shopper.
Completed
: All the order items have been shipped (in one or multiple shipments), and the money for order items + shipping cost has been captured (or marked as Paid). As soon as the final shipment is completed, the order will be completed as well. GraphQL calls this order status “SHIPPED”.Archived
: Completed orders can be hidden from the default list view, and possibly from the API responses, when you choose to archive them. Archiving doesn’t trigger any special automations, but you may still be interested in marking an completed order as archived in your ERP, if you wish to.Canceled
: This order will not be completed, it has been canceled either by manual intervention in the AMS, or via the API. Canceling the order also cancels the payment, so this process is irreversible, you won’t be able to capture the order total after you have canceled it. WARNING: It is highly incorrect to cancel the order after it’s been captured. Please only cancel orders before that happens. If payment has already been captured, you should create a Return and refund the money instead.
Fetching orders#
Since the orders are created mostly in front ends (using webshop APIs or Showroom), likely the very first action your integration will take is to read that new order info. You have a number of ways to do it.
First, if you implement Centra Webhooks plugin running in “Integration API” mode, you can configure it to be notified about the new orders and shipments as soon as they are created. The easiest scenario will be when you receive a webhook of type: order
, action: insert
, together with the order number. Right when that happens, you can use GraphQL API to fetch this one specific order and sync it with your ERP.
1
2
3
4
5
6
7
8
9
10
11
12
13
query getOrders {
orders(where: { number: 14 }) {
number
status
isOnHold
shippingAddress {
firstName
lastName
city
}
...customerId
}
}
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
{
"data": {
"orders": [
{
"number": 14,
"status": "PENDING",
"isOnHold": false,
"shippingAddress": {
"firstName": "Jon",
"lastName": "Snow",
"city": "Winterfell"
},
"customer": {
"id": 4,
"email": "jon.snow@example.com"
}
}
]
},
"extensions": {
"complexity": 229,
"permissionsUsed": [
"Order:read",
"Order.shippingAddress:read",
"Purchaser:read"
],
"appVersion": "v0.32.3"
}
}
Depending on the fields you chose to return from the query, you can immediately access all the data relevant to your ERP, like name, address(es), list of ordered products, customer ID and email, order totals, tax breakdown, etc.
Locking orders
Depending on your design, there is one function that you might be very interested in: The ability to lock the orders using the API. The idea behind the locking mechanism is that you might want to prevent AMS backend users from editing or processing the orders which should be exclusively handled by the API. Therefore, locking ensures that only your API integration will be able to add/cancel order items, create and complete shipments, etc.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mutation lockOrders {
setOrdersLock(
input: {
orders: [{ number: 39514 }, { number: 39515 }]
isLocked: true }
) {
userErrors {
message
path
}
orders {
number
status
isLocked
shippingAddress {
firstName
lastName
}
}
}
}
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
{
"data": {
"setOrdersLock": {
"userErrors": [],
"orders": [
{
"number": 39514,
"status": "PROCESSING",
"isLocked": true,
"shippingAddress": {
"firstName": "Pio",
"lastName": "Sym"
}
},
{
"number": 39515,
"status": "PROCESSING",
"isLocked": true,
"shippingAddress": {
"firstName": "Pio",
"lastName": "Sym"
}
}
]
}
},
"extensions": {
"complexity": 131,
"permissionsUsed": [
"Order.isLocked:write",
"Order:read",
"Order.shippingAddress:read"
],
"appVersion": "v0.30.0"
}
}
Order updates#
Now that we’ve read the order, we can choose what to do with it. First of all, if there were any mistakes, typos in names or addresses or similar, you can update basic order fields using this mutation (similar mutation exists for Wholesale):
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
mutation updateDtcBasicFields {
updateDirectToConsumerOrder(
order: {
# id: "1497ccf644db871e1e4026d101bde6f3"
number: 14
}
input: {
shippingAddress: {
firstName: "Jon"
lastName: "Snow"
address1: "Teststr. 1"
address2: "1b"
city: "Stockholm"
zipCode: "12345"
email: "jon.snow@example.com"
}
billingAddress: {
firstName: "Jon"
lastName: "Snow"
address1: "Teststr. 1"
address2: "1b"
city: "Stockholm"
zipCode: "12345"
email: "jon.snow@example.com"
}
customer: {
id: 4
}
customerInfo: {
firstName: "Jon"
lastName: "Snow"
email: "jon.snow@example.com"
}
isInternal: false
}
) {
order {
...orderInfo
}
userErrors { message path }
}
}
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
116
117
118
119
{
"data": {
"updateDirectToConsumerOrder": {
"order": {
"id": "1497ccf644db871e1e4026d101bde6f3",
"number": 14,
"isOnHold": false,
"lines": [
{
"id": 68,
"product": {
"name": "Basic Jacket"
},
"quantity": 10,
"taxPercent": 25,
"unitPrice": {
"value": 675,
"currency": {
"id": 3,
"code": "SEK"
},
"conversionRate": 1
},
"hasAnyDiscount": false,
"unitOriginalPrice": {
"value": 675,
"currency": {
"id": 3,
"code": "SEK"
},
"conversionRate": 1
},
"lineValue": {
"value": 6750,
"currency": {
"id": 3,
"code": "SEK"
},
"conversionRate": 1
}
}
],
"discountsApplied": [
{
"value": {
"value": 0,
"currency": {
"id": 3,
"code": "SEK"
},
"conversionRate": 1
},
"date": "2022-11-09T13:15:11+01:00"
}
],
"shippingAddress": {
"firstName": "Jon",
"lastName": "Snow",
"address1": "Teststr. 1",
"address2": "1b",
"city": "Stockholm",
"zipCode": "12345",
"stateOrProvince": null,
"cellPhoneNumber": null,
"phoneNumber": "+4684026100",
"faxNumber": null,
"email": "jon.snow@example.com",
"companyName": null,
"attention": null,
"vatNumber": null,
"country": {
"id": 6,
"name": "Sweden"
},
"state": null
},
"billingAddress": {
"firstName": "Jon",
"lastName": "Snow",
"address1": "Teststr. 1",
"address2": "1b",
"city": "Stockholm",
"zipCode": "12345",
"stateOrProvince": null,
"cellPhoneNumber": null,
"phoneNumber": "+4684026100",
"faxNumber": null,
"email": "jon.snow@example.com",
"companyName": null,
"attention": null,
"vatNumber": null,
"country": {
"id": 6,
"name": "Sweden"
},
"state": null
},
"customer": {
"email": "jon.snow@example.com",
"firstName": "Jon",
"lastName": "Snow"
}
},
"userErrors": []
}
},
"extensions": {
"complexity": 229,
"permissionsUsed": [
"Order:write",
"Order:read",
"Order.shippingAddress:read",
"Order.billingAddress:read",
"Purchaser:read",
"Product:read"
],
"appVersion": "v0.32.3"
}
}
Next, let’s talk about modifying the order lines, which translates to adding or removing products to/from an order after it has been placed in Centra. The most common use case is canceling an order item which was sold by mistake, when you realize you don’t have the stock to actually fulfill it. If that’s the case, you can cancel this order line, and later ship only available products, while capturing less money that was authorized, so that the customer pays the right order total.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
mutation updateDtcCancel {
updateDirectToConsumerOrder(
order: {
number: 14
}
input: {
cancelLines: [
{
line: {
id: 68
}
quantity: 1
stockAction: RELEASE_BACK_TO_WAREHOUSE
}
]
cancellationComment: "Some good reason"
}
) {
order {
...orderInfo
}
userErrors { message path }
}
}
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
116
117
118
119
120
{
"data": {
"updateDirectToConsumerOrder": {
"order": {
"id": "1497ccf644db871e1e4026d101bde6f3",
"number": 14,
"status": "PENDING",
"isOnHold": false,
"lines": [
{
"id": 68,
"product": {
"name": "Basic Jacket"
},
"quantity": 9,
"taxPercent": 25,
"unitPrice": {
"value": 675,
"currency": {
"id": 3,
"code": "SEK"
},
"conversionRate": 1
},
"hasAnyDiscount": false,
"unitOriginalPrice": {
"value": 675,
"currency": {
"id": 3,
"code": "SEK"
},
"conversionRate": 1
},
"lineValue": {
"value": 6075,
"currency": {
"id": 3,
"code": "SEK"
},
"conversionRate": 1
}
}
],
"discountsApplied": [
{
"value": {
"value": 0,
"currency": {
"id": 3,
"code": "SEK"
},
"conversionRate": 1
},
"date": "2022-11-09T13:15:11+01:00"
}
],
"shippingAddress": {
"firstName": "Jon",
"lastName": "Snow",
"address1": "Teststr. 1",
"address2": "1b",
"city": "Stockholm",
"zipCode": "12345",
"stateOrProvince": null,
"cellPhoneNumber": null,
"phoneNumber": "+4684026100",
"faxNumber": null,
"email": "jon.snow@example.com",
"companyName": null,
"attention": null,
"vatNumber": null,
"country": {
"id": 6,
"name": "Sweden"
},
"state": null
},
"billingAddress": {
"firstName": "Jon",
"lastName": "Snow",
"address1": "Teststr. 1",
"address2": "1b",
"city": "Stockholm",
"zipCode": "12345",
"stateOrProvince": null,
"cellPhoneNumber": null,
"phoneNumber": "+4684026100",
"faxNumber": null,
"email": "jon.snow@example.com",
"companyName": null,
"attention": null,
"vatNumber": null,
"country": {
"id": 6,
"name": "Sweden"
},
"state": null
},
"customer": {
"email": "jon.snow@example.com",
"firstName": "Jon",
"lastName": "Snow"
}
},
"userErrors": []
}
},
"extensions": {
"complexity": 229,
"permissionsUsed": [
"Order:write",
"Order:read",
"Order.shippingAddress:read",
"Order.billingAddress:read",
"Purchaser:read",
"Product:read"
],
"appVersion": "v0.32.3"
}
}
When it comes to adding order lines, be very mindful about it. While it’s usually perfectly OK in Wholesale orders, where the order oftentimes will be paid by invoice and there is no payment authorization, please note that adding a product to a DTC order would increase the order total, which would then be higher than the amount that was authorized during purchase. If you do that, you won’t be able to capture more money than was previously authorized.
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
mutation updateWholesaleAddProducts {
updateWholesaleOrder(
order: {
# id: "c8b6e87b1d9408f845a0440d226696df"
number: 3957
}
input: {
addLines: [
{
display: {
id: 1
}
productSize: {
id: 1
}
quantity: 1
unitPrice: {
value: 110.00
currencyIsoCode: "SEK"
}
taxGroup: {
id: 2
}
}
]
}
) {
order {
...orderInfo
}
userErrors { message path }
}
}
Special case - changing quantity of the existing order line
If you wish for the updateOrder
mutation to add quantity to an existing order line, instead of creating a new order line, you can do that if you match the parameters of addLines
input precisely with the existing line. This means that you must specify exactly the same:
Display ID
Product size ID
Unit price (both currency and amount)
Order line comment (if any)
Delivery window
Changing any of these properties will result in Centra recognising this input as a new order line, instead of changing the existing one. If the name of the product has changed between when the order was placed and when you're modifying the order lines, this product will always be added as a new order line, since the order holds the historical name of the product, which will not match products in Centra any more.
If you instead wish to decrease the order line quantity, you can call updateOrder
mutation with cancelLines input, specifying the order line ID and quantity to be cancelled. Cancelling all quantities available will cancel the entire order line.
Confirming an order#
Now that our order looks as expected, let’s process it further. First step (possible to skip with Store Settings) is to confirm the order. Once done, an order confirmation email can be sent to the shopper, depending on your setup and mailer of choice.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mutation confirmOrder {
confirmOrder(
input: {
order: {
number: 14
}
}
) {
order {
number
status
}
userErrors { message path }
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"data": {
"confirmOrder": {
"order": {
"number": 14,
"status": "CONFIRMED"
},
"userErrors": []
}
},
"extensions": {
"complexity": 112,
"permissionsUsed": [
"Order:write",
"Order:read"
],
"appVersion": "v0.32.3"
}
}
Shipment creation and capture#
After the order is confirmed, you can now create shipments and capture the money. You can read more about capture with the Integration API here.
I mentioned before that captures are most closely related to shipments, and in most cases you should only be capturing money for the items you are about to ship (+ shipping cost). Centra supports split payment captures, if the integration with the PSP allows for it, so you won’t have any problems with that, as long as you remember to only include the shipping cost on one of the shipments.
There is another way, though, which can make things easier in some integrations. In GraphQL we’ve added a mutation which allows you to capture the entire order amount, even before you have shipments created. This might be convenient in some cases, e.g. if you’re selling “virtual” goods, like gift cards, and need to capture the payment immediately to avoid fraud. This might also be a good idea if you expect that the goods will not be shipped for a while (e.g. you’re selling pre-order products), while you only have a specific time available to perform the capture, based on the payment method used (usually 14/30/60 days after auth).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mutation captureOrder {
captureOrder(order: {id: "05ac658a8e815571fdba2984eb358932"
# number: 10 }) {
userErrors {
message
path
}
paymentHistoryEntry {
createdAt
status
entryType
externalReference
value {value}
paymentMethod
paramsJSON
}
order {
id
}
}
}
If you choose to only capture the money for items that were actually shipped, you can split your shipment processing into 3 stages:
First, let’s create a shipment which is not paid for, only specifying which order lines you would like to include in this shipment.
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
mutation createShipment {
createShipment (
input: {
order: { number: 39790 }
lines: [{ orderLine: { id: 17635 }, quantity: 2 }]
isGoodToGo: true
isPaid: false
createdAt: "2022-06-23 15:47:12"
shippedAt: null
sendEmail: false
additionalMessage: "Additional message"
allocateDemand: true
}
) {
userErrors {
message
path
}
shipment {
...shipmentDetails
}
}
}
fragment shipmentDetails on Shipment {
id
number
createdAt
updatedAt
isGoodToGo
isShipped
isPaid
paidAt
additionalMessage
isShipped
shippedAt
numberOfPackages
trackingNumber
returnTrackingNumber
internalShippingCost {
value
}
grandTotal(includingTax: true) {
value
}
carrierInformation {
carrierName
serviceName
}
adminUser {
id
}
discountsApplied {
value {
formattedValue
}
}
lines {
id
quantity
lineValue {
formattedValue
}
}
shippingAddress {
firstName
lastName
country {
name
code
}
state {
name
code
}
address1
address2
city
zipCode
stateOrProvince
cellPhoneNumber
phoneNumber
faxNumber
email
}
shipmentPlugin {
id
status
name
}
}
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
{
"data": {
"createShipment": {
"userErrors": [],
"shipment": {
"id": 345,
"number": "39790-1",
"createdAt": "2022-06-23T15:47:12+02:00",
"updatedAt": "2023-01-16T10:31:34+01:00",
"isGoodToGo": true,
"isShipped": false,
"isPaid": false,
"paidAt": null,
"additionalMessage": "Additional message",
"shippedAt": null,
"numberOfPackages": 0,
"trackingNumber": null,
"returnTrackingNumber": null,
"internalShippingCost": {
"value": 0
},
"grandTotal": {
"value": 900
},
"carrierInformation": null,
"adminUser": null,
"discountsApplied": [],
"lines": [
{
"id": 465,
"quantity": 2,
"lineValue": {
"formattedValue": "700.00 SEK"
}
}
],
"shippingAddress": {
"firstName": "Pio",
"lastName": "Sym",
"country": {
"name": "Sweden",
"code": "SE"
},
"state": null,
"address1": "Addr 1",
"address2": null,
"city": "City",
"zipCode": "12345",
"stateOrProvince": "State",
"cellPhoneNumber": "123456789",
"phoneNumber": null,
"faxNumber": null,
"email": "test@test.com"
},
"shipmentPlugin": null
}
}
},
"extensions": {
"complexity": 131,
"permissionsUsed": [
"Shipment:write",
"Shipment:read",
"AdminUser:read",
"Order:read",
"Shipment.shippingAddress:read",
"StorePlugin:read"
],
"appVersion": "v0.34.6"
}
}
Such shipment can later be captured or marked as paid at any point.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mutation captureShipment {
captureShipment(id: 123) {
userErrors {
message
path
}
paymentHistoryEntry {
createdAt
status
entryType
externalReference
value {value}
paymentMethod
paramsJSON
}
shipment {
id
number
isCaptured
capturedAt
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mutation updateShipmentMarkPaid {
updateShipment (
id: 345
input: {
isPaid: true
}
) {
userErrors {
message
path
}
shipment {
...shipmentDetails
}
}
}
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
{
"data": {
"updateShipment": {
"userErrors": [],
"shipment": {
"id": 345,
"number": "39790-1",
"createdAt": "2022-06-23T15:47:12+02:00",
"updatedAt": "2023-01-16T10:35:35+01:00",
"isGoodToGo": true,
"isShipped": false,
"isPaid": true,
"paidAt": "2023-01-16T10:35:35+01:00",
"additionalMessage": "Additional message",
"shippedAt": null,
"numberOfPackages": 0,
"trackingNumber": null,
"returnTrackingNumber": null,
"internalShippingCost": {
"value": 0
},
"grandTotal": {
"value": 900
},
"carrierInformation": null,
"adminUser": null,
"discountsApplied": [],
"lines": [
{
"id": 465,
"quantity": 2,
"lineValue": {
"formattedValue": "700.00 SEK"
}
}
],
"shippingAddress": {
"firstName": "Pio",
"lastName": "Sym",
"country": {
"name": "Sweden",
"code": "SE"
},
"state": null,
"address1": "Addr 1",
"address2": null,
"city": "City",
"zipCode": "12345",
"stateOrProvince": "State",
"cellPhoneNumber": "123456789",
"phoneNumber": null,
"faxNumber": null,
"email": "test@test.com"
},
"shipmentPlugin": null
}
}
},
"extensions": {
"complexity": 131,
"permissionsUsed": [
"Shipment:write",
"Shipment:read",
"AdminUser:read",
"Order:read",
"Shipment.shippingAddress:read",
"StorePlugin:read"
],
"appVersion": "v0.34.6"
}
}
You can combine the two steps above, and create shipments which should be captured immediately upon creation:
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
mutation createShipmentWithCapturing {
createShipment(
input: {
order: {
id: "05ac658a8e815571fdba2984eb358932"
# number: 10
}
lines: [
{ orderLine: { id: 10 }, quantity: 1 }
]
capture: true # this will enable capturing
}
) {
userErrors {
message
path
}
shipment {
id
isCaptured
capturedAt
order {
paymentHistory(where: {entryType: [CAPTURE, CAPTURE_REQUEST]}) {
createdAt
status
entryType
externalReference
value {value}
paymentMethod
paramsJSON
}
}
}
}
}
Once the shipment is Good-to-go and paid for, you can ship it out. This is done by completing the shipment, at which stage you can add the shipment info (like carrier, service and tracking number), set the shipped date and choose whether or not to send out the shipping confirmation email to your shopper. In GraphQL shipped
parameter is not a boolean, but rather a timestamp of when this shipment was completed. Good news - this timestamp supports the same options as PHP date, so setting it can be as easy as sending shippedAt: now
. Centra will save the right timestamp on the shipment for you.
Examples of the date could be "yesterday", "21/10/2023", "-1 hour". It cannot be in the future. You can find more information about the PHP date here.
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
mutation completeShipment {
completeShipment (
id: 345
input: {
shippedAt: "2023-01-09T15:18:33+01:00"
sendEmail: true
shipmentInfo: {
carrier: "Carrier name"
service: "Service name"
packagesNumber: 1
trackingNumber: "1234trackingcode"
returnTrackingNumber: "1234returncode"
internalShippingCost: { currencyIsoCode: "SEK", value: 12 }
}
}
) {
userErrors {
message
path
}
shipment {
...shipmentDetails
}
}
}
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
{
"data": {
"completeShipment": {
"userErrors": [],
"shipment": {
"id": 346,
"number": "39790-1",
"createdAt": "2022-06-23T15:47:12+02:00",
"updatedAt": "2023-01-16T10:39:23+01:00",
"isGoodToGo": true,
"isShipped": true,
"isPaid": true,
"paidAt": "2023-01-16T10:39:17+01:00",
"additionalMessage": "Additional message",
"shippedAt": "2023-01-09T15:18:33+01:00",
"numberOfPackages": 1,
"trackingNumber": "1234trackingcode",
"returnTrackingNumber": "1234returncode",
"internalShippingCost": {
"value": 12
},
"grandTotal": {
"value": 900
},
"carrierInformation": {
"carrierName": "Carrier name",
"serviceName": "Service name"
},
"adminUser": null,
"discountsApplied": [],
"lines": [
{
"id": 466,
"quantity": 2,
"lineValue": {
"formattedValue": "700.00 SEK"
}
}
],
"shippingAddress": {
"firstName": "Pio",
"lastName": "Sym",
"country": {
"name": "Sweden",
"code": "SE"
},
"state": null,
"address1": "Addr 1",
"address2": null,
"city": "City",
"zipCode": "12345",
"stateOrProvince": "State",
"cellPhoneNumber": "123456789",
"phoneNumber": null,
"faxNumber": null,
"email": "test@test.com"
},
"shipmentPlugin": null
}
}
},
"extensions": {
"complexity": 131,
"permissionsUsed": [
"Shipment:write",
"Shipment:read",
"AdminUser:read",
"Order:read",
"Shipment.shippingAddress:read",
"StorePlugin:read"
],
"appVersion": "v0.34.6"
}
}
Returns#
If products from an order get returned by a customer or packages are delivered back to the warehouse due to delivery issues etc, a return can be created. A return is connected to a specific shipment.
Refunds are currently not supported with the Integration API, but can be handled with Order API (REST) until the mutation is available. You can read about refunds in REST API here.
There are three specific options to consider when creating a Return:
releaseItemsBackToWarehouse
: the items will be sent back to the warehouse they originally came fromsendItemsToDifferentWarehouse
: items will be sent to a warehouse specified by the userremoveItemsFromStock
: physical products will not be returned, there will be no increase in stock for any warehouse
Additional costs, such as handling fees, shipping costs, or voucher value, can also be specified in this mutation. These values can not be greater the the remaining value of these costs on shipment.
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
mutation createreturn {
createReturn(
input: {
shipment: { id: 1338 }
lines: [
{ shipmentLine: { id: 2943 }, quantity: 2 }
{ shipmentLine: { id: 2944 }, quantity: 2 }
]
returnStockActionPolicy: { releaseItemsBackToWarehouse: true }
shippingCost: { value: { currencyIsoCode: "SEK", value: 10.00 } }
voucherValue: { fromShipment: true }
handlingCost: { fromShipment: true }
comment: "new comment"
}
) {
userErrors {
message
path
}
return {
id
lines {
id
}
}
}
}
If you would like to complete a return using the Integration API you can do it by running completeReturn
mutation. First you can query the returns to find the needed id.
1
2
3
4
5
6
7
8
9
10
11
query returns {
returns {
id
status
lines {
shipmentLine {
id
}
}
}
}
In the completeReturn
mutation you can also pass the information on whether an email with return confirmation should be sent to the customer. You can’t complete a return that already has a completed status.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mutation completereturn {
completeReturn(
id: 315
input: {
sendEmail:true
}
) {
userErrors {
message
path
}
return {
id
status
}
}
}
You also have the option to un-complete a return if needed.
1
2
3
4
5
6
7
8
9
10
11
12
mutation uncompletereturn {
uncompleteReturn(id: 315) {
userErrors {
message
path
}
return {
id
status
}
}
}
If a return was created by mistake and needs to be deleted, you also have that possibility.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
mutation deleteReturn {
deleteReturn(id: 436) {
userErrors {
message
path
}
return {
id
lines {
id
}
}
}
}
All you need to provide is the return ID. The stock will be re-allocated according to the policy that was selected when creating the return. If the return was created using the releaseItemsBackToWarehouse
stock policy, the order and shipment lines will be re-allocated based on the allocation rule assigned to the order.
If you used sendItemsToDifferentWarehouse
, the order and shipment lines will be re-allocated, the stock will be re-allocated from the warehouse specified by the user when creating the return.
In case you chose to removeItemsFromStock
, the shipment lines quantity will be updated to reflect the change but there will be no re-allocation. The idea is that the items were not added back to stock, the user can decide whether to re-allocate it again.
Customers - custom attributes#
When the order is placed in Centra, and the shopper was not registered and logged in, Centra will auto-create an un-registered customer to connect the order to. If the user was logged in, the order will be connected to their registered customer in Centra AMS backend.
GraphQL API can be used to manipulate these customers after they are created. Let's take an example of a simple dynamic attribute with 2 input
text elements - External Customer ID
and Customer shipping ID
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'cus_extra' => [
'desc' => 'Customer',
'group' => 'customer',
'readonly' => false,
'elements' => [
'id' => [
'desc' => 'External Customer ID',
'type' => 'input'
],
'ship_id' => [
'desc' => 'Customer shipping ID',
'type' => 'input'
],
],
],
Find the customer by email#
Remember, for each unique email Centra can be used on 2 customers - one registered and one not. There's no way to merge them or migrate the orders between these customers, but both should be available via GQL API. In my case I only have one user using such email address.
Request
We are re-using the fragment attributes
on other calls as well.
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
query getCustomerWithAttributes {
customers(where: { email: { equals: "symek+333@centra.com" } }) {
id
lastName
email
...attributes
}
}
fragment attributes on ObjectWithAttributes {
attributes {
type {
name
isMapped
}
description
objectType
elements {
key
description
kind
... on AttributeStringElement {
value
}
... on AttributeChoiceElement {
isMulti
selectedValue
selectedValueName
}
... on AttributeFileElement {
url
}
... on AttributeImageElement {
url
width
height
mimeType
}
}
}
}
Response
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
{
"data": {
"customers": [
{
"id": 27,
"lastName": "Sym",
"email": "symek+333@centra.com",
"attributes": [
{
"type": {
"name": "cus_extra",
"isMapped": false
},
"description": "Customer",
"objectType": "Customer",
"elements": [
{
"key": "id",
"description": "External Customer ID",
"kind": "INPUT",
"value": "12345"
},
{
"key": "ship_id",
"description": "Customer shipping ID",
"kind": "INPUT",
"value": "9999"
}
]
}
]
}
]
},
"extensions": {
"complexity": 180,
"permissionsUsed": [
"Customer:read",
"Customer.attributes:read",
"Attribute:read"
],
"appVersion": "v1.7.0"
}
}
Set the customer attributes#
Now that we know the customer ID and attribute keys, we can call mutation setCustomerAttribute
in order to change their values.
Request
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
mutation setCustomerAttribute {
assignAttributes(
input: {
objectType: Customer
objectId: 27
dynamicAttributes: [
{
attributeTypeName: "cus_extra"
attributeElementKey: "id"
attributeElementValue: "New value"
},
{
attributeTypeName: "cus_extra"
attributeElementKey: "ship_id"
attributeElementValue: "Completely new value"
}
]
}
) {
userErrors {
message
path
}
object {
... on Customer {
id
email
firstName
lastName
}
...attributes
}
}
}
Response
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
{
"data": {
"assignAttributes": {
"userErrors": [],
"object": {
"id": 27,
"email": "symek+333@centra.com",
"firstName": "Pio",
"lastName": "Sym",
"attributes": [
{
"type": {
"name": "cus_extra",
"isMapped": false
},
"description": "Customer",
"objectType": "Customer",
"elements": [
{
"key": "id",
"description": "External Customer ID",
"kind": "INPUT",
"value": "New value"
},
{
"key": "ship_id",
"description": "Customer shipping ID",
"kind": "INPUT",
"value": "Completely new value"
}
]
}
]
}
}
},
"extensions": {
"complexity": 120,
"permissionsUsed": [
"Attribute:write",
"Customer.attributes:read",
"Attribute:read"
],
"appVersion": "v1.7.0"
}
}