Managing Fulfillment Inbound (FBA) Shipments¶
Warning
The following includes features added in v1.0dev16 related to Datatype models.
Models can be called from the API class that uses them. For example, to use the
Address
model attached to the
InboundShipments
API:
from mws import InboundShipments
# from the class itself:
my_address = InboundShipments.Address(...)
# or from an instance of the class:
inbound_shipments_api = InboundShipments(...)
my_address = inbound_shipments_api.Address(...)
Note
Examples in this document use MWSResponse preview features.
MWS handles Fulfillment Inbound Shipments, also known as FBA (for “Fulfillment By Amazon”) through the Fulfillment Inbound Shipment API section. Users should familiarize themselves with this section of the API in MWS documentation before getting started.
In python-amazon-mws, this API is covered by InboundShipments
.
Basic steps to create a shipment in MWS¶
For a quick overview, MWS requires the following pattern to creating FBA shipments:
Send a request to
create_inbound_shipment_plan
with all items you wish to ship, along with their quantities, conditions, prep details, and so on.MWS will respond with one or more shipment plans, indicating where to send each of your items. Multiple shipments may be requested, and the same item may have its quantities split between these shipments. Each plan also returns the FBA Shipment ID needed to create a shipment, as well as the ID and address of the Fulfillment Center that will expect that shipment.
For each shipment plan, send a
create_inbound_shipment
request with the items, quantities, and other details identified in the plan.Optionally, it is possible to use
update_inbound_shipment
to add planned items for a new shipment to an existing shipment under certain conditions. Using this option improperly may violate the terms of your seller account, so use with caution!
We’ll look at each of these steps in detail below.
Warning
MWS does not provide a sandbox for testing functionality. If you use examples from this guide for testing purposes, you will need to use live data to do it, and will be creating real FBA shipments. Please use this guide at your own risk.
Some things to keep in mind when testing this functionality:
Make note of any Shipment IDs for shipments you generate with these examples.
Use custom shipment names to help identify test shipments, such as “TEST_IGNORE”, so you can more easily find those shipments in Seller Central, if you lose track of them in testing.
Inform other members of your organization that you are conducting tests, particularly if they use Seller Central or other MWS-related tooling to check on shipment statuses.
Leaving test shipments in WORKING or SHIPPED statuses may have an impact on your product inventory. We advise changing these to CANCELLED when you complete your testing.
Requesting a shipment plan¶
We start by informing Amazon we have items we wish to ship, requesting a shipment plan through MWS.
You will need:
MWS credentials to authenticate with MWS (not in scope for these docs).
A valid ship-from address, presumably the address of the facility where you will be shipping items from.
A list of Seller SKUs for items in your product catalog to add to new shipments.
Create the API instance¶
To begin, create an instance of InboundShipments
as you would any other API class in python-amazon-mws.
You will then use this API class instance to initiate requests to MWS.
from mws import InboundShipments
# assuming MWS credentials are stored in environment variables (your setup may vary):
inbound_api = InboundShipments(
access_key=os.environ("MWS_ACCESS_KEY"),
secret_key=os.environ("MWS_SECRET_KEY"),
account_id=os.environ("MWS_ACCOUNT_ID"),
)
Create your ship-from address¶
Next, set up your ship-from address, which is required for the three core operations related to FBA shipments: planning, creation, and updating.
The simplest way to store your ship-from address is to create an instance of the
Address
model:
my_address = inbound_api.Address(
name="My Warehouse",
address_line_1="555 Selling Stuff Lane",
address_line_2="Suite 404",
city="New York",
district_or_county="Brooklyn",
state_or_province_code="NY",
country_code="US",
postal_code="11265",
)
This model closely follows the structure of MWS’s Datatype of the same name. You should refer to MWS documentation for this Datatype to ensure all necessary elements of your address are included.
Note
If you’re curious, you can use any model’s .to_params()
method to return a dictionary containing the
request parameters of that model and their values.
my_address.to_params()
# {'Name': 'My Warehouse', 'AddressLine1': '555 Selling Stuff Lane', 'AddressLine2': 'Suite 404', 'City': 'New York', 'DistrictOrCounty': 'Brooklyn', 'StateOrProvinceCode': 'NY', 'CountryCode': 'US', 'PostalCode': '11265'}
This method also accepts a prefix
argument, which adds the prefix string plus '.'
before each parameter key:
my_address.to_params("ShipFromAddress")
# {'ShipFromAddress.Name': 'My Warehouse', 'ShipFromAddress.AddressLine1': '555 Selling Stuff Lane', 'ShipFromAddress.AddressLine2': 'Suite 404', 'ShipFromAddress.City': 'New York', 'ShipFromAddress.DistrictOrCounty': 'Brooklyn', 'ShipFromAddress.StateOrProvinceCode': 'NY', 'ShipFromAddress.CountryCode': 'US', 'ShipFromAddress.PostalCode': '11265'}
Using .to_params()
in your own code is usually not necessary, as most request methods will convert the
model instance to parameters automatically.
Optional: Store your ship-from address on the API instance¶
If you plan to make several requests in a row related to the same ship-from address, you can store the address on
an instance of InboundShipments
API as .from_address
:
inbound_api.from_address = my_address
When using this option, you can omit passing from_address=my_address
as an argument in the request examples below.
All relevant request methods (create_inbound_shipment_plan
, create_inbound_shipment
, and
update_inbound_shipment
) will pass the stored from_address
to these requests automatically.
In any case, supplying a from_address
argument to one of these methods will be used as an override, regardless of
the address stored within the API instance.
Request a shipment plan¶
Amazon’s workflow for creating a shipment uses the following pattern:
Create a shipment plan by sending a
CreateInboundShipmentPlan
request. This informs Amazon which items you intend to ship and the total quantity for each, as well as any prep details, item conditions, and so on.MWS responds with one or more planned shipments for those items. They may request certain items are sent to certain fulfillment centers, and may even split quantities for some items to multiple facilities. You must use the planned shipments to create your actual shipments.
Send a
CreateInboundShipment
request for each planned shipment. This should include the ShipmentId, DestinationFulfillmentCenterId, and any items and quantities returned in the response fromCreateInboundShipmentPlan
, so that the new shipment matches the planned one.A successful request to
CreateInboundShipment
will create an FBA Shipment, which you can further interact with through MWS or on Seller Central.
We’ll start by creating the shipment plan, for which we need a list of items.
Building a list of planned items¶
Each item in your shipment plan can be represented by an instance of
InboundShipmentPlanRequestItem
,
which closely follows the MWS Datatype of the same name:
item1 = inbound_api.InboundShipmentPlanRequestItem('MY-SKU-1', 36)
item2 = inbound_api.InboundShipmentPlanRequestItem('MY-SKU-2', 12)
my_items = [item1, item2]
The only required arguments for the model are sku
and quantity
, which are sufficient for loose item
shipments of new items when prep details do not need to be specified.
Note
You can add more detail to an InboundShipmentPlanRequestItem
instance, depending on your needs.
If you were sending, for example, an item that comes in case-packs of 12, in NewOEM condition, with a particular
ASIN, and requires Amazon to prep each item with Polybagging; you might create that item model like so:
my_condition = inbound_api.ItemCondition.NEW_OEM # or the string "NewOEM"
my_prep_details = inbound_api.PrepDetails(
prep_instruction=PrepInstruction.POLYBAGGING, # or "Polybagging"
prep_owner=PrepDetails.AMAZON # or "AMAZON"
)
detailed_item = inbound_api.InboundShipmentPlanRequestItem(
sku='MY-OTHER-SKU',
quantity=48,
quantity_in_case=12,
asin='B0123456789',
condition=my_condition,
prep_details_list=[my_prep_details],
)
Again for the curious, detailed_item.to_params()
looks like so:
detailed_item.to_params()
# {'SellerSKU': 'MY-OTHER-SKU', 'ASIN': 'B0123456789', 'Condition': 'NewOEM', 'Quantity': 48, 'QuantityInCase': 12, 'PrepDetailsList.member.1.PrepInstruction': 'Polybagging', 'PrepDetailsList.member.1.PrepOwner': 'AMAZON'}
Sending the request¶
Now that we have our items handy, it’s time to make our request for a shipment plan:
# using `inbound_api`, `my_address` and `my_items` from previous examples
resp = inbound_api.create_inbound_shipment_plan(my_items, from_address=my_address)
Other arguments you can provide include:
country_code
orsubdivision_code
, the country or country subdivision you are planning to send a shipment to.country_code
defaults to"US"
;subdivision_code
(which refers to a subdivision of India specifically) defaults toNone
.According to MWS documentation, providing both options will return an error.
label_preference
, a preference for label preparation. Defaults toNone
, which MWS may interpret as “SELLER_LABEL” internally.
And note that the from_address
argument is optional if the address has been
stored on the API instance.
Processing shipment plans¶
If our request to create shipment plans was successful, MWS will respond with an XML document containing plan details.
python-amazon-mws will automatically parse this response, giving us access to the
Python representation of the response in resp.parsed
.
For reference, we will use the following example XML response from create_inbound_shipment_plan
. You can access
this document in your own response by checking resp.original.text
:
<?xml version="1.0"?>
<CreateInboundShipmentPlanResponse
xmlns="http://mws.amazonaws.com/FulfillmentInboundShipment/2010-10-01/">
<CreateInboundShipmentPlanResult>
<InboundShipmentPlans>
<member>
<DestinationFulfillmentCenterId>ABE2</DestinationFulfillmentCenterId>
<LabelPrepType>SELLER_LABEL</LabelPrepType>
<ShipToAddress>
<City>Breinigsville</City>
<CountryCode>US</CountryCode>
<PostalCode>18031</PostalCode>
<Name>Amazon.com</Name>
<AddressLine1>705 Boulder Drive</AddressLine1>
<StateOrProvinceCode>PA</StateOrProvinceCode>
</ShipToAddress>
<EstimatedBoxContentsFee>
<TotalUnits>10</TotalUnits>
<FeePerUnit>
<CurrencyCode>USD</CurrencyCode>
<Value>0.10</Value>
</FeePerUnit>
<TotalFee>
<CurrencyCode>USD</CurrencyCode>
<Value>10.0</Value>
</TotalFee>
</EstimatedBoxContentsFee>
<Items>
<member>
<FulfillmentNetworkSKU>FNSKU00001</FulfillmentNetworkSKU>
<Quantity>1</Quantity>
<SellerSKU>SKU00001</SellerSKU>
<PrepDetailsList>
<PrepDetails>
<PrepInstruction>Taping</PrepInstruction>
<PrepOwner>AMAZON</PrepOwner>
</PrepDetails>
</PrepDetailsList>
</member>
<member>
...
</member>
</Items>
<ShipmentId>FBA0000001</ShipmentId>
</member>
<member>
...
</member>
</InboundShipmentPlans>
</CreateInboundShipmentPlanResult>
<ResponseMetadata>
<RequestId>babd156d-8b2f-40b1-a770-d117f9ccafef</RequestId>
</ResponseMetadata>
</CreateInboundShipmentPlanResponse>
Gathering shipment details¶
To begin, we can access each shipment plan in the parsed response like so:
# Using the `resp` object from our previous examples
for plan in resp.parsed.InboundShipmentPlans.member:
...
Each plan
contains metadata required for creating a new shipment. These include:
plan.ShipmentId
, the FBA shipment ID Amazon generates for the new shipment plan.plan.DestinationFulfillmentCenterId
, the short code for a Fulfillment Center planning to receive this shipment.plan.LabelPrepType
, the label preparation type for this shipment.
In addition to these data points, you should consider gathering the following data as arguments for the
create_inbound_shipment
request method:
shipment_name
(required), a human-readable name to help identify your shipment without relying on shipment IDs.shipment_status
, the initial status of the shipment. Defaults to “WORKING”, indicating the shipment will remain “open” so that items and quantities can still be changed before it is shipped.The following constants can be used for this argument:
InboundShipments.STATUS_WORKING
InboundShipments.STATUS_SHIPPED
InboundShipments.STATUS_CANCELLED
InboundShipments.STATUS_CANCELED
(alias forSTATUS_CANCELLED
)
case_required
, a boolean indicating that items in the shipment are either all case-packed (ifTrue
) or all loose items (ifFalse
). Defaults toFalse
.box_contents_source
, a string indicating a source of box content data for packages within the shipment, orNone
indicating no box contents source. Defaults toNone
.The following constants can be used for this argument:
InboundShipments.BOX_CONTENTS_FEED
, indicating contents will be provided in a Feed of type_POST_FBA_INBOUND_CARTON_CONTENTS_
.InboundShipments.BOX_CONTENTS_2D_BARCODE
, indicating contents will be provided using 2D barcodes present on the cartons of the shipment.
We will illustrate how to use these data points later in this doc.
Converting plan items to shipment items¶
While the request to create_inbound_shipment_plan
makes use of the
InboundShipmentPlanRequestItem
model to
transmit item data, this model is not sufficient for passing data to create_inbound_shipment
and
update_inbound_shipment
requests, as they require slightly different parameters. We will need to use the
InboundShipmentItem
model, instead.
We can pass data to this model in one of three ways:
Manually processing item data from the response:
for plan in resp.parsed.InboundShipmentPlans.member: shipment_items = [] for item in plan.Items.member: new_item = inbound_api.InboundShipmentItem( sku=item.SellerSKU, quantity=item.Quantity, ) shipment_items.append(new_item)
Using
InboundShipmentItem.from_plan_item
to construct an item automatically from each item in the response:for plan in resp.parsed.InboundShipmentPlans.member: shipment_items = [] for item in plan.Items.member: new_item = inbound_api.InboundShipmentItem.from_plan_item(item) shipment_items.append(new_item)
Using helper method
shipment_items_from_plan
to return a list of items from the entire plan automatically:for plan in resp.parsed.InboundShipmentPlans.member: shipment_items = inbound_api.shipment_items_from_plan(plan)
Note
Using InboundShipmentItem.from_plan_item
or shipment_items_from_plan
, each item will automatically
store the fnsku
of each planned item. This data is ignored in calls to create_inbound_shipment
and
update_inbound_shipment
, but can be useful for tracking items internally.
Using either of these methods, the list of shipment_items
can be used as the items
argument to either the
create_inbound_shipment
or update_inbound_shipment
request method.
Adding quantity_in_case
and release_date
values
Item data provided by a plan
is sufficient for most data required for items, but some data points must be
added manually:
Case-pack information, specifically the
quantity_in_case
argument, is not supplied by the response fromcreate_inbound_shipment_plan
, even if this information was provided in the request itself.Pre-order items must provide an additional
release_date
data point.
In the first two examples above, these data points can be added as arguments when constructing the new item:
# using InboundShipmentItem(...):
new_item = inbound_api.InboundShipmentItem(
sku=item.SellerSKU,
quantity=item.Quantity,
quantity_in_case=...,
release_date=...,
)
# using InboundShipmentItem.from_plan_item(...):
new_item = inbound_api.InboundShipmentItem.from_plan_item(
item,
quantity_in_case=...,
release_date=...,
)
# Confirm this data has been added:
print(new_item.quantity_in_case, new_item.release_date)
In either case, when working with multiple items per shipment plan, you will need to determine which SKU these data
refer to. You should be able to rely on item.SellerSKU
to identify those SKUs.
Adding extra data when processing items in bulk
When processing a planned shipment’s items in bulk, adding quantity_in_case
and/or release_date
values to
each item can be done using the overrides
argument to shipment_items_from_plan
.
overrides
expects a dictionary with SellerSKUs as its keys. The values of this dict can be either:
A dict containing keys
quantity_in_case
and/orrelease_date
(all other keys are ignored):overrides = { 'mySku1': { 'quantity_in_case': 12, 'release_date': datetime.datetime(2020-12-25), }, }
An instance of
ExtraItemData
:overrides = { 'mySku2': inbound_api.ExtraItemData( quantity_in_case=12, release_date=datetime.datetime(2020-12-25), ), }
You should construct this set of overrides for all SKUs sent in your original request to
create_inbound_shipment_plan
. You can then use the same set of overrides on any planned shipment resulting
from that request:
overrides = {...}
for plan in resp.parsed.InboundShipmentPlans.member:
shipment_items = inbound_api.shipment_items_from_plan(plan, overrides=overrides)
Creating shipments¶
Putting everything together up to this point, we can create a new FBA shipment using the
create_inbound_shipment
method:
# with optional overrides
overrides = {
'mySku1': inbound_api.ExtraItemData(...),
'mySku2': inbound_api.ExtraItemData(...),
}
for plan in resp.parsed.InboundShipmentPlans.member:
# Gather our items for the planned shipment
shipment_items = inbound_api.shipment_items_from_plan(plan, overrides=overrides)
# Send the request to create a new shipment
new_shipment_resp = inbound_api.create_inbound_shipment(
shipment_id=plan.ShipmentId,
shipment_name="My Shiny New FBA Shipment",
destination=plan.DestinationFulfillmentCenterId,
items=shipment_items,
label_preference=plan.LabelPrepType,
)
For help with additional arguments - such as shipment_status
, case_required
, box_contents_source
,
or from_address
- see Gathering shipment details.
Updating shipments¶
Creating a shipment is not the end of the story, of course. It is sometimes necessary to make changes to an
already-created shipment. For this, we use
update_inbound_shipment
.
update_inbound_shipment
’s arguments are identical to those of create_inbound_shipment
, with the exception that
all arguments besides shipment_id
are optional. Generally, supplying a value to one of those arguments will
overwrite that value of the given shipment, such as:
Setting
shipment_status=InboundShipments.STATUS_CANCELLED
to cancel a shipment;Changing the
from_address
;etc.
Changing item quantities¶
Item quantities on a shipment can be changed by providing a list of InboundShipmentItem
instances for the items
argument of update_inbound_shipment
. The details of the submitted items will overwrite details of those items in the
existing shipment based on matching SellerSKUs.
Amazon will expect the total quantity for an item: there is no mechanism for adding or subtracting a quantity from the existing total. For example, if a shipment contains 24 units of an item and you want to add 12 of that item, you will need to submit a total quantity of 36 in the update request:
resp = inbound_api.update_inbound_shipment(
shipment_id="FBAMYSHIPMENT",
items=[
inbound_api.InboundShipmentItem(
sku="MySku1",
quantity=36,
)
]
)
It is up to you how you keep track of these quantity changes in your process. One way might be to cache these details
in some local database. Another might be querying the current total quantity using a request to
list_inbound_shipment_items
, then
calculating the new total:
my_shipment = "FBAMYSHIPMENT"
# Set our change quantities as "deltas", with SKU as key and the change as value
quantity_deltas = {
'mySku1': 12, # add 12
'mySku2': -6, # remove 6
}
update_items = []
list_resp = inbound_api.list_inbound_shipment_items(shipment_id=my_shipment)
for item in list_resp.parsed.ItemData.member:
if item.SellerSKU in quantity_deltas:
new_quantity = item.QuantityShipped + quantity_deltas[item.SellerSKU]
# Negative quantities not permitted, so set 0 as a minimum using `max`:
new_quantity = max([new_quantity, 0])
# Add items to a list for updates:
update_items.append(
inbound_api.InboundShipmentItem(item.SellerSKU, new_quantity)
)
if update_items:
update_resp = inbound_api.update_inbound_shipment(
shipment_id=my_shipment,
items=update_items,
)
Adding items from a new shipment plan¶
Under certain conditions, items from a new shipment plan can be added to one of your existing shipments in WORKING status. In this way, you can keep a shipment “open” in your own facility, adding new items to the same shipment before “closing” it and sending it to Amazon’s fulfillment network.
Follow the same steps as Requesting a shipment plan, then inspect the contents of the planned shipments (see Processing shipment plans).
Generally, you may be able to add newly-planned items to an existing shipment if the following details match in the target “WORKING” shipment:
DestinationFulfillmentCenterId
LabelPrepType
Whether both shipments are designated for hazmat items.
Note
In the author’s experience, this detail may not be apparent through MWS ahead of time: you may simply need to attempt to add the item and handle whatever error occurs afterward.
Forgiveness instead of permission, as they say.
Whether the two shipments require case packs or not.
This list is not exhaustive, so use best judgment and follow Amazon’s guidance where necessary.
If you determine that a planned item can be added to one of your existing shipments, add that item to an
update_inbound_shipment
request for the given shipment ID.
As mentioned in Changing item quantities, remember to use the total quantity of an item being updated, not
the change in quantity, if the item is already present in the given shipment. If you are not tracking these quantities
in your own application, you may wish to send a request to
list_inbound_shipment_items
to
obtain the current quantity of a matching item before sending the update request.