Skip to content

SDKs

Ruby SDK

The Ruby SDK feels at home in Rails: an initializer, an Active Job adapter and a rake task for workers. It works just as well in plain Ruby, Sinatra or Hanami.

halyardv3.2.5 Source Changelog
On this page

Install

Add the package to your project:

gem install halyard
Other package managers
bundle add halyard# or without Bundlergem install halyard

Requirements:

  • Ruby 3.2 or newer
  • Rails 7.1+ for the Railtie and the Active Job adapter (optional)

Keep your API key and webhook secret out of source control. The examples on this page read them from the environment:

.env
HALYARD_KEY=hy_test_51Hq8Zx2rTmV9cKpL3dHALYARD_WEBHOOK_SECRET=whsec_7mYb2Qe9LkT4

Configure the client

Create one client and share it across your app. It holds a connection pool, so making a new client per request wastes connections.

config/initializers/halyard.rb
require "halyard" Halyard.configure do |config|  config.api_key = ENV.fetch("HALYARD_KEY")  config.region = "eu"       # keep data in Frankfurt  config.timeout = 10        # per request, in seconds  config.max_retries = 3     # network errors, 429 and 5xxend # Or build a client explicitly:halyard = Halyard::Client.new(api_key: ENV.fetch("HALYARD_KEY"), region: "eu")

Client options

regionStringdefault: "us"
Region your account lives in: "us", "eu" or "ap".
timeoutNumericdefault: 30
Seconds before a single request is aborted. Retries get a fresh timeout.
max_retriesIntegerdefault: 2
Automatic retries for connection errors, 429 and 5xx responses, with exponential backoff.
base_urlString
Override the API host, for example to point at a local mock server in tests.
loggerLoggerdefault: Rails.logger
Where request logs go. Set the level to :debug to log request IDs.

Create a job

A job needs a queue, a task name and a JSON payload. It runs as soon as a worker is free, or at run_at if you set one.

Ruby
job = Halyard.jobs.create(  queue: "emails",  task: "send-welcome-email",  payload: { user_id: "usr_8KfQ2" },  run_at: 10.minutes.from_now, # optional  max_attempts: 5) puts job.id     # => "job_3Rk9Tq7Xw2"puts job.status # => "scheduled"

Halyard.jobs uses the client from Halyard.configure. Payload keys come back as strings in workers, the same as JSON, so prefer string keys when you read them.

Run a worker

Workers long-poll Halyard for jobs on the queues you name and call the handler registered for each task. Run as many worker processes as you like; Halyard hands each job to exactly one of them at a time.

app/tasks/send_welcome_email.rb
class SendWelcomeEmail  include Halyard::Task   task "send-welcome-email", queue: "emails"   def perform(job)    user = User.find(job.payload["user_id"])    UserMailer.welcome(user).deliver_now    job.log("Sent welcome email to #{user.email}")  endend
Start the worker
bundle exec rake halyard:work QUEUES=emails CONCURRENCY=10

Return from perform and the attempt succeeds. Raise and Halyard records the error and schedules a retry with backoff. Raise Halyard::NonRetryableError to skip the remaining attempts. The rake task loads every class that includes Halyard::Task and traps TERM to finish running jobs.

Publish an event

Events describe something that happened. Halyard fans each one out to every subscription that matches its type, whether that subscription starts a job or calls a webhook. Publishing is fire-and-forget: you get an evt_ ID back in a few milliseconds.

Ruby
event = Halyard.events.publish(  type: "order.paid",  data: { order_id: "ord_77Hc1", amount: 4200, currency: "eur" }) puts event.id # => "evt_9Vb2Lm4Qa1"

Verify webhooks

Halyard signs every webhook delivery with your endpoint's secret. Verify the signature before you trust the body; the SDK does the HMAC check, the timestamp check and the JSON parsing in one call.

app/controllers/halyard_webhooks_controller.rb
class HalyardWebhooksController < ActionController::API  def create    event = Halyard::Webhook.verify(      request.raw_post,      request.headers["Halyard-Signature"],      ENV.fetch("HALYARD_WEBHOOK_SECRET")    )     Rails.logger.warn("Job failed #{event.data["job"]["id"]}") if event.type == "job.failed"    head :no_content  rescue Halyard::SignatureVerificationError    head :bad_request  endend

Halyard::Webhook.verify checks the HMAC SHA-256 signature and rejects timestamps older than five minutes. Pass tolerance: 600 to change the window. Use request.raw_post, never params.

Handle errors

Failed API calls raise a typed error with the API's type, code and the request_id to quote to support. Retryable failures have already been retried by the time you see them.

Ruby
begin  Halyard.jobs.create(queue: "emails", task: "send-welcome-email", payload: payload)rescue Halyard::RateLimitError => e  Rails.logger.warn("Slow down, retry in #{e.retry_after}s")  raiserescue Halyard::InvalidRequestError => e  Rails.logger.error("Bad parameter #{e.param}: #{e.message}")  raiserescue Halyard::Error => e  Rails.logger.error([e.type, e.code, e.request_id].join(" ")) # req_4Tn8Wc2Ke7  raiseend
Ruby SDK error classes
ErrorRaised when
Halyard::ErrorBase class. Every error below inherits from it and carries type, code, status and request_id.
Halyard::InvalidRequestError400 or 422. A parameter is missing or invalid; param names it.
Halyard::AuthenticationError401. The key is missing, revoked or for the wrong environment.
Halyard::PermissionDeniedError403. The key's scopes don't allow this call.
Halyard::NotFoundError404. The ID doesn't exist or belongs to another project.
Halyard::ConflictError409. Includes idempotency_mismatch when a key is reused with a different body.
Halyard::RateLimitError429. retry_after holds the seconds to wait.
Halyard::ConnectionErrorThe request never got a response: DNS, TLS or timeout.

The full list of types and codes is on the Errors page.

Typed payloads

Payloads are JSON on the wire, but they don't have to be untyped in your code. Describe each task's payload once and the SDK checks both ends: the code that enqueues and the handler that runs.

app/tasks/send_welcome_email.rb
class WelcomeEmailPayload < Halyard::Payload  attribute :user_id, :string  attribute :locale, :string, default: "en"   validates :user_id, presence: true  validates :locale, inclusion: { in: %w[en de] }end class SendWelcomeEmail  include Halyard::Task   task "send-welcome-email", queue: "emails", payload: WelcomeEmailPayload   def perform(job)    job.payload.user_id # => "usr_8KfQ2", validated  endend

Ruby has no compile step, so typing happens at runtime: Halyard::Payload uses Active Model attributes and validations, and invalid payloads are rejected when you enqueue them. The gem also ships RBS signatures in sig/ for Steep and Sorbet users.

Framework integrations

The SDK works anywhere your language runs. These are the patterns we recommend for the frameworks we see most.

Rails

The Railtie loads config/initializers/halyard.rb and app/tasks. To move existing Active Job classes over without rewriting them, switch the queue adapter: each perform_later becomes a Halyard job with the job class as its task name.

config/application.rb
config.active_job.queue_adapter = :halyard
config/routes.rb
Rails.application.routes.draw do  post "/webhooks/halyard", to: "halyard_webhooks#create"end
app/jobs/welcome_email_job.rb
class WelcomeEmailJob < ApplicationJob  queue_as :emails # retries and backoff come from the Halyard queue   def perform(user)    UserMailer.welcome(user).deliver_now  endend WelcomeEmailJob.perform_later(user)

Reference

Every method on the client maps to an endpoint in the API reference. Source, issues and release notes for the Ruby SDK live at code.halyard.example/sdks/halyard-ruby.

Suggested