PHPackages                             slash-dw/idempotency-kit - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [HTTP &amp; Networking](/categories/http)
4. /
5. slash-dw/idempotency-kit

ActiveLibrary[HTTP &amp; Networking](/categories/http)

slash-dw/idempotency-kit
========================

HTTP-layer idempotency middleware for Laravel with atomic locking, path validation, payload fingerprinting, and alert system

v0.0.2(1mo ago)028↓16.7%1MITPHPPHP ^8.5CI passing

Since May 4Pushed 1mo agoCompare

[ Source](https://github.com/slash-dw/idempotency-kit)[ Packagist](https://packagist.org/packages/slash-dw/idempotency-kit)[ RSS](/packages/slash-dw-idempotency-kit/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (8)Versions (3)Used By (1)

slash-dw/idempotency-kit
========================

[](#slash-dwidempotency-kit)

HTTP-layer idempotency middleware for Laravel. Prevents duplicate processing of mutating requests by caching responses keyed to client-provided idempotency keys, with atomic locking, path validation, payload fingerprinting, and an alert system.

Requirements
------------

[](#requirements)

- PHP **^8.5**
- Laravel **^13.0**
- Cache store with atomic lock support (Redis recommended for production; Memcached, database and file drivers are also supported)

Installation
------------

[](#installation)

```
composer require slash-dw/idempotency-kit
```

Publish the configuration file:

```
php artisan vendor:publish --tag=idempotency-kit-config
```

The middleware is auto-registered with the alias `idempotent`.

How It Works
------------

[](#how-it-works)

1. The client sends a mutating request (POST, PUT, PATCH) with an `Idempotency-Key` header containing a UUID v4.
2. If the same key + path + payload was already processed, the cached response is returned with `Idempotency-Replayed: true`.
3. If the same key arrives while the first request is still processing, the second request receives `HTTP 409 Conflict` with a `Retry-After: 1` header. The client waits one second and retries; it then receives the cached response.
4. If the same key is sent with a different payload or to a different route, the response is `HTTP 422 Unprocessable Entity`. This protects against fraud and accidental misuse.

Basic Usage
-----------

[](#basic-usage)

### Route Middleware

[](#route-middleware)

```
// routes/api.php
use SlashDw\IdempotencyKit\Http\Middleware\IdempotentMiddleware;

Route::post('/subscriptions', SubscriptionController::class)
    ->middleware('idempotent');

// With custom TTL (7 days for financial operations):
Route::post('/payments', PaymentController::class)
    ->middleware('idempotent:604800');
```

### Fluent Configuration Helper

[](#fluent-configuration-helper)

```
use SlashDw\IdempotencyKit\Http\Middleware\IdempotentMiddleware;

Route::post('/orders', OrderController::class)
    ->middleware(IdempotentMiddleware::using(ttl: 3600, scope: 'ip'));
```

### Per-Route Override: `required` and `enabled`

[](#per-route-override-required-and-enabled)

The middleware accepts two boolean overrides that complement the global config and let individual routes deviate without changing application-wide defaults.

**`required: true`** — force the `Idempotency-Key` header to be mandatory on this route even when the global `required` config is `false`. Use this for critical mutating endpoints where the client absolutely must send a key.

```
// Reject /register requests that omit the Idempotency-Key header (HTTP 400)
Route::post('/register', RegisteredUserController::class)
    ->middleware(IdempotentMiddleware::using(required: true));
```

**`enabled: false`** — opt this route out of idempotency entirely even when the global `enabled` config is `true`. Use this for naturally idempotent endpoints (login, logout, heartbeat) where the protection adds no value.

```
// Login is naturally idempotent — skip the middleware for this route
Route::post('/login', AuthenticatedSessionController::class)
    ->middleware(IdempotentMiddleware::using(enabled: false));
```

**Combine overrides** when needed:

```
// 7-day TTL + mandatory header for payment operations
Route::post('/payments', PaymentController::class)
    ->middleware(IdempotentMiddleware::using(ttl: 604800, required: true));
```

The same `required` and `enabled` parameters are also available on the `#[Idempotent]` PHP attribute as metadata.

### Global Middleware (All Enforced Methods)

[](#global-middleware-all-enforced-methods)

```
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->api(append: [
        \SlashDw\IdempotencyKit\Http\Middleware\IdempotentMiddleware::class,
    ]);
})
```

### PHP Attribute Metadata

[](#php-attribute-metadata)

The `#[Idempotent]` attribute documents intent and per-handler overrides for tooling and documentation generators. Middleware itself still has to be wired up via routes.

```
use SlashDw\IdempotencyKit\Http\Attributes\Idempotent;

#[Idempotent(ttl: 86400)]
class PaymentController extends Controller
{
    public function store(Request $request): JsonResponse { /* ... */ }
}
```

Client Usage
------------

[](#client-usage)

Generate a UUID v4 key per logical operation. The same key must be reused on every retry.

```
curl -X POST https://api.example.com/subscriptions \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{"plan_id": "pro-monthly"}'
```

### Response Scenarios

[](#response-scenarios)

ScenarioStatusHeaderFirst request`200`/`201``Idempotency-Replayed: false`Retry, same key + body + pathoriginal status`Idempotency-Replayed: true`Concurrent in-flight duplicate`409 Conflict``Retry-After: 1`Same key, different body`422 Unprocessable Entity`—Same key, different route`422 Unprocessable Entity`—Missing key on enforced method`400 Bad Request`—Invalid key format`400 Bad Request`—Scope Configuration
-------------------

[](#scope-configuration)

Scoping isolates keys between callers to prevent cross-user replay.

```
// config/idempotency_kit.php

'scope' => 'user',   // authenticated user ID (fallback: IP)
'scope' => 'ip',     // requester's IP address
'scope' => 'global', // no isolation (use only when truly user-agnostic)

// Custom resolver (multi-tenant: scope by company):
'resolver' => App\Idempotency\CompanyScopeResolver::class,
```

### Custom Scope Resolver

[](#custom-scope-resolver)

```
use SlashDw\IdempotencyKit\Contracts\ScopeResolverContract;
use Illuminate\Http\Request;

final class CompanyScopeResolver implements ScopeResolverContract
{
    public function resolve(Request $request): string
    {
        return (string) (auth()->user()?->company_id ?? $request->ip());
    }
}
```

Alert System
------------

[](#alert-system)

When the same idempotency key is replayed `threshold` times (default 5), the package dispatches an `IdempotencyKeyAbused` event. Subscribe a listener to react.

```
// app/Providers/EventServiceProvider.php
use SlashDw\IdempotencyKit\Events\IdempotencyKeyAbused;
use App\Listeners\NotifyIdempotencyAbuse;

protected $listen = [
    IdempotencyKeyAbused::class => [NotifyIdempotencyAbuse::class],
];
```

```
// app/Listeners/NotifyIdempotencyAbuse.php
use SlashDw\IdempotencyKit\Events\IdempotencyKeyAbused;

final class NotifyIdempotencyAbuse
{
    public function handle(IdempotencyKeyAbused $event): void
    {
        logger()->warning('Idempotency key replayed excessively', [
            'key'   => $event->idempotencyKey,
            'scope' => $event->scope,
            'hits'  => $event->hitCount,
            'route' => $event->route,
        ]);
    }
}
```

The event re-fires every time the hit count is a positive multiple of the threshold (5, 10, 15…).

Transient Errors
----------------

[](#transient-errors)

Responses with these HTTP status codes are **not cached**, so clients can safely retry once the transient condition resolves:

- `408` Request Timeout
- `429` Too Many Requests
- `503` Service Unavailable
- `504` Gateway Timeout

All other responses (2xx success, permanent 4xx errors) are cached. Configure the list in `config/idempotency_kit.php → transient_error_codes`.

Security Notes
--------------

[](#security-notes)

- **Use UUID v4 keys.** Sequential or timestamp-based keys are predictable and vulnerable to enumeration. UUID v4 provides 122 bits of entropy. The package validates the format by default.
- **Scope is mandatory.** Keys are always namespaced (user / IP / account) so an attacker who learns one user's key cannot replay another user's operation.
- **Payload fingerprinting (SHA-256).** The request body is hashed and verified on retry. Reusing the same key with different parameters (e.g., a different payment amount) is rejected with HTTP 422.
- **Path validation.** A key generated for `/payments` cannot be replayed against `/refunds`.
- **Constant-time comparison.** Internal key checks use `hash_equals()` to prevent timing-based information leakage.
- **Transient error caching is disabled.** Failed-server responses do not poison the cache.

Configuration Reference
-----------------------

[](#configuration-reference)

See `config/idempotency_kit.php` for complete inline documentation.

KeyDefaultPurpose`enabled``true`Global on/off switch`header``Idempotency-Key`Request header (IETF RFC)`methods``[POST, PUT, PATCH, DELETE]`Enforced HTTP methods`cache_store``null`Laravel cache driver name`ttl``86400` (24 h)Response cache duration in seconds`lock_timeout``10`Atomic lock max hold (seconds)`scope``user`Built-in scope strategy`resolver``null`Custom `ScopeResolverContract` class`duplicate_behaviour``replay``replay` or `exception` on duplicate`required``true`Reject requests without the header`key_validation.enabled``true`Validate key format`key_validation.pattern`UUID v4 regexPCRE pattern for keys`key_validation.max_length``255`Maximum key length`transient_error_codes``[408, 429, 503, 504]`Status codes not cached`alert.enabled``true`Dispatch event on abuse`alert.threshold``5`Replay count before event firesDevelopment
-----------

[](#development)

```
composer install
composer run-script test      # PHPUnit
composer run-script lint      # Pint --test
composer run-script format    # Pint apply
composer run-script analyse   # PHPStan level 8
composer run-script ci        # All checks together
```

License
-------

[](#license)

MIT

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance93

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community5

Small or concentrated contributor base

Maturity42

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

36d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/0541792c880c46a9d5eee49504ad45c4191d4a1c89c111a959d71268c83b61f8?d=identicon)[slash-dw](/maintainers/slash-dw)

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/slash-dw-idempotency-kit/health.svg)

```
[![Health](https://phpackages.com/badges/slash-dw-idempotency-kit/health.svg)](https://phpackages.com/packages/slash-dw-idempotency-kit)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3325.1M337](/packages/psalm-plugin-laravel)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

76318.2M110](/packages/laravel-mcp)[illuminate/auth

The Illuminate Auth package.

9327.9M1.2k](/packages/illuminate-auth)[api-platform/laravel

API Platform support for Laravel

59156.3k10](/packages/api-platform-laravel)[illuminate/routing

The Illuminate Routing package.

1239.0M2.8k](/packages/illuminate-routing)[spatie/laravel-export

Create a static site bundle from a Laravel app

670139.5k6](/packages/spatie-laravel-export)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
