ShortMessage
ShortMessage is a Rails Engine that sends SMS messages via the GTX Messaging REST API v2 and receives delivery status callbacks.
Requires: Ruby >= 3.2, Rails >= 6.1
Table of Contents
- ShortMessage
- Table of Contents
- Installation
- Configuration
- Configuration reference
- Usage
- Message attributes
- Delivery Reports
- Events
short_message.deliveredshort_message.status_updated- Email Notifications
- Customization
- Status code labels
- Extending the request payload
- Development
- Testing DLR callbacks locally
- Building & Releasing
- Local Development (using in another project)
- Upgrade Guide (1.x → 2.0)
- License
Installation
Add to your Gemfile:
gem 'short_message'
Install dependencies:
bundle install
Run the install generator:
rails generate short_message:install
Copy and run the migrations:
bundle exec rails railties:install:migrations FROM=short_message
bundle exec rails db:migrate
Configuration
The generator creates config/initializers/short_message.rb. Edit it to suit your setup:
ShortMessage.configure do |config|
# --- Required ---
config.api_base_url = "https://rest.gtx-messaging.net"
config.api_key = ENV.fetch("GTX_API_KEY")
config.response_format = "json" # "json", "xml", or "plain"
# --- SMS defaults ---
config.default_sms_sender = "MyApp" # sender name or number used when none is set on the message
# --- Delivery reports (DLR) ---
config.dlr_mask = 3 # 1 = delivered, 2 = undelivered, 3 = both
config.callback_token = ENV["SHORT_MESSAGE_CALLBACK_TOKEN"]
config.dlr_url = "https://example.com/short_message/messages/status?token=#{config.callback_token}"
# --- HTTP timeouts ---
config.open_timeout = 5 # seconds
config.read_timeout = 30 # seconds
# --- Email notifications (optional) ---
config.default_mail_sender = "sms@example.com"
config.admin_notification_email = "admin@example.com" # set nil to disable error emails
config.reload_notification_email = "billing@example.com" # notified on HTTP 402 from GTX
end
Configuration reference
| Key | Required | Default | Description |
|---|---|---|---|
api_base_url |
yes | "https://rest.gtx-messaging.net" |
GTX API endpoint |
api_key |
yes | — | GTX API authentication key |
response_format |
no | "json" |
"json", "xml", or "plain" |
default_sms_sender |
no | — | Fallback sender when none is set on the message |
dlr_mask |
no | 3 |
Delivery report event mask |
dlr_url |
no | — | Callback URL GTX posts status updates to |
callback_token |
no | — | Token validated on incoming status callbacks |
open_timeout |
no | 5 |
HTTP connection open timeout (seconds) |
read_timeout |
no | 30 |
HTTP read timeout (seconds) |
default_mail_sender |
no | — | From address for notification emails |
admin_notification_email |
no | — | Receives error notifications; set nil to disable |
reload_notification_email |
no | — | Receives payment-required (HTTP 402) alerts |
Usage
Create a ShortMessage::Message and call deliver:
sms = ShortMessage::Message.new(
sender: "+41791234567", # or a name up to 11 chars
recipient: "+41799876543",
text: "Hello World!"
)
sms.deliver # => true on success, false on failure
sender is optional when config.default_sms_sender is configured.
Recipient normalization: if recipient starts with 00, the gem automatically converts it to + format (for example 0041791234567 -> +41791234567). Any other non-E.164 recipient format is rejected before sending.
After a successful delivery, sms.message_key holds the provider-assigned message ID and sms.provider_http_status holds the HTTP status code returned by GTX.
Message attributes
| Attribute | Description |
|---|---|
sender |
SMS sender name or number |
recipient |
SMS recipient phone number in E.164 (+...) |
text |
Message body |
message_key |
Unique ID assigned by GTX after delivery |
status_code |
Delivery status code (updated via DLR callback) |
provider_http_status |
HTTP status from GTX at delivery time |
provider_status |
Status string from GTX response |
provider_response_body |
Full raw response payload from GTX |
error_code |
Error code if delivery failed |
error_message |
Error description if delivery failed |
submitted_at |
Timestamp when message was submitted to GTX |
delivered_at |
Timestamp when delivery confirmed |
Delivery Reports
ShortMessage mounts a status-update endpoint at /short_message/messages/status. Configure GTX to POST (or GET) to this URL with the message ID and status:
POST /short_message/messages/status?id=<message_key>&status=<code>&token=<callback_token>
| Parameter | Description |
|---|---|
id |
The message_key of the message to update |
status |
Numeric status code from GTX |
token |
Required when config.callback_token is set |
Response codes
| HTTP | Meaning |
|---|---|
| 200 | Status updated successfully |
| 400 | Missing id or status parameter |
| 401 | Invalid or missing callback token |
| 404 | Message not found |
When config.callback_token is set, every incoming callback must include token=<value> or it will be rejected with a 401.
Events
ShortMessage fires ActiveSupport::Notifications events so you can react to delivery and status changes without monkey-patching.
short_message.delivered
Fired after a message is successfully sent to GTX.
ActiveSupport::Notifications.subscribe('short_message.delivered') do |name, start, finish, id, payload|
Rails.logger.info "SMS #{payload[:key]} delivered"
end
Payload keys: key (the message_key assigned by GTX).
short_message.status_updated
Fired when a delivery report callback is received.
ActiveSupport::Notifications.subscribe('short_message.status_updated') do |name, start, finish, id, payload|
Activity.create(
message: "Message #{payload[:key]} status → #{payload[:status]}"
)
end
Payload keys: key (message ID), status (new status code).
Email Notifications
When admin_notification_email is configured, ShortMessage sends an email on delivery failure or unexpected exceptions. Set it to nil to disable.
When reload_notification_email is configured, ShortMessage sends an email when GTX returns HTTP 402 (payment required / credit exhausted).
Customization
Status code labels
Status code descriptions are stored in locale files under config/locales/. Add or override keys there to customise the text returned by message.status_text.
# config/locales/short_message.en.yml
en:
short_message:
status:
code_1: "Delivered to handset"
code_2: "Delivery failed"
code_4: "Queued on SMSC"
code_8: "Delivered to SMSC"
code_16: "Not delivered to SMSC"
sms.status_text # => "Delivered to handset"
Extending the request payload
Override build_deliver_params_hash in an initializer to add or change parameters sent to GTX:
ShortMessage::Message.module_eval do
private
def build_deliver_params_hash
super.merge("coding" => 2) # e.g. enable UCS-2 encoding
end
end
Development
Clone the repo and install dependencies:
git clone https://github.com/asaurer/short_message.git
cd short_message
bundle install
Run the test suite (migrations run automatically):
bundle exec rake test
Testing DLR callbacks locally
GTX needs a publicly reachable HTTPS URL to deliver status callbacks. Use ngrok to expose your local server:
ngrok http 3000
Then configure the tunnel URL in your initializer:
config.callback_token = "dev-secret"
config.dlr_url = "https://abc123.ngrok.io/short_message/messages/status?token=dev-secret"
Restart your Rails server after changing the initializer. Once a message is delivered, GTX will POST the status update to the ngrok URL which forwards it to your local app.
You can also simulate a callback manually without ngrok:
curl -X POST "http://localhost:3000/short_message/messages/status" \
-d "id=YOUR_MESSAGE_KEY&status=1&token=dev-secret"
Replace YOUR_MESSAGE_KEY with the value of sms.message_key after calling sms.deliver.
Building & Releasing
Build the gem locally:
gem build short_message.gemspec
This creates short_message-<version>.gem in the project root.
Install the built gem locally to verify it works:
gem install short_message-<version>.gem
Push to RubyGems:
gem push short_message-<version>.gem
You need a RubyGems account with push access to the short_message gem. Credentials are stored in ~/.gem/credentials.
Release checklist:
- Update
lib/short_message/version.rbwith the new version number - Update
CHANGELOG.mdwith the release notes - Commit the version bump and changelog
- Build and push the gem
- Tag the release:
git tag v<version> && git push origin v<version>
Local Development (using in another project)
When you want to work on short_message and test changes inside a host Rails app simultaneously, point the host app's Gemfile at your local checkout instead of RubyGems:
# host-app/Gemfile
gem 'short_message', path: '/path/to/short_message'
Then in the host app:
bundle install
Bundler symlinks the gem from the local path, so every change you make in the short_message directory is picked up immediately on the next request (or reload! in the console) without re-running bundle install.
If the gem has pending migrations that need to land in the host app's database:
bundle exec rails railties:install:migrations FROM=short_message
bundle exec rails db:migrate
To re-generate the initializer or locales after making changes to the generator templates:
bundle exec rails generate short_message:install
When you are done and want to switch back to the published gem, restore the Gemfile entry:
gem 'short_message', '~> 2.0'
Upgrade Guide (1.x → 2.0)
- Replace old GTX config keys with
api_base_url,api_key, andresponse_format. - Remove any initializer override of
build_deliver_params_string(renamed tobuild_deliver_params_hash). - Install and run the new migration:
bundle exec rails railties:install:migrations FROM=short_message
bundle exec rails db:migrate
If the short_message_messages table already
exists you will see Mysql2::Error: Table 'short_message_messages' already exists.
This happens because railties:install:migrations re-copies the original
create migration under a new timestamp. Mark it as already applied, then migrate:
bundle exec rails runner \
"ActiveRecord::Base.connection.execute(
\"INSERT IGNORE INTO schema_migrations (version) \" \
\"VALUES ('$(ls db/migrate/*create_short_message_messages.short_message.rb \
| grep -oP '[0-9]+(?=_)')' )\")"
bundle exec rails db:migrate
SQLite users: replace INSERT IGNORE with INSERT OR IGNORE.
- Optionally refresh the initializer and locales:
bundle exec rails generate short_message:install
- Configure delivery reports — set
config.dlr_urlto your callback endpoint andconfig.callback_tokenfor request validation:
config.callback_token = ENV["SHORT_MESSAGE_CALLBACK_TOKEN"]
config.dlr_url = "https://your-app.example/short_message/messages/status?token=#{config.callback_token}"
In the GTX portal, ensure delivery report callbacks are enabled for your API key and that your app's callback URL is publicly reachable over HTTPS.
License
MIT — see MIT-LICENSE.
