New

Just launched: Vueblocks, a brand-new search engine for Vue packages! Explore Vueblocks →

grazulex/laravel-api-idempotency

RFC-compliant idempotency support for Laravel APIs - Prevent duplicate operations, ensure safe retries
1,007 12
Install
composer require grazulex/laravel-api-idempotency
Latest Version:v1.2.3
PHP:^8.4
Laravel:12 · 13
Older releases:11
License:MIT
Last Updated:Nov 3, 2026
Links: GitHub  ·  Packagist
Maintainer: Grazulex

Laravel API Idempotency

Complete API idempotency lifecycle management for Laravel - Prevent duplicate operations, ensure safe retries

[!TIP] What Laravel API Idempotency does for you — Stop duplicate charges, double orders and repeated side effects when a client retries a request. Add one middleware and your API safely returns the same response for the same Idempotency-Key — no more defensive code in every controller.

This package is free and maintained on my own time. If it saves you hours, a small contribution helps me keep it going: 💖 GitHub Sponsors · ☕ Buy Me a Coffee · PayPal

Latest Version on Packagist Tests Total Downloads License


Features

  • RFC Draft Compliant - Follows IETF Idempotency-Key Header Draft
  • Multiple Storage Drivers - Cache, Redis, Database, or DynamoDB
  • Payload Fingerprinting - SHA256 verification prevents key reuse with different data
  • Conflict Detection - Handles concurrent requests with wait/reject strategies
  • Scoping - Scope keys to user, tenant, IP, or custom resolver
  • Zero Configuration - Works out of the box with sensible defaults
  • Artisan Commands - Stats, cleanup, and management tools
  • Testing Helpers - Fluent testing API with IdempotencyFake

Requirements

  • PHP 8.4+
  • Laravel 12.x or 13.x

Installation

composer require grazulex/laravel-api-idempotency

Publish the configuration:

php artisan vendor:publish --tag="api-idempotency-config"

For database driver, publish migrations:

php artisan vendor:publish --tag="api-idempotency-migrations"
php artisan migrate

Quick Start

Apply the middleware to routes:

// routes/api.php
Route::post('/payments', [PaymentController::class, 'store'])
    ->middleware('idempotent');

Route::post('/orders', [OrderController::class, 'store'])
    ->middleware('idempotent:required'); // Key required

Client request with idempotency key:

curl -X POST https://api.example.com/payments \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pay_abc123_unique_key" \
  -d '{"amount": 9999, "currency": "EUR"}'

First request: Executes normally, response cached Same key again: Returns cached response, no re-execution

Response Headers

HTTP/1.1 201 Created
Idempotency-Key: pay_abc123_unique_key
X-Idempotent-Replayed: false

On replay:

HTTP/1.1 201 Created
Idempotency-Key: pay_abc123_unique_key
X-Idempotent-Replayed: true
X-Original-Request-Time: 2025-01-15T10:30:00+00:00

Middleware Options

// Custom TTL (seconds)
->middleware('idempotent:ttl=172800')  // 48 hours

// Require key (returns 400 if missing)
->middleware('idempotent:required')

// Custom scope
->middleware('idempotent:scope=team')

// Combined options
->middleware('idempotent:required,ttl=3600')

PHP Attributes

use Grazulex\ApiIdempotency\Attributes\Idempotent;
use Grazulex\ApiIdempotency\Attributes\IdempotentExcept;

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

    #[IdempotentExcept]
    public function index() { /* ... */ } // Excluded
}

Programmatic Usage

use Grazulex\ApiIdempotency\Facades\Idempotency;

// Check if already processed
if ($cached = Idempotency::get($key)) {
    return $cached->toResponse();
}

// Store manually
Idempotency::store($key, response()->json($data, 201));

// Skip caching (e.g., for validation errors)
Idempotency::skip();

Key Generation

use Grazulex\ApiIdempotency\Support\IdempotencyKey;

// Generate unique key
$key = IdempotencyKey::generate();         // "idem_01HQ3K4M..."
$key = IdempotencyKey::generate('pay');    // "pay_01HQ3K4M..."

// Deterministic key from data
$key = IdempotencyKey::fromData([
    'user_id' => 123,
    'action' => 'create_payment',
]);

Artisan Commands

# View statistics
php artisan idempotency:stats

# Cleanup expired keys
php artisan idempotency:cleanup

# Remove specific key
php artisan idempotency:forget pay_abc123

# List recent keys
php artisan idempotency:list --limit=20

Events

use Grazulex\ApiIdempotency\Events\IdempotentRequestProcessed;
use Grazulex\ApiIdempotency\Events\IdempotentRequestReplayed;
use Grazulex\ApiIdempotency\Events\IdempotentConflictDetected;
use Grazulex\ApiIdempotency\Events\IdempotentPayloadMismatch;

Configuration

// config/api-idempotency.php
return [
    'enabled' => env('API_IDEMPOTENCY_ENABLED', true),
    'header' => env('API_IDEMPOTENCY_HEADER', 'Idempotency-Key'),

    'key' => [
        'required' => false,
        'min_length' => 10,
        'max_length' => 255,
        'pattern' => '/^[a-zA-Z0-9_-]+$/',
    ],

    // Drivers: cache, redis, database, dynamodb
    'driver' => env('API_IDEMPOTENCY_DRIVER', 'cache'),

    'drivers' => [
        'cache' => [
            'store' => 'default',
            'prefix' => 'idempotency:',
        ],
        'redis' => [
            'connection' => 'default',
            'prefix' => 'idempotency:',
        ],
        'database' => [
            'connection' => null,
            'table' => 'idempotency_keys',
        ],
        'dynamodb' => [
            'table' => 'idempotency_keys',
            'region' => 'eu-west-1',
        ],
    ],

    'ttl' => env('API_IDEMPOTENCY_TTL', 86400), // 24 hours

    'conflict' => [
        'strategy' => 'wait', // or 'reject'
        'wait_timeout' => 10,
        'retry_interval' => 100,
    ],

    'fingerprint' => [
        'enabled' => true,
        'algorithm' => 'sha256',
        'include_path' => true,
        'include_method' => true,
        'include_body' => true,
        'exclude_fields' => ['timestamp', 'nonce'],
    ],

    'scope' => [
        'enabled' => true,
        'resolver' => 'user', // user, tenant, ip, or callable
    ],

    'logging' => [
        'enabled' => true,
        'log_hits' => true,
        'log_conflicts' => true,
    ],
];

Testing

use Grazulex\ApiIdempotency\Facades\Idempotency;

public function test_idempotency(): void
{
    Idempotency::fake();

    // Your test code...

    Idempotency::assertStored('expected_key');
    Idempotency::assertReplayed('expected_key');
    Idempotency::assertStoredCount(5);
}

Integration testing:

public function test_payment_is_idempotent(): void
{
    $key = 'test_key_' . uniqid();
    $payload = ['amount' => 9999];

    $response1 = $this->postJson('/api/payments', $payload, [
        'Idempotency-Key' => $key,
    ]);

    $response1->assertStatus(201)
        ->assertHeader('X-Idempotent-Replayed', 'false');

    $response2 = $this->postJson('/api/payments', $payload, [
        'Idempotency-Key' => $key,
    ]);

    $response2->assertStatus(201)
        ->assertHeader('X-Idempotent-Replayed', 'true')
        ->assertJson($response1->json());
}

Quality Tools

# Run tests
composer test

# Code style (Laravel Pint)
composer pint

# Static analysis (PHPStan Level 5)
composer analyse

Error Responses

Code Status Description
IDEMPOTENCY_KEY_MISSING 400 Key required but not provided
IDEMPOTENCY_KEY_INVALID 400 Key format invalid
IDEMPOTENCY_PAYLOAD_MISMATCH 422 Same key, different payload
IDEMPOTENCY_CONFLICT 409 Request in progress with same key

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security-related issues, please email security@grazulex.dev instead of using the issue tracker.

Credits

Support This Package

Laravel API Idempotency is free, open source and maintained on my own time. If it saves you hours, here is how you can give back:

License

The MIT License (MIT). Please see License File for more information.


See Also

Related Packages

webrek/laravel-idempotency

Safe request retries for Laravel APIs via the Idempotency-Key header.

1,176 0
bluebeetle/idempotency-middleware

Idempotency middleware for your Laravel API.

10,068 3
kayrunm/replay

A simple package for idempotent requests in Laravel.

2,580 5
eufaturo/idempotency-middleware

Idempotency middleware for your Laravel API.

1,040 0
square1/laravel-idempotency

Add idempotency to Laravel-based APIs, preventing duplicate request processing.

306,807 119

Version History

Version Released PHP Laravel License
v1.2.3 ^8.4 ^12.0|^13.0 MIT
v1.2.2 ^8.4 ^12.0|^13.0 MIT
v1.2.1 ^8.4 ^12.0|^13.0 MIT
v1.2.0 ^8.4 ^12.0|^13.0 MIT
v1.1.0 ^8.3 ^12.0|^13.0 MIT