{"id":1103,"date":"2026-09-24T09:15:30","date_gmt":"2026-09-24T08:15:30","guid":{"rendered":"https:\/\/ruby-doc.org\/blog\/?p=1103"},"modified":"2026-09-24T11:58:08","modified_gmt":"2026-09-24T10:58:08","slug":"building-a-ruby-api-client","status":"publish","type":"post","link":"https:\/\/ruby-doc.org\/learn\/building-a-ruby-api-client\/","title":{"rendered":"Building a Ruby API Client: REST Requests, Signed Calls and WebSocket Streams"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">A Ruby script that fetches one price is easy enough to leave in a single file. Add account balances, signed requests and a feed that stays open all afternoon, and that file starts getting awkward. The HTTP code needs one sort of error handling. The streaming code needs another. Neither belongs in a Rails controller.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This walkthrough builds a small exchange client around those boundaries. Ruby handles the request parameters, signatures and JSON responses; a separate WebSocket class receives trade events. Binance Spot supplies the example endpoints, but the Ruby design is useful for other integrations too.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Start with public data. Get one request working, inspect the returned hash, then add authentication. There is no need to debug credentials and network code at the same time.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Give each Ruby class one job<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Use <code>Net::HTTP<\/code> for the REST side. It keeps the first version close to Ruby&#8217;s standard library and makes the bytes being signed easy to follow. For the stream, use <code>faye-websocket<\/code> with EventMachine.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Put this in a <code>Gemfile<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>source \"https:\/\/rubygems.org\"\n\ngem \"faye-websocket\"\ngem \"eventmachine\"\ngem \"json\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Run <code>bundle install<\/code>, then create <code>lib\/exchange_client\/rest_client.rb<\/code> and <code>lib\/exchange_client\/ws_client.rb<\/code>. Keep the executable example in <code>demo.rb<\/code> at the project root.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The namespace will be <code>ExchangeClient<\/code>. Defining the module explicitly avoids the missing-constant error you can get by opening <code>ExchangeClient::RestClient<\/code> before <code>ExchangeClient<\/code> exists.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Developers coming from browser code may recognise <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Global_Objects\/JSON\/parse\">JSON.parse<\/a>. That reference describes JavaScript. Here, <code>require \"json\"<\/code> loads Ruby&#8217;s parser, and the resulting objects are Ruby hashes and arrays.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There is a server-side question behind this work as well: which component owns balances, order matching and outgoing events? Reading about an <a href=\"https:\/\/merehead.com\/blog\/create-open-source-crypto-exchange\/\">open source cryptocurrency exchange<\/a> provides some background for that discussion. Our Ruby client has a narrower responsibility: send requests and turn responses into something the application can use.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Build the REST client before adding the stream<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Save the following in <code>lib\/exchange_client\/rest_client.rb<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>require \"net\/http\"\nrequire \"uri\"\nrequire \"json\"\nrequire \"openssl\"\n\nmodule ExchangeClient\n  class HTTPError &lt; StandardError\n    attr_reader :status, :retry_after\n\n    def initialize(status, retry_after)\n      @status = status\n      @retry_after = retry_after\n      super(\"Exchange returned HTTP #{status}\")\n    end\n  end\n\n  class RestClient\n    BASE_URL = \"https:\/\/api.binance.com\"\n\n    def initialize(api_key: nil, api_secret: nil)\n      @api_key = api_key\n      @api_secret = api_secret\n    end\n\n    def ticker(symbol)\n      request(Net::HTTP::Get, \"\/api\/v3\/ticker\/price\",\n              { symbol: symbol })\n    end\n\n    def account\n      request(Net::HTTP::Get, \"\/api\/v3\/account\", {}, signed: true)\n    end\n\n    def limit_order(symbol:, side:, quantity:, price:,\n                    client_order_id:, test: true)\n      unless quantity.is_a?(String) &amp;&amp; price.is_a?(String)\n        raise ArgumentError, \"Use decimal strings for quantity and price\"\n      end\n\n      params = {\n        symbol: symbol, side: side, type: \"LIMIT\",\n        timeInForce: \"GTC\", quantity: quantity, price: price,\n        newClientOrderId: client_order_id\n      }\n      path = test ? \"\/api\/v3\/order\/test\" : \"\/api\/v3\/order\"\n      request(Net::HTTP::Post, path, params, signed: true)\n    end\n\n    def order(symbol:, client_order_id:)\n      params = { symbol: symbol, origClientOrderId: client_order_id }\n      request(Net::HTTP::Get, \"\/api\/v3\/order\", params, signed: true)\n    end\n\n    def cancel_order(symbol:, order_id:)\n      params = { symbol: symbol, orderId: order_id }\n      request(Net::HTTP::Delete, \"\/api\/v3\/order\", params, signed: true)\n    end\n\n    private\n\n    def request(request_class, path, params, signed: false)\n      headers = {}\n\n      if signed\n        if @api_key.to_s.empty? || @api_secret.to_s.empty?\n          raise ArgumentError, \"Private requests require API credentials\"\n        end\n\n        params = params.merge(\n          timestamp: (Time.now.to_r * 1000).to_i,\n          recvWindow: 5000\n        )\n        headers&#91;\"X-MBX-APIKEY\"] = @api_key\n      end\n\n      query = URI.encode_www_form(params)\n      if signed\n        signature = OpenSSL::HMAC.hexdigest(\"SHA256\", @api_secret, query)\n        query = \"#{query}&amp;signature=#{signature}\"\n      end\n\n      uri = URI.join(BASE_URL, path)\n      uri.query = query unless query.empty?\n      http_request = request_class.new(uri.request_uri, headers)\n\n      response = Net::HTTP.start(\n        uri.host, uri.port, use_ssl: true,\n        open_timeout: 5, read_timeout: 10, write_timeout: 10,\n        max_retries: 0\n      ) do |http|\n        http.request(http_request)\n      end\n\n      unless response.is_a?(Net::HTTPSuccess)\n        raise HTTPError.new(response.code.to_i, response&#91;\"Retry-After\"])\n      end\n\n      JSON.parse(response.body)\n    end\n  end\nend<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">There are a few deliberate choices here. Public requests work without credentials. Private requests refuse to run when credentials are missing. Timeouts are set explicitly, and the underlying HTTP client&#8217;s automatic retries are disabled so the application can decide what to repeat.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The signature deserves a close look. Ruby encodes the parameters once, signs that string and sends it unchanged with the signature appended. Rebuilding the query after signing can leave the request and its signature disagreeing about the bytes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This example supports HMAC API keys. It does not implement RSA or Ed25519 signing. The timestamp comes from the local clock, so clock synchronisation matters when private calls start failing despite apparently correct credentials.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Try a public request from a Ruby script<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Create <code>demo.rb<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>require_relative \"lib\/exchange_client\/rest_client\"\n\nclient = ExchangeClient::RestClient.new\nticker = client.ticker(\"BTCUSDT\")\nputs \"#{ticker.fetch('symbol')}: #{ticker.fetch('price')}\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Run it with <code>bundle exec ruby demo.rb<\/code>. Using <code>fetch<\/code> makes a missing field visible. A quiet <code>nil<\/code> is less helpful when an upstream response has changed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Prices arrive as strings. Leave them that way while passing them through the client. If your application needs arithmetic, use <code>BigDecimal<\/code> and decide how rounding should work before converting values back to request strings. A binary floating-point approximation is an unnecessary complication here.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Add credentials only where they are needed<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">For account data, construct another client using environment variables:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>client = ExchangeClient::RestClient.new(\n  api_key: ENV.fetch(\"EXCHANGE_API_KEY\"),\n  api_secret: ENV.fetch(\"EXCHANGE_API_SECRET\")\n)\n\nbalances = client.account.fetch(\"balances\")\nputs \"Received #{balances.length} balance entries\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Injecting credentials through the constructor also makes this class easier to test. A unit test can supply dummy values without changing the process environment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Keep keys out of source files and request logs. If you use a local <code>.env<\/code> file, exclude it from version control and load it explicitly with your chosen configuration tool. A public price feed needs no key; an account reader should have only the permissions its job requires.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Validate an order without submitting it for execution<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>limit_order<\/code> method defaults to Binance&#8217;s test-order endpoint. It checks the request without sending an order to the matching engine. This is request validation on the configured service, not a simulated fill or a separate testnet.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>require \"securerandom\"\n\nclient_order_id = \"ruby-#{SecureRandom.hex(12)}\"\n\nresult = client.limit_order(\n  symbol: \"BTCUSDT\",\n  side: \"BUY\",\n  quantity: \"0.001\",\n  price: \"60000.00\",\n  client_order_id: client_order_id\n)\n\np result<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Those numbers illustrate the Ruby method call. They may fail the symbol&#8217;s current filters. Check tick size, quantity step size and notional requirements before constructing an actual order.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Passing <code>test: false<\/code> changes this method to the real order endpoint. Before doing that, persist the order intent and its client ID. If the response disappears, you need a record of what you attempted.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">An order timeout leaves a question to resolve. The server may have accepted the request while the response was lost. Query the order using its client ID and reconcile its status before deciding whether to submit anything else. A client ID helps identify an order; it is not a permanent guarantee that repeated submissions cannot create another one.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Receive trade events with a separate WebSocket class<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Polling works for an occasional refresh. For updates pushed over an open connection, <a href=\"https:\/\/blog.postman.com\/how-do-websockets-work\/\">you need<\/a> a WebSocket client or another streaming mechanism supported by the service. The following example uses Binance&#8217;s public trade stream.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Save this in <code>lib\/exchange_client\/ws_client.rb<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>require \"faye\/websocket\"\nrequire \"eventmachine\"\nrequire \"json\"\n\nmodule ExchangeClient\n  class WsClient\n    WS_URL = \"wss:\/\/stream.binance.com:9443\/ws\"\n\n    def initialize(symbol, &amp;on_trade)\n      raise ArgumentError, \"Provide a trade handler\" unless on_trade\n\n      @symbol = symbol.downcase\n      @on_trade = on_trade\n    end\n\n    def connect\n      socket = Faye::WebSocket::Client.new(\"#{WS_URL}\/#{@symbol}@trade\")\n\n      socket.on :message do |event|\n        begin\n          data = JSON.parse(event.data)\n        rescue JSON::ParserError\n          warn \"Discarded an invalid JSON message\"\n          next\n        end\n\n        if data.is_a?(Hash) &amp;&amp; data&#91;\"e\"] == \"trade\"\n          @on_trade.call(data)\n        end\n      end\n\n      socket.on :error do |_event|\n        warn \"WebSocket reported a protocol error\"\n      end\n\n      socket.on :close do |event|\n        warn \"WebSocket closed with code #{event.code}\"\n        EM.stop\n      end\n\n      socket\n    end\n  end\nend<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Run the stream from <code>demo.rb<\/code> after the REST example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>require_relative \"lib\/exchange_client\/ws_client\"\n\nEM.run do\n  ExchangeClient::WsClient.new(\"BTCUSDT\") do |trade|\n    puts \"Trade #{trade.fetch('t')}: #{trade.fetch('p')}\"\n  end.connect\nend<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The executable owns the EventMachine loop. The client only opens a socket within it. That separation matters later when one process manages several connections.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Keep the callback short. A slow database write or synchronous HTTP request inside it can hold up other work on the reactor thread. Hand heavier processing to a worker through a bounded queue, with an explicit policy for what happens when the queue fills.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Make disconnects visible before automating recovery<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This small example stops when its socket closes. A service expected to run all day needs a reconnection policy, a shutdown path and a way to show consumers that its data is stale.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Binance limits a stream connection to 24 hours, so a disconnect is not necessarily evidence of a bug. Faye&#8217;s underlying WebSocket driver replies to ping frames automatically; adding an invented <code>socket.pong<\/code> handler to this class is unnecessary.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For reconnects, replace the close handler&#8217;s <code>EM.stop<\/code> with scheduling inside the existing reactor. Keep an attempt counter across failures, cap the delay and add random variation. Reset that counter after a period of stable operation. Never call <code>sleep<\/code> in the reactor, and cancel a pending reconnect when the application is shutting down.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A trade stream reports trades. Building an order book takes additional work: a depth snapshot, buffered depth events and update-ID checks. After a sequence gap, rebuild the book. Updating one <code>last_price<\/code> variable does not reconstruct bids and asks.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Put retries and tests at the application boundary<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The REST class raises an <code>HTTPError<\/code> with the status and <code>Retry-After<\/code> value. That leaves room for the caller to treat a rate limit differently from a rejected parameter. Respect the server&#8217;s delay on rate-limit responses. Repeatedly hammering the same request makes recovery harder.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A failed public read is a reasonable candidate for a bounded retry. An uncertain order submission needs reconciliation first. Avoid one broad rescue block that treats every failure as permission to send the request again.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Before using this in a Rails job or a long-running service, test the boundaries that can lose or duplicate work: the exact signed query, a timeout after submission, a malformed JSON response and a stream that closes while messages are being processed. Use recorded fixtures or test doubles for those checks.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There is plenty left to build around these classes: structured errors, connection reuse, metrics and durable order records. The useful starting point is that each concern now has somewhere to go. The REST client owns request construction. The stream client delivers events. Your Ruby application decides what those events mean and what to do when a request has no clear outcome.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>A Ruby script that fetches one price is easy enough to leave in a single file. Add account balances, signed requests and a feed that stays open all afternoon, and that file starts getting awkward. The HTTP code needs one sort of error handling. The streaming code needs another. Neither belongs in a Rails controller. [&hellip;]<\/p>\n","protected":false},"author":3,"featured_media":1706,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[19],"tags":[],"class_list":["post-1103","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ruby-projects"],"blocksy_meta":{"styles_descriptor":{"styles":{"desktop":"","tablet":"","mobile":""},"google_fonts":[],"version":7}},"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.5 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Building a Ruby API Client: REST Requests, Signed Calls and WebSocket Streams - Ruby-Doc Learn<\/title>\n<meta name=\"robots\" content=\"noindex, nofollow\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Building a Ruby API Client: REST Requests, Signed Calls and WebSocket Streams - Ruby-Doc Learn\" \/>\n<meta property=\"og:description\" content=\"A Ruby script that fetches one price is easy enough to leave in a single file. Add account balances, signed requests and a feed that stays open all afternoon, and that file starts getting awkward. The HTTP code needs one sort of error handling. The streaming code needs another. Neither belongs in a Rails controller. [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/ruby-doc.org\/learn\/building-a-ruby-api-client\/\" \/>\n<meta property=\"og:site_name\" content=\"Ruby-Doc Learn\" \/>\n<meta property=\"article:published_time\" content=\"2026-09-24T08:15:30+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-09-24T10:58:08+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/building-a-ruby-api-client.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1672\" \/>\n\t<meta property=\"og:image:height\" content=\"941\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"James Britt\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"James Britt\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"6 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-ruby-api-client\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-ruby-api-client\\\/\"},\"author\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/james-britt\\\/#james-britt\"},\"headline\":\"Building a Ruby API Client: REST Requests, Signed Calls and WebSocket Streams\",\"datePublished\":\"2026-09-24T08:15:30+00:00\",\"dateModified\":\"2026-09-24T10:58:08+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-ruby-api-client\\\/\"},\"wordCount\":1218,\"publisher\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-ruby-api-client\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/building-a-ruby-api-client.png\",\"articleSection\":[\"Ruby Projects\"],\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-ruby-api-client\\\/\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-ruby-api-client\\\/\",\"name\":\"Building a Ruby API Client: REST Requests, Signed Calls and WebSocket Streams - Ruby-Doc Learn\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-ruby-api-client\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-ruby-api-client\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/building-a-ruby-api-client.png\",\"datePublished\":\"2026-09-24T08:15:30+00:00\",\"dateModified\":\"2026-09-24T10:58:08+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-ruby-api-client\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-ruby-api-client\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-ruby-api-client\\\/#primaryimage\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/building-a-ruby-api-client.png\",\"contentUrl\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/building-a-ruby-api-client.png\",\"width\":1672,\"height\":941},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-ruby-api-client\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Building a Ruby API Client: REST Requests, Signed Calls and WebSocket Streams\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#website\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/\",\"name\":\"Ruby-Doc Learn\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#organization\",\"name\":\"Ruby-Doc Learn\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/ruby-doc-logo-transparent.png\",\"contentUrl\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/ruby-doc-logo-transparent.png\",\"width\":1650,\"height\":305,\"caption\":\"Ruby-Doc Learn\"},\"image\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/james-britt\\\/#james-britt\",\"name\":\"James Britt\",\"image\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/jgb_self-portrait-20140914.png\",\"description\":\"Creator and long-term maintainer of Ruby-Doc.org, founder of Neurogami, and author of practical Ruby tutorials for Ruby-Doc Learn.\",\"sameAs\":[\"https:\\\/\\\/jamesbritt.com\\\/\",\"https:\\\/\\\/neurogami.com\\\/\",\"https:\\\/\\\/www.oreilly.com\\\/pub\\\/au\\\/2595\",\"https:\\\/\\\/www.rubyevents.org\\\/profiles\\\/james-britt\"],\"url\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/james-britt\\\/\",\"jobTitle\":\"Ruby developer and writer\",\"knowsAbout\":[\"Ruby programming language\",\"Ruby documentation\",\"JRuby\",\"Open Sound Control\",\"Software development\"],\"affiliation\":{\"@type\":\"Organization\",\"name\":\"Ruby-Doc.org\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Building a Ruby API Client: REST Requests, Signed Calls and WebSocket Streams - Ruby-Doc Learn","robots":{"index":"noindex","follow":"nofollow"},"og_locale":"en_US","og_type":"article","og_title":"Building a Ruby API Client: REST Requests, Signed Calls and WebSocket Streams - Ruby-Doc Learn","og_description":"A Ruby script that fetches one price is easy enough to leave in a single file. Add account balances, signed requests and a feed that stays open all afternoon, and that file starts getting awkward. The HTTP code needs one sort of error handling. The streaming code needs another. Neither belongs in a Rails controller. [&hellip;]","og_url":"https:\/\/ruby-doc.org\/learn\/building-a-ruby-api-client\/","og_site_name":"Ruby-Doc Learn","article_published_time":"2026-09-24T08:15:30+00:00","article_modified_time":"2026-09-24T10:58:08+00:00","og_image":[{"width":1672,"height":941,"url":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/building-a-ruby-api-client.png","type":"image\/png"}],"author":"James Britt","twitter_card":"summary_large_image","twitter_misc":{"Written by":"James Britt","Est. reading time":"6 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/ruby-doc.org\/learn\/building-a-ruby-api-client\/#article","isPartOf":{"@id":"https:\/\/ruby-doc.org\/learn\/building-a-ruby-api-client\/"},"author":{"@id":"https:\/\/ruby-doc.org\/learn\/james-britt\/#james-britt"},"headline":"Building a Ruby API Client: REST Requests, Signed Calls and WebSocket Streams","datePublished":"2026-09-24T08:15:30+00:00","dateModified":"2026-09-24T10:58:08+00:00","mainEntityOfPage":{"@id":"https:\/\/ruby-doc.org\/learn\/building-a-ruby-api-client\/"},"wordCount":1218,"publisher":{"@id":"https:\/\/ruby-doc.org\/learn\/#organization"},"image":{"@id":"https:\/\/ruby-doc.org\/learn\/building-a-ruby-api-client\/#primaryimage"},"thumbnailUrl":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/building-a-ruby-api-client.png","articleSection":["Ruby Projects"],"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/ruby-doc.org\/learn\/building-a-ruby-api-client\/","url":"https:\/\/ruby-doc.org\/learn\/building-a-ruby-api-client\/","name":"Building a Ruby API Client: REST Requests, Signed Calls and WebSocket Streams - Ruby-Doc Learn","isPartOf":{"@id":"https:\/\/ruby-doc.org\/learn\/#website"},"primaryImageOfPage":{"@id":"https:\/\/ruby-doc.org\/learn\/building-a-ruby-api-client\/#primaryimage"},"image":{"@id":"https:\/\/ruby-doc.org\/learn\/building-a-ruby-api-client\/#primaryimage"},"thumbnailUrl":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/building-a-ruby-api-client.png","datePublished":"2026-09-24T08:15:30+00:00","dateModified":"2026-09-24T10:58:08+00:00","breadcrumb":{"@id":"https:\/\/ruby-doc.org\/learn\/building-a-ruby-api-client\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/ruby-doc.org\/learn\/building-a-ruby-api-client\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/ruby-doc.org\/learn\/building-a-ruby-api-client\/#primaryimage","url":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/building-a-ruby-api-client.png","contentUrl":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/building-a-ruby-api-client.png","width":1672,"height":941},{"@type":"BreadcrumbList","@id":"https:\/\/ruby-doc.org\/learn\/building-a-ruby-api-client\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/ruby-doc.org\/learn\/"},{"@type":"ListItem","position":2,"name":"Building a Ruby API Client: REST Requests, Signed Calls and WebSocket Streams"}]},{"@type":"WebSite","@id":"https:\/\/ruby-doc.org\/learn\/#website","url":"https:\/\/ruby-doc.org\/learn\/","name":"Ruby-Doc Learn","description":"","publisher":{"@id":"https:\/\/ruby-doc.org\/learn\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/ruby-doc.org\/learn\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/ruby-doc.org\/learn\/#organization","name":"Ruby-Doc Learn","url":"https:\/\/ruby-doc.org\/learn\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/ruby-doc.org\/learn\/#\/schema\/logo\/image\/","url":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/ruby-doc-logo-transparent.png","contentUrl":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/ruby-doc-logo-transparent.png","width":1650,"height":305,"caption":"Ruby-Doc Learn"},"image":{"@id":"https:\/\/ruby-doc.org\/learn\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/ruby-doc.org\/learn\/james-britt\/#james-britt","name":"James Britt","image":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/jgb_self-portrait-20140914.png","description":"Creator and long-term maintainer of Ruby-Doc.org, founder of Neurogami, and author of practical Ruby tutorials for Ruby-Doc Learn.","sameAs":["https:\/\/jamesbritt.com\/","https:\/\/neurogami.com\/","https:\/\/www.oreilly.com\/pub\/au\/2595","https:\/\/www.rubyevents.org\/profiles\/james-britt"],"url":"https:\/\/ruby-doc.org\/learn\/james-britt\/","jobTitle":"Ruby developer and writer","knowsAbout":["Ruby programming language","Ruby documentation","JRuby","Open Sound Control","Software development"],"affiliation":{"@type":"Organization","name":"Ruby-Doc.org","url":"https:\/\/ruby-doc.org\/"}}]}},"_links":{"self":[{"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/posts\/1103","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/comments?post=1103"}],"version-history":[{"count":5,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/posts\/1103\/revisions"}],"predecessor-version":[{"id":1713,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/posts\/1103\/revisions\/1713"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/media\/1706"}],"wp:attachment":[{"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/media?parent=1103"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/categories?post=1103"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/tags?post=1103"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}