0.0
The project is in a healthy, maintained state
A modern Ruby client library for the Laneful email API, providing easy integration for email sending, webhooks, and analytics.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

~> 0.21
~> 2.6
 Project Readme

Laneful Ruby SDK

A modern Ruby client library for the Laneful email API with support for sending emails, templates, tracking, and webhooks.

Requirements

  • Ruby 3.0 or higher
  • Bundler (for dependency management)

Installation

Using Bundler

Add the following to your Gemfile:

gem 'laneful-ruby'

Then run:

bundle install

Manual Installation

git clone https://github.com/lanefulhq/laneful-ruby.git
cd laneful-ruby
bundle install
bundle exec rake install

Quick Start

require 'laneful'

# Create client
client = Laneful::Client.new(
  'https://your-endpoint.send.laneful.net',
  'your-auth-token'
)

# Create email
email = Laneful::Email::Builder.new
  .from(Laneful::Address.new('sender@example.com', 'Your Name'))
  .to(Laneful::Address.new('recipient@example.com', 'Recipient Name'))
  .subject('Hello from Laneful Ruby SDK!')
  .text_content('This is a simple test email.')
  .build

# Send email
response = client.send_email(email)
puts "Email sent successfully!"

Features

  • Send single or multiple emails
  • Plain text and HTML content
  • Email templates with dynamic data
  • File attachments
  • Email tracking (opens, clicks, unsubscribes)
  • Custom headers and reply-to addresses
  • Visible from_header and request-level mail_settings
  • Scheduled sending
  • Webhook signature verification
  • Domain management (list, create, verify, update email track, delete)
  • Unsubscribe groups
  • Deliverability analytics (spam-ratio radar, Google Postmaster, Microsoft SNDS)
  • Comprehensive error handling
  • Modern Ruby 3.0+ features

Examples

Simple Text Email

email = Laneful::Email::Builder.new
  .from(Laneful::Address.new('sender@example.com'))
  .to(Laneful::Address.new('user@example.com'))
  .subject('Simple Email')
  .text_content('This is a simple text email.')
  .build

response = client.send_email(email)

HTML Email with Tracking

tracking = Laneful::TrackingSettings.new(opens: true, clicks: true, unsubscribes: false)

email = Laneful::Email::Builder.new
  .from(Laneful::Address.new('sender@example.com'))
  .to(Laneful::Address.new('user@example.com'))
  .subject('HTML Email with Tracking')
  .html_content('<h1>Welcome!</h1><p>This is an <strong>HTML email</strong> with tracking enabled.</p>')
  .text_content('Welcome! This is an HTML email with tracking enabled.')
  .tracking(tracking)
  .build

response = client.send_email(email)

Template Email

email = Laneful::Email::Builder.new
  .from(Laneful::Address.new('sender@example.com'))
  .to(Laneful::Address.new('user@example.com'))
  .subject('Welcome!') # Required, also for template emails
  .template_id('welcome-template')
  .template_data({
    'name' => 'John Doe',
    'company' => 'Acme Corp'
  })
  .build

response = client.send_email(email)

Email with Attachments

# Create attachment from file
attachment = Laneful::Attachment.from_file('/path/to/document.pdf')

email = Laneful::Email::Builder.new
  .from(Laneful::Address.new('sender@example.com'))
  .to(Laneful::Address.new('user@example.com'))
  .subject('Document Attached')
  .text_content('Please find the document attached.')
  .attachment(attachment)
  .build

response = client.send_email(email)

Multiple Recipients

email = Laneful::Email::Builder.new
  .from(Laneful::Address.new('sender@example.com'))
  .to(Laneful::Address.new('user1@example.com'))
  .to(Laneful::Address.new('user2@example.com', 'User Two'))
  .cc(Laneful::Address.new('cc@example.com'))
  .bcc(Laneful::Address.new('bcc@example.com'))
  .subject('Multiple Recipients')
  .text_content('This email has multiple recipients.')
  .build

response = client.send_email(email)

Scheduled Email

# Schedule for 24 hours from now
send_time = Time.now.to_i + (24 * 60 * 60)

email = Laneful::Email::Builder.new
  .from(Laneful::Address.new('sender@example.com'))
  .to(Laneful::Address.new('user@example.com'))
  .subject('Scheduled Email')
  .text_content('This email was scheduled.')
  .send_time(send_time)
  .build

response = client.send_email(email)

Multiple Emails

emails = [
  Laneful::Email::Builder.new
    .from(Laneful::Address.new('sender@example.com'))
    .to(Laneful::Address.new('user1@example.com'))
    .subject('Email 1')
    .text_content('First email content.')
    .build,
  Laneful::Email::Builder.new
    .from(Laneful::Address.new('sender@example.com'))
    .to(Laneful::Address.new('user2@example.com'))
    .subject('Email 2')
    .text_content('Second email content.')
    .build
]

response = client.send_emails(emails)

Mail settings (sandbox and message IDs)

response = client.send_email(
  email,
  Laneful::MailSettings.new(sandbox_mode: true, return_message_ids: true)
)
# response['message_ids'] is present when return_message_ids is true

Duplicate send protection

Pass an idempotency_key (1 to 36 bytes) and reuse it when you retry a send, for example after a timeout. The client does not retry on its own.

response = client.send_email(
  email,
  Laneful::MailSettings.new(idempotency_key: "order-#{order_id}")
)

Laneful remembers a key per organization for one hour. A repeated request with the same key gets status accepted with no message IDs and sends nothing, even if the emails differ, so use a new key for every distinct send. Detection is best effort. A request that fails with HTTP 500 doesn't keep its key, so a retry with it is processed as a new request: before resending after a 500, check your webhook events (a request event means Laneful accepted the email). Sandbox mode requests are not deduplicated. A key outside 1 to 36 bytes raises Laneful::ValidationException.

Tracking with an unsubscribe group

tracking = Laneful::TrackingSettings.new(
  opens: true,
  clicks: true,
  unsubscribes: true,
  unsubscribe_group_id: 123,
  # ignored if unsubscribe_group_id is set
  unsubscribe_group_name: 'Newsletters'
)

Custom Timeout

client = Laneful::Client.new(
  'https://your-endpoint.send.laneful.net',
  'your-auth-token',
  timeout: 60  # 60 second timeout
)

Domain, unsubscribe groups, and analytics

These endpoints live on the organization API host. Point the client at it (https://api.laneful.net):

client = Laneful::Client.new(
  'https://api.laneful.net',
  'your-auth-token'
)

Unsubscribe groups

groups = client.list_unsubscribe_groups(42, Laneful::ListUnsubscribeGroupsParams.new(limit: 50))
created = client.create_unsubscribe_group(42, 'Newsletters')
updated = client.update_unsubscribe_group(42, created.unsubscribe_group_id, 'Weekly Newsletters')

Domains

list = client.list_domains(42, Laneful::ListDomainsParams.new(limit: 50))
domain = client.create_domain(42, Laneful::CreateDomainRequest.new(
  domain: 'mydomain.com',
  tracking: 'tracking',
  return_path: 'return-path'
))
domain = client.get_domain(42, 'mydomain.com')
domain = client.verify_domain(42, 'mydomain.com')

# Set the email track; pass '' to clear it, or omit email_track_id to leave it unchanged
domain = client.update_domain(42, 'mydomain.com', Laneful::UpdateDomainRequest.new(
  'e59f0a35-05bc-4516-b585-c06f69c3e67e'
))

client.delete_domain(42, 'mydomain.com')

Deliverability analytics

radar = client.list_domain_spam_ratio_radar(
  Laneful::ListDomainSpamRatioRadarParams.new(
    workspace_ids: [1, 2],
    domain: 'example.com',
    start_date: '2026-09-01',
    end_date: '2026-09-08'
  )
)

postmaster = client.list_google_postmaster_spam_reports(
  Laneful::ListGooglePostmasterSpamReportsParams.new(domain: 'example.com')
)

snds = client.list_snds_reports(Laneful::ListSndsReportsParams.new(ip: '203.0.113.5'))

Webhook Verification

# In your webhook handler
payload = request.body.read
signature = request.headers['x-webhook-signature']
secret = 'your-webhook-secret'

if Laneful::WebhookVerifier.verify_signature(secret, payload, signature)
  # A single event or a batch (JSON array)
  webhook_data = Laneful::WebhookVerifier.parse_webhook_payload(payload)
  webhook_data[:events].each do |event|
    # Handle each event, e.g. by event['event']
  end
else
  # Invalid signature
  head :unauthorized
end

Each parsed event contains every field Laneful sends. Every event has id, event, email, message_id, lane_id, tag, timestamp and metadata (the webhook_data sent with the email); lane_id is empty on request events. Event-specific fields such as url, client_ip, is_machine, code, is_hard, classification, reason and mx_host are included too. id is unique per event and stays the same when Laneful delivers the event again on retry or in a batch, so use it to skip events you have already processed. Events of types the SDK doesn't list are kept rather than rejecting the payload, so skip the ones you don't handle.

See examples/src/webhook_receiver_example.rb for a runnable HTTP receiver.

Error Handling

begin
  response = client.send_email(email)
  puts "Email sent successfully"
rescue Laneful::ValidationException => e
  puts "Validation error: #{e.message}"
rescue Laneful::ApiException => e
  puts "API error: #{e.message}"
  puts "Status code: #{e.status_code}"
  puts "Error message: #{e.error_message}"
rescue Laneful::HttpException => e
  puts "HTTP error: #{e.message}"
  puts "Status code: #{e.status_code}"
rescue StandardError => e
  puts "Unexpected error: #{e.message}"
end

API Reference

Laneful::Client

Constructor

Laneful::Client.new(base_url, auth_token, timeout: 30)
  • base_url - The base URL of the Laneful API
  • auth_token - The authentication token
  • timeout - Request timeout in seconds (optional, default: 30)

Methods

  • send_email(email, mail_settings = nil) - Sends a single email
  • send_emails(emails, mail_settings = nil) - Sends multiple emails
  • list_unsubscribe_groups / create_unsubscribe_group / update_unsubscribe_group
  • list_domains / get_domain / create_domain / update_domain / verify_domain / delete_domain
  • list_domain_spam_ratio_radar / list_google_postmaster_spam_reports / list_snds_reports

Laneful::Email::Builder

Required Fields

  • from(address) - Sender address
  • subject(subject) - Email subject (also required for template emails)

Optional Fields

  • from_header(address) - Visible From header
  • to(address) - Recipient addresses
  • cc(address) - CC addresses
  • bcc(address) - BCC addresses
  • text_content(content) - Plain text content
  • html_content(content) - HTML content
  • template_id(id) - Template ID
  • template_data(data) - Template data
  • attachment(attachment) - File attachments
  • headers(headers) - Custom headers
  • reply_to(address) - Reply-to address
  • send_time(time) - Scheduled send time (Unix timestamp)
  • webhook_data(data) - Webhook data
  • tag(tag) - Email tag
  • tracking(tracking) - Tracking settings

Laneful::Address

Laneful::Address.new(email, name = nil)
  • email - The email address (required)
  • name - The display name (optional)

Laneful::Attachment

# From file
Laneful::Attachment.from_file(file_path)

# From raw data
Laneful::Attachment.new(filename, content_type, content)

Laneful::TrackingSettings

Laneful::TrackingSettings.new(
  opens: false,
  clicks: false,
  unsubscribes: false,
  unsubscribe_group_id: nil,
  unsubscribe_group_name: nil
)

Laneful::MailSettings.new(sandbox_mode: true, return_message_ids: true)
Laneful::MailSettings.new(idempotency_key: 'order-1234') # 1 to 36 bytes

Laneful::WebhookVerifier

Laneful::WebhookVerifier.verify_signature(secret, payload, signature)
Laneful::WebhookVerifier.parse_webhook_payload(payload) # => { is_batch:, events: [event hashes with every field] }

Exception Types

  • Laneful::ValidationException - Thrown when input validation fails
  • Laneful::ApiException - Thrown when the API returns an error response
  • Laneful::HttpException - Thrown when HTTP communication fails
  • Laneful::LanefulException - Base exception class for all SDK exceptions

Development

Running Tests

bundle exec rspec

Code Quality

bundle exec rubocop

Documentation

bundle exec yard doc

License

This project is licensed under the MIT License - see the LICENSE file for details.