Project

nahook

0.0
The project is in a healthy, maintained state
Ruby client for sending webhooks and managing resources through the Nahook API. Supports direct endpoint delivery, fan-out by event type, batch operations, and full management API access.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

Nahook Ruby SDK

Official Ruby SDK for the Nahook webhook platform. Send webhooks, fan-out by event type, and manage resources programmatically.

Installation

Add to your Gemfile:

gem "nahook"

Or install directly:

gem install nahook

Requirements: Ruby 3.0+

Quick Start

Sending Webhooks (Client)

require "nahook"

client = Nahook::Client.new("nhk_us_your_api_key")

# Send to a specific endpoint
result = client.send("ep_abc123", payload: { order_id: "12345", status: "paid" })
puts result["deliveryId"] # => "del_..."

# Fan-out by event type (delivers to all subscribed endpoints)
result = client.trigger("order.paid", payload: { order_id: "12345" })
puts result["deliveryIds"] # => ["del_1", "del_2"]

# With metadata
client.trigger("order.paid",
  payload: { order_id: "12345" },
  metadata: { "source" => "checkout" }
)

Managing Resources (Management)

mgmt = Nahook::Management.new("nhm_your_management_token")

# Endpoints
endpoints = mgmt.endpoints.list("ws_abc123")
endpoint  = mgmt.endpoints.create("ws_abc123",
  url: "https://example.com/webhook",
  type: "webhook",
  description: "Production webhook"
)
mgmt.endpoints.update("ws_abc123", endpoint["id"], is_active: false)
mgmt.endpoints.delete("ws_abc123", endpoint["id"])

# Event Types
mgmt.event_types.create("ws_abc123", name: "order.paid", description: "Fired when an order is paid")
types = mgmt.event_types.list("ws_abc123")

# Applications
app = mgmt.applications.create("ws_abc123", name: "Acme Corp", external_id: "acme_123")
mgmt.applications.list("ws_abc123", limit: 10, offset: 0)
mgmt.applications.list_endpoints("ws_abc123", app["id"])
mgmt.applications.create_endpoint("ws_abc123", app["id"], url: "https://acme.com/hook")

# Subscriptions
mgmt.subscriptions.create("ws_abc123", "ep_def456", event_type_ids: ["evt_ghi789"])
mgmt.subscriptions.list("ws_abc123", "ep_def456")
mgmt.subscriptions.delete("ws_abc123", "ep_def456", "evt_ghi789")

# Environments
env = mgmt.environments.create("ws_abc123", name: "Staging", slug: "staging")
mgmt.environments.list("ws_abc123")
mgmt.environments.get("ws_abc123", env["id"])
mgmt.environments.update("ws_abc123", env["id"], name: "Pre-production")
mgmt.environments.delete("ws_abc123", env["id"])

# Event Type Visibility
mgmt.environments.list_event_type_visibility("ws_abc123", "env_abc123")
mgmt.environments.set_event_type_visibility("ws_abc123", "env_abc123", "evt_abc123", published: true)

# Portal Sessions
session = mgmt.portal_sessions.create("ws_abc123", "app_jkl012")
puts session["url"] # Redirect your customer here

Deliveries

Read access to a workspace's webhook deliveries. There is no create/update/delete on this resource — deliveries are produced by the ingestion path and observed through these methods.

# List deliveries for an endpoint, newest-first, cursor-paginated.
# `next_cursor` is OPAQUE — pass it back verbatim to fetch the next page.
page = mgmt.deliveries.list("ws_abc123", "ep_def456", limit: 50)
page.data.each { |d| puts "#{d['id']} #{d['status']}" }

while page.next_cursor
  page = mgmt.deliveries.list("ws_abc123", "ep_def456", cursor: page.next_cursor)
  page.data.each { |d| puts "#{d['id']} #{d['status']}" }
end

# Filter by status. Valid values: "pending", "delivering", "delivered",
# "scheduled_retry", "failed", "dead_letter".
failed = mgmt.deliveries.list("ws_abc123", "ep_def456", status: "failed")

# Fetch a single delivery (metadata only).
delivery = mgmt.deliveries.get("ws_abc123", "del_xyz")
puts delivery["status"]         # => "delivered"
puts delivery["totalAttempts"]  # => 1
puts delivery["hasPayload"]     # => true

# Fetch with the payload envelope. The envelope is a tagged hash whose
# "status" is one of: "available", "forbidden", "processing", "not_found",
# "error". The endpoint stays HTTP 200 for all 5 -- the envelope status
# carries access-level reality, so do NOT rescue on the non-"available" ones.
delivery = mgmt.deliveries.get("ws_abc123", "del_xyz", include_payload: true)
case delivery["payload"]["status"]
when "available"
  body         = delivery["payload"]["data"]          # decoded JSON
  content_type = delivery["payload"]["contentType"]   # "application/json"
when "forbidden"
  # Workspace plan does not include payload storage.
when "processing"
  # Delivery still in flight; payload may be racing the read.
when "not_found"
  # Terminal delivery without stored payload (older row, or plan was lower at ingest).
when "error"
  # Transient infrastructure failure -- safe to retry.
end

# List attempts for a delivery in chronological order (oldest first).
# The attempt "status" is an opaque string, not an enum.
attempts = mgmt.deliveries.get_attempts("ws_abc123", "del_xyz")
attempts.each { |a| puts "##{a['attemptNumber']} #{a['status']} #{a['responseStatusCode']}" }

Client Options

Nahook::Client

client = Nahook::Client.new("nhk_us_...",
  timeout_ms: 30_000,                   # milliseconds, default
  retries: 3                            # retry on 5xx/429/network errors
)

Configuration

The SDK automatically routes requests to the correct regional API based on your API key prefix (nhk_us_... -> US, nhk_eu_... -> EU, nhk_ap_... -> Asia Pacific). No configuration needed.

To override the base URL (for testing or local development):

client = Nahook::Client.new("nhk_us_...", base_url: "http://localhost:3001")

For unit tests, mock the SDK client at the dependency injection boundary. For integration tests, override the base URL to point at a local server.

Custom HTTP adapter / Faraday connection

The SDK ships with the :net_http_persistent Faraday adapter by default, which keeps a per-thread TCP/TLS pool — back-to-back send / trigger calls reuse the same connection and skip the DNS + TCP + TLS handshake. For most apps that's all you need.

Two escape hatches when you want more control:

Swap the adapter. Useful for high-throughput workloads on :typhoeus (curl-multi) without building a full Faraday connection:

require "faraday/typhoeus"  # add the gem to your Gemfile

client = Nahook::Client.new("nhk_us_...", adapter: :typhoeus)

Whatever adapter you name must be available — Faraday 2 split adapters into separate gems (faraday-typhoeus, faraday-net_http, etc.).

Supply a fully-configured Faraday connection. When you need middleware (Datadog / OpenTelemetry instrumentation), a custom proxy, mTLS, request retries beyond what the SDK ships, etc.:

conn = Faraday.new(url: "https://us.api.nahook.com") do |f|
  f.options.timeout      = 15
  f.options.open_timeout = 5
  # Plug in your tracing/observability middleware. The exact symbol/class
  # depends on the gem — e.g. `Datadog::Tracing::Contrib::Faraday::Middleware`
  # from `ddtrace`, or `OpenTelemetry::Instrumentation::Faraday::Middlewares::TracerMiddleware`.
  # f.use SomeTracingMiddleware
  f.adapter :net_http_persistent
end

client = Nahook::Client.new("nhk_us_...", connection: conn)

When connection: is supplied, base_url, timeout_ms, and adapter are ignored — the connection you pass in is used verbatim. The same adapter: and connection: kwargs are accepted by Nahook::Management.new.

Nahook::Management

mgmt = Nahook::Management.new("nhm_...",
  timeout_ms: 30_000                    # milliseconds, default
)
# Note: Management does not support retries

Batch Operations

# Send to multiple endpoints (max 20)
result = client.send_batch([
  { endpoint_id: "ep_abc", payload: { order: 1 } },
  { endpoint_id: "ep_def", payload: { order: 2 }, idempotency_key: "key-2" }
])

# Fan-out multiple event types (max 20)
result = client.trigger_batch([
  { event_type: "order.paid", payload: { order_id: "123" } },
  { event_type: "user.created", payload: { user_id: "456" }, metadata: { "source" => "api" } }
])

Idempotency

The send method auto-generates a UUID idempotency key if you don't provide one:

# Auto-generated idempotency key
client.send("ep_abc", payload: { order: 1 })

# Explicit idempotency key
client.send("ep_abc", payload: { order: 1 }, idempotency_key: "order-1-v1")

Error Handling

begin
  client.send("ep_abc", payload: { test: true })
rescue Nahook::APIError => e
  puts e.message       # Human-readable message
  puts e.status        # HTTP status code
  puts e.code          # Machine-readable error code
  puts e.retryable?    # true for 5xx and 429
  puts e.auth_error?   # true for 401, or 403 with token_disabled
  puts e.not_found?    # true for 404
  puts e.rate_limited? # true for 429
  puts e.retry_after   # Retry-After header value (seconds), if present
rescue Nahook::NetworkError => e
  puts e.message       # "Network error: ..."
  puts e.original_error # Original exception
rescue Nahook::TimeoutError => e
  puts e.message       # "Request timed out after 30000ms"
  puts e.timeout_ms    # Timeout in milliseconds
rescue Nahook::Error => e
  # Catch-all for any SDK error
end

Retry Logic

When retries is configured on Nahook::Client, the SDK automatically retries on:

  • HTTP 5xx responses
  • HTTP 429 (rate limited) -- respects Retry-After header
  • Network connection failures
  • Request timeouts

Retry delay uses exponential backoff with full jitter (base 500ms, max 10s).

License

MIT