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
Section titled “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
Section titled “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
Section titled “Key Components”Services
Section titled “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
Section titled “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 |
Automatic Rate Retrieval
Section titled “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 WyShippingif order&.edi_orchestrator_partner&.start_with?('amazon_seller') options[: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 }endManual Rate Retrieval
Section titled “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]}" endendLabel Purchase
Section titled “Label Purchase”# After user selects an Amazon Buy Shipping ratepurchaser = 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.uploadsendVoid Label
Section titled “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 shipmentendDatabase Schema
Section titled “Database Schema”Migration
Section titled “Migration”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
Section titled “Shipment Model Extensions”# In Shipment modeljsonb_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 auditCarrier Handling
Section titled “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
Section titled “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
Section titled “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
Section titled “UI Integration”Rate Display
Section titled “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
Section titled “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
Section titled “Configuration”Orchestrator Settings
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “EDI Ship Confirmation”When labels are purchased via Amazon Buy Shipping:
Carrier Mapping
Section titled “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
Section titled “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" endThis ensures Amazon receives the correct carrier code (e.g., “UPS”) rather than “AmazonSeller”.
Early Label Purchase
Section titled “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.
- 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
Section titled “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
Section titled “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
Section titled “Voiding”Via WyShipping
Section titled “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
Section titled “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
Section titled “Error Handling”All API calls return structured Data.define result objects:
result = amz_client.get_rates(options)if result.success # Use result.rates, result.request_tokenelse Rails.logger.error("AMZ-BS Error: #{result.error}")endExceptions 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)endCarrier Codes
Section titled “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
Section titled “Troubleshooting”No Amazon Rates Displayed
Section titled “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
Section titled “”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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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