Project

notisend

0.0
The project is in a healthy, maintained state
Full-coverage Ruby client for the Notisend email API: single messages, templates, recipient lists, parameters, recipients, segments, organizations, campaigns, webhooks and balance. Faraday-based, with pagination helpers, typed errors and custom SSL certificate support.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

 Project Readme

Notisend

Gem Version

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

message = client.messages.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.messages.get(id: message['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.messages.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.