waservix 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +116 -0
  4. data/lib/waservix.rb +196 -0
  5. metadata +52 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c3051c5426d31968d5f9c5fb891871698ec55b7c9f8efbbd91cd4b1f6f17dc2d
4
+ data.tar.gz: e45c7a50952b53cc4d9ddb756f9abbce88424cd8aa2af374bba63fa6be1b931c
5
+ SHA512:
6
+ metadata.gz: 7f035fc9c42f8ca4699a738efaf5f13fb1ba3cd8d152a2b9b8a4337e94ea5cfc2a2d67cb68d52957678e8b4b7ba9441fc4433ffa83c155f1d8d4fd1aee0d9f6b
7
+ data.tar.gz: 752ff0e00f931b4e02242faf9c2bc9af29a921ca25c08090b0ef42f0ee2816d33a3c8d7bcbfecd0e14d90b4e8d69e504fa43214b2be4d34ce4a966b582aec74e
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tekservix
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # WAservix Ruby client
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/waservix.svg)](https://rubygems.org/gems/waservix)
4
+
5
+ Ruby client for the [WAservix WhatsApp API Gateway](https://tekservix.com/waservix/).
6
+
7
+ WAservix links your own WhatsApp number by QR code (the same way WhatsApp Web does) and gives you a REST API to send text, images, video, audio, documents, locations, polls and more, with webhooks for incoming messages and status updates.
8
+
9
+ - Ruby 2.7+, no dependencies (standard library only)
10
+ - Works in Rails, Sinatra, Hanami, Sidekiq jobs and scripts
11
+ - Webhook check built in
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ gem install waservix
17
+ ```
18
+
19
+ or in your Gemfile:
20
+
21
+ ```ruby
22
+ gem 'waservix'
23
+ ```
24
+
25
+ ## Quick start
26
+
27
+ Copy your device's API key from the WAservix dashboard (Devices -> API key) and keep it on your server.
28
+
29
+ ```ruby
30
+ require 'waservix'
31
+
32
+ wa = WAservix::Client.new(api_key: ENV['WASERVIX_KEY'])
33
+
34
+ res = wa.send_text('+15551234567', 'Hello Aline, your order is ready')
35
+ res['msgId'] # => 58213
36
+ res['status'] # => "in_progress"
37
+ ```
38
+
39
+ ## Sending
40
+
41
+ Messages are queued and sent one at a time per number, so each call returns right away with `status: "in_progress"` and a `msgId`.
42
+
43
+ ```ruby
44
+ # Image with caption
45
+ wa.send_image('+15551234567', 'https://example.com/product.jpg', caption: 'Back in stock')
46
+
47
+ # Document
48
+ wa.send_document('+15551234567', 'https://example.com/inv-1042.pdf', file_name: 'Invoice-1042.pdf', caption: 'Your invoice')
49
+
50
+ # Anything the API supports, including groups (JID ending in @g.us)
51
+ wa.send_message('120363040000000000@g.us', text: 'Team meeting at 3pm')
52
+ wa.send_message('+15551234567', location: { latitude: -1.9441, longitude: 30.0619, name: 'Our shop' })
53
+ ```
54
+
55
+ ## Status, contacts and groups
56
+
57
+ ```ruby
58
+ info = wa.get_message_info(58213) # queued, sent, delivered, read, played or failed
59
+ check = wa.on_whatsapp('+15551234567') # { "exists" => true, "jid" => "..." }
60
+ conn = wa.get_status # { "status" => "connected" }
61
+
62
+ wa.edit_message(58213, 'Corrected text') # own text messages, about 15 minutes
63
+ groups = wa.list_groups
64
+ ```
65
+
66
+ Anything else in the [API reference](https://tekservix.com/waservix/docs/) is one call away:
67
+
68
+ ```ruby
69
+ wa.request(:post, '/messages/read', { msgId: '3EB0X123456789' })
70
+ ```
71
+
72
+ ## Webhooks
73
+
74
+ Set a webhook URL and events for the number in the dashboard. Each request carries `X-Webhook-Signature`, which equals the device's webhook secret. Rails example:
75
+
76
+ ```ruby
77
+ class WaWebhooksController < ActionController::API
78
+ def create
79
+ unless WAservix.verify_webhook(request.headers['X-Webhook-Signature'], ENV['WASERVIX_WEBHOOK_SECRET'])
80
+ return head :unauthorized
81
+ end
82
+
83
+ payload = JSON.parse(request.raw_post)
84
+ if payload['event'] == 'messages.received'
85
+ m = payload['data']['messages']
86
+ IncomingWhatsAppJob.perform_later(m['key']['cleanedSenderPn'], m['messageBody'])
87
+ end
88
+ head :ok # acknowledge fast; do slow work in a job
89
+ end
90
+ end
91
+ ```
92
+
93
+ ## Errors
94
+
95
+ Failed calls raise `WAservix::Error` with `status`, `message`, field `errors` and, on 429, `retry_after` in seconds. A 503 means the device is reconnecting: retry shortly.
96
+
97
+ Never put the key in JavaScript or a mobile app: anyone could read it. Call the API from your server.
98
+
99
+ ## Migrating from WaSender
100
+
101
+ WAservix accepts the same `/send-message` body and webhook fields as WaSender, so most integrations switch by changing the base URL and key. See the [compatibility notes](https://tekservix.com/waservix/docs/).
102
+
103
+ ## Use it responsibly
104
+
105
+ WAservix is not Meta's official WhatsApp Business Platform. There's no business verification and no per-conversation fee, but WhatsApp's spam rules apply to your number directly. Message people who expect to hear from you, warm up new numbers slowly and keep the built-in account protection on.
106
+
107
+ ## More from Tekservix
108
+
109
+ - [SMS Gora SMS Gateway](https://tekservix.com/smsgora/): send SMS through your own Android phones (`gem install smsgora`)
110
+ - [AI Sales Agent](https://tekservix.com/ai-sales-agent): answers customers on WhatsApp and SMS using your website's content
111
+ - [Website to App](https://tekservix.com/mobile-apps): turn your website into Android and iOS apps
112
+ - WAservix is also available for Node.js, Python, PHP, Go, Java/Kotlin and .NET: see [github.com/TekservixHQ](https://github.com/TekservixHQ)
113
+
114
+ ## License
115
+
116
+ MIT
data/lib/waservix.rb ADDED
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'openssl'
6
+ require 'uri'
7
+
8
+ # Ruby client for the WAservix WhatsApp API Gateway.
9
+ #
10
+ # wa = WAservix::Client.new(api_key: ENV['WASERVIX_KEY'])
11
+ # wa.send_text('+15551234567', 'Hello Aline, your order is ready')
12
+ #
13
+ # Docs: https://tekservix.com/waservix/docs/
14
+ module WAservix
15
+ VERSION = '1.0.0'
16
+ DEFAULT_BASE_URL = 'https://tekservix.com/waservix/api'
17
+
18
+ # Raised when the API answers with an error. A 503 means the device is reconnecting.
19
+ class Error < StandardError
20
+ attr_reader :status, :errors, :body, :retry_after
21
+
22
+ def initialize(message, status: nil, errors: nil, body: nil, retry_after: nil)
23
+ super(message)
24
+ @status = status
25
+ @errors = errors
26
+ @body = body
27
+ @retry_after = retry_after
28
+ end
29
+ end
30
+
31
+ class Client
32
+ attr_reader :base_url
33
+
34
+ # api_key: device API key (wpk_...) or a personal access token for session management.
35
+ def initialize(api_key:, base_url: DEFAULT_BASE_URL, timeout: 30)
36
+ raise ArgumentError, 'WAservix: api_key is required (Devices -> API key in the WAservix dashboard)' if api_key.nil? || api_key.empty?
37
+
38
+ @api_key = api_key
39
+ @base_url = base_url.sub(%r{/+\z}, '')
40
+ @timeout = timeout
41
+ end
42
+
43
+ # Call any documented endpoint, e.g. request(:get, '/groups'). Returns the `data` field.
44
+ def request(method, path, body = nil)
45
+ uri = URI(@base_url + path)
46
+ klass = { get: Net::HTTP::Get, post: Net::HTTP::Post, put: Net::HTTP::Put, delete: Net::HTTP::Delete }.fetch(method.to_s.downcase.to_sym)
47
+ req = klass.new(uri)
48
+ req['Authorization'] = "Bearer #{@api_key}"
49
+ req['Accept'] = 'application/json'
50
+ req['User-Agent'] = "waservix-ruby/#{VERSION}"
51
+ unless body.nil?
52
+ req['Content-Type'] = 'application/json'
53
+ req.body = JSON.generate(body)
54
+ end
55
+
56
+ res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https',
57
+ open_timeout: @timeout, read_timeout: @timeout) { |http| http.request(req) }
58
+ status = res.code.to_i
59
+ text = res.body.to_s
60
+ json = begin
61
+ text.empty? ? nil : JSON.parse(text)
62
+ rescue JSON::ParserError
63
+ nil
64
+ end
65
+
66
+ if status >= 400 || (json.is_a?(Hash) && json['success'] == false)
67
+ retry_after = res['Retry-After']
68
+ raise Error.new((json.is_a?(Hash) && json['message']) || "HTTP #{status}",
69
+ status: status,
70
+ errors: json.is_a?(Hash) ? json['errors'] : nil,
71
+ body: json || text,
72
+ retry_after: retry_after && Float(retry_after, exception: false))
73
+ end
74
+ json.is_a?(Hash) && json.key?('data') ? json['data'] : json
75
+ end
76
+
77
+ # Send any message type to a number or group JID (...@g.us). Fields use the API names:
78
+ # text, imageUrl, videoUrl, audioUrl, documentUrl, fileName, stickerUrl, contact, location,
79
+ # poll, replyTo, mentions, viewOnce. Returns { 'msgId' => ..., 'jid' => ..., 'status' => 'in_progress' }.
80
+ def send_message(to, **fields)
81
+ raise ArgumentError, 'send_message: "to" is required (international number or group JID)' if to.nil? || to.empty?
82
+
83
+ request(:post, '/send-message', { to: to }.merge(fields.reject { |_, v| v.nil? }))
84
+ end
85
+
86
+ def send_text(to, text)
87
+ send_message(to, text: text)
88
+ end
89
+
90
+ def send_image(to, image_url, caption: nil)
91
+ send_message(to, imageUrl: image_url, text: caption)
92
+ end
93
+
94
+ def send_document(to, document_url, file_name: nil, caption: nil)
95
+ send_message(to, documentUrl: document_url, fileName: file_name, text: caption)
96
+ end
97
+
98
+ # Status: queued, sent, delivered, read, played or failed.
99
+ def get_message_info(msg_id)
100
+ request(:get, "/messages/#{esc(msg_id)}/info")
101
+ end
102
+
103
+ # Edit one of your own text messages (about 15 minutes after sending).
104
+ def edit_message(msg_id, text)
105
+ request(:put, "/messages/#{esc(msg_id)}", { text: text })
106
+ end
107
+
108
+ def delete_message(msg_id)
109
+ request(:delete, "/messages/#{esc(msg_id)}")
110
+ end
111
+
112
+ def resend_message(msg_id)
113
+ request(:post, "/messages/#{esc(msg_id)}/resend")
114
+ end
115
+
116
+ # Is this number on WhatsApp? Returns { 'exists' => true/false, 'jid' => ... }.
117
+ def on_whatsapp(phone)
118
+ request(:get, "/on-whatsapp/#{esc(phone)}")
119
+ end
120
+
121
+ # connected, connecting, need_scan, disconnected, logged_out or expired.
122
+ def get_status
123
+ request(:get, '/status')
124
+ end
125
+
126
+ def get_user
127
+ request(:get, '/user')
128
+ end
129
+
130
+ def list_contacts
131
+ request(:get, '/contacts')
132
+ end
133
+
134
+ def get_contact(phone)
135
+ request(:get, "/contacts/#{esc(phone)}")
136
+ end
137
+
138
+ def save_contact(phone, name)
139
+ request(:put, '/contacts', { phone: phone, name: name })
140
+ end
141
+
142
+ def list_groups
143
+ request(:get, '/groups')
144
+ end
145
+
146
+ def get_group_metadata(group_jid)
147
+ request(:get, "/groups/#{esc(group_jid)}/metadata")
148
+ end
149
+
150
+ # Session management (needs a personal access token, not a device key).
151
+ def list_sessions
152
+ request(:get, '/whatsapp-sessions')
153
+ end
154
+
155
+ def get_session(id)
156
+ request(:get, "/whatsapp-sessions/#{esc(id)}")
157
+ end
158
+
159
+ def create_session(**params)
160
+ request(:post, '/whatsapp-sessions', params)
161
+ end
162
+
163
+ # method: 'qr', or 'code' with phone: for pairing codes.
164
+ def connect_session(id, method: 'qr', phone: nil)
165
+ body = { method: method }
166
+ body[:phone] = phone if phone
167
+ request(:post, "/whatsapp-sessions/#{esc(id)}/connect", body)
168
+ end
169
+
170
+ def get_session_qr_code(id)
171
+ request(:get, "/whatsapp-sessions/#{esc(id)}/qrcode")
172
+ end
173
+
174
+ private
175
+
176
+ def esc(value)
177
+ URI.encode_www_form_component(value.to_s).gsub('+', '%20')
178
+ end
179
+ end
180
+
181
+ # Constant-time check of the X-Webhook-Signature header against your device's webhook secret.
182
+ def self.verify_webhook(signature, secret)
183
+ return false if signature.nil? || signature.empty? || secret.nil? || secret.empty?
184
+ secure_compare(signature.to_s, secret.to_s)
185
+ end
186
+
187
+ # Constant-time string comparison (works on every Ruby version).
188
+ def self.secure_compare(a, b)
189
+ return false unless a.bytesize == b.bytesize
190
+
191
+ diff = 0
192
+ a.bytes.zip(b.bytes) { |x, y| diff |= x ^ y }
193
+ diff.zero?
194
+ end
195
+ private_class_method :secure_compare
196
+ end
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: waservix
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Tekservix
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-11-10 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: 'Send WhatsApp messages, images and documents from your own number with
14
+ WAservix: status tracking, contacts, groups and webhooks. Works in Rails, Sinatra
15
+ and plain Ruby.'
16
+ email: support@tekservix.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - LICENSE
22
+ - README.md
23
+ - lib/waservix.rb
24
+ homepage: https://tekservix.com/waservix/
25
+ licenses:
26
+ - MIT
27
+ metadata:
28
+ homepage_uri: https://tekservix.com/waservix/
29
+ documentation_uri: https://tekservix.com/waservix/docs/
30
+ source_code_uri: https://github.com/TekservixHQ/waservix-ruby
31
+ bug_tracker_uri: https://github.com/TekservixHQ/waservix-ruby/issues
32
+ rubygems_mfa_required: 'true'
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '2.7'
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubygems_version: 3.5.22
49
+ signing_key:
50
+ specification_version: 4
51
+ summary: Official Ruby client for the WAservix WhatsApp API Gateway
52
+ test_files: []