Savon

Heavy metal SOAP client

GitHub RubyGems

SOAP Client Examples

Complete request-response cycle

This example walks through a typical SOAP integration from start to finish: creating a client, discovering what operations are available, making a request, and navigating the response.

# Create a client from a WSDL
client = Savon.client(
  wsdl: "https://example.com/UserService?wsdl",
  pretty_print_xml: true, # logs formatted XML (useful while developing)
  log: true
)

# List available operations
client.operations
# => [:find_user, :create_user, :delete_user]

# Call an operation
response = client.call(:find_user, message: { id: 42 })

# The response body is a nested Hash with snakecase Symbol keys
response.body
# => { find_user_response: { user: { id: "42", name: "Luke", email: "luke@example.com" } } }

WSSE authentication with timestamp

Many enterprise services require WS-Security credentials. If you’re getting auth failures despite correct credentials, also try adding wsse_timestamp: true.

client = Savon.client(
  wsdl: "https://example.com/SecureService?wsdl",
  wsse_auth: ["username", "password", :digest],
  wsse_timestamp: true
)

response = client.call(:get_account, message: { account_id: "12345" })
account = response.body[:get_account_response][:account]

Error handling

def find_user(id)
  response = client.call(:find_user, message: { id: id })
  response.body[:find_user_response][:user]
rescue Savon::SOAPFault => error
  fault = error.to_hash[:fault]
  raise "Service error #{fault[:faultcode]}: #{fault[:faultstring]}"
rescue Savon::HTTPError => error
  raise "HTTP #{error.http.code}"
end

Key conversion

By default, symbol keys are converted to lowerCamelCase XML tags. You can change this globally with convert_request_keys_to.

# Default: :lower_camelcase
# :user_id => <userId>
client.call(:find_user, message: { user_id: 42 })

# :camelcase - use when the service expects UpperCamelCase element names
# :user_id => <UserId>
client = Savon.client(wsdl: "...", convert_request_keys_to: :camelcase)

# :upcase - use when the service expects UPPER_CASE element names
# :user_id => <USER_ID>
client = Savon.client(wsdl: "...", convert_request_keys_to: :upcase)

# :none - symbols are output as-is, no conversion applied
# :user_id => <user_id>
client = Savon.client(wsdl: "...", convert_request_keys_to: :none)

# String keys always bypass conversion regardless of the convert_request_keys_to setting
# 'UserId' => <UserId> even with the default :lower_camelcase in effect
client.call(:find_user, message: { "UserId" => 42 })

Nested message structures

Nested hashes map to nested XML elements. This is the standard way to build structured SOAP messages.

response = client.call(:create_order, message: {
  order: {
    customer_id: "CUST-042",
    shipping_address: {
      street: "1428 Elm St",
      city:   "Springfield",
      zip:    "12345"
    },
    notes: "Leave at door"
  }
})

Arrays in messages

Pass an array as a value to produce repeated XML elements.

# Produces: <id>1</id><id>2</id><id>3</id> inside <userIds>
response = client.call(:find_users, message: {
  user_ids: { id: [1, 2, 3] }
})

SOAP responses are typically deeply nested. Use Ruby’s dig to reach the value you need.

response = client.call(:get_order, message: { order_id: "ORD-001" })

response.body
# => {
#      get_order_response: {
#        order: {
#          id:     "ORD-001",
#          status: "shipped",
#          items:  {
#            item: [
#              { name: "Widget", qty: "2" },
#              { name: "Gadget", qty: "1" }
#            ]
#          }
#        }
#      }
#    }

order = response.body.dig(:get_order_response, :order)
order[:status]                       # => "shipped"
order.dig(:items, :item, 0, :name)   # => "Widget"

Handling errors without raising

By default Savon raises on SOAP faults. Set raise_errors: false to handle them yourself.

client = Savon.client(wsdl: "...", raise_errors: false)

response = client.call(:find_user, message: { id: 99 })

if response.soap_fault?
  fault = response.body[:fault]
  puts "#{fault[:faultcode]}: #{fault[:faultstring]}"
elsif response.http_error?
  puts "HTTP #{response.http.code}"
else
  response.body[:find_user_response][:user]
end

Filtering sensitive values from logs

When logging is enabled, use filters to redact sensitive values from the output.

client = Savon.client(
  wsdl: "...",
  log: true,
  filters: [:password, :credit_card_number, :ssn]
)
# Filtered values appear as ***FILTERED*** in the log output

Session cookies

Some services authenticate via a first call and then require the session cookie on subsequent requests.

auth_response = client.call(:login, message: { username: "luke", password: "secret" })
cookies = auth_response.http.cookies

client.call(:find_user, message: { id: 42 }, cookies: cookies)

Without a WSDL

When no WSDL is available, provide the endpoint and namespace manually. You will also need to set soap_action per request.

client = Savon.client(
  endpoint: "https://service.example.com/users",
  namespace: "http://v1.example.com/users"
)

client.call(:find_user,
  soap_action: "http://v1.example.com/users/FindUser",
  message: { user_id: 42 }
)