Verify a Sume Avatar Video Webhook in Ruby (HMAC-SHA256)

A Ruby verifier for Sume job webhooks: sign timestamp.raw_body with HMAC-SHA256, accept a rotation header, refuse an empty secret, and dedupe on job_id.

4 min readSume
All posts

A finished Avatar Video job can call your server. To trust the call, recompute the HMAC-SHA256 of the timestamp, a dot and the raw request body, then compare it with the x-sume-webhook-signature header. The Ruby below does that, refuses an empty secret and accepts a header carrying several signatures during secret rotation.

The scheme is from Sume's webhooks docs, read on 2026-10-05: the headers are x-sume-webhook-timestamp and x-sume-webhook-signature, formatted sume-v1=<hex>, with a 5-minute tolerance.

What does the verifier do?

Pass the exact raw bytes of the body, not a re-serialized hash, because the signature covers them. The function returns false for an empty secret, a malformed or stale timestamp, a tampered body or a non-matching signature. The header can hold comma-separated entries, and any matching entry is accepted. The comparison is constant-time.

require "openssl"

def same_bytes?(a, b)
  return false unless a.bytesize == b.bytesize
  a.bytes.zip(b.bytes).reduce(0) { |acc, (x, y)| acc | (x ^ y) }.zero?
end

def verify_sume_webhook(raw_body, timestamp, header, secret, tolerance: 300)
  return false if secret.to_s.empty?
  ts = Integer(timestamp, 10)
  return false if (Time.now.to_i - ts).abs > tolerance
  digest = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{ts}.#{raw_body}")
  expected = "sume-v1=#{digest}"
  matched = false
  header.to_s.split(",").each do |entry|
    candidate = entry.strip
    matched = true if candidate.start_with?("sume-v1=") && same_bytes?(candidate, expected)
  end
  matched
rescue ArgumentError, TypeError
  false
end

What else should the handler do?

Respond with a 2xx quickly. Sume's docs describe up to 10 attempts at 30-second spacing with a 10-second timeout, so a slow handler is retried. Use job_id as the idempotency key, so a repeated delivery does not render or charge twice on your side.

Then fetch the result from the job with the endpoints in Jobs and results instead of trusting fields in the webhook alone. Webhooks are an alternative to polling job status.

How do you check it?

Sign a sample body with a test secret, check it returns true, then flip one byte of the body and check it returns false. Also try an empty secret, which should always be false.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume