Ankorstore Ordering API
ℹ️ This section of API allows to manage different types of orders in the system. Depending on the order type, there are different endpoints available to manage them. Before starting to work with the API, it is recommended to read the documentation to better understand the differences between the order types, their workflow and lifecycle. ## 💡 General conceptions - _Internal Order_ - A regular type of order created via Ankorstore platform. It may have different types of shipping and payment. The client of such an order is always an existing retailer entity, available in Ankorstore. - _External Order_ - A special type of order created by a brand for an external customer, which might not exist in Ankorstore. This type of order does not expect retailer as a reference to the Ankorstore entity, but rather allows brand to provide a custom client details, such as address, contact information etc. The orders of this type in the current version of the platform are fulfilled by the Fulfillment Centers only and do not support any other type of shipping. - _Master Order_ - Not a separate type of order, but rather a wrapper over other order types. It allows to have a single reference to the order, regardless of its type. A _Master Order_ usually has a one-to-one relationship with either _Internal Order_ or _External Order_ so either of these order can be accessed via the _Master Order_ reference. ## 💡 Working with Master Orders ### Access different types of orders via Master Order As mentioned above, the _Master Order_ is a wrapper over other order types. It allows to have a single reference to the underlying order, regardless of its type. The available _Master Order_ endpoints allow to fetch a list of available _Master Orders_ and also get a single _Master Order_ by its ID and, following JSON:API specification, including the underlying orders as relations. This approach allows to deal with different order types in a unified way (e.g. listing, pagination). However, it is still possible to access the underlying _Internal Orders_ directly, in order to keep the backward compatibility. graph LR A[Master Order] --> B[Internal Order] A --> C[External Order] _Master Order_ as a standalone resource might not bring much value, but the _Master Order_ endpoints allow to include a corresponding wrapped orders as relations, which per se contain a useful payload. ### Includes The _Master Order_ has a one-to-one relationship with either _Internal Order_ or _External Order_. Hence, passing query parameter `?include=internalOrder`, `?include=externalOrder` or by combining different includes, (separated by comma e.g. `?include=internalOrder,externalOrder`) you will receive wrapped order as a relation. For example, let's say you have only 2 orders, one of them is _Internal Order_, another is _External Order_. If you don't include any relation into request to the [List Master Orders](#tag/Ordering/operation/list-master-orders), ``` [GET] /api/v1/master-orders ``` the response will contain only a list of identifiers without any wrapped orders: ```json5 { //... "data": [ { "type": "master-orders", "id": "a470c8d6-1bda-4612-b0bd-3ea2a81a9e89", }, { "type": "master-orders", "id": "0ca13de1-8e4b-4e67-a147-185cc5f6f57f", }, //... ], //... } ``` But with the inclusion of the wrapped order relations to the same endpoint ``` [GET] /api/v1/master-orders?include=internalOrder,externalOrder ``` the result will contain included information about the actual orders: ```json5 { //... "data": [ { "type": "master-orders", "id": "a470c8d6-1bda-4612-b0bd-3ea2a81a9e89", "relationships": { "internalOrder": { "data": { "type": "internal-orders", "id": "a470c8d6-1bda-4612-b0bd-3ea2a81a9e89" } }, } }, { "type": "master-orders", "id": "0ca13de1-8e4b-4e67-a147-185cc5f6f57f", "relationships": { "externalOrder": { "data": { "type": "external-orders", "id": "0ca13de1-8e4b-4e67-a147-185cc5f6f57f" } } } }, //... ], "included": [ { "type": "internal-orders", "id": "a470c8d6-1bda-4612-b0bd-3ea2a81a9e89", "attributes": { // Internal order attributes here } }, { "type": "master-orders", "id": "0ca13de1-8e4b-4e67-a147-185cc5f6f57f", "attributes": { // External order attributes here } }, //... ] //... } ``` To learn more about JSON:API include mechanism, please refer to the [official docs](https://jsonapi.org/format/#fetching-includes). ## 💡 Working with Internal Orders Despite the fact that the _Internal Orders_ can be accessed via the _Master Order_ endpoints, it is still possible to access them directly, in order to keep the backward compatibility. Moreover, some actions (e.g. transiting an _Internal Order_ to the next state) are only available via the direct _Internal Order_ endpoints. ### Includes For every endpoint that returns the _Internal Order_ resource you may pass an `?include=` query parameter that will return extra information inside the `included` root level object. The supported resources to include are `retailer`, `billingItems`, `orderItems`, `orderItems.productVariant` and `orderItems.productVariant.product`. Please note that, if you include `orderItems.productVariant` you do not need to specify `orderItems` separately. It will be automatically included. As an example, to include all data possible you would do `?include=retailer,billingItems,orderItems.productVariant.product` #### Product options usage is deprecated Please note, that we have fully completed migrating our system to the new product model with product options replaced by product variants. It means, the usage of product options is deprecated and will be removed in the future versions of the API. Please, consider updating your API clients in favor of using `orderItems.productVariant` instead of `orderItems.productOption` to avoid any issues in the future. For the time being, the API still supports both approaches, but we strongly encourage you to not rely on the deprecated resources anymore because we cannot fully guarantee their stability due to some technical limitations. ### Important Fields within Internal Order resource If the field contains no data, it will be `null`. The only exception currently to this is `shippingOverview` which can be viewed below. The fields listed below are not all the fields, please see the API specification for all of them. ### Status `status` The status field is the current state of the order, below is a table that describes each step: | Status | Description | |-----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `ankor_confirmed` | The order has been confirmed by Ankorstore and now a decision can be made whether to accept or reject this order. | | `rejected` | The order has been rejected, Ankorstore needs to approve this rejection. If it does, the status will moved to `cancelled`. | | `brand_confirmed` | The order has been accepted. it now needs to be shipped to the retailer. | | `shipping_labels_generated` | The order has been shipped with Ankorstore and the labels have been generated - these are now available in the order resource data within `shippingOverview`. | | `fulfillment_requested` | The brand has chosen to fulfill the order, or the order was automatically fulfilled. The order will remain in this state until it has been fulfilled by the fulfillment provider. | | `shipped` | The order has either been picked up by an Ankorstore carrier (e.g UPS) or the brand has chosen to ship with their own carrier. | | `received` | The retailer has received the package, if shipped with Ankorstore this is automatic from the carrier otherwise for custom shipping the retailer has to manually acknowledge this. | | `reception_refused` | The retailer has refused the reception of the package - if Ankorstore approves the reason the order will move to cancelled. The status can also move to `received` if the retailers issues have been resolved by the brand or Ankorstore. | | `invoiced` | The final invoice has been issued for this order. | | `brand_paid` | The brand has been paid in full for this order. | | `cancelled` | The order is cancelled. | flowchart LR ankor_confirmed -- accept order -->brand_confirmed ankor_confirmed -- reject order -->rejected-->cancelled brand_confirmed -- shipping method: custom -->shipped brand_confirmed -- shipping method: ankorstore -->shipping_labels_generated brand_confirmed -- shipping method: fulfillment -->fulfillment_requested shipping_labels_generated-->shipped fulfillment_requested-->shipped shipped-->reception_refused-->cancelled shipped-->received-->invoiced-->brand_paid reception_refused-->received #### Not to be confused: Status vs Shipping Flow The above diagram shows what status the order will be in **based** on the actions performed in the API. The most confusing may be the shipping flow. To reach the `shipped` or `shipping_labels_generated` status, a shipping quote must always be generated and then confirmed. Please see [shipping an order](#tag/Order/Shipping-an-Order) for further details. ### Shipping Method `shippingMethod` What shipment method has been chosen for this order. Either `ankorstore` for using Ankorstore's shipping service, `custom` for using the brand's own carrier or `fulfillment` if the order is shipped from a fulfillment centre This field is `null` when no choice has yet been made. ### Shipping Overview `shippingOverview` #### Field Availability The object `shippingOverview` is only available when retrieving a single order. This data is not available when retrieving a list of orders. ### Submitted At `submittedAt` This is the date the retailer submitted their whole order. ### Shipped At `shippedAt` This is when the brand shipped the order. ### Brand Paid At `brandPaidAt` This is when Ankorstore pays the brand, if it is `null` then payment is still pending. ### Interacting with Internal Orders When a brand receives a new order, it arrives in the `ankor_confirmed` status. The brand can then perform certain transitions using a single endpoint: `POST /api/v1/orders/{id}/-actions/transition` This endpoint will accept different payloads depending on what transition is provided. This page will detail each transition that may be performed and when. All the implementation details and fields can be found in the endpoint documentation. #### No going back! Once an order is accepted or rejected it cannot be reversed. If this was a mistake the brand can contact Ankorstore through the normal channels to revert the order to `ankor_confirmed`. If an order is rejected you need to provide a detailed reject reason. Otherwise, after the order is accepted the brand then needs to ship it. Please see [shipping](#tag/Shipping) for further details. ### Accepting an Internal Order Can be performed when the order is in `ankor_confirmed`. In order to accept an order without any item modifications a simple payload detailing the transition will suffice: ```json { "data": { "type": "brand-validates" } } ``` The order will move from `ankor_confirmed` to `brand_confirmed`. ### Accepting an Internal Order with modifications Can be performed when the order is in `ankor_confirmed`. Sometimes you may wish to modify the order before accepting it if for example you do not have the items in stock or further communication has taken place with the retailer. The order items can **only be modified at this stage**. The rules of the payload are as follows: - To leave an item's quantity as is, do not include it in the payload below - To update an item's quantity, include it in the payload below with the new quantity (cannot be above the current quantity) - To remove an item entirely, set the item's quantity to 0. You cannot remove all items, please reject the order in this scenario. ```json { "data": { "type": "brand-validates", "attributes": { "orderItems": [ { "id": "a470c8d6-1bda-4612-b0bd-3ea2a81a9e89", "type": "order-items", "attributes": { "quantity": 12 } }, { "id": "0ca13de1-8e4b-4e67-a147-185cc5f6f57f", "type": "order-items", "attributes": { "quantity": 0 } } ] } } } ``` The order will move from `ankor_confirmed` to `brand_confirmed`. ### Rejecting an Internal Order Can be performed when the order is in `ankor_confirmed`. To reject an order you must provide a reason listed below for the `rejectType` field. - `BRAND_ALREADY_HAS_CUSTOMER_IN_THE_AREA` - Brand already has a customer in the area - `BRAND_CANNOT_DELIVER_TO_THE_AREA` - Brand cannot make a delivery to the target area - `BRAND_HAS_EXCLUSIVE_DISTRIBUTOR_IN_THE_REGION` - The brand already has an exclusive distributor in the target region - `BUYER_NOT_A_RETAILER` - The buyer does not seem to be a retailer - `ORDER_ITEMS_PRICES_INCORRECT` - The prices of items in this order were incorrect - `PAYMENT_ISSUES_WITH_RETAILER` - Brand had payment issues in the past with this retailer - `PREPARATION_TIME_TOO_HIGH` - The time needed for the order preparation is excessively high - `PRODUCT_OUT_OF_STOCK` - Order cannot be processed because one or multiple ordered products are not in the stock - `PURCHASE_NOT_FOR_RESALE` - This purchase does not seem to be for resale - `RETAILER_AGREED_TO_DO_CHANGES_TO_ORDER` - The retailer agreed so that they can do changes to the order - `RETAILER_NOT_GOOD_FIT_FOR_BRAND` - The retailer who made an order does not fit the brand's criteria - `RETAILER_VAT_NUMBER_MISSING` - The retailer VAT number is missing - `OTHER` - If no reason above is suitable you can use value, but you **MUST** provide a `rejectReason` which accepts a plain text string of 1000 characters. ### Example rejection ```json { "data": { "type": "brand-rejects", "attributes": { "rejectType": "ORDER_ITEMS_PRICES_INCORRECT" } } } ``` ### Example rejection with OTHER ```json { "data": { "type": "brand-rejects", "attributes": { "rejectType": "OTHER", "rejectReason": "a different reason" } } } ``` The order will move from `ankor_confirmed` to `cancelled`. ### Resetting an Internal Order's generated shipping labels Can be performed when the order is in `shipping_labels_generated`. To reject an order you must provide a reason listed below for the `resetType` field. - `BRAND_NEED_MORE_LABELS_TO_SHIP` - Brand needs more shipping labels to ship - `BRAND_PUT_WRONG_WEIGHT_DIMENSIONS` - Brand put wrong parcel weight and/or dimensions - `BRAND_SHIPS_WITH_DIFFERENT_CARRIER` - Brand wants to ship with a different carrier - `PROBLEM_DURING_SHIPPING_LABEL_GENERATION` - There was a problem during the shipping labels generation - `RETAILER_ASKED_DELIVERY_ADDRESS_CHANGE` - The retailer asked for a delivery address change - `SHIPPING_ADDRESS_MISMATCHES_PICKUP_ADDRESS` - The shipping address mismatches the pickup address - `OTHER` - If no reason above is suitable you can use this value, but you **MUST** provide a `reason` which accepts a plain text string of 300 characters maximum. #### Example reset shipping labels ```json { "data": { "type": "brand-resets-shipping-labels-generation", "attributes": { "resetType": "BRAND_PUT_WRONG_WEIGHT_DIMENSIONS" } } } ``` #### Example reset shipping labels with OTHER ```json { "data": { "type": "brand-resets-shipping-labels-generation", "attributes": { "rejectType": "OTHER", "reason": "a different reason" } } } ``` The order will move from `shipping_labels_generated` to `brand_confirmed`. ### Requesting fulfillment by Ankorstore Logistics for an Internal Order Can be performed when the order is in `brand_confirmed`. Ankorstore Logistics will fulfill the order and ship it to the retailer. The order will move from `brand_confirmed` to `fulfillment_requested`. Must be preceded by a [fulfillability check](#tag/Ordering/operation/internal-order-is-fulfillable), since it requires the brand to be enrolled with Ankorstore logistics, and sufficient stock to be available in an Ankorstore warehouse. Validation errors can include: - `not_a_fulfillment_brand` - The brand is not enrolled with Ankorstore Logistics - `unavailable_items` - One or more items are not available in the warehouse. The response will include a list of the unavailable items, which map to` productVariant.fulfillableId` - `already_being_fulfilled` - The order is already being fulfilled - `not_available_for_international_orders` - Fulfillment is currently not available for orders crossing a customs border - `missing_hs_code_for_international_order` - One or more products are missing a Harmonized System code (HS code), required for customs clearance on non-EU destinations - `stock_is_being_transferred` - The stock is currently being transferred between warehouses ### Available operations on Internal Orders - [List Internal Orders](#tag/Ordering/operation/list-internal-orders) - [Retrieve a single Internal Order](#tag/Ordering/operation/get-internal-order) - [Transition an Internal Order](#tag/Ordering/operation/transition-internal-order) - accept, reject order etc. - [List shipping quotes](#tag/Shipping/operation/list-order-shipping-quotes) - get a list of shipping quotes for a given order - [Confirm shipping quote](#tag/Shipping/operation/confirm-order-shipping-quote) - confirm the shipping quote and move the order into its shipping phase - [Schedule pickup for order](#tag/Shipping/operation/ship-internal-order-schedule-pickup) - schedule a pickup for the order (if shipped with Ankorstore) - [Ship order with custom](#tag/Shipping/operation/ship-internal-order-custom) - generate a quote for shipping with your own carrier - [Check fulfillability](#tag/Ordering/operation/internal-order-is-fulfillable) - check whether an order is fulfillable by Ankorstore Logistics You can also find some details about shipping in the dedicated [Shipping](#tag/Shipping) section, ## 💡 Working with External Orders The information about _External Orders_ can be only listed and retrieved as a relation of _Master Order_ endpoints. However, some _External Order_-specific operations are also available, see the section below. ### How to create External Order? The creation of _External Order_ is as easy as just sending one request to the API. The process of creation is asynchronous, because it involves some 3rd party services, such as Fulfillment Center operators, who needs to confirm the possibility of fulfillment (availability of the stock, etc.). For better understanding, you can treat the creation of _External Order_ an "intent to create an order with particular items for particular client". So after submission, the intent is either accepted or rejected by API, depending on the different factors (such as availability of the stock, etc.). More on this below. #### Preparing for External Order creation Usually, before sending the actual `Create External Order` request, you need collect some information in order to make the request valid. The request payload is described in details in the [endpoint specification](#tag/Ordering/operation/ordering-create-non-ankorstore-fulfillment-order) but here we will take a general look at the necessary pieces of the information required for the request: 1. **Valid set of fulfillables.** The endpoint accepts the list of [fulfillable items](#tag/Fulfillment/About-Fulfillment). The identifiers should be valid and the quantities should be evenly divisible by the batch size. Otherwise, the request might be rejected by the API. 2. **Valid shipping address.** This is one of the most important parts of the order, please make sure the address is completely valid, the provided postal code and phone number match the specified country etc. Otherwise, any mistake in the address fields could be only fixed with the help of the Ankorstore Support team. 3. **Generate unique identifier (UUID) for the order.** Despite this field is optional, considering the asynchronous nature of the process, it is highly recommended to provide it. This will allow you to immediately get the information about the order, check its status etc. Omitting this field will result in the generation of the identifier by API and will only make sense if you don't want to check the result immediately ("fire-and-forget"). The last 2 steps can be done independently, but the first step is a bit more complicated, because it requires the knowledge about the available products and their identifiers. This information can be obtained via the [Catalog API](#tag/Products) and [Fulfillment API](#tag/Fulfillment). ### A practical example The assumption here is that you already authenticated and able to access the API. If not, please follow the [Authentication](#tag/Authentication) section first. Here is an example of the real use-case for the External Order creation. Let's say, you'd like to create an order for the following client: | Field | Value | |--------------|-------------------------------------------| | Client name | John Doe | | Company name | ACME | | Phone | +33 612345678 | | Email | john@acme.com | | Address | 123, Rue de Foo Bar, Paris, 75001, France | with the following content: | Product SKU | Quantity | |-------------|----------| | AB-1234 | 12 | | CD-00-XY | 5 | In this scenario, you should take the following steps: #### [1] Find the fulfillable identifiers for the given products This can be achieved by sending a request to the [List Product Variants](#tag/Catalog/operation/list-product-variants-with-stock) ``` [GET] /api/v1/product-variants?filter[sku]=AB-1234,CD-00-XY ``` and finding the fulfillable identifiers for the desired products. As a result of this step, you should have the following information: | Product SKU | Quantity | Fulfillable ID | |-------------|----------|--------------------------------------| | AB-1234 | 12 | 1ac37dbf-f25d-4c0c-bcc8-ad8af946b541 | | CD-00-XY | 5 | e5e91243-d11a-47a3-9308-22af70da5bc4 | #### [2] Find the information about the available stocks for the products This information can be retrieved by sending a request to [List Fulfillable Endpoint](#tag/Fulfillment/operation/fulfillment-list-fulfillable). With the fulfillable identifiers from the previous step, you can send the following request: ``` [GET] /api/v1/fulfillment/fulfillable?fulfillableIds[]=1ac37dbf-f25d-4c0c-bcc8-ad8af946b541&=fulfillableIds[]=e5e91243-d11a-47a3-9308-22af70da5bc4 ``` and get quantity information for the given fulfillables from the response. For each requested fulfillable item, the response of this endpoint contains 2 properties: - `unitQuantity` - the total available quantity of the product, in minimal atomic units (e.g. bottles) - `batchQuantity` - the total available quantity of the batches (packs) of the product (e.g. boxes of bottles) Having these 2 properties, you should check 2 things, before move on: 1. The planned quantity for the corresponding product in the order should not exceed `unitQuantity` from the response. If the requested quantity is greater than the `unitQuantity`, the request will be rejected. 2. The requested quantity of the product should be evenly divisible by the "batchSize", which can be calculated by dividing returned `unitQuantity` by the `batchQuantity`. This is required because the Fulfillment Center can only fulfill products by the whole batches (packs). Violating this rule will result in the rejection of the request. After these simply calculations, you should end up with the following information: | Product SKU | Quantity | Fulfillable ID | Batch Size | |-------------|----------|--------------------------------------|------------| | AB-1234 | 12 | 1ac37dbf-f25d-4c0c-bcc8-ad8af946b541 | 3 | | CD-00-XY | 5 | e5e91243-d11a-47a3-9308-22af70da5bc4 | 5 | as you can see here, the quantities are evenly divisible by the batch sizes, which means the products information is ready to go. For instance, the products from our example will be fulfilled by 4 packs of `AB-1234` and 1 pack of `CD-00-XY`. #### [3] Validate the shipping address The next step is to validate the shipping address. As it was mentioned before, the address properties should correspond to each other and be valid. So the user information in this example looks valid and the only thing left is to distribute the address fields to the corresponding properties: ```json5 { "country": "FR", // ISO 3166-1 alpha-2 country code "postalCode": "75001", // Valid postal code for France "city": "Paris", "street": "123, Rue de Foo Bar", // 60 characters max, otherwise will not fit the place on the printed label "company": "ACME", "firstName": "John", "lastName": "Doe", "phone": "+33 612345678", // Valid phone number for France "email": "john@acme.com" } ``` #### [4] Generate unique identifier for the order The next step is to generate UUID (preferably [UUIDv6](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-6)) for the order being created. You can use any library of your choice to generate it, the only requirement is that it should follow the specification. #### [5] Send the request to the API At this point, you are ready to send the request to the API. What is left to do is to prepare API request payload by putting together all the information collected in the previous steps. For the exact payload structure, please refer to the [endpoint specification](#tag/Ordering/operation/ordering-create-non-ankorstore-fulfillment-order). The successful acceptance of the request does not mean 100% guarantee that the order will be fulfilled by Fulfillment Centre. The validation on the Fulfillment Centre side is usually taking some time, so you should check the status of the order by sending request to the [Get Master Order](#tag/Ordering/operation/get-master-order) endpoint, using the generated UUID as an identifier and including the `externalOrder` relation. The status of the included _External Order_ will indicate the actual status of the order. ## ⚠️ Deprecation notice Some of the endpoints and documents from this section are deprecated and will be removed in the future. You can temporarily still find them [here](#tag/Deprecated).