Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

millionsend

Official Ruby SDK for MillionSend — a self-hostable, Resend-compatible email API on AWS SES.

The API is wire-compatible with Resend, and this gem deliberately mirrors the shape of resend, so migrating is mostly a find-and-replace: swap the constant, set base_url to your instance.

Install

gem install millionsend

Or in a Gemfile:

gem "millionsend"

Requires Ruby 3.0+. Only the standard library is used at runtime (net/http, json).

Quickstart

require "millionsend"

Millionsend.api_key  = "ms_123"
Millionsend.base_url = "https://mail.acme.dev"

email = Millionsend::Emails.send(
  from: "Acme <onboarding@acme.dev>",
  to: "delivered@resend.dev",
  subject: "Hello from MillionSend",
  html: "<strong>It works!</strong>"
)

puts email[:id]

Every call returns a symbol-keyed Hash on success and raises a Millionsend::Error on any non-2xx response (see Error handling).

Configuration

Millionsend.api_key  = "ms_123"                # falls back to ENV["MILLIONSEND_API_KEY"]
Millionsend.base_url = "https://mail.acme.dev" # falls back to ENV["MILLIONSEND_BASE_URL"],
                                               # then http://localhost:3001

MillionSend is self-hosted, so there is no cloud default — set base_url to your deployment in production. An explicitly assigned value always wins over the environment. Params are symbol-keyed hashes and map straight to the wire (Ruby's snake_case is already the wire's snake_case: reply_to, scheduled_at, audience_id).

Resources

Emails

Millionsend::Emails.send(payload, idempotency_key: "order-42") # POST /emails
Millionsend::Emails.get(id)                                    # GET  /emails/:id
Millionsend::Emails.cancel(id)                                 # POST /emails/:id/cancel (scheduled only)

Millionsend::Batch.send([payload_a, payload_b], idempotency_key: "run-7") # up to 100

to, cc, bcc and reply_to accept either a string or an array. Emails.create is an alias of Emails.send (as is Batch.create), mirroring Resend.

Audiences & contacts

audience = Millionsend::Audiences.create(name: "Registered users")
Millionsend::Audiences.list(limit: 20, after: cursor)
Millionsend::Audiences.get(id)
Millionsend::Audiences.remove(id)

Millionsend::Contacts.create(audience_id: audience[:id], email: "ada@acme.dev",
                             first_name: "Ada", properties: { plan: "pro" })
Millionsend::Contacts.get("ada@acme.dev", audience_id: audience[:id]) # by id or email
Millionsend::Contacts.get(contact_id)                                  # top-level, by id
Millionsend::Contacts.update(id: contact_id, unsubscribed: true, first_name: nil) # nil clears
Millionsend::Contacts.remove("ada@acme.dev", audience_id: audience[:id])
Millionsend::Contacts.list(audience_id: audience[:id], limit: 50)

# Topic subscriptions (granular unsubscribe) — mirrors resend's contacts.topics.update
Millionsend::Contacts.topics_update("ada@acme.dev", [{ id: topic_id, subscription: "opt_out" }])

Contacts are addressable by id or email; when an update hash carries both, the email wins. Omit audience_id: to use the top-level /contacts endpoints.

Topics

Millionsend::Topics.create(name: "Product updates", default_subscription: "opt_in")
Millionsend::Topics.get(id)
Millionsend::Topics.list    # bare { data: [...] } — topics are unpaginated
Millionsend::Topics.remove(id)

Broadcasts

broadcast = Millionsend::Broadcasts.create(
  audience_id: audience[:id],
  from: "Acme <news@acme.dev>",
  subject: "Launch",
  html: "<p>Hi {{{FIRST_NAME|there}}}</p>"
)
Millionsend::Broadcasts.list
Millionsend::Broadcasts.get(broadcast[:id])
Millionsend::Broadcasts.update(broadcast[:id], subject: "Launch 🚀") # draft only
Millionsend::Broadcasts.send(broadcast[:id], scheduled_at: "2026-09-01T09:00:00Z") # omit to send now
Millionsend::Broadcasts.cancel(broadcast[:id]) # scheduled only
Millionsend::Broadcasts.remove(broadcast[:id]) # draft only

Segments (MillionSend extension)

Dynamic segments are a saved filter over an audience's contacts — a MillionSend superset with no Resend equivalent, served under /segments2.

segment = Millionsend::Segments.create(
  name: "Pro plan",
  audience_id: audience[:id],
  filter: { match: "all", conditions: [{ field: "property:plan", op: "equals", value: "pro" }] }
)
Millionsend::Segments.get(segment[:id]) # includes a live contact_count
Millionsend::Segments.list
Millionsend::Segments.update(segment[:id], name: "Pro tier")
Millionsend::Segments.remove(segment[:id])

Error handling

No { data, error } tuple — a non-2xx response raises. The base class is Millionsend::Error, which carries #status_code, #name (the stable snake_case discriminant), and #message. Subclasses are keyed on name, so you can rescue a specific failure:

begin
  Millionsend::Emails.get(id)
rescue Millionsend::NotFoundError => e
  warn "no such email: #{e.message}"
rescue Millionsend::Error => e
  warn "#{e.name} (#{e.status_code || 'transport'}): #{e.message}"
end

Subclasses: ValidationError, NotFoundError, RestrictedApiKeyError, SendingPausedError, InvalidIdempotentRequestError, and ApplicationError (the fallback). Client-side and transport failures that never reached the API raise with #status_code == nil.

Migrating from Resend

- require "resend"
- Resend.api_key = "re_123"
- Resend::Emails.send(from: "...", to: "...", subject: "Hi", html: "<p>hi</p>")
+ require "millionsend"
+ Millionsend.api_key  = "ms_123"
+ Millionsend.base_url = "https://mail.acme.dev"
+ Millionsend::Emails.send(from: "...", to: "...", subject: "Hi", html: "<p>hi</p>")

Method names, nesting and payloads match. Notes:

  • Domains and API keys are managed in the MillionSend dashboard, not via the API, so there are no Domains / ApiKeys resources here.
  • Resend's Contacts.topics.update becomes Millionsend::Contacts.topics_update (Ruby has no nested-module method on a module function).
  • Millionsend::Segments is the distinct dynamic-filter feature (/segments2), not Resend's audiences alias. Use Millionsend::Audiences for a straight port.

License

MIT

About

MillionSend Ruby SDK — RubyGems: millionsend (MIT)

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages