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.