They are not wrong. But here is why I still do it.

My last client review ended with someone asking me, half-joking, whether I also make my coffee using a tamper-evident seal. Everyone laughed. I smiled. Then I pointed to the incident report where a silent payload mutation burned three hours of debugging on a staging server, and the room got quiet.
Checksums are not glamorous. They do not show up in API design tutorials. They are never the feature in a launch post. But in three years of building production APIs, they have caught things that should not have been catchable and they caught them before those things became customer-facing incidents.
This is how I implement them, why they matter and what a third-party penetration test confirmed about what they actually prevent.
What a Checksum Actually Does in This Context
A checksum here is a keyed hash of the request payload that the sender computes and the receiver verifies. The sender and receiver share a secret key. The sender hashes the raw request body with that key and sends the hash in a request header. The receiver computes the same hash independently and compares. If they do not match, the request gets rejected.
This is not novel. AWS Signature Version 4 does something similar. Stripe webhook verification works this way. The concept is well-established. What surprises me is how few in-house APIs bother with it.
There are three distinct problems this solves.
Problem 1: Payload Tampering
If your API sits behind a reverse proxy, a load balancer or any middleware you do not fully control, someone or something can modify your payload in transit. This sounds paranoid until you work with enterprise clients who route traffic through security appliances that “inspect and forward” requests. Those appliances do not always forward what they received.
More practically: any man-in-the-middle with access to the network layer can modify a request body. TLS protects data in transit between endpoints, but it does not protect you from tampering at those endpoints or from bugs in the infrastructure between your service and the caller.
A checksum makes tampering detectable. The moment a byte changes, the hash changes. Validation fails. You know.
Problem 2: Silent Corruption
This one is less dramatic but more common. Serialization bugs, encoding mismatches, broken middleware and even some message queue implementations can silently mangle a payload. JSON that was valid when it left the client arrives subtly wrong at your server.
PHP is not immune to this. Encoding a float in one locale and decoding it in another can shift values. A queue driver that double-encodes a string will produce different bytes than the sender computed. Without a checksum, your app processes the corrupted data, probably without errors, and produces wrong output that you discover weeks later.
TCP has its own error-detection at the transport layer, but that operates below your application. It catches network-level bit corruption. It does not catch application-layer mutations from proxies, queue processing or your own pipeline transformations.
Problem 3: Replay Attacks (Partially)
A checksum alone does not stop replay attacks. I want to be clear about that. If someone intercepts a valid signed request and sends it again, the signature is still valid. You need a timestamp and a nonce for full replay protection.
But a checksum is the foundation. It proves the payload was authored by someone who holds the shared secret. When you pair it with a short request expiry window and additional binding layers, the attack surface shrinks to something genuinely difficult to exploit. More on the layering shortly.
The Sample Implementation (Read This Note First)
Before I show the code, I want to be direct about something. What follows is a simplified illustration to help you understand the concept and the moving parts. It is intentionally stripped down.
The checksum system I actually run in production is more hardened than this. The construction of the checksum string itself, the secret derivation, the binding inputs and the validation logic are all considerably more involved than what you will see below. I am not sharing the full implementation, partly for obvious reasons and partly because the right implementation depends heavily on your threat model and architecture. What I can show you is the skeleton, and the skeleton is real enough to be useful.
With that said:
Generating the checksum on the client side
// SIMPLIFIED SAMPLE — not a production implementation
$payload = json_encode($data);
$secret = config('services.internal_api.secret');
$checksum = hash_hmac('sha256', $payload, $secret);Send this alongside your request:
X-Checksum: {$checksum}
X-Timestamp: {time()}
X-Client-ID: {$clientId}Verifying in a Laravel middleware
<?php
// SIMPLIFIED SAMPLE - illustrative only
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class VerifyPayloadChecksum
{
public function handle(Request $request, Closure $next): mixed
{
$receivedChecksum = $request->header('X-Checksum');
$timestamp = (int) $request->header('X-Timestamp');
$secret = config('services.internal_api.secret');
if (! $receivedChecksum || ! $timestamp) {
return response()->json(['error' => 'Missing integrity headers'], 400);
}
// Reject stale requests
if (abs(time() - $timestamp) > 300) {
return response()->json(['error' => 'Request expired'], 400);
}
$rawBody = $request->getContent();
$expectedChecksum = hash_hmac('sha256', $rawBody, $secret);
if (! hash_equals($expectedChecksum, $receivedChecksum)) {
return response()->json(['error' => 'Checksum mismatch'], 400);
}
return $next($request);
}
}Two things in this sample that are not negotiable regardless of how you extend it:
hash_equals() is not optional. PHP's === is vulnerable to timing attacks on string comparison. hash_equals() runs in constant time regardless of where the strings differ, which prevents an attacker from measuring response times to reverse-engineer the expected hash one byte at a time.
$request->getContent() retrieves the raw body before Laravel has parsed it. You want to hash exactly what was transmitted, not a re-serialized version. If you hash the parsed array and re-encode it, you will produce mismatches from key ordering and float precision differences.
The Multiple Guards: Why One Layer Is Not Enough
The production version does not rely on the checksum alone. The checksum is the anchor. Around it, we stack additional guards that make the whole system much harder to break even if one layer is somehow bypassed.
IP binding. Each registered client has an expected source IP or CIDR range on record. The checksum validates first, but we also confirm that the request origin matches what we have registered for that client. A valid checksum from an unexpected IP is still rejected. This handles scenarios where a secret leaks to someone operating from a different network context entirely.
Nonce tracking. Every request carries a unique nonce. We record seen nonces in Redis with a TTL matching the request expiry window. A nonce we have already seen is rejected immediately, even if the checksum and timestamp are both valid. This closes the replay window that the timestamp alone leaves open.
Request fingerprinting. The actual string we hash in production is not just the raw body. The input to the hash function includes a combination of values from the request context that the legitimate client would know and that an attacker reconstructing a request from captured traffic would not easily reproduce. This means even someone who intercepts a valid request and understands the header structure cannot trivially forge a valid checksum for a modified payload.
Algorithm versioning. We do not lock in a single hashing algorithm permanently. The system supports versioned algorithm identifiers so we can migrate clients to a stronger construction without a hard cutover. The algorithm version travels with the request, and the validator handles the negotiation.
Each guard is independent. If one fails, the request is rejected. There is no “almost valid.”
What the Pentest Found
We had a third-party security firm run a penetration test against the API. This was not a checkbox exercise. They ran a targeted engagement with access to intercepted traffic samples from a test environment, attempting to forge requests using observed patterns.
Their finding on the checksum specifically: they could not reconstruct a valid checksum for a modified payload within the engagement window. They captured requests, understood the header structure and attempted signature forgery even with the timestamp and IP guards temporarily disabled in the test environment. The checksum builder held. The way the input string is constructed before hashing made it impractical to reverse from observed traffic, because the construction depends on context that is not exposed in the headers or the body.
The report noted that the API was among the harder targets they had engaged with at that scale, specifically because a compromised or replayed request still has to pass checksum validation before anything else happens. Injection attempts, parameter tampering and payload fuzzing all fail at the checksum layer before they reach any application logic.
That last point is the one worth sitting with.
The Real Incident
We were running a service-to-service integration between a Laravel API and a third-party logistics platform. The logistics provider had a middleware layer that would “normalize” incoming JSON before forwarding it to our endpoint. We did not know this was happening.
One of the fields they normalized was a numeric string representing a package weight: "1.50". Their system was stripping trailing zeros. It was arriving as "1.5". In most contexts, harmless. In our system, that field drove a lookup against a rate table keyed on exact string values. "1.50" matched. "1.5" did not. Requests were falling through to a default rate silently. No errors. No log noise. Just wrong pricing calculations quietly accumulating.
We did not have checksum verification on that integration at the time. We added it after. When we turned it on against a replay of the affected traffic, every single one of those requests failed checksum validation immediately, because the bytes the logistics platform forwarded did not match the bytes our internal service had signed.
The fix took 20 minutes. The discovery without checksums took three days of log archaeology.
What About HTTPS?
Yes, TLS encrypts the channel. No, that is not the same thing.
TLS protects the payload from being read or modified while it travels between two endpoints. It does not protect you from modifications made at an endpoint before forwarding, bugs in your proxy configuration, vulnerable middleware or internal networks where TLS is terminated at the load balancer and traffic travels unencrypted between your own services.
Checksums protect payload integrity from the moment the originator signs it to the moment the consumer verifies it. TLS and payload signing are complementary, not redundant.
What This Lets You Stop Worrying About
This is where the value proposition gets genuinely interesting from a security prioritization standpoint.
When every incoming request is checksum-verified before it reaches your application logic, a large category of API attack vectors becomes dramatically less concerning. SQL injection through a request body requires the body to arrive intact and undetected. Parameter tampering requires the parameters to pass integrity validation. Fuzzing and malformed input attacks require the payload to survive the checksum check first.
They do not. The checksum layer rejects them before your controllers, validators or query builders ever see the request.
This does not mean you stop writing parameterized queries or validating input. You still do all of that. Defense in depth is still the right model. But the threat surface you are actively managing at the API boundary shifts. I spend less time worrying about what a malicious actor can inject through API endpoints and more time on what the checksum does not cover: authentication flows, authorization logic, secret management and egress behavior.
That mental shift is worth something real. Security attention is finite. Knowing which layer is holding the line lets you direct the remaining attention where it actually matters right now.
When You Probably Do Not Need This
Public read APIs where payload integrity is not critical. Webhook endpoints where the provider already signs payloads (Stripe and GitHub handle this themselves). Simple internal tooling where the threat model genuinely does not justify the engineering overhead.
For internal service-to-service APIs, payment-adjacent flows, anything touching customer data or any integration where you do not fully control both ends of the pipe: do it. Then layer the guards.
The Paranoia Is the Point
My clients call it paranoia. I call it assuming the network is unreliable and the infrastructure is imperfect. Both things are true more often than people admit.
The pentest came back with a clean finding on the checksum layer. The penetration testers could not break it within the engagement window. That is not a guarantee of anything, but it is a result. I will take that over discovering a three-day debugging session in the incident log.
The paranoia earned its keep.


