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.
On this page
Install
Add the package to your project:
gem install halyardbundle add halyard# or without Bundlergem install halyardRequirements:
- 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:
HALYARD_KEY=hy_test_51Hq8Zx2rTmV9cKpL3dHALYARD_WEBHOOK_SECRET=whsec_7mYb2Qe9LkT4Configure 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.
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"timeoutNumericdefault: 30max_retriesIntegerdefault: 2base_urlStringloggerLoggerdefault: Rails.logger: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.
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.
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}") endendbundle exec rake halyard:work QUEUES=emails CONCURRENCY=10Return 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.
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.
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 endendHalyard::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.
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| Error | Raised when |
|---|---|
| Halyard::Error | Base class. Every error below inherits from it and carries type, code, status and request_id. |
| Halyard::InvalidRequestError | 400 or 422. A parameter is missing or invalid; param names it. |
| Halyard::AuthenticationError | 401. The key is missing, revoked or for the wrong environment. |
| Halyard::PermissionDeniedError | 403. The key's scopes don't allow this call. |
| Halyard::NotFoundError | 404. The ID doesn't exist or belongs to another project. |
| Halyard::ConflictError | 409. Includes idempotency_mismatch when a key is reused with a different body. |
| Halyard::RateLimitError | 429. retry_after holds the seconds to wait. |
| Halyard::ConnectionError | The 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.
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 endendRuby 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.active_job.queue_adapter = :halyardRails.application.routes.draw do post "/webhooks/halyard", to: "halyard_webhooks#create"endclass 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.
Other SDKs