Module: Mailblastr::Webhooks
- Defined in:
- lib/mailblastr/webhooks.rb
Class Method Summary collapse
-
.create(params) ⇒ Object
Create a webhook.
-
.delete(webhook_id) ⇒ Object
DELETE /webhooks/:id.
-
.get(webhook_id) ⇒ Object
GET /webhooks/:id.
-
.list(params = {}) ⇒ Object
GET /webhooks.
-
.rotate(webhook_id) ⇒ Object
Rotate the signing secret; the new plaintext secret is returned once and the old one stops verifying immediately.
-
.test(webhook_id) ⇒ Object
Send a synchronous test delivery and return the endpoint's live result.
-
.update(webhook_id, params) ⇒ Object
PATCH /webhooks/:id — params: { endpoint:, events:, status: }.
-
.verify(payload, headers, secret, tolerance: 300) ⇒ Object
Verify a webhook delivery's Svix-style signature against your endpoint's signing secret.
Class Method Details
.create(params) ⇒ Object
Create a webhook. The plaintext signing secret is returned ONCE, only here. POST /webhooks — params: { endpoint:, events: [...], secret: }
10 11 12 |
# File 'lib/mailblastr/webhooks.rb', line 10 def create(params) Client.request(:post, "/webhooks", body: params) end |
.delete(webhook_id) ⇒ Object
DELETE /webhooks/:id
58 59 60 |
# File 'lib/mailblastr/webhooks.rb', line 58 def delete(webhook_id) Client.request(:delete, "/webhooks/#{Client.path_escape(webhook_id)}") end |
.get(webhook_id) ⇒ Object
GET /webhooks/:id
15 16 17 |
# File 'lib/mailblastr/webhooks.rb', line 15 def get(webhook_id) Client.request(:get, "/webhooks/#{Client.path_escape(webhook_id)}") end |
.list(params = {}) ⇒ Object
GET /webhooks
20 21 22 |
# File 'lib/mailblastr/webhooks.rb', line 20 def list(params = {}) Client.request(:get, "/webhooks", query: Client.pagination(params)) end |
.rotate(webhook_id) ⇒ Object
Rotate the signing secret; the new plaintext secret is returned once and the old one stops verifying immediately. POST /webhooks/:id/rotate
31 32 33 |
# File 'lib/mailblastr/webhooks.rb', line 31 def rotate(webhook_id) Client.request(:post, "/webhooks/#{Client.path_escape(webhook_id)}/rotate") end |
.test(webhook_id) ⇒ Object
Send a synchronous test delivery and return the endpoint's live result. POST /webhooks/:id/test
A FAILED delivery is still HTTP 200, so this does NOT raise when your endpoint rejects the test. The outcome is the "ok" key:
{ "object" => "webhook_test", "id" => "<id>",
"ok" => true, "status" => 200 } # endpoint accepted it
{ "object" => "webhook_test", "id" => "<id>",
"ok" => false, "error" => "lookup_failed" } # it did not
result = Mailblastr::Webhooks.test(id)
warn "test delivery failed: #{result['error']}" unless result["ok"]
"status" is your endpoint's HTTP status when it responded at all; "error" says why the delivery failed (e.g. "lookup_failed", "webhook missing or disabled"). It is a single attempt — no retries are scheduled.
53 54 55 |
# File 'lib/mailblastr/webhooks.rb', line 53 def test(webhook_id) Client.request(:post, "/webhooks/#{Client.path_escape(webhook_id)}/test") end |
.update(webhook_id, params) ⇒ Object
PATCH /webhooks/:id — params: { endpoint:, events:, status: }
25 26 27 |
# File 'lib/mailblastr/webhooks.rb', line 25 def update(webhook_id, params) Client.request(:patch, "/webhooks/#{Client.path_escape(webhook_id)}", body: params) end |
.verify(payload, headers, secret, tolerance: 300) ⇒ Object
Verify a webhook delivery's Svix-style signature against your endpoint's signing secret. Pure local computation (OpenSSL HMAC-SHA256) — no HTTP.
payload MUST be the exact raw request body string (do not re-serialize
parsed JSON). headers is a Hash carrying svix-id / svix-timestamp /
svix-signature (read case-insensitively; array values use the first
element). The signature header may carry multiple space-separated
v1,<base64> entries — any one match makes the delivery valid.
Returns { valid: true } or { valid: false, reason: "..." }.
result = Mailblastr::Webhooks.verify(request.raw_post, request.headers.to_h, secret)
head :unauthorized unless result[:valid]
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/mailblastr/webhooks.rb', line 75 def verify(payload, headers, secret, tolerance: 300) id = read_header(headers, "svix-id") = read_header(headers, "svix-timestamp") sig_header = read_header(headers, "svix-signature") return { valid: false, reason: "missing_headers" } if id.nil? || .nil? || sig_header.nil? return { valid: false, reason: "missing_secret" } if secret.nil? || secret.to_s.empty? # Optional timestamp freshness check (default 5 minutes; 0 disables). if tolerance && tolerance.positive? ts = begin Integer(, 10) rescue ArgumentError, TypeError nil end return { valid: false, reason: "invalid_timestamp" } if ts.nil? return { valid: false, reason: "timestamp_out_of_tolerance" } if (Time.now.to_i - ts).abs > tolerance end signed = "#{id}.#{}.#{payload}" digest = OpenSSL::HMAC.digest("SHA256", secret_to_key(secret), signed) expected = [digest].pack("m0") # strict base64, no newline sig_header.split(" ").each do |part| part = part.strip next if part.empty? sig = part.start_with?("v1,") ? part[3..] : part return { valid: true } if secure_compare(sig, expected) end { valid: false, reason: "no_match" } end |