Unofficial Ruby SDK for the Lettermint transactional email API. Based on the official Python SDK.
The SDK provides two API clients:
- SendingAPI (aliased as
Client) — Project-level email sending withx-lettermint-tokenauthentication - TeamAPI — Team-level management (domains, projects, webhooks, etc.) with
lm_team_*token authentication
Add to your Gemfile:
gem 'lettermint'Or install directly:
gem install lettermintrequire 'lettermint'
# SendingAPI for email sending (aliased as Client for backward compatibility)
client = Lettermint::SendingAPI.new(api_token: 'your-project-token')
response = client.email
.from('sender@example.com')
.to('recipient@example.com')
.subject('Hello from Ruby')
.html('<h1>Welcome!</h1>')
.text('Welcome!')
.deliver
puts response.message_id# TeamAPI for team-level operations (requires lm_team_* token)
team_api = Lettermint::TeamAPI.new(team_token: 'lm_team_your-token')
# List domains
domains = team_api.domains.list
puts domains['data'].map { |d| d['domain'] }
# Get team info
team = team_api.team.get
puts team['name']client.email
.from('sender@example.com')
.to('recipient1@example.com', 'recipient2@example.com')
.subject('Hello')
.deliverclient.email
.from('sender@example.com')
.to('recipient@example.com')
.cc('cc1@example.com', 'cc2@example.com')
.bcc('bcc@example.com')
.subject('Hello')
.deliverclient.email
.from('sender@example.com')
.to('recipient@example.com')
.reply_to('reply@example.com')
.subject('Hello')
.deliverclient.email
.from('John Doe <john@example.com>')
.to('Jane Doe <jane@example.com>')
.subject('Hello')
.deliverrequire 'base64'
content = Base64.strict_encode64(File.binread('document.pdf'))
# Regular attachment
client.email
.from('sender@example.com')
.to('recipient@example.com')
.subject('Your Document')
.attach('document.pdf', content)
.deliver
# Inline attachment (for embedding in HTML)
client.email
.from('sender@example.com')
.to('recipient@example.com')
.subject('Welcome')
.html('<img src="cid:logo@example.com">')
.attach('logo.png', logo_content, content_id: 'logo@example.com')
.deliverYou can also use the EmailAttachment value object:
attachment = Lettermint::EmailAttachment.new(
filename: 'document.pdf',
content: content,
content_id: nil
)
client.email
.from('sender@example.com')
.to('recipient@example.com')
.attach(attachment)
.deliverclient.email
.from('sender@example.com')
.to('recipient@example.com')
.subject('Hello')
.headers({ 'X-Custom-Header' => 'value' })
.deliverclient.email
.from('sender@example.com')
.to('recipient@example.com')
.subject('Hello')
.metadata({ campaign_id: '123', user_id: '456' })
.tag('welcome-campaign')
.deliverclient.email
.from('sender@example.com')
.to('recipient@example.com')
.subject('Hello')
.route('my-route')
.deliverPrevent duplicate sends when retrying failed requests:
client.email
.from('sender@example.com')
.to('recipient@example.com')
.subject('Hello')
.idempotency_key('unique-request-id')
.deliverVerify webhook signatures to ensure authenticity:
require 'lettermint'
webhook = Lettermint::Webhook.new(secret: 'your-webhook-secret')
# Verify using headers (recommended)
payload = webhook.verify_headers(request.headers, request.body)
# Or verify using the signature directly
payload = webhook.verify(
request.body,
request.headers['X-Lettermint-Signature']
)
puts payload['event']For one-off verification:
payload = Lettermint::Webhook.verify_signature(
request.body,
request.headers['X-Lettermint-Signature'],
secret: 'your-webhook-secret'
)Adjust the timestamp tolerance (default: 300 seconds):
webhook = Lettermint::Webhook.new(secret: 'your-webhook-secret', tolerance: 600)The webhook verifier validates timestamp freshness and HMAC integrity but does not
track previously seen deliveries. Within the tolerance window (default 300 seconds),
a captured request could be replayed. Your application should deduplicate incoming
webhooks using the x-lettermint-delivery header value as an idempotency key -- for
example, by recording processed delivery IDs in a database or cache and rejecting
duplicates.
The TeamAPI provides access to team-level management operations. It requires a team token (prefixed with lm_team_).
team_api = Lettermint::TeamAPI.new(team_token: 'lm_team_your-token')| Resource | Methods |
|---|---|
team |
get, update, usage, members |
domains |
list, create, find, delete, verify_dns, verify_dns_record, update_projects |
projects |
list, create, find, update, delete, rotate_token, add_member, remove_member, update_members, routes |
webhooks |
list, create, find, update, delete, test, deliveries, delivery, regenerate_secret |
messages |
list, find, html, text, source, events |
suppressions |
list, create, delete |
routes |
list, create, find, update, delete, verify_inbound_domain |
stats |
get |
# Domains
team_api.domains.create(domain: 'mail.example.com')
team_api.domains.verify_dns('domain-id')
# Projects
projects = team_api.projects.list(sort: '-created_at')
project = team_api.projects.create(name: 'My Project')
team_api.projects.rotate_token(project['id'])
# Webhooks
team_api.webhooks.create(
name: 'My Webhook',
url: 'https://example.com/webhook',
events: ['message.sent', 'message.delivered']
)
# Messages (search and retrieve)
messages = team_api.messages.list(status: 'delivered', tag: 'welcome')
html_body = team_api.messages.html('message-id')
# Suppressions
team_api.suppressions.create(
emails: ['bounce@example.com'],
reason: 'hard_bounce',
scope: 'project',
project_id: 'project-id'
)
# Stats
stats = team_api.stats.get(from: '2026-01-01', to: '2026-01-31')All list methods support cursor-based pagination:
# First page
result = team_api.domains.list(page_size: 10)
# Next page
if result['meta']['next_cursor']
next_page = team_api.domains.list(
page_size: 10,
page_cursor: result['meta']['next_cursor']
)
end# Sort by field (prefix with - for descending)
team_api.projects.list(sort: '-created_at')
team_api.domains.list(sort: 'domain')
# Filter by field
team_api.messages.list(status: 'delivered', from_email: 'noreply@example.com')
team_api.domains.list(status: 'verified')The SendingAPI provides raw HTTP methods for accessing API endpoints not yet wrapped in typed methods:
client = Lettermint::SendingAPI.new(api_token: 'your-api-token')
# GET with query params
response = client.get('/some-endpoint', params: { limit: 10 })
# POST with JSON body
response = client.post('/some-endpoint', data: { key: 'value' })
# PUT
response = client.put('/some-endpoint/123', data: { key: 'new-value' })
# DELETE
client.delete('/some-endpoint/123')require 'lettermint'
client = Lettermint::SendingAPI.new(api_token: 'your-api-token')
begin
response = client.email
.from('sender@example.com')
.to('recipient@example.com')
.subject('Hello')
.deliver
rescue Lettermint::AuthenticationError => e
# 401/403 errors (invalid or revoked token)
puts "Auth error #{e.status_code}: #{e.message}"
rescue Lettermint::RateLimitError => e
# 429 errors
puts "Rate limited, retry after: #{e.retry_after}s"
rescue Lettermint::ValidationError => e
# 422 errors (e.g., daily limit exceeded)
puts "Validation error: #{e.error_type}"
puts "Response: #{e.response_body}"
rescue Lettermint::ClientError => e
# 400 errors
puts "Client error: #{e.message}"
rescue Lettermint::TimeoutError => e
# Request timeout
puts "Timeout: #{e.message}"
rescue Lettermint::ConnectionError => e
# Network-level failures (DNS, connection refused, etc.)
puts "Connection error: #{e.message}"
puts "Original: #{e.original_exception}" if e.original_exception
rescue Lettermint::HttpRequestError => e
# Other HTTP errors
puts "HTTP error #{e.status_code}: #{e.message}"
endbegin
payload = webhook.verify_headers(headers, body)
rescue Lettermint::InvalidSignatureError
puts 'Invalid signature - request may be forged'
rescue Lettermint::TimestampToleranceError
puts 'Timestamp too old - possible replay attack'
rescue Lettermint::WebhookJsonDecodeError => e
puts "Invalid JSON in payload: #{e.original_exception}"
rescue Lettermint::WebhookVerificationError => e
puts "Verification failed: #{e.message}"
endSet defaults once at application boot (e.g., in a Rails initializer):
Lettermint.configure do |config|
config.timeout = 60
endAll clients created afterward inherit these defaults.
client = Lettermint::SendingAPI.new(api_token: 'your-api-token', timeout: 10)- Ruby >= 3.2
- Faraday ~> 2.0
MIT