Ruby wrapper for Fedex RESTful APIs.
Add to your Gemfile:
gem 'fedex_apis', git: 'github.com/fishisfast/fedex_apis'
Initialize the client with your FedEx API credentials:
require 'fedex_apis'
# Option 1: Use environment variables from .env file
# Create a .env file with:
# FEDEX_APIS_HOST=https://apis-sandbox.fedex.com
# FEDEX_APIS_CLIENT_ID=your_client_id
# FEDEX_APIS_CLIENT_SECRET=your_client_secret
# FEDEX_APIS_ACCOUNT_NUMBER=your_account_number
require 'dotenv/load'
client = FedexApis::Client.new
# Option 2: Use environment variables (without .env file)
# Set FEDEX_APIS_HOST, FEDEX_APIS_CLIENT_ID, FEDEX_APIS_CLIENT_SECRET, FEDEX_APIS_ACCOUNT_NUMBER
client = FedexApis::Client.new
# Option 3: Pass credentials directly
client = FedexApis::Client.new(
host: 'https://apis-sandbox.fedex.com',
client_id: 'your_client_id',
client_secret: 'your_client_secret',
account_number: 'your_account_number'
)Get shipping rates for a shipment:
rate_params = {
requestedShipment: {
shipper: {
address: {
postalCode: 19726,
countryCode: "US"
}
},
recipient: {
address: {
postalCode: 2116,
countryCode: "AU"
}
},
shipDateStamp: "2024-01-15",
pickupType: "DROPOFF_AT_FEDEX_LOCATION",
serviceType: "INTERNATIONAL_PRIORITY",
rateRequestType: ["LIST", "ACCOUNT"],
requestedPackageLineItems: [
{
weight: {
units: "KG",
value: 10
}
}
]
}
}
rate = client.rate(rate_params)
if rate.success?
quotes = rate.output['rateReplyDetails']
quotes.each do |quote|
puts "Service: #{quote['serviceType']}"
puts "Total: #{quote['ratedShipmentDetails'][0]['totalNetCharge']}"
end
endTrack a shipment by tracking number:
tracking_params = {
trackingInfo: [
{
trackingNumberInfo: {
trackingNumber: "794972072690"
}
}
],
includeDetailedScans: true
}
tracking = client.track(tracking_params)
if tracking.success?
results = tracking.output['completeTrackResults']
results.each do |result|
track_result = result['trackResults'][0]
puts "Status: #{track_result['latestStatusDetail']['description']}"
puts "Delivery Date: #{track_result['dateAndTimes'][0]['dateTime']}"
end
endCreate a single package shipment and get the label:
shipment_params = {
labelResponseOptions: "LABEL",
requestedShipment: {
shipper: {
contact: {
personName: "John Sender",
phoneNumber: "1234567890",
companyName: "Sender Company"
},
address: {
streetLines: ["123 Sender St"],
city: "Austin",
stateOrProvinceCode: "TX",
postalCode: "73301",
countryCode: "US"
}
},
recipients: [
{
contact: {
personName: "Jane Recipient",
phoneNumber: "9876543210",
companyName: "Recipient Company"
},
address: {
streetLines: ["456 Recipient Ave"],
city: "Los Angeles",
stateOrProvinceCode: "CA",
postalCode: "90001",
countryCode: "US"
}
}
],
shipDatestamp: "2024-01-15",
serviceType: "FEDEX_GROUND",
packagingType: "YOUR_PACKAGING",
pickupType: "USE_SCHEDULED_PICKUP",
shippingChargesPayment: {
paymentType: "SENDER"
},
labelSpecification: {
imageType: "PDF",
labelStockType: "PAPER_85X11_TOP_HALF_LABEL"
},
requestedPackageLineItems: [
{
weight: {
units: "LB",
value: 10
}
}
]
}
}
shipment = client.create_shipment(shipment_params)
if shipment.success?
# Get tracking number
puts "Tracking Number: #{shipment.tracking_number}"
# ⭐ Consistent Interface - Get label content (works with both URL and base64)
shipment.save_label('label.pdf') # Simplest - saves directly to file
# Or get the binary content
label_binary = shipment.label_content
File.write('label.pdf', label_binary, mode: 'wb')
# Get all documents
shipment.documents.each do |doc|
puts "Document: #{doc['contentType']} - #{doc['docType']}"
end
endCreate a shipment with multiple packages:
shipment_params = {
labelResponseOptions: "LABEL",
requestedShipment: {
# ... shipper and recipient details ...
requestedPackageLineItems: [
{ weight: { units: "LB", value: 10 } },
{ weight: { units: "LB", value: 15 } },
{ weight: { units: "LB", value: 8 } }
]
}
}
shipment = client.create_shipment(shipment_params)
if shipment.success?
# Master tracking number for the entire shipment
puts "Master Tracking: #{shipment.master_tracking_number}"
# Individual tracking numbers for each package
shipment.tracking_numbers.each_with_index do |tn, i|
puts "Package #{i + 1}: #{tn}"
end
# ⭐ Consistent Interface - Save all labels (works with both URL and base64)
saved_files = shipment.save_labels('./labels')
saved_files.each { |file| puts "Saved: #{file}" }
# Or get all label contents as binary data
shipment.label_contents.each_with_index do |content, i|
puts "Label #{i + 1}: #{content.bytesize} bytes"
end
endValidate shipment details before creating:
validation = client.validate_shipment(shipment_params)
if validation.success?
puts "Shipment is valid!"
else
puts "Validation errors:"
validation.errors.each { |e| puts e['message'] }
endCancel a shipment using its tracking number:
cancel = client.cancel_shipment("794972072690")
if cancel.success?
puts "Shipment cancelled successfully"
end⭐ Recommended: Use the Consistent Interface
The gem provides methods that handle FedEx's inconsistent label formats (URL or base64) automatically:
shipment = client.create_shipment(params)
# Save label to file (handles both formats automatically)
shipment.save_label('label.pdf')
# Get label as binary data
label_binary = shipment.label_content
File.write('label.pdf', label_binary, mode: 'wb')
# For multi-package shipments
shipment.save_labels('./labels') # Saves as label_1.pdf, label_2.pdf, etc.
# Or get all contents
shipment.label_contents.each_with_index do |content, i|
File.write("label_#{i + 1}.pdf", content, mode: 'wb')
endConsistent Interface Methods (Recommended):
label_content- Get label as binary string (handles URL or base64)label_contents- Get all labels as array of binary strings (MPS)save_label(path)- Save label to file (handles URL or base64)save_labels(directory, prefix: 'label')- Save all labels to directory (MPS)
Low-Level Access Methods (If you need format-specific handling):
label_url/label_urls- Get URL if availablelabel_encoded?- Check if base64 encodedlabel_data/label_data_all- Get raw base64 stringlabel/labels- Get full label object(s)
Why a consistent interface? FedEx API behavior is unpredictable:
- Same request can return different formats at different times
- Multi-package shipments often return base64
- Single-package can be either URL or base64
- Sandbox and production both exhibit this behavior
The consistent interface handles all cases automatically, so you don't need to check formats.
All API methods return a resource object with status information:
shipment = client.create_shipment(params)
if shipment.success?
# Process successful response
puts shipment.tracking_number
else
# Handle errors
puts "Status: #{shipment.status}"
if shipment.has_errors?
shipment.errors.each do |error|
puts "Error: #{error['message']}"
end
end
if shipment.has_alerts?
shipment.alerts.each do |alert|
puts "Alert: #{alert['message']}"
end
end
endFedEx API configs are stored in .env file. Create one with cp .env.sample .env. That's enough for running tests since requests are already recorded with VCR, but you will have to update this file with real credentials in case you need to create or update VCR cassettes. Be careful to not commit real credentials. It's also important to remove credentials from VCR cassettes before committing it.
After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
You can you docker compose for development:
docker compose updocker compose run gem bundle exec rake test
Bug reports and pull requests are welcome on GitHub at https://github.com/fishisfast/fedex_apis.
The gem is available as open source under the terms of the MIT License.