
September 07, 2026
13 min read
By Kokil Thapa | Last reviewed: September 2026
When a payment gateway, logistics partner, or government portal pushes data into your application, username-and-password auth is the wrong tool. Laravel Signed API Requests for Third Parties let external systems prove request integrity and authenticity using a shared secret and an HMAC signature—without issuing user tokens or building a full OAuth server. On production Laravel applications I maintain, signed inbound webhooks and partner callbacks fail less often than token-based flows because the partner signs exactly what they send. This guide walks through a pattern that works on Laravel 12 and 13 with PHP 8.3+, from canonical string construction through middleware, replay protection, and partner documentation. For broader API design context, see our Laravel API best practices and building RESTful APIs with Laravel.
What Are Laravel Signed API Requests for Third Parties?
Signed API requests solve a specific integration problem: a third-party server needs to POST order updates, payment confirmations, or document status changes to your Laravel app, and you must verify that the payload was not tampered with and actually came from that partner. Unlike Laravel Passport or Sanctum, which issue bearer tokens tied to users or clients, request signing binds authentication to the exact bytes on the wire.
The pattern is familiar if you have integrated Stripe, Khalti, or PayPal webhooks. The sender computes HMAC-SHA256(canonical_string, secret), sends the digest in a header (commonly X-Signature or X-Hub-Signature-256), and your Laravel middleware recomputes the same value. If the digests differ, you return 401 Unauthorized before any business logic executes—a fail-closed posture that prevents forged callbacks from creating orders or marking invoices paid.
Signed requests differ from Laravel signed URLs, which protect browser-accessible links with query-string signatures. API request signing operates on POST, PUT, and PATCH bodies where the raw payload must be preserved. A common production mistake is letting Laravel parse JSON before verification; any middleware that reads $request->all() first can change whitespace or key order relative to what the partner signed. Always call $request->getContent() for the canonical input.
When signing beats token auth
- Server-to-server webhooks — payment gateways, SMS delivery reports, shipping label updates.
- Batch importers — partners push nightly CSV or JSON files to a single endpoint.
- Legacy integrations — partners cannot implement OAuth but can add two HTTP headers.
- High-trust, low-volume partners — law-firm document portals or notary status feeds where you issue one secret per organisation.
For public mobile or SPA clients, stick with Sanctum or Passport. Signing shines when both ends are servers you control or trust with a shared secret. If you need help scoping an integration, our API development services cover partner onboarding and webhook hardening.
How Do You Implement HMAC Signature Verification in Laravel?
Implementation breaks into four pieces: a partner credentials table, a canonical string builder, verification middleware, and route registration. The following works on Laravel 13 with PHP 8.3 or higher and Redis 8.10 for nonce storage.
Step 1: Store partner secrets
Create an api_partners migration with at least slug, secret (encrypted), is_active, and optional allowed_ips. Never expose secrets in API responses. Rotate by adding a secondary secret_previous column and accepting signatures from either secret for a grace window.
php artisan make:migration create_api_partners_table
php artisan make:model ApiPartner
php artisan make:middleware VerifyPartnerSignature Step 2: Define the canonical string
Document the exact format partners must sign. Consistency matters more than cleverness. A format I use on production integrations:
METHOD + "\n" +
PATH + "\n" +
TIMESTAMP + "\n" +
SHA256(raw_body) Example canonical string for POST /api/v1/webhooks/orders:
POST
/api/v1/webhooks/orders
1725589200
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 Partners send three headers: X-Partner-Id, X-Timestamp (Unix epoch seconds), and X-Signature (hex-encoded HMAC-SHA256). Use the Base64 encoder and decoder during partner testing if they prefer Base64 digests—just pick one encoding and document it.
Step 3: Write verification middleware
Register the middleware in bootstrap/app.php (Laravel 11+) or app/Http/Kernel.php on older apps:
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'partner.signed' => \App\Http\Middleware\VerifyPartnerSignature::class,
]);
}) Core middleware logic:
<?php
namespace App\Http\Middleware;
use App\Models\ApiPartner;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpFoundation\Response;
class VerifyPartnerSignature
{
public function handle(Request $request, Closure $next): Response
{
$partnerId = $request->header('X-Partner-Id');
$timestamp = $request->header('X-Timestamp');
$signature = $request->header('X-Signature');
if (! $partnerId || ! $timestamp || ! $signature) {
return response()->json(['error' => 'Missing signature headers'], 401);
}
if (abs(time() - (int) $timestamp) > 300) {
return response()->json(['error' => 'Request expired'], 401);
}
$partner = ApiPartner::query()
->where('slug', $partnerId)
->where('is_active', true)
->first();
if (! $partner) {
return response()->json(['error' => 'Unknown partner'], 401);
}
$bodyHash = hash('sha256', $request->getContent());
$path = '/'.ltrim($request->path(), '/');
if (! str_starts_with($path, '/api')) {
$path = '/api/'.$request->path();
}
$canonical = implode("\n", [
strtoupper($request->method()),
$path,
$timestamp,
$bodyHash,
]);
$expected = hash_hmac('sha256', $canonical, $partner->secret);
if (! hash_equals($expected, $signature)) {
return response()->json(['error' => 'Invalid signature'], 401);
}
$nonceKey = "partner_nonce:{$partnerId}:{$signature}";
if (! Cache::add($nonceKey, 1, now()->addMinutes(6))) {
return response()->json(['error' => 'Replay detected'], 409);
}
$request->attributes->set('api_partner', $partner);
return $next($request);
}
} Apply it to webhook routes:
// routes/api.php
Route::middleware(['partner.signed', 'throttle:partner-webhooks'])
->prefix('v1/webhooks')
->group(function () {
Route::post('/orders', [OrderWebhookController::class, 'store']);
Route::post('/payments', [PaymentWebhookController::class, 'store']);
}); Pair signing with rate limiting and API throttling so a leaked secret cannot flood your endpoints. On the Nepal Gift Card platform and similar Laravel eCommerce builds, signed payment callbacks sit behind both signature middleware and IP allowlists where the gateway publishes fixed egress ranges.
Step 4: Provide a reference client
Ship a minimal PHP or curl example in your partner docs. PHP signing side:
$method = 'POST';
$path = '/api/v1/webhooks/orders';
$timestamp = (string) time();
$body = json_encode(['order_id' => 'ORD-9921', 'status' => 'paid']);
$bodyHash = hash('sha256', $body);
$canonical = implode("\n", [$method, $path, $timestamp, $bodyHash]);
$signature = hash_hmac('sha256', $canonical, $partnerSecret);
// Send with headers X-Partner-Id, X-Timestamp, X-Signature For deeper outbound patterns—when your Laravel app signs requests to external APIs—see Laravel payment integrations and the Khalti integration guide, which follow vendor-specific canonical formats.
How Do You Protect Signed API Requests from Replay Attacks?
A valid signature alone is not enough. An attacker who intercepts a signed POST can replay it until the secret rotates. You need timestamp skew limits and idempotency or nonce tracking.
- Timestamp window — reject requests older than five minutes. Allow small clock skew (±30 seconds) if partners run unsynchronised VMs.
- Nonce or signature deduplication — store each
X-Signaturevalue in Redis with a TTL slightly longer than your timestamp window. Duplicate signatures return409 Conflict. - Idempotent handlers — even with nonce checks, design controllers to tolerate duplicate delivery. Payment webhooks especially may arrive twice.
- HTTPS only — signing does not encrypt; TLS prevents interception on the wire.
- Secret rotation — document a quarterly rotation process and support dual secrets during migration.
On legal-tech portals such as Mijar Law Associates, document status webhooks use idempotency keys derived from the partner's case reference so duplicate POSTs update nothing rather than creating duplicate audit rows. Queue the handler with ShouldBeUnique or a database unique constraint on (partner_id, external_id) for belt-and-braces protection.
Log verification failures with partner ID and reason, but never log the raw secret or full signature in production. Forward repeated failures to alerting—three invalid signatures from one IP in a minute often means someone is probing with a stolen payload.
Laravel Signed Requests vs OAuth Tokens: Which Should You Use?
Both authenticate API traffic, but they optimise for different shapes of integration. The table below summarises what I recommend when scoping partner work.
| Criteria | HMAC signed requests | OAuth 2 / Sanctum tokens |
|---|---|---|
| Best for | Server-to-server webhooks, fixed partners | User-delegated access, mobile/SPA clients |
| Credential type | Shared secret per partner | Bearer token with expiry and scopes |
| Payload binding | Signs exact body bytes | Token authorises action, body unsigned |
| Revocation | Rotate secret, disable partner row | Revoke token, shorten TTL |
| Partner effort | Low — two or three headers | Higher — token exchange flow |
| Laravel packages | Custom middleware (~80 lines) | Sanctum, Passport built-in |
| Replay risk | Requires timestamp + nonce design | Token expiry reduces window |
Use signed requests when the partner sends data to you and will not maintain token refresh logic. Use Sanctum or Passport when partners call your API on behalf of users who logged in through your application. Hybrid setups are normal: Sanctum for your frontend, HMAC middleware for inbound webhooks. Read how to build a REST API in Laravel the right way for routing and versioning patterns that keep both auth modes cleanly separated under /api/v1/.
How Do You Document and Test Signed Endpoints for Integration Partners?
Partners break integrations when the canonical string rules live only in your head. Treat signing spec as part of the API contract, alongside response schemas and error codes. I document four sections: header names, canonical format with a worked example, sample request in curl, and error responses (401, 409).
Generate OpenAPI descriptions with API documentation using Scribe for Laravel, adding custom security schemes for your HMAC headers. For SDK distribution patterns, see SDK design for your public API. Version webhook paths under /api/v1/ and plan breaking canonical changes for v2 per your Laravel API versioning strategy.
Local testing workflow
- Create a sandbox partner row with a known secret in your local database seeders.
- Run
php artisan serveand POST from a standalone PHP script or Postman pre-request script that computes HMAC. - Assert middleware returns
401when you alter one body byte—proves binding works. - Replay the same request twice; second call should return
409. - Write a Feature test using
$this->call()with raw content and custom headers so CI catches regressions.
Feature test skeleton:
public function test_partner_webhook_requires_valid_signature(): void
{
$partner = ApiPartner::factory()->create(['slug' => 'acme']);
$body = json_encode(['ref' => 'INV-1']);
$timestamp = (string) time();
$path = '/api/v1/webhooks/orders';
$canonical = "POST\n{$path}\n{$timestamp}\n".hash('sha256', $body);
$sig = hash_hmac('sha256', $canonical, $partner->secret);
$response = $this->call('POST', $path, [], [], [], [
'HTTP_X-Partner-Id' => 'acme',
'HTTP_X-Timestamp' => $timestamp,
'HTTP_X-Signature' => $sig,
'CONTENT_TYPE' => 'application/json',
], $body);
$response->assertOk();
} External references worth linking in your partner PDF: the Laravel middleware documentation for registration details, and RFC 2104 on HMAC for the cryptographic baseline. If partners ask about constant-time comparison, point them to PHP's hash_equals()—never use == on signature strings.
For long-running enterprise application development engagements, I deliver a Postman collection with pre-request scripts that auto-sign payloads so partner QA teams can test without writing code first. That cuts integration time from weeks to days on projects like directory platforms and booking systems.
Key Takeaways
- Build the canonical string from method, path, timestamp, and SHA-256 of the raw body—never from parsed JSON.
- Verify signatures in dedicated middleware with
hash_equals()before controllers run; fail closed on any mismatch. - Combine a five-minute timestamp window with Redis nonce caching to block replay attacks.
- Make webhook handlers idempotent with unique constraints on partner-supplied reference IDs.
- Document the signing spec with worked examples; ship a Postman collection or PHP reference client.
- Use HMAC for inbound server webhooks; reserve Sanctum or Passport for user-context outbound API access.
People Also Ask
Does Laravel have built-in API request signing like signed URLs?
Laravel ships ValidateSignature middleware for signed URLs with query parameters, but not for JSON POST bodies from third parties. You implement API request signing with custom middleware using hash_hmac()—typically under 100 lines. The mental model matches Laravel signed URLs, but the canonical input includes the request body hash.
Should the signature cover query parameters too?
If partners send meaningful data in query strings, include the sorted, URL-encoded query string in your canonical format—or reject query parameters entirely for webhook POSTs. Mixed approaches cause signature drift. Most production webhook designs use POST bodies only and ignore query strings except for optional versioning flags excluded from signing.
How do you rotate partner secrets without downtime?
Store secret and secret_previous on the partner model. Middleware tries validation against the current secret first, then the previous one. Give partners two weeks to switch, then null out secret_previous. Log which secret matched so you know who has not migrated.
Is HMAC-SHA256 enough for compliance-sensitive data?
HMAC proves integrity and authenticity between parties who share a secret. It does not encrypt payload contents—TLS does that in transit. For legal or financial document webhooks, require HTTPS, restrict source IPs where possible, and audit every verified request. Signing satisfies most PCI-adjacent webhook requirements when combined with transport encryption.
Ship Partner Integrations You Can Trust
Laravel Signed API Requests for Third Parties give you a boring, testable authentication layer that survives real-world webhook retries, clock skew, and partners who read documentation once. The pattern scales from a single Khalti callback on a Nepali eCommerce site to multi-tenant legal portals receiving document status feeds. Start with a explicit canonical format, middleware that reads raw bytes, and Feature tests that break when someone refactors the signing string.
If you are planning payment gateways, partner data feeds, or outbound API integrations and want the signing layer built correctly from day one, review our web development services or modern Laravel architecture best practices. For ongoing webhook monitoring after launch, support and maintenance covers alerting and secret rotation. Contact us to discuss your integration scope.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

