ShortMessage

Gem Version

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


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:

  1. Update lib/short_message/version.rb with the new version number
  2. Update CHANGELOG.md with the release notes
  3. Commit the version bump and changelog
  4. Build and push the gem
  5. 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)

  1. Replace old GTX config keys with api_base_url, api_key, and response_format.
  2. Remove any initializer override of build_deliver_params_string (renamed to build_deliver_params_hash).
  3. 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.

  1. Optionally refresh the initializer and locales:
   bundle exec rails generate short_message:install
  1. Configure delivery reports — set config.dlr_url to your callback endpoint and config.callback_token for 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.