Go Fiber v3: BodyRaw for Sume webhooks, and copy it before you queue
Sign-check a Sume webhook in Go Fiber with c.BodyRaw() and hmac.Equal, then copy the bytes before handing them to a goroutine, since Fiber reuses them.

In Go Fiber, verify a Sume webhook with c.BodyRaw(), not c.Body(): BodyRaw returns the bytes as sent, while Body() decompresses according to Content-Encoding. Recompute HMAC SHA-256 over <timestamp>.<raw_body>, compare each sume-v1= header entry with hmac.Equal, and send 204 through c.SendStatus. If you queue the event for a goroutine, copy the bytes first.
That last step is the Fiber-specific trap. Handlers return before your worker runs, and the buffer is not yours after that.
What does Fiber say about the body buffer?
The Fiber Ctx documentation says the value Body() returns is valid only within the handler, that you should not store references, and that you should make copies or use the Immutable setting instead. It also notes that the fiber.Ctx instance is pooled and reused after the handler returns.
For a webhook that means go process(c.BodyRaw()) is a data race that surfaces as signature failures or corrupted JSON under load. Copy with append([]byte(nil), raw...) before the handoff, or write the event to durable storage inside the handler, which Sume's docs recommend anyway before you return 2xx.
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strconv"
"strings"
"time"
)
func verify(secret, ts, header string, body []byte) bool {
t, err := strconv.ParseInt(ts, 10, 64)
d := time.Now().Unix() - t
if secret == "" || header == "" || err != nil || d > 300 || d < -300 {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(strconv.FormatInt(t, 10) + "."))
mac.Write(body)
want := []byte("sume-v1=" + hex.EncodeToString(mac.Sum(nil)))
ok := false
for _, e := range strings.Split(header, ",") {
if hmac.Equal([]byte(strings.TrimSpace(e)), want) {
ok = true
}
}
return ok
}How do I register the route?
Fiber v3 handlers take fiber.Ctx by value as an interface. Read headers with c.Get, verify against BodyRaw, and make the copy only after the check passes.
package main
import (
"os"
"github.com/gofiber/fiber/v3"
)
func main() {
app := fiber.New()
app.Post("/hooks/sume", func(c fiber.Ctx) error {
raw := c.BodyRaw()
if !verify(os.Getenv("SUME_COM_WEBHOOK_SIGNING_SECRET"),
c.Get("x-sume-webhook-timestamp"),
c.Get("x-sume-webhook-signature"), raw) {
return c.SendStatus(fiber.StatusUnauthorized)
}
event := append([]byte(nil), raw...) // own the bytes before queueing
go store(event)
return c.SendStatus(fiber.StatusNoContent)
})
app.Listen(":8080")
}
func store(event []byte) { /* insert keyed by job_id or request_id */ }Body or BodyRaw, which one for which job?
Both read the request body, but they answer different questions.
| Accessor | What Fiber documents | Use for Sume |
|---|---|---|
c.Body() | Decompresses by Content-Encoding, bounded by the app BodyLimit; valid only within the handler | Parsing after verification, if a proxy compresses |
c.BodyRaw() | Raw bytes, no decompression | Computing the signature |
c.Get(name) | Returns a request header value | x-sume-webhook-timestamp and x-sume-webhook-signature |
What mistakes show up most often?
Most Fiber failures are not cryptography bugs. They are lifetime and encoding bugs around the body, and they appear only under concurrent load, which is why a local curl test passes and production does not.
Run go test -race against a handler test that fires several signed requests in parallel while a goroutine reads the stored bytes. If the race detector stays quiet and every signature verifies, the copy is doing its job.
- Verify against
BodyRaw(), notBody(). - Copy the bytes before any goroutine, channel or queue touches them.
- Compare with
hmac.Equalfor everysume-v1=entry. - Refuse an empty secret in
verifyitself.
What else does Sume need from the receiver?
Return 2xx inside 10 seconds. Sume retries network errors and non-2xx responses up to 10 times, so a goroutine panic that happens after you answered 204 loses the event silently. Write to a durable queue inside the handler, then process it elsewhere.
Dedupe on job_id for job events and request_id for run events, and keep polling the status_url for anything the ten attempts did not deliver.
Sources
Related posts
More in Developers
- Grok Imagine Video 1.5 Lite on Vercel AI Gateway vs Sume /v1/videos
Vercel added Grok Imagine Video 1.5 Lite to AI Gateway on Oct 8. What generateVideo gives you, and the equivalent text-to-video call on Sume's /v1/videos.
- Grok Imagine Video 1.5 Lite batch API: what xAI documents
xAI's Lite page lists a $0.02 per second price, 10 requests per second, batch support and two regions. Sume jobs are async instead; here is how they differ.
- Image-to-video with sound by API: Sume ids that take a first frame
Kandinsky 6.0 has an image-to-audio-video mode. On Sume these ids take a first_frame image and return sound, and these do not. Request code included.
- Inngest Bursty Concurrency 3x: will Sume answer queue_full?
Inngest can lift step concurrency 3x for 60 minutes a month. Sume's limit does not move: size bursts to accepted capacity and retry queue_full with one key.
Written by Sume