Skip to content
Browse the docs

Frameworks and agents · Published 2026-10-31 · Updated 2026-10-31 · AgentiSend

Send email with Sinatra

A Sinatra POST block that sends one booking by calling a method which posts with Net::HTTP.

To send email with Sinatra, add a post route whose block posts JSON to POST /emails. Sinatra's introduction says a route is an HTTP method paired with a path, and shows post '/' do … end as the block that creates something. There is no Ruby package for this API, and this page does not invent one. Send email from Rails is the controller method; Send email with Ruby is the same standard library with no route. The method below checks the address, sends a booking with an idempotency key derived from the guest, and returns the message id or the refusal's code and fix. The suite runs the method with ruby send.rb against a local AgentiSend server. It does not boot Sinatra.

Install

No gem to add. The method uses Net::HTTP. The route from the introduction, pointed at /send:

require 'json'
require 'sinatra'

post '/send' do
  payload = JSON.parse(request.body.read)
  result = send_booking(payload)
  status result['status']
  JSON.generate(result['body'])
end

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

# Sinatra — the method a `post '/send'` block calls.
#
# There is no Ruby package for this API. This file posts JSON to POST /emails
# with Net::HTTP. A Sinatra route renders the hash this method returns. The
# framework is not booted here.
#
# AGENTISEND_API_KEY is the key. MAIL_FROM is an address on a domain you have
# verified. AGENTISEND_BASE_URL is optional and defaults to the public API.
#
# Run: ruby send.rb

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

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

# @param input [Hash]
# @return [Hash] status and the JSON body the route would render
def send_booking(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 guest, not the moment: a retry of the same note replays.
  request['idempotency-key'] = "booking/#{email}"
  request.body = JSON.generate(
    'from' => from,
    'to' => email,
    'subject' => 'Your booking',
    'text' => "Booking 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

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

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

Try it

ruby send.rb

The script prints one JSON object per step: the booking, 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, which the route sets as the response status. The error catalogue lists every code.

Next