Amazon Buy Shipping Integration
Amazon's Shipping V2 API ("Buy Shipping") integration provides discounted carrier rates with A-to-Z Buy Shipping protections for Amazon marketplace orders, enabling label purchasing and tracking directly from Heatwave.
Overview
When processing Amazon marketplace orders, the system can:
- Retrieve Amazon's negotiated carrier rates (UPS, USPS, FedEx, Amazon Logistics)
- Display rates alongside regular carriers for comparison in the shipping workflow
- Purchase shipping labels via Amazon's API with inline PDF delivery
- Populate tracking numbers automatically
- Void/cancel labels if needed
- Gain A-to-Z Buy Shipping protections against late delivery claims
Architecture
Mirrors the existing Ship with Walmart (SWW) pattern:
┌─────────────────────────────────────────────────────────────────────────┐
│ Rate Retrieval Flow │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Delivery needs shipping ──► Is Amazon Order? ──► Fetch Buy Shipping │
│ │ Rates │
│ No │ │
│ │ ▼ │
│ ▼ Merge with FedEx/UPS rates│
│ Skip AMZ ───────────────►──┘ │
│ │ │
│ ▼ │
│ Display combined rates │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ Label Purchase Flow │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ User selects AMZ rate ──► purchaseShipment API ──► Inline label PDF │
│ │ │
│ ▼ │
│ Attach to Shipment │
│ │ │
│ ▼ │
│ Populate tracking │
│ │ │
│ ▼ │
│ Trigger ship confirm to Amazon│
└─────────────────────────────────────────────────────────────────────────┘
Unlike Walmart SWW (which requires a separate label download step), Amazon returns the label PDF inline during purchaseShipment, simplifying the flow.
Key Components
Services
| Service | Purpose |
|---|---|
Edi::Amazon::ShipWithAmazon |
Core API client for Shipping V2 endpoints |
Shipping::AmazonSeller |
Shipping carrier class following Shipping::Base pattern |
Edi::Amazon::ShippingLabelPurchaser |
Orchestrates label purchase/void workflow |
Edi::MarketplaceLabelPurchaser |
Base class with factory routing for marketplace labels |
API Endpoints Used
| Endpoint | Method | Purpose |
|---|---|---|
/shipping/v2/shipments/rates |
POST | Get eligible shipping rate offerings |
/shipping/v2/shipments |
POST | Purchase a shipment (returns label inline) |
/shipping/v2/shipments/{id}/documents |
GET | Download label PDF (fallback) |
/shipping/v2/shipments/{id}/cancel |
PUT | Cancel/void a purchased shipment |
/shipping/v2/tracking |
GET | Get tracking information |
Usage
Automatic Rate Retrieval
For Amazon marketplace orders, Buy Shipping rates are automatically included in shipping rate comparison:
# In Delivery#retrieve_shipping_costs
# Amazon order info is automatically detected and passed to WyShipping
if order&.edi_orchestrator_partner&.start_with?('amazon_seller')
[:amazon_order_info] = {
partner: order.edi_orchestrator_partner.to_sym,
order_id: order.edi_po_number,
deliver_by_date: order.requested_deliver_by,
ship_by_date: company.next_valid_ship_date
}
end
Manual Rate Retrieval
orchestrator = Edi::Amazon::Orchestrator.new(:amazon_seller_central_us)
amz_client = Edi::Amazon::ShipWithAmazon.new(orchestrator)
result = amz_client.get_rates(
amazon_order_id: '111-2222222-3333333',
ship_from: {
name: 'WarmlyYours',
street1: '590 Telser Rd',
city: 'Lake Zurich',
state: 'IL',
zip: '60047',
country: 'US',
phone: '8475407775'
},
ship_to: { # Required for sandbox; optional for production AMAZON channel
name: 'John Doe',
street1: '123 Main St',
city: 'Chicago',
state: 'IL',
zip: '60601',
country: 'US'
},
packages: [{ weight: 5.0, length: 12, width: 10, height: 8 }],
ship_date: Date.current
)
if result.success
puts "Token: #{result.request_token}"
result.rates.each do |rate|
puts "#{rate[:carrier_name]} #{rate[:service_name]}: $#{rate[:total_charge]}"
end
end
Label Purchase
# After user selects an Amazon Buy Shipping rate
purchaser = Edi::Amazon::ShippingLabelPurchaser.new(shipment)
result = purchaser.purchase_label(selected_rate)
if result.success
puts "Tracking: #{result.tracking_number}"
puts "Shipment ID: #{result.label_id}"
# Label PDF automatically attached to shipment.uploads
end
Void Label
purchaser = Edi::Amazon::ShippingLabelPurchaser.new(shipment)
result = purchaser.void_label
if result[:success]
puts "Label voided successfully"
# Tracking, carrier, amz_shipment_id, and amz_metadata cleared from shipment
end
Database Schema
Migration
# db/migrate/20260303200000_add_amazon_buy_shipping_fields_to_shipments.rb
add_column :shipments, :amz_shipment_id, :string # Amazon's shipment ID (used for cancellation)
add_column :shipments, :amz_metadata, :jsonb # Typed metadata via jsonb_accessor
# db/migrate/20260307000000_add_amazon_buy_shipping_options.rb
# Seeds ShippingOption records for AMZBS carrier services (UPS, USPS, FedEx PTP rates)
Shipment Model Extensions
# In Shipment model
jsonb_accessor :amz_metadata,
amz_carrier: :string, # Actual carrier: UPS, USPS, FedEx
amz_service_name: :string, # e.g., "Ground"
amz_total_charge: :decimal, # Label cost
amz_purchased_at: :datetime, # When label was purchased
amz_order_id: :string # Amazon order ID for audit
Carrier Handling
For Amazon Buy Shipping shipments, carrier data is stored in two places:
shipment.carrier="AmazonSeller"— Indicates this is an Amazon Buy Shipping labelshipment.amz_carrier="UPS"/"FedEx"/"USPS"— The actual underlying carrier
This dual storage allows:
- UI to identify Amazon Buy Shipping shipments and display appropriate badges
- Tracking to use the actual carrier's tracking system
- EDI confirms to send the correct carrier info to Amazon
ShippingCost Rate Data
Amazon rates store additional fields in rate_data:
{
amz_rate_id: 'rate_abc',
amz_request_token: 'tok_xyz',
amz_carrier_id: 'UPS',
amz_carrier_name: 'UPS',
amz_service_id: 'UPS_GROUND',
amz_service_name: 'Ground',
amz_total_charge: 12.50,
amz_transit_days: 3,
amz_delivery_window: { start: '...', end: '...' },
amz_benefits: nil,
days_in_transit: 3,
raw: { ... } # Full Amazon response for debugging
}
Rate Token Lifecycle
Amazon's getRates returns a requestToken that must be passed to purchaseShipment. For AMAZON channel orders, this token has a limited validity window (~10 minutes). The token is stored in rate_data when rates are fetched and flows through ShippingCost → rate selection → label purchase.
If the token expires, rates must be re-fetched before purchasing. The ShippingLabelPurchaser#purchase_label method validates token presence and returns a clear failure_result ("Rate missing request_token — token may have expired, re-fetch rates") so callers know to refresh. Expired tokens surface as user-facing errors on the delivery page, prompting the user to click "Refresh Rates".
UI Integration
Rate Display
Amazon rates are displayed with a distinctive prefix:
Amazon Buy Shipping: UPS Ground - $12.50 (3 days)
Amazon Buy Shipping: USPS Priority Mail - $8.25 (2 days)
Helper Methods
<%# Badge for Amazon Buy Shipping %>
<%= amz_bs_badge %> <%# Renders: <span class="badge bg-warning text-dark">Amazon Buy Shipping</span> %>
<%# Check if rate is from Amazon Buy Shipping %>
<% if is_amz_bs_rate?(shipping_cost) %>
<!-- Show Amazon-specific info -->
<% end %>
<%# Combined rate display with badge %>
<%= format_shipping_rate_with_marketplace_badge(shipping_cost) %>
Configuration
Orchestrator Settings
Buy Shipping is controlled via settings in Edi::Amazon::Orchestrator:
amazon_seller_central_us: {
# ... other settings
buy_shipping_enabled: true,
buy_shipping_business_id: 'AmazonShipping_US',
transporter_profile: :amazon_sc_seller_api
}
amazon_seller_central_ca: {
# ... other settings
buy_shipping_enabled: true,
buy_shipping_business_id: 'AmazonShipping_US', # CA routes through US endpoint
transporter_profile: :amazon_sc_seller_api
}
To disable Buy Shipping for a partner, set buy_shipping_enabled: false.
Business ID Mapping
Amazon Shipping V2 requires an x-amzn-shipping-business-id header per marketplace region:
| Partner | Business ID |
|---|---|
amazon_seller_central_us |
AmazonShipping_US |
amazon_seller_central_ca |
AmazonShipping_US |
amazon_seller_central_uk |
AmazonShipping_UK |
amazon_seller_central_fr |
AmazonShipping_FR |
amazon_seller_central_de |
AmazonShipping_FR |
amazon_seller_central_es |
AmazonShipping_ES |
amazon_seller_central_it |
AmazonShipping_IT |
Credentials
Uses existing SP-API credentials configured in credentials.yml. No separate credentials needed — Buy Shipping uses the same LWA + SigV4 auth chain as orders, catalog, and feeds:
amazon_sc_seller_api:
client_id: '...'
client_secret: '...'
api_host: sellingpartnerapi-na.amazon.com
refresh_token: '...'
Seller Central Setup
Completed prerequisites:
- Carrier Terms & Conditions accepted in Seller Central > Settings > Shipping Settings
- SP-API IAM roles verified (same roles used for Orders/Feeds/Catalog)
EDI Ship Confirmation
When labels are purchased via Amazon Buy Shipping:
Carrier Mapping
Edi::Amazon::ConfirmMessageProcessor handles carrier info for ship confirm messages:
CARRIER_TO_CARRIER_CODE_MAP_HASH = {
'FedEx': 'FedEx',
'UPS': 'UPS',
'USPS': 'USPS',
'AmazonSeller': 'Amazon' # Fallback if amz_carrier not set
}
Effective Carrier Resolution
For Amazon Buy Shipping shipments (shipment.carrier == 'AmazonSeller'), the processor resolves the actual carrier from metadata:
effective_carrier = if shipment.carrier == 'AmazonSeller' && shipment.amz_carrier.present?
shipment.amz_carrier # e.g., "UPS", "FedEx"
else
shipment.carrier # Falls back to "AmazonSeller"
end
This ensures Amazon receives the correct carrier code (e.g., "UPS") rather than "AmazonSeller".
Early Label Purchase
Extended from the Walmart SWW early label flow. All early label methods in Order branch
on edi_orchestrator_partner.start_with?('amazon_seller') to use the correct API, message
format, and shipper class.
Flow
- User checks "Purchase label early" on the shipping page
- Order proceeds through normal CR hold / payment flow
- When order reaches
awaiting_deliveries,purchase_early_label_if_requestedfires build_early_label_shippercreatesShipping::AmazonSeller(orShipping::WalmartSeller)- Rates are fetched, matched to the delivery's selected shipping cost by
amz_carrier_id+amz_service_id - Label is purchased via
Shipping::AmazonSeller#create_label; PDF stored inline fire_early_label_edi_confirmsends an Amazon-format ship confirm (packageDetail)- Warehouse picks/packs; at
complete_picked,early_label_shipments_match?checks for dimension/weight changes - If mismatch detected,
void_early_label!cancels viaShipWithAmazon#cancel_shipmentand resets the flag - At ship-label time,
ShippingLabelPurchaser#use_early_purchased_labelreuses the label (if no mismatch) or a fresh label is purchased
Key Methods
Order#marketplace_early_label_eligible?— checks both Walmart and Amazon eligibilityOrder#amazon_buy_shipping_eligible?— checks EDI order +amazon_sellerpartner +buy_shipping_enabled?Order#build_early_label_shipper— returnsShipping::AmazonSellerorShipping::WalmartSellerOrder#match_early_label_rate— matches byamz_carrier_id+amz_service_id(Amazon) orsww_carrier_id+sww_service_type(Walmart)Order#fire_early_label_edi_confirm_amazon— builds AmazonpackageDetailformat confirm and sends immediatelyOrder#void_early_label!— routes toShipWithAmazon#cancel_shipmentfor Amazon ordersEdi::Amazon::ShippingLabelPurchaser#use_early_purchased_label— transfers early label from order to shipment
Warehouse Change Handling
If the warehouse changes packaging after an early label was purchased:
- Mismatch detection compares current shipment dimensions/weights against the early label snapshot (>20% or >2 inches / >1 lb threshold)
- If mismatch, the early label is voided via Amazon's cancel API
- A new label is purchased through the normal ship-label flow
- A new ship confirm is sent with the updated tracking number
- Amazon allows multiple
confirmShipmentcalls; the latest tracking supersedes the earlier one
Voiding
Via WyShipping
WyShipping.void_amazon_buy_shipping_delivery handles delivery-level voiding:
- Finds shipments with
amz_shipment_id(or non-emptyamz_metadata) - Creates
Edi::Amazon::ShippingLabelPurchaserfor each shipment - Calls
void_labelwhich invokesShipWithAmazon#cancel_shipment - Clears tracking, carrier,
amz_shipment_id, andamz_metadatafrom shipment - Returns aggregate result with voided/failed counts
Graceful Handling
If amz_shipment_id is blank (label metadata missing), void_label clears metadata silently and returns success — avoids blocking the void flow for orphaned records.
Error Handling
All API calls return structured Data.define result objects:
result = amz_client.get_rates()
if result.success
# Use result.rates, result.request_token
else
Rails.logger.error("AMZ-BS Error: #{result.error}")
end
Exceptions within API calls are caught and returned as failed results (never raised to callers):
# get_rates, purchase_shipment, cancel_shipment, get_tracking all follow this pattern:
rescue StandardError => e
RatesResult.new(success: false, error: e.message)
end
Carrier Codes
config/initializers/carrier_codes.rb includes the AmazonSeller entry:
{
internal: 'AmazonSeller',
scac: nil,
shipengine: nil,
gem: nil,
shipsurance: nil,
type: :parcel,
active: true,
notes: 'Marketplace carrier via Amazon Buy Shipping API; underlying carrier in amz_metadata'
}
Troubleshooting
No Amazon Rates Displayed
- Verify the order is from Amazon marketplace (
order.edi_orchestrator_partner.start_with?('amazon_seller')) - Check that
order.edi_po_numberis present - Verify
buy_shipping_enabled: truein orchestrator config - Check Rails logs for
[AMZ-BS]prefixed errors - Confirm carrier T&C is accepted in Seller Central
"shipTo address" Error in Sandbox
The Amazon sandbox does not resolve shipTo from AMAZON channel order IDs. The integration always includes shipTo explicitly (populated from Shipping::Base destination address fields), which works in both sandbox and production.
Label Creation Failed
- Check the rate has valid
amz_request_tokenandamz_rate_id(tokens expire in ~10 minutes) - Verify addresses are valid and complete
- Check package dimensions are within carrier limits
- Review
[AMZ-BS]log entries for API error details
Tracking Not Resolving
For AmazonSeller shipments, tracking uses shipment.amz_carrier (not shipment.carrier). If amz_carrier is blank, WyShipping.track_shipment returns an error. Verify amz_metadata was populated during label purchase.
Differences from Ship with Walmart
| Aspect | Amazon Buy Shipping | Ship with Walmart |
|---|---|---|
| Label delivery | Inline in purchase response (Base64 PDF) | Separate download step required |
| Cancellation key | amz_shipment_id |
Carrier + tracking number |
| Channel type | AMAZON (order address resolved server-side) |
N/A (address always required) |
| Business ID header | x-amzn-shipping-business-id per region |
N/A |
| Auth | LWA + SigV4 (shared with all SP-API) | Walmart OAuth (separate) |
| Metadata column | amz_metadata JSONB |
sww_metadata JSONB |
| Service code prefix | AMZBS_ |
SWW_ |
| Badge color | Warning (yellow) | Primary (blue) |
Test Coverage
123 tests / 315 assertions across 5 test files:
| Test File | Coverage |
|---|---|
test/services/edi/amazon/ship_with_amazon_test.rb |
API client: payload builders, mocked HTTP flows for all 5 endpoints, result structs, error handling |
test/services/shipping/amazon_seller_test.rb |
Carrier class: validation, find_rates/create_label/void_label with stubbed client, rate estimate building |
test/services/edi/amazon/shipping_label_purchaser_test.rb |
Label purchaser: factory routing, purchase/void lifecycle, early label reuse, validation |
test/services/edi/amazon/buy_shipping_integration_test.rb |
Integration: WyShipping routing, Order eligibility, ConfirmMessageProcessor mapping, carrier codes |
test/services/edi/amazon/early_label_test.rb |
Early label: Amazon shipper construction, rate matching, inline PDF storage, Amazon-format EDI confirm, void routing, mismatch detection |
Run tests with: PARALLEL_WORKERS=1 mise exec -- bundle exec rails test test/services/edi/amazon/ship_with_amazon_test.rb test/services/shipping/amazon_seller_test.rb test/services/edi/amazon/shipping_label_purchaser_test.rb test/services/edi/amazon/buy_shipping_integration_test.rb test/services/edi/amazon/early_label_test.rb
Key Files
| File | Purpose |
|---|---|
app/services/edi/amazon/ship_with_amazon.rb |
API client wrapping Shipping V2 endpoints |
app/services/shipping/amazon_seller.rb |
Carrier class for WyShipping rate comparison |
app/services/edi/amazon/shipping_label_purchaser.rb |
Label purchase/void lifecycle |
app/services/edi/marketplace_label_purchaser.rb |
Base class with factory routing |
app/services/edi/amazon/orchestrator.rb |
Partner config with buy_shipping_enabled flag |
app/services/wy_shipping.rb |
Carrier routing, void flow, track flow |
app/services/edi/amazon/confirm_message_processor.rb |
Ship confirmation with carrier mapping |
app/models/shipment.rb |
amz_metadata JSONB accessors |
app/models/order.rb |
amazon_buy_shipping_eligible?, early label methods |
app/models/delivery.rb |
amazon_order_info in rate retrieval, intentional selection |
app/helpers/deliveries_helper.rb |
amz_bs_badge, is_amz_bs_rate? helpers |
config/initializers/carrier_codes.rb |
AmazonSeller carrier entry |
db/migrate/20260303200000_add_amazon_buy_shipping_fields_to_shipments.rb |
Schema migration |
Related Documentation
doc/features/SHIP_WITH_WALMART.md— Ship with Walmart integration (analogous pattern)doc/features/WALMART_EARLY_LABEL_PURCHASE.md— Early label purchase flow (shared with Amazon).agents/skills/webhooks/SKILL.md— Webhook handling patterns