Notisend
Full-coverage Ruby client for the Notisend email API: single messages, templates, recipient lists, parameters, recipients (including bulk import and search), segments, organizations, campaigns, webhooks and balance.
Built on Faraday 2.x. Ruby 3.4+ (developed against Ruby 4.0).
Based on stale notisend-ruby repo.
Installation
bundle add notisend
Or in your Gemfile:
gem 'notisend'
Configuration
Configure once globally (e.g. in a Rails initializer):
Notisend.configure do |config|
config.api_token = 'your-api-token' # default: ENV['NOTISEND_API_TOKEN']
# config.base_url = Notisend::RESERVE_BASE_URL # alternate URL, see below
# config.open_timeout = 10 # seconds
# config.timeout = 30 # seconds
# config.verify_ssl = true # default: true
# config.ca_bundle_file = '/path/to/ca.pem' # custom CA bundle
# config.logger = Logger.new($stdout) # logs method/path/status, never the token
end
Notisend.client.balance.get
Or build explicit client instances (each option falls back to the global config):
client = Notisend::Client.new(api_token: 'other-token')
other = Notisend::Client.new(api_token: 'second-account', base_url: Notisend::RESERVE_BASE_URL)
Environment variables
| Variable | Meaning |
|---|---|
NOTISEND_API_TOKEN |
API token (used when api_token is not set explicitly) |
NOTISEND_VERIFY_SSL |
Set to 'false' to disable SSL verification |
NOTISEND_CA_BUNDLE_FILE |
Path to a custom CA bundle file |
Alternate base URL
If your IP is blocked or restricted (RKN), use the reserve endpoint:
Notisend.configure { |c| c.base_url = Notisend::RESERVE_BASE_URL } # https://api-reserve.msndr.net/v1
Custom SSL certificates
For environments with a custom certificate chain (e.g. Russian MinTsifra CA or a corporate proxy), point the client at your CA bundle:
Notisend.configure do |config|
config.ca_bundle_file = '/usr/local/share/ca-certificates/russian_trusted_ca.pem'
end
SSL verification stays on by default; Faraday::SSLError is raised as Notisend::SSLError.
Usage
All methods return parsed JSON as plain hashes with string keys. Paginated endpoints
return a Notisend::Collection (see Pagination below).
Balance
client.balance.get # => { 'tariff' => {...}, 'balance' => 12965.96 }
Messages
= client..deliver(
from_email: 'alice@example.org',
to: 'bob@example.org',
subject: 'Hello',
text: 'Hello, Bob!',
html: '<h1>Hello, Bob!</h1>', # text and/or html is required
from_name: 'Alice', # optional
payment: 'credit', # optional: subscriber_priority (default),
# credit_priority, subscriber, credit
smtp_headers: { 'Client-Id' => '123' }, # optional
attachments: ['/path/to/file.pdf'] # optional: paths and/or IO objects, total <= 5 MB
)
client..get(id: ['id']) # => includes 'status' and 'events'
Templates
template = client.templates.create(from_email: 'hello@world.com', subject: 'Hello [%name%]',
html: '<h1>Hello [%name%]</h1>', preset_params: ['name'])
client.templates.to_pending(id: template['id']) # submit for moderation
client.templates.list
client.templates.get(id: template['id'])
client.templates.deliver(template_id: template['id'], to: 'bob@example.org',
params: { 'name' => 'Bob' }, attachments: ['/path/to/file.pdf'])
Lists (recipient groups)
list = client.lists.create(title: 'My Recipients')
client.lists.list(page_number: 1, page_size: 100)
client.lists.get(id: list['id'])
client.lists.update(id: list['id'], title: 'New Title')
client.lists.delete(id: list['id']) # => true
List parameters
param = client.parameters.create(list_id: list['id'], title: 'Age', kind: 'numeric')
# kinds: string (default), numeric, date, boolean, geo
client.parameters.list(list_id: list['id'])
client.parameters.update(list_id: list['id'], id: param['id'], title: 'Years')
client.parameters.delete(list_id: list['id'], id: param['id']) # => true
Recipients
recipient = client.recipients.create(
list_id: list['id'],
email: 'alice@example.org',
values: [{ parameter_id: param['id'], value: '22' }], # optional
tags: ['buyer'], # optional
unconfirmed: true # optional
)
client.recipients.get(list_id: list['id'], id: recipient['id'])
client.recipients.update(list_id: list['id'], id: recipient['id'],
email: 'alice@example.org',
values: [{ parameter_id: param['id'], destroy: 'true' }])
client.recipients.list(list_id: list['id'], page_size: 1000)
client.recipients.delete(list_id: list['id'], id: recipient['id']) # => true
# Bulk import (up to 10_000 recipients per call)
import = client.recipients.import(
list_id: list['id'],
recipients: [
{ email: 'alice@example.org', values: [{ parameter_id: param['id'], value: '22' }] },
{ email: 'bob@example.org' }
],
run_triggers: 'trigger_fresh', # optional: trigger_any | trigger_fresh
callback_url: 'https://my.app/import-callback' # optional
)
import['status'] # => 'queued'
# Find which lists contain an email
client.recipients.search(email: 'alice@example.org')
Segments
client.segments.list
Organizations
org = client.organizations.create(name: 'My Organization', address: 'Lenina 40',
country: 'Russia', city: 'Tomsk',
phone: '+7-3822-123-456', zip: '634000')
client.organizations.list
client.organizations.get(id: org['id'])
client.organizations.current # default organization
client.organizations.set_current(id: org['id'])
client.organizations.update(id: org['id'], city: 'Moscow')
client.organizations.delete(id: org['id']) # => true
Campaigns
campaign = client.campaigns.create(
from_email: 'hello@world.com',
subject: 'Hello World',
html: '<h1>Hello World</h1>',
lists: [{ id: list['id'] }], # or segment_id: 5
attachments: ['/path/to/file'] # optional
)
client.campaigns.deliver(id: campaign['id']) # send now
client.campaigns.schedule(id: campaign['id'], # or send later
start_at: '30.10.2022 13:00', time_zone: 'Moscow')
client.campaigns.list(statistic: false) # statistic: false is much faster
client.campaigns.get(id: campaign['id'])
Webhooks
hook = client.webhooks.create(title: 'Delivery hooks', url: 'https://my.app/webhook',
kinds: ['api'], events: %w[delivered hard_bounced])
client.webhooks.list
client.webhooks.get(id: hook['id'])
client.webhooks.update(id: hook['id'], status: 'inactive')
client.webhooks.delete(id: hook['id']) # => deleted webhook hash
client.webhooks.kinds # => { 'kinds' => ['campaign', 'campaign_transactional', ...] }
client.webhooks.events # => { 'events' => ['delivered', 'opened', ...] }
Pagination
Endpoints that return collections are wrapped in Notisend::Collection:
lists = client.lists.list(page_size: 50)
lists.total_count # => 123
lists.total_pages # => 3
lists.page_number # => 1
lists.each { |l| puts l['title'] } # Enumerable over the current page
lists.next_page # => Collection or nil
lists.auto_paging_each { |l| puts l['title'] } # lazily walks ALL pages
client.lists.list.auto_paging_each.map { |l| l['id'] } # as an Enumerator
The raw response envelope (including non-standard keys like query in recipient search)
is available via collection.raw.
Error handling
All errors inherit from Notisend::Error:
| Class | Raised on |
|---|---|
Notisend::ConfigurationError |
Missing API token |
Notisend::SSLError |
SSL verification failure |
Notisend::ConnectionError |
Network failure / timeout |
Notisend::ApiError |
Any non-2xx API response (base class) |
Notisend::BadRequest |
400 |
Notisend::Unauthorized |
401 |
Notisend::Forbidden |
403 |
Notisend::NotFound |
404 |
Notisend::PreconditionFailed |
412 (e.g. page_size over the limit) |
Notisend::UnprocessableEntity |
422 |
Notisend::TooManyRequests |
429 (rate limit) |
Notisend::ServerError |
5xx |
begin
client..deliver(from_email: 'a@b.c', to: 'x@y.z', subject: 'Hi', text: 'Hello')
rescue Notisend::TooManyRequests => e
sleep(e.retry_after || 60) # parsed from 'Try again in N seconds'
retry
rescue Notisend::ApiError => e
e.status # => HTTP status
e.detail # => first error detail from the response
e.errors # => full parsed errors array
end
GET requests are automatically retried (twice) on connection failures and timeouts. POST requests are never retried — a delivery must not be sent twice.
Development
bundle install
bundle exec rake test # tests
bundle exec rubocop # lint
bundle exec rake # both
bin/console # interactive console
Contributing
Bug reports and pull requests are welcome at https://github.com/amdest/notisend.
License
The gem is available as open source under the terms of the MIT License.