Module: AbacatePay::Webhooks

Defined in:
lib/abacate_pay/webhooks.rb,
lib/abacate_pay/webhooks/event.rb

Overview

Verification and parsing of inbound AbacatePay webhooks.

Webhook bodies are the least trusted input an integration handles: they arrive unauthenticated on a public endpoint. Every entry point here treats missing, malformed, and hostile input as an expected case and surfaces it as a typed SDK error, never as a raw parser or NoMethodError.

Defined Under Namespace

Classes: Event, PayloadError, SignatureError

Constant Summary collapse

PUBLIC_KEY =

AbacatePay signs every delivery with this fixed key, published at https://docs.abacatepay.com/pages/webhooks/security and hard-coded in the Node, Python and Go samples there.

It is public and global, so it proves only that the body was not altered in transit, it does NOT prove the request came from AbacatePay, since anyone can compute a valid signature with it. Origin is authenticated by the webhookSecret query parameter; see verify_secret!. Use both.

"t9dXRhHHo3yDEj5pVDYz0frf7q6bMKyMRmxxCPIPp3RCplBfXRxqlC6ZpiWmOqj4L63qEaeUOtrCI8P0VMU" \
"go6iIga2ri9ogaHFs0WIIywSMg0q7RmBfybe1E5XJcfC4IW3alNqym0tXoAKkzvfEjZxV6bE0oG2zJrNNYmU" \
"CKZyV0KZ3JS8Votf9EAWWYdiDkMkpbMdPggfh1EqHlVkMiTady6jOR3hyzGEHrIz2Ret0xHKMbiqkr9HS1Jh" \
"NHDX9"

Class Method Summary collapse

Class Method Details

.construct_event(payload:, signature:, secret: PUBLIC_KEY) ⇒ Event

Verifies a webhook signature and parses the body in one step.

This is the only entry point that cannot be used to act on an unauthenticated payload, and is what integrations should call.

Parameters:

  • payload (String)

    The raw request body

  • signature (String)

    The X-Webhook-Signature header value

  • secret (String) (defaults to: PUBLIC_KEY)

    Your webhook secret/public key

Returns:

  • (Event)

    The verified, parsed event

Raises:



113
114
115
116
# File 'lib/abacate_pay/webhooks.rb', line 113

def self.construct_event(payload:, signature:, secret: PUBLIC_KEY)
  verify!(payload: payload, signature: signature, secret: secret)
  parse(payload)
end

.parse(payload) ⇒ Event

Parses a webhook payload into an Event object.

Prefer construct_event, which refuses to parse a body it has not authenticated first.

Parameters:

  • payload (String)

    The raw JSON request body

Returns:

  • (Event)

    The parsed event

Raises:



93
94
95
96
97
98
99
100
# File 'lib/abacate_pay/webhooks.rb', line 93

def self.parse(payload)
  data = JSON.parse(payload.to_s)
  raise PayloadError, "Expected a JSON object, got #{data.class}" unless data.is_a?(Hash)

  Event.new(data)
rescue JSON::ParserError => e
  raise PayloadError, "Malformed webhook payload: #{e.message}"
end

.valid?(payload:, signature:, secret: PUBLIC_KEY) ⇒ Boolean

Checks if a webhook signature is valid.

Never raises for untrusted input, a missing header, an empty secret, or a forged signature all return false.

Parameters:

  • payload (String)

    The raw request body

  • signature (String)

    The X-Webhook-Signature header value

  • secret (String) (defaults to: PUBLIC_KEY)

    Your webhook secret/public key

Returns:

  • (Boolean)


78
79
80
81
82
83
# File 'lib/abacate_pay/webhooks.rb', line 78

def self.valid?(payload:, signature:, secret: PUBLIC_KEY)
  verify!(payload: payload, signature: signature, secret: secret)
  true
rescue SignatureError
  false
end

.verify!(payload:, signature:, secret: PUBLIC_KEY) ⇒ true

Verifies the X-Webhook-Signature header: HMAC-SHA256 over the raw body, base64-encoded.

Parameters:

  • payload (String)

    The raw request body

  • signature (String)

    The X-Webhook-Signature header value

  • secret (String) (defaults to: PUBLIC_KEY)

    HMAC key. Defaults to PUBLIC_KEY, which is what AbacatePay signs with.

Returns:

  • (true)

    if signature is valid

Raises:



42
43
44
45
46
47
48
49
50
# File 'lib/abacate_pay/webhooks.rb', line 42

def self.verify!(payload:, signature:, secret: PUBLIC_KEY)
  raise SignatureError, "Missing webhook signature" if signature.nil? || signature.to_s.empty?
  raise SignatureError, "Missing webhook secret" if secret.nil? || secret.to_s.empty?

  expected = base64_hmac(secret.to_s, payload.to_s)
  raise SignatureError, "Invalid webhook signature" unless secure_compare(expected, signature.to_s)

  true
end

.verify_secret!(received:, expected:) ⇒ true

Verifies the webhookSecret query parameter, which is what actually authenticates the request as coming from AbacatePay.

The HMAC signature alone cannot do this: it is computed with a public key.

Parameters:

  • received (String)

    The webhookSecret query parameter as received

  • expected (String)

    The secret you configured on the webhook

Returns:

  • (true)

    if they match

Raises:



61
62
63
64
65
66
67
# File 'lib/abacate_pay/webhooks.rb', line 61

def self.verify_secret!(received:, expected:)
  raise SignatureError, "Missing webhook secret parameter" if received.nil? || received.to_s.empty?
  raise SignatureError, "Missing expected webhook secret" if expected.nil? || expected.to_s.empty?
  raise SignatureError, "Invalid webhook secret" unless secure_compare(expected.to_s, received.to_s)

  true
end