Docs
Outbound WebhooksSigning & Verification

Signing & Verification

Authenticate incoming webhook requests using HMAC-SHA256 signatures.

Moviie signs every delivery so you can verify that the request came from the platform and that the body has not been tampered with in transit.

Request headers

Every dispatch includes the following HTTP headers:

HeaderMeaning
Content-TypeAlways application/json.
User-AgentMoviie-Webhooks/1.0.
X-Moviie-EventCanonical event identifier (for example video.upload.started).
X-Moviie-Event-IdStable envelope id (evt_ + UUID). Same value as data.id.
X-Moviie-Delivery-IdPersisted delivery row id in Moviie's database.
X-Moviie-AttemptRetry counter: 1 on the first try, then increments with each automatic retry.
X-Moviie-SignatureHMAC-SHA256 over the raw body: present only when a signing secret is configured.

Depending on the authentication mode you configure for the endpoint, Moviie also sends Authorization: Bearer <token> or appends authorization=<token> to the query string. No other query parameters are added by the dispatcher.

Verifying the signature

When your endpoint has a signing secret configured, each delivery includes X-Moviie-Signature: sha256=<hex> computed over the raw request body. If no secret is set, the header is absent: in that case, rely on your URL allowlist and transport security until you configure signing.

Verify the signature before parsing the JSON body. Never rely on the data fields alone to authenticate a request.

Node.js

import { createHmac, timingSafeEqual } from "crypto"

function verifyWebhookSignature(
  rawBody: string,
  signatureHeader: string,
  secret: string
): boolean {
  const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex")
  const sigBuf = Buffer.from(signatureHeader)
  const expBuf = Buffer.from(expected)
  if (sigBuf.length !== expBuf.length) return false
  return timingSafeEqual(sigBuf, expBuf)
}

// In your handler: load the secret from env or a secrets manager:
const signingSecret = process.env.MOVIIE_WEBHOOK_SECRET!
const rawBody = await request.text()
const sig = request.headers.get("x-moviie-signature") ?? ""
if (!verifyWebhookSignature(rawBody, sig, signingSecret)) {
  return new Response("Unauthorized", { status: 401 })
}
const payload = JSON.parse(rawBody)

Python

import hashlib
import hmac

def verify_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
    mac = hmac.new(secret.encode(), raw_body, hashlib.sha256)
    expected = "sha256=" + mac.hexdigest()
    return hmac.compare_digest(signature_header, expected)

Go

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
)

func verifySignature(body []byte, signatureHeader, secret string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(body)
	expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(signatureHeader), []byte(expected))
}

Managing your signing secret

Your signing secret is available in Settings → Webhooks → [Endpoint] → Signing secret. Use Regenerate to rotate it: all deliveries dispatched after regeneration use the new secret immediately.

On this page