How to Store and Serve Private Files in Laravel
When a Laravel site handles invoices, identity documents, or other files that only specific users should see, store each file outside the…
When a provider sends an HTTP webhook to your service, verify its signature before you trust the payload or trigger any action. The provider creates a webhook signature from the message using a secret shared with your service. Your service uses the same secret to calculate an expected signature and compares it with the one the provider sent. This check helps confirm that the message matches what the provider signed and that the sender had access to the shared secret.
Most providers use HMAC, or hash-based message authentication code, for this calculation. The exact header, secret format, hashing algorithm, and message contents vary by provider, so follow that provider’s verification instructions rather than applying one universal formula. Read the request body as raw bytes, verify the signature, and only then parse and process the event.
Check the provider’s documentation for the signature header and secret. Also check which algorithm and encoding it uses, and exactly what data it signs. Some providers sign only the body. Others include a timestamp or another value. For example, GitHub signs the body with HMAC-SHA256, while TabaPay signs the timestamp, a period, and the raw body with HMAC-SHA256.
Keep the original request body as bytes until verification succeeds. Parsing JSON and serializing it again can change whitespace or key order, so the bytes no longer match those the provider signed and the valid signature fails. TabaPay’s verification instructions explicitly require signing the raw body before parsing it.
Use the secret in the format the provider specifies. TabaPay instructs receivers to use its issued secret string as the HMAC key without Base64-decoding it. Other providers may specify a different format. Use the provider’s instructions rather than assuming that every encoded-looking secret needs decoding.
Read the raw body first. Then extract and validate the required headers, check any required timestamp, calculate the expected signature, and compare it with the supplied signature. Parse the body only after the check passes. If a required header is missing or malformed, reject the request instead of guessing how to interpret it.
Use a constant-time comparison function from a trusted cryptographic library. The function takes roughly the same amount of time whether a few or many leading characters match, making it harder for an attacker to learn about the signature by repeatedly measuring response times. Provider documentation, including CELITECH’s verification guidance, recommends this approach.
Pass both signature values to the comparison function in the same form. A provider might send the digest as hexadecimal or Base64, or include a prefix such as sha256=.... Follow the provider’s format when handling prefixes: remove or parse them, decode the value if required, then compare the resulting bytes. Do not compare values in one encoding with values in another.
Avoid logging the signing secret or full signature material. If verification fails, log enough information to investigate, such as the provider, endpoint, failure category, and event identifier when available. Keep secrets out of source control and client-side code; rotate a secret if it has been exposed.
A valid signature confirms that the request matches a message signed with the secret. It does not by itself prove that the message is new. An attacker who captures a valid request could send it again. Providers that include a signed timestamp let your service reject messages outside an allowed time window. The window must match the provider’s instructions: for example, Calendly’s guidance describes a roughly 180-second limit, while Talentir specifies a five-minute tolerance.
Some providers leave freshness checks to the receiver. TabaPay, for instance, says the receiver must choose and enforce a timestamp window. Check the timestamp after parsing the signed header and before accepting the request. Keep the server clock accurate, and follow the provider’s rules for future timestamps as well as old ones.
Timestamp checks do not replace duplicate detection. Store a provider’s event ID or idempotency key and use it to prevent the same event from triggering an action twice. Retries can be signed with a new timestamp, so a repeated event may pass the freshness check while still describing an event your service already handled. TabaPay’s documentation describes fresh signatures on retries and recommends deduplication with an idempotency key.
Providers do not share a universal signature format. For example, Shopify uses a Base64-encoded HMAC-SHA256 signature over the body, while Twilio’s signature calculation includes the full URL. Stripe and Calendly include a timestamp with the body in their signature calculations. Using one provider’s verifier for another can silently reject valid requests or accept a request under the wrong assumptions.
Give each provider a separate endpoint and configuration, with its own secret and verification code path. Use an official provider library when one is available; Stripe’s official libraries, for example, provide helpers that verify webhook signatures. For a shared format such as Standard Webhooks, use a compatible library and confirm that its behavior matches the provider’s instructions.
Secret rotation also depends on the provider. Some providers temporarily accept signatures made with both the new and previous secrets. During that overlap, accept a request if it verifies under either currently valid secret, then remove the old secret when the overlap ends. Follow the provider’s documented rotation period rather than inventing one.
After verification, validate that the payload has the fields and types your code expects. Signature verification confirms the signed content; it does not guarantee that the event is relevant to your service or safe for every downstream operation. Apply the same access controls and business rules you use for other input.
If processing will take time, store the verified event and acknowledge receipt promptly. Then process the event asynchronously. This keeps webhook requests from timing out while your service performs slower tasks. If signature verification fails, follow the provider’s response and retry rules. A 4xx response tells some providers to treat the delivery as a permanent rejection, while a 5xx may trigger a retry; TabaPay documents this distinction for its webhooks. Do not assume every provider handles failed deliveries the same way.
Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.
Take a look at vroni.com