Skip to content

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.

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

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.

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
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

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')
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
}
end
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
# 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
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
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)
# 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

For Amazon Buy Shipping shipments, carrier data is stored in two places:

  • shipment.carrier = "AmazonSeller" — Indicates this is an Amazon Buy Shipping label
  • shipment.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

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
}

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”.

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)
<%# 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) %>

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.

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

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: '...'

Completed prerequisites:

  1. Carrier Terms & Conditions accepted in Seller Central > Settings > Shipping Settings
  2. SP-API IAM roles verified (same roles used for Orders/Feeds/Catalog)

When labels are purchased via Amazon Buy Shipping:

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
}

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”.

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.

  1. User checks “Purchase label early” on the shipping page
  2. Order proceeds through normal CR hold / payment flow
  3. When order reaches awaiting_deliveries, purchase_early_label_if_requested fires
  4. build_early_label_shipper creates Shipping::AmazonSeller (or Shipping::WalmartSeller)
  5. Rates are fetched, matched to the delivery’s selected shipping cost by amz_carrier_id + amz_service_id
  6. Label is purchased via Shipping::AmazonSeller#create_label; PDF stored inline
  7. fire_early_label_edi_confirm sends an Amazon-format ship confirm (packageDetail)
  8. Warehouse picks/packs; at complete_picked, early_label_shipments_match? checks for dimension/weight changes
  9. If mismatch detected, void_early_label! cancels via ShipWithAmazon#cancel_shipment and resets the flag
  10. At ship-label time, ShippingLabelPurchaser#use_early_purchased_label reuses the label (if no mismatch) or a fresh label is purchased
  • Order#marketplace_early_label_eligible? — checks both Walmart and Amazon eligibility
  • Order#amazon_buy_shipping_eligible? — checks EDI order + amazon_seller partner + buy_shipping_enabled?
  • Order#build_early_label_shipper — returns Shipping::AmazonSeller or Shipping::WalmartSeller
  • Order#match_early_label_rate — matches by amz_carrier_id + amz_service_id (Amazon) or sww_carrier_id + sww_service_type (Walmart)
  • Order#fire_early_label_edi_confirm_amazon — builds Amazon packageDetail format confirm and sends immediately
  • Order#void_early_label! — routes to ShipWithAmazon#cancel_shipment for Amazon orders
  • Edi::Amazon::ShippingLabelPurchaser#use_early_purchased_label — transfers early label from order to shipment

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 confirmShipment calls; the latest tracking supersedes the earlier one

WyShipping.void_amazon_buy_shipping_delivery handles delivery-level voiding:

  1. Finds shipments with amz_shipment_id (or non-empty amz_metadata)
  2. Creates Edi::Amazon::ShippingLabelPurchaser for each shipment
  3. Calls void_label which invokes ShipWithAmazon#cancel_shipment
  4. Clears tracking, carrier, amz_shipment_id, and amz_metadata from shipment
  5. Returns aggregate result with voided/failed counts

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.

All API calls return structured Data.define result objects:

result = amz_client.get_rates(options)
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

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'
}
  1. Verify the order is from Amazon marketplace (order.edi_orchestrator_partner.start_with?('amazon_seller'))
  2. Check that order.edi_po_number is present
  3. Verify buy_shipping_enabled: true in orchestrator config
  4. Check Rails logs for [AMZ-BS] prefixed errors
  5. Confirm carrier T&C is accepted in Seller Central

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.

  1. Check the rate has valid amz_request_token and amz_rate_id (tokens expire in ~10 minutes)
  2. Verify addresses are valid and complete
  3. Check package dimensions are within carrier limits
  4. Review [AMZ-BS] log entries for API error details

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.

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)

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

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
  • 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