Authorization
Last updatedGraphQL Access Token#
An access token is a credential that is bound to certain set of permissions. The set of permissions is decided during token generation. It is not bound to any specific user by the application, but it might be issued with specific user in mind.
Access token has an obligatory expiration time after which it will no longer authorize any requests.
Obtaining Access Token via AMS#
For GraphQL access you need user token with correct permissions. This could be done in the backend AMS. Navigate to System -> Api Tokens
and then add a new token by clicking + Integration API TOKEN
button.
Here we are able to provide restrictions, select permissions, and expiration time. Requirements for generating token are:
- Providing description (it is a good practice to provide a description that allows unambiguous token identification)
- At least one permission
Expiration time is optional - the default value equals 30 days.
Token Revocation#
Access tokens can be revoked in the AMS when necessary. Navigate to System -> Api Tokens
and select the token that you want to invalidate.
This is the moment when good practice of naming tokens unambiguously pays off. When token
details are displayed use the X Revoke
button.
Authorizing Requests#
Header#
One way to authorize the request is to provide an Authorization
header:
1
2
3
POST *base*/graphql
Authorization: Bearer <access token>
CURL example:
1
2
3
4
5
curl "${BASE_URL}/graphql" \
-X POST \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"query":"{ __schema { types { name } } }"}'
Cookie#
Another way to authorize request to GraphGL API is to add a cookie named graphql-access
with only the access token as value.
1
2
3
POST *base*/graphql
Cookie: graphql-access=<access token>
CURL example:
1
2
3
4
5
curl "${BASE_URL}/graphql" \
-X POST \
-H "Cookie: graphql-access=${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"query":"{ __schema { types { name } } }"}'
For instructions on how to attach a header or cookie in your API client refer to the client's documentation.
Permissions#
The list of permissions is changing as new permissions are added to match new queries and mutations.
During your integration's initial tests in QA, it's worth to note all the information returned in extensions.permissionsUsed
, so that you know exactly which permissions are required for use cases covered in your integration. This way you can later use API tokens with minimal permissions when you move to Production, which we highly recommend. Running Production integrations on tokens with full admin permissions is considered bad practice, and a potential security vulnerability.
If you call the API without required permissions, you will be informed about this explicitly:
Request:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
orderConnection(
last: 10, before: "bnVtYmVyOjE2Ng==", where: {storeType: WHOLESALE}
)
{
totalCount
pageInfo{hasPreviousPage, hasNextPage, startCursor, endCursor}
edges{
node{
number
status
grandTotal{
value
currency {code}
}
orderDate
}
cursor
}
}
}
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
{
"errors": [
{
"message": "You need Order:read permission to access orderConnection.",
"extensions": {
"category": "authorization"
},
"locations": [
{
"line": 6,
"column": 3
}
],
"path": [
"orderConnection"
]
}
],
"extensions": {
"complexity": 50,
"permissionsUsed": [
"Order:read"
]
}
}
New, simplified permissions#
In the upcoming Centra 3.7 version we will release changes simplifying permissions in Integration API (GraphQL).
Previous convention#
Most of the main types have a permission associated with it, like Product:read
and Product:write
. On top of that, relations between types were also secured separately. For example, Product.Brand:read
would allow you to read brands associated with a product, but wouldn’t affect your ability to read other brands – for that there was Brand:read
. This convention resulted in many granular permissions.
Why and what we change#
Reason 1: Such granularity is not actually needed. If a token is granted permission to Account:read
, it should be enough to read accounts from Invoice.account
, Return.account
, or SalesRepresentative.accounts
. Thus instead of Invoice.Account:read
, Return.Account:read
and SalesRepresentative.Account:read
there will be the only permission Account:read
. It will significantly reduce the number of permissions used, and therefore their management will be simplified.
Reason 2: Inconsistency of sub-permissions. Sometimes scalar fields are guarded, sometimes sub-types are guarded, but they look the same: Product.InternalComment:read
(scalar) Product.Attribute:read
(type).
The new release will use a field name instead of a return type. The aforementioned permissions will become Product.attributes:read
and Product.internalComment:read
for attributes
and internalComment
.
It would also make it clear, which field it is about when there are two fields with the same return type. For example, different addresses PurchaseOrder.shippingAddress:read
and PurchaseOrder.supplierAddress:read
instead of PurchaseOrder.Address:read
for both. And make it clear that Purchaser.Order:read
is actually about Purchaser.totalOrders:read
.
Reason 3: Currently, it’s not possible to secure the same type with separate permissions. The changes will enable this possibility as for shippingAddress
and supplierAddress
.
New convention#
Nested permissions will be used only for:
- Attributes
- Internal comments
- Stock
- Addresses
- Other sensitive information, like
AdminUser.email
The second part of nested permissions will always match the field name.
Deprecated permission handling#
The old permissions will still work for now but will be marked as deprecated. In case of using a deprecated permission, a new section in responses will appear: extensions
> deprecatedPermissionsUsed
.
We will monitor the usage of the deprecated permissions to make sure they are not used, before we delete them completely.
How to prepare#
Recommended actions:
- Run all GQL queries, which are in use, towards updated QA servers,
- Note down all (new) permissions used, then add them to your tokens.
Roadmap#
- 21.03.2022 – release of the new + deprecated permissions on QA servers
- 04.04.2022 – release on production servers
- 16.05.2022 – release of removing deprecated permissions on QA servers
- 30.05.2022 – release on production servers
Additional notes#
Please note, some types have new sub-permissions, and using top type permissions on them is marked as deprecated. It only means the usage of this permission is deprecated for this specific field, but the permission itself could be still active and it will be clearly stated in the new deprecatedPermissionsUsed
section. For example, the query
1
2
3
4
5
{
invoices(limit: 1) {
billingAddress {city}
}
}
...will tell Invoice:read
is deprecated but it’s only for Invoice.billingAddress
. Invoice:read
is still an active permission.
1
2
3
4
5
6
7
"permissionsUsed": [
"Invoice:read",
"Invoice.billingAddress:read"
],
"deprecatedPermissionsUsed": [
"Field: Invoice.billingAddress, deprecated: Invoice:read, current: Invoice.billingAddress:read"
],
Deprecations in the latest GQL API versions#
We're making some changes to our GraphQL Integration API because we want it to reflect business concepts better and need to align the naming.
We are doing our best not to introduce breaking changes so that existing queries still work, but you can switch to using new names at any time. For example, when a field is renamed, a new field is added, and the old one still works. When a returned type changes, the old type is turned into an interface so that fragments explicitly specifying types are not broken.
Moreover, we are now returning a list of deprecated fields used under extensions, so you can see exactly whether your queries use deprecated fields and when they will be deleted.
Example response:
1
2
3
4
5
6
7
8
9
10
11
12
{
"data": {
...
},
"extensions": {
"deprecatedFieldsUsed": [
"Field: Query.displays, reason: Use ObjectWithTranslations instead of Localizable, date of removal: 2023-09-04",
"Field: Display.localized, reason: Renamed localized to translations, date of removal: 2023-09-04",
"Field: LanguageTranslation.translations, reason: Renamed to fields, date of removal: 2023-09-04"
],
}
}
These are the changes that will be made:
Date and time scalars#
All input and output fields using dates and date-times have been changed to using custom scalar types: Date
and (in most cases) DateTimeTz
. This change was announced in October 2022 and required us to disable strict type checks of variables until all partners stop type-hinting dates as String
.
Before:
1
2
3
4
5
query lastOrders($fromDate: String!) {
orders(where: {orderDate: {from: $fromDate}}) {
...
}
}
After:
1
2
3
4
5
query lastOrders($fromDate: DateTimeTz!) {
orders(where: {orderDate: {from: $fromDate}}) {
...
}
}
Rename of WarehouseDelivery to StockChange#
Due to the fact that WarehouseDelivery
does not cover all use cases that are available and that will come in the future. There are multiple ways stock balance can be changed, where only some are warehouse deliveries (a.k.a inbound deliveries). Thus, WarehouseDelivery
will be renamed to StockChange
.
Before:
1
2
3
4
5
query stockChanges($filter: WarehouseDeliveryFilter!) {
warehouseDeliveryConnection(where: $filter, last: 10) {
...
}
}
After:
1
2
3
4
5
query stockChanges($filter: StockChangeFilter!) {
stockChangeConnection(where: $filter, last: 10) {
...
}
}
Rename Localization to Translation#
As localization is much more of a general term that includes time zones, currency, etc., we've decided to rename Localization
to Translation
. This will also be consistent with naming in Centra's admin panel.
Before:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
query displayTranslations {
displays {
id
name
...translations
}
}
fragment translations on Localizable { # deprecated interface
localized {
language { code }
translations {
field
value
}
}
}
After:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
query displayTranslations {
displays {
id
name
...translations
}
}
fragment translations on ObjectWithTranslations {
translations {
language { code }
fields {
field
value
}
}
}
Separation of Customer and Buyer - Purchaser will be deprecated#
Centra has two different "customer" types. One in DTC, called Customer
, and one in Wholesale, called Buyer
(which is connected to Account
). Because the two types have mostly the same fields, they have been grouped under a common interface named Purchaser
. This, after communication with several parties, has been deemed to be somewhat confusing, and therefore it's been decided to deprecate Purchaser
.
Since Purchaser
will no longer be there, types referencing it are now split into DTC and Wholesale subtypes: Shipment
is now an interface shared by DirectToConsumerShipment
and WholesaleShipment
. Similarly, Return
, Invoice
, OrderHistoryEntry
, and EmailHistoryEntry
are now interfaces with two implementations each.
Before:
1
2
3
4
5
6
7
8
query returns {
returns(where: {purchaserId: 1, storeType: DIRECT_TO_CONSUMER}) {
id
purchaser {
email
}
}
}
After:
1
2
3
4
5
6
7
8
9
10
query returns {
returns(where: {customerId: 1, storeType: DIRECT_TO_CONSUMER}) {
id
...on DirectToConsumerReturn {
customer {
email
}
}
}
}
Discount renamed to Voucher#
Discounts can be given in multiple ways in Centra. For example, campaigns discount product prices, but also a manual discount on an order line may be given by a Centra admin user. In order to create less confusion, Discount
, which represents vouchers in Centra, will be renamed to Voucher
.
Before:
1
2
3
4
5
6
7
8
mutation addVoucher($input: DiscountCreateInput!) {
createDiscount(input: $input) {
discount {
id
}
userErrors { message, path }
}
}
After:
1
2
3
4
5
6
7
8
mutation addVoucher($input: VoucherCreateInput!) {
createVoucher(input: $input) {
voucher {
id
}
userErrors { message, path }
}
}
Additional changes#
These fields were deprecated before but now received a concrete date of removal:
AllocationRule.warehouses
– warehouses are now under geo-priorities; there may be a different set of warehouses depending on the countryAddress.otherPhoneNumber
– renamed tophoneNumber
Purchaser/Buyer/Customer.otherPhoneNumber
– also renamed tophoneNumber
Customer.sex
– renamed togender
Shipment.emailSentAt
– useshippedAt
, which contains the same dateSizeChart.isEnabled
– will always returntrue
since we don’t disable size chartsMutation.removeProductMedia
– renamed todeleteProductMedia
Currency.shippingOptions
– this direct relation is not supported anymore; one can filtershippingOptions
bycurrencyId
insteadSize.productSizes
– this relation is also not supported anymore; one can filterproductSizes
bysizeId
Store.totalPurchasers
– this can be achieved withQuery.counters.customers
filtered bystoreId
Store.totalOrders
– similarly, this is also available fromQuery.counters.orders
filtered bystoreId
graphQLAccessTokenCount
- usecounters.graphQLAccess
insteadremoveSizeChart
- use deleteSizeChart insteadremoveMeasurementChart
- usedeleteMeasurementChart
insteadremoveCampaign
- usedeleteCampaign
insteadProductSizeUpdateInput
-GTIN
has been renamed toEAN
,UPC
was added as a separate fieldProductSizeCreateInput
-GTIN
has been renamed toEAN
,UPC
was added as a separate fieldProductSizeFilter
-GTIN
has been renamed toEAN
,UPC
was added as a separate fieldProductSize.GTIN
-GTIN
has been renamed toEAN
,UPC
was added as a separate fieldReturn.account
- moved from theReturn
interface toWholesaleReturn
DirectToConsumerReturn.account
- moved from theReturn
interface toWholesaleReturn
User warnings - not required, but important!#
GraphQL mutations return HTTP 200 response, and the way issues are communicated is through the userErrors
field in payloads. However, not all issues are equal, and some non-critical ones actually don't prevent mutations from succeeding.
This is especially important for batch actions like price updates (setPrices), where it is really important to save all other prices rather than failing because of one that is wrong for some trivial reason, e.g.:
- "Duplicate product ID 123 skipped"
- "Duplicate variant ID 456 skipped"
- "Product variant with id 789 not assigned to product 123"
- "Product ID 345 is a bundle with dynamic price type, changing its prices has no effect, skipped"
Sometimes warnings are purely informative, like "Weight unit has been changed from KILOGRAMS to POUNDS", or "Weight has been rounded to 3 decimal places".
userErrors
from now on contain only errors, and userWarnings
- all other issues. They both have a message and a path.
1
2
3
4
5
6
7
8
9
10
11
12
mutation updateProductWeight {
updateProduct(id: 1, input: {
weight: {
value: 11
unit: POUNDS
}
}) {
product { id, weight { formattedValue } }
userErrors { message path }
userWarnings { message path } # NEW!
}
}
It makes sense to add userWarnings
to all mutations, even if you don’t expect anything like mentioned above. New warnings can be added in the future.