Skip to content
Browse the docs

Frameworks and agents · Published 2026-09-29 · Updated 2026-09-30 · AgentiSend

Send email from Rails

A Rails controller method that posts one receipt with Net::HTTP, an idempotency key, and the refusal's code and fix.

To send email from Rails, call one method from the controller action that posts JSON to POST /emails with Net::HTTP. There is no Ruby package for this API, and this page does not invent one. The method below checks the address, sends with an idempotency key derived from the recipient, and returns the message id, or the refusal's code and fix. It is run by the test suite against a local AgentiSend server. The run calls the method; it does not boot Rails.

Install

No gem to add. The method uses the standard library. In a controller:

def create
  render json: ReceiptsController.send_receipt(params.permit(:email, :note).to_h), status: :ok
end

Use the status the method returns when the call was refused.

Environment

VariableRequiredWhat it is
AGENTISEND_API_KEYyesA key with sending_access.
MAIL_FROMyesThe From address, on a domain you have verified.
AGENTISEND_BASE_URLnoDefaults to https://api.agentisend.com.

The route

# frozen_string_literal: true

# Rails — the method a controller action calls.
#
# There is no Ruby SDK. ReceiptsController#send_receipt posts JSON to
# POST /emails with Net::HTTP, which is what the action would call. Rails
# itself is not booted: the method is the one the route holds.
#
# AGENTISEND_API_KEY, MAIL_FROM, and optionally AGENTISEND_BASE_URL.
# Run: ruby send.rb

require 'json'
require 'net/http'
require 'uri'

module ReceiptsController
  module_function

  ADDRESS = /^[^@\s]+@[^@\s]+\.[^@\s]+$/

  # @param input [Hash]
  # @return [Hash] status and the JSON body the action would render
  def send_receipt(input)
    email = input.fetch('email', '').to_s.strip
    note = input.fetch('note', '').to_s
    unless ADDRESS.match?(email)
      return { 'status' => 400, 'body' => { 'error' => 'email must be one address, like you@example.com' } }
    end

    from = ENV.fetch('MAIL_FROM') { raise 'Set MAIL_FROM to an address on a domain you have verified.' }
    base = (ENV['AGENTISEND_BASE_URL'] || 'https://api.agentisend.com').sub(%r{/+$}, '')
    uri = URI("#{base}/emails")
    request = Net::HTTP::Post.new(uri)
    request['authorization'] = "Bearer #{ENV.fetch('AGENTISEND_API_KEY')}"
    request['content-type'] = 'application/json'
    request['accept'] = 'application/json'
    # The recipient, not the moment: a retry of the same note replays.
    request['idempotency-key'] = "receipt/#{email}"
    request.body = JSON.generate(
      'from' => from,
      'to' => email,
      'subject' => 'Your receipt',
      'text' => "Receipt note: #{note}"
    )

    response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
      http.request(request)
    end
    parsed = JSON.parse(response.body)
    status = response.code.to_i
    if status >= 400
      error = parsed.fetch('error', {})
      return { 'status' => status, 'body' => { 'code' => error['code'], 'fix' => error['fix'] } }
    end

    { 'status' => status, 'body' => { 'id' => parsed['id'] } }
  end
end

def emit(step, result)
  puts JSON.generate({ 'step' => step, 'status' => result['status'] }.merge(result['body']))
end

if __FILE__ == $PROGRAM_NAME
  emit('send', ReceiptsController.send_receipt('email' => 'rails@example.com', 'note' => 'first'))
  emit('retry', ReceiptsController.send_receipt('email' => 'rails@example.com', 'note' => 'first'))
  emit('invalid', ReceiptsController.send_receipt('email' => 'not-an-address', 'note' => 'first'))
  emit('refused', ReceiptsController.send_receipt('email' => 'rails@example.com', 'note' => 'second'))
end
examples/rails/send.rb, run by the test suite against a local AgentiSend server

Try it

ruby send.rb

The file prints one JSON object per step: the receipt, the same request again, a malformed address answered before any request, and a changed note under the same key.

What comes back when it goes wrong

Every refused request carries code, message, fix and docs_url. The method returns code and fix with the API's status. The error catalogue lists every code.

Next