I can't tell you exactly what's wrong, but I can tell you how to see what's happening. Try putting it into verbose mode, so you can see what data and headers are actually being sent:
curl = Curl::Easy.new
# Make it verbose, prints to stderr
curl.verbose = true
paypal_result = curl.http_post("https://api.sandbox.paypal.com/v1/oauth2/token", "grant_type=client_credentials") do |http|
# etc.
end
You can compare with the CLI version by using the -v
flag.
Hope that helps you discover the difference.