A Rails app that receives all incoming emails via SendGrid's Inbound Parse webhook, extracts OTP codes, and exposes a simple API to retrieve them.
- SendGrid Inbound Parse forwards all incoming emails to this app
- Action Mailbox receives the email via the SendGrid ingress
- OtpMailbox parses the email body and extracts OTP codes (4-8 digit codes)
- API lets you query the latest OTP for any email address
- Ruby 3.3.9
- Rails 8.1
bundle install
bin/rails db:migrate
bin/rails server| Variable | Description |
|---|---|
ACTION_MAILBOX_INGRESS_PASSWORD |
Password for authenticating SendGrid webhook requests |
- Go to SendGrid → Settings → Inbound Parse
- Add your domain/subdomain (e.g.,
otp.yourdomain.com) - Set the webhook URL to:
https://actionmailbox:YOUR_PASSWORD@yourdomain.com/rails/action_mailbox/sendgrid/inbound_emails - Check "POST the raw, full MIME message"
Point your domain's MX record to SendGrid:
MX otp.yourdomain.com mx.sendgrid.net (priority 10)
Each endpoint returns an HTML page by default. Append a .json extension to the path to get a JSON response instead.
GET /otp/john
GET /otp/john.json # JSON response
GET /otp/john.json?after=2024-01-15T10:30:00Z # most recent after a time
JSON Response:
{
"email": "john@otpinbox.dev",
"otp_code": "123456",
"subject": "Your verification code",
"sender": "noreply@service.com",
"received_at": "2024-01-15T10:30:00Z"
}GET /otp/john/all
GET /otp/john/all.json # JSON response
JSON Response:
{
"email": "john@otpinbox.dev",
"count": 2,
"otp_records": [
{
"otp_code": "123456",
"subject": "Your verification code",
"sender": "noreply@service.com",
"received_at": "2024-01-15T10:30:00Z"
}
]
}GET /otp/john/stream
Opens a Server-Sent Events stream that stays open and pushes the next OTP for john@otpinbox.dev the moment it arrives, then closes the connection. Delivery is push-based (Action Cable pub/sub) — there is no polling.
The event is named otp and its id is the OTP record id:
event: otp
id: 42
data: {"email":"john@otpinbox.dev","otp_code":"123456","subject":"Your verification code","sender":"noreply@service.com","received_at":"2024-01-15T10:30:00Z"}
Reconnect behaviour: after an OTP is delivered the server closes the stream. A browser's EventSource automatically reconnects, sending the Last-Event-ID header; the server treats that reconnect as "already delivered" and responds 204 No Content, which stops EventSource from reconnecting. A fresh connection (no Last-Event-ID) waits for the next OTP. If none arrives within 5 minutes the connection closes and the client reconnects to keep waiting.
Browser:
const source = new EventSource("/otp/john/stream");
source.addEventListener("otp", (event) => {
const otp = JSON.parse(event.data);
console.log("Code:", otp.otp_code);
source.close();
});cURL:
curl -N https://yourdomain.com/otp/john/stream400- Invalidaftertimestamp404- No OTP found for the given user
Use the Action Mailbox conductor UI in development to test incoming emails:
http://localhost:3000/rails/conductor/action_mailbox/inbound_emails