Notix
Guides

Send email from Rails.

Point ActionMailer at the relay and every mailer you already have, Devise included, sends through Notix without a code change. Add a small service object for the sends that need a template and an idempotency key, and a controller that verifies delivery webhooks.

Rails 7.1 or later.

The smtp_settings keys below are the ones the Rails 8.0 guide documents. Older versions accept the same hash; enable_starttls_auto is the older name for the TLS flag.

A verified domain and an API key in credentials.

Every from-address must be on a domain verified in the dashboard. The key and the webhook secret go in the encrypted credentials file, never in a committed environment file.

Steps

From an untouched app to a queued receipt.

  1. Put the key and the webhook secret in credentials.

    bin/rails credentials:edit
    # bin/rails credentials:edit
    notix:
      api_key: notix_xxxxxxxxxxxx
      webhook_secret: whsec_xxxxxxxxxxxx
    
  2. Option A: point ActionMailer at the relay.

    smtp.usenotix.dev on 587 with STARTTLS, the username notix, the API key as the password. Devise, password resets and every mailer view you already have now go through Notix, with the same log, suppression and webhooks as the API.

    config/environments/production.rb
    # config/environments/production.rb
    config.action_mailer.delivery_method = :smtp
    config.action_mailer.smtp_settings = {
      address:         "smtp.usenotix.dev",
      port:            587,
      domain:          "acme.com",
      user_name:       "notix",
      password:        Rails.application.credentials.dig(:notix, :api_key),
      authentication:  "plain",
      enable_starttls: true,
      open_timeout:    5,
      read_timeout:    5
    }
    config.action_mailer.default_options = { from: "receipts@acme.com" }
    config.action_mailer.raise_delivery_errors = true
    
    app/mailers/order_mailer.rb
    # app/mailers/order_mailer.rb
    class OrderMailer < ApplicationMailer
      def receipt
        @order = params[:order]
        mail(to: @order.customer_email, subject: "Your receipt for order #{@order.number}")
      end
    end
    
    # Anywhere in the app: queued through Active Job, delivered over the relay.
    OrderMailer.with(order: order).receipt.deliver_later
    
  3. Option B: the API for templates and idempotency.

    When the template lives in Notix or the send must never go out twice, call the API from a service object and give the job an idempotency key derived from the record.

    app/services/notix.rb
    # app/services/notix.rb
    require "net/http"
    require "json"
    
    class Notix
      Error = Class.new(StandardError)
      BASE = URI("https://app.usenotix.dev")
    
      def initialize(api_key: Rails.application.credentials.dig(:notix, :api_key))
        @api_key = api_key
      end
    
      # The idempotency key makes a retried job return the original id.
      def send_email(payload, idempotency_key:)
        request = Net::HTTP::Post.new("/api/v1/emails")
        request["Authorization"] = "Bearer #{@api_key}"
        request["Content-Type"] = "application/json"
        request["Idempotency-Key"] = idempotency_key
        request.body = payload.to_json
    
        response = Net::HTTP.start(BASE.host, BASE.port, use_ssl: true, open_timeout: 5, read_timeout: 15) do |http|
          http.request(request)
        end
        body = JSON.parse(response.body)
        return body["emailId"] if response.is_a?(Net::HTTPSuccess)
    
        error = body.fetch("error", {})
        raise Error, "#{response.code} #{error["code"]}: #{error["message"]}"
      end
    end
    
  4. Receive delivery status on a webhook.

    Skip forgery protection on the controller, read request.raw_post rather than the parsed params, and check the signature before touching the event.

    app/controllers/notix_webhooks_controller.rb
    # config/routes.rb
    post "/webhooks/notix", to: "notix_webhooks#create"
    
    # app/controllers/notix_webhooks_controller.rb
    class NotixWebhooksController < ActionController::Base
      skip_forgery_protection # Notix is not a browser session
      TOLERANCE_MS = 5 * 60 * 1000
    
      def create
        raw = request.raw_post # the exact bytes; never re-serialise params
        timestamp = request.headers["X-Notix-Timestamp"].to_s
        signature = request.headers["X-Notix-Signature"].to_s
    
        now_ms = (Time.now.to_f * 1000).to_i
        return head :bad_request if (now_ms - timestamp.to_i).abs > TOLERANCE_MS
    
        secret = Rails.application.credentials.dig(:notix, :webhook_secret)
        expected = "v1=" + OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{raw}")
        return head :bad_request unless ActiveSupport::SecurityUtils.secure_compare(expected, signature)
    
        event = JSON.parse(raw)
        case event["type"]
        when "email.delivered"
          Order.find_by(receipt_email_id: event.dig("data", "id"))&.update!(receipt_delivered_at: Time.current)
        when "email.bounced"
          User.where(email: event.dig("data", "to")).update_all(email_invalid: true) if event.dig("data", "bounce", "type") == "Permanent"
        end
    
        head :ok
      end
    end
    
In production

What to check before real users depend on it.

The Ruby guide has the plain HTTP contract, a retry loop and a Rack handler; the Node.js guide covers the production checklist in full. The contract is the same from Rails.

Development and test.

Keep delivery_method = :test in test and :letter_opener or the log in development, so only production talks to the relay.

Bounces.

A permanent bounce suppresses the address automatically. The webhook above marks the user's email invalid so your UI can ask for a new one; see bounce handling.

Reference.

Request and response shapes, error codes and every webhook event are in the API reference.

Questions

Rails, answered.

How do I send email from Rails with Notix?
Two ways. Point ActionMailer's smtp_settings at smtp.usenotix.dev with the username notix and your API key as the password, and every existing mailer, including Devise's, sends through Notix unchanged. Or call the JSON API from a small service object when you want templates with variables and an idempotency key on retried jobs. Both land in the same message log with the same suppression list.
Will Devise's confirmation and password-reset emails work?
Yes, with no code changes. Devise uses ActionMailer, so once smtp_settings point at the relay its confirmation, reset and unlock emails go through Notix. Set default_options from to an address on your verified domain, because the relay refuses a from-address on a domain you have not verified.
Where do I keep the API key?
In the encrypted credentials file: bin/rails credentials:edit, then Rails.application.credentials.dig(:notix, :api_key). The master key lives outside the repository. Do not put the key in an initializer or an environment file that is committed.
How do retried jobs avoid sending twice?
Give the send an Idempotency-Key that names the message, for example order-4471-receipt. When retry_on runs the job again, the API recognises the key and returns the original emailId within 24 hours instead of sending a second email. A different body with the same key answers 409 NOT_UNIQUE.
How do I verify a Notix webhook in a Rails controller?
Read request.raw_post, take X-Notix-Timestamp and X-Notix-Signature, compute a hex HMAC-SHA256 of "timestamp.rawBody" with the webhook secret, prefix it with v1=, and compare with ActiveSupport::SecurityUtils.secure_compare. Reject timestamps more than five minutes from now, and call skip_forgery_protection on the controller because the request carries no CSRF token.
deliver_later or the API job?
deliver_later is fine for mail you author as ActionMailer views. Use the API job when the template lives in Notix, when you need the idempotency key, or when you want the emailId stored against the record so delivery events can be matched later.

Send your first email in five minutes.

Verify a domain, copy an API key, make one call. The free plan does not ask for a card.