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.

ServicePurpose
Edi::Amazon::ShipWithAmazonCore API client for Shipping V2 endpoints
Shipping::AmazonSellerShipping carrier class following Shipping::Base pattern
Edi::Amazon::ShippingLabelPurchaserOrchestrates label purchase/void workflow
Edi::MarketplaceLabelPurchaserBase class with factory routing for marketplace labels
EndpointMethodPurpose
/shipping/v2/shipments/ratesPOSTGet eligible shipping rate offerings
/shipping/v2/shipmentsPOSTPurchase a shipment (returns label inline)
/shipping/v2/shipments/{id}/documentsGETDownload label PDF (fallback)
/shipping/v2/shipments/{id}/cancelPUTCancel/void a purchased shipment
/shipping/v2/trackingGETGet 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:

PartnerBusiness ID
amazon_seller_central_usAmazonShipping_US
amazon_seller_central_caAmazonShipping_US
amazon_seller_central_ukAmazonShipping_UK
amazon_seller_central_frAmazonShipping_FR
amazon_seller_central_deAmazonShipping_FR
amazon_seller_central_esAmazonShipping_ES
amazon_seller_central_itAmazonShipping_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.

AspectAmazon Buy ShippingShip with Walmart
Label deliveryInline in purchase response (Base64 PDF)Separate download step required
Cancellation keyamz_shipment_idCarrier + tracking number
Channel typeAMAZON (order address resolved server-side)N/A (address always required)
Business ID headerx-amzn-shipping-business-id per regionN/A
AuthLWA + SigV4 (shared with all SP-API)Walmart OAuth (separate)
Metadata columnamz_metadata JSONBsww_metadata JSONB
Service code prefixAMZBS_SWW_
Badge colorWarning (yellow)Primary (blue)

123 tests / 315 assertions across 5 test files:

Test FileCoverage
test/services/edi/amazon/ship_with_amazon_test.rbAPI client: payload builders, mocked HTTP flows for all 5 endpoints, result structs, error handling
test/services/shipping/amazon_seller_test.rbCarrier class: validation, find_rates/create_label/void_label with stubbed client, rate estimate building
test/services/edi/amazon/shipping_label_purchaser_test.rbLabel purchaser: factory routing, purchase/void lifecycle, early label reuse, validation
test/services/edi/amazon/buy_shipping_integration_test.rbIntegration: WyShipping routing, Order eligibility, ConfirmMessageProcessor mapping, carrier codes
test/services/edi/amazon/early_label_test.rbEarly 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

FilePurpose
app/services/edi/amazon/ship_with_amazon.rbAPI client wrapping Shipping V2 endpoints
app/services/shipping/amazon_seller.rbCarrier class for WyShipping rate comparison
app/services/edi/amazon/shipping_label_purchaser.rbLabel purchase/void lifecycle
app/services/edi/marketplace_label_purchaser.rbBase class with factory routing
app/services/edi/amazon/orchestrator.rbPartner config with buy_shipping_enabled flag
app/services/wy_shipping.rbCarrier routing, void flow, track flow
app/services/edi/amazon/confirm_message_processor.rbShip confirmation with carrier mapping
app/models/shipment.rbamz_metadata JSONB accessors
app/models/order.rbamazon_buy_shipping_eligible?, early label methods
app/models/delivery.rbamazon_order_info in rate retrieval, intentional selection
app/helpers/deliveries_helper.rbamz_bs_badge, is_amz_bs_rate? helpers
config/initializers/carrier_codes.rbAmazonSeller carrier entry
db/migrate/20260303200000_add_amazon_buy_shipping_fields_to_shipments.rbSchema 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