module

Manza::Webhook

Verifies incoming webhook deliveries. Pure functions — no HTTP.

Every delivery carries X-Manza-Signature (hex HMAC-SHA256 of "<timestamp>.<raw body>" under the endpoint's signing secret) and X-Manza-Timestamp (Unix seconds, fresh on every attempt). The legacy X-Zazu-* names carry the same values.

Pass the raw request body, never re-serialized JSON:

event = Manza::Webhook.construct_event(
  context.request.body.try(&.gets_to_end) || "", context.request.headers, signing_secret
)
event["event"] # => "payment.authorization_requested"

Constants

DEFAULT_TOLERANCE = 300.seconds

Class methods

construct_event(payload : String, headers : HTTP::Headers, secret : String, tolerance : Time::Span | Nil = DEFAULT_TOLERANCE, now : Time = Time.utc) : JSON::Any

Reads X-Manza-Signature / X-Manza-Timestamp (any case, falling back to X-Zazu-*), verifies them, and returns the parsed event, always a JSON object. Raises Manza::ArgumentError when a header is missing, the signature does not match, the timestamp is outside tolerance, or the body is not a JSON object.

Source
sign(payload : String, timestamp : String | Int, secret : String) : String

Hex HMAC-SHA256 of "<timestamp>.<payload>". For tests and local tools.

Source
verify?(payload : String | Nil, signature : String | Nil, timestamp : String | Int | Nil, secret : String | Nil, tolerance : Time::Span | Nil = DEFAULT_TOLERANCE, now : Time = Time.utc) : Bool

True when the signature matches (constant time) and the timestamp is within tolerance of now. tolerance: nil skips the timestamp check (for replaying a captured delivery in a local tool or test, never on a live endpoint). Never raises on bad input; returns false.

Source