PHPackages                             rasuvaeff/yii3-webhooks - 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. [API Development](/categories/api)
4. /
5. rasuvaeff/yii3-webhooks

ActiveLibrary[API Development](/categories/api)

rasuvaeff/yii3-webhooks
=======================

HMAC-signed webhook infrastructure for Yii3 — outbound signing, inbound verification, replay protection, retry policy

v1.0.2(1mo ago)02562BSD-3-ClausePHPPHP 8.3 - 8.5CI passing

Since Jun 19Pushed 2w agoCompare

[ Source](https://github.com/rasuvaeff/yii3-webhooks)[ Packagist](https://packagist.org/packages/rasuvaeff/yii3-webhooks)[ Docs](https://github.com/rasuvaeff/yii3-webhooks)[ RSS](/packages/rasuvaeff-yii3-webhooks/feed)WikiDiscussions master Synced 2w ago

READMEChangelogDependencies (21)Versions (4)Used By (2)

rasuvaeff/yii3-webhooks
=======================

[](#rasuvaeffyii3-webhooks)

[![Stable Version](https://camo.githubusercontent.com/16db8ca19c7bdb89c90d31d18325f98af140e9e7004e987c3661843f946e57ec/68747470733a2f2f706f7365722e707567782e6f72672f7261737576616566662f796969332d776562686f6f6b732f762f737461626c65)](https://packagist.org/packages/rasuvaeff/yii3-webhooks)[![Total Downloads](https://camo.githubusercontent.com/7c951e3c9bcbe7e25f899993de7589c10deafcb438238efbfae4ffffe65a41ca/68747470733a2f2f706f7365722e707567782e6f72672f7261737576616566662f796969332d776562686f6f6b732f646f776e6c6f616473)](https://packagist.org/packages/rasuvaeff/yii3-webhooks)[![Build](https://github.com/rasuvaeff/yii3-webhooks/actions/workflows/build.yml/badge.svg)](https://github.com/rasuvaeff/yii3-webhooks/actions)[![Static analysis](https://github.com/rasuvaeff/yii3-webhooks/actions/workflows/static-analysis.yml/badge.svg)](https://github.com/rasuvaeff/yii3-webhooks/actions)[![Psalm Level](https://camo.githubusercontent.com/72b2894b5a3ab66d081bc658674d698410395811eafcd0dfe9c4e9aac42b0f7e/68747470733a2f2f73686570686572642e6465762f6769746875622f7261737576616566662f796969332d776562686f6f6b732f6c6576656c2e737667)](https://shepherd.dev/github/rasuvaeff/yii3-webhooks)[![PHP](https://camo.githubusercontent.com/a6c4c6e3bca032fd6abf26ad9927d434835fe7ba5d73a1f298ea04b7f6032a81/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f7261737576616566662f796969332d776562686f6f6b732f706870)](https://packagist.org/packages/rasuvaeff/yii3-webhooks)[![License](https://camo.githubusercontent.com/cd766433c01b324b8f604d92d1f29678e41446c35def076a72d57a192b4dbcd6/68747470733a2f2f706f7365722e707567782e6f72672f7261737576616566662f796969332d776562686f6f6b732f6c6963656e7365)](https://packagist.org/packages/rasuvaeff/yii3-webhooks)[Русская версия](README.ru.md)

HMAC-signed webhook infrastructure for Yii3: outbound signing, inbound verification, replay protection, and delivery retry policy. It signs the exact payload bytes you send or receive; no hard HTTP client dependency — bring your own dispatcher.

> Using an AI coding assistant? [llms.txt](llms.txt) has a compact API reference you can use.

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

[](#requirements)

- PHP 8.3+
- `psr/clock` ^1.0

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

[](#installation)

```
composer require rasuvaeff/yii3-webhooks
```

Usage
-----

[](#usage)

### Signing an outbound webhook

[](#signing-an-outbound-webhook)

```
use Rasuvaeff\Yii3Webhooks\HmacSha256Signer;
use Rasuvaeff\Yii3Webhooks\WebhookEndpoint;
use Rasuvaeff\Yii3Webhooks\WebhookEvent;

$signer = new HmacSha256Signer();
$endpoint = new WebhookEndpoint(
    url: 'https://partner.example.com/webhook',
    secret: 'whsec_...',
);

$event = WebhookEvent::create(
    type: 'order.created',
    payload: json_encode(['orderId' => 42]),
);

$timestamp = $clock->now()->getTimestamp();
$signature = $signer->sign(
    payload: $event->getPayload(),
    secret: $endpoint->getSecret(),
    timestamp: $timestamp,
    eventId: $event->getId(),
);

// Add to outgoing request:
// X-Webhook-Id:
// X-Webhook-Signature: t=1717228800,v1=
$header = $signature->toHeaderValue();
```

### Verifying an inbound webhook

[](#verifying-an-inbound-webhook)

```
use Rasuvaeff\Yii3Webhooks\HmacSha256Signer;
use Rasuvaeff\Yii3Webhooks\WebhookSignature;
use Rasuvaeff\Yii3Webhooks\WebhookVerifier;

$verifier = new WebhookVerifier(
    signer: new HmacSha256Signer(),
    clock: $clock,
    toleranceSeconds: 300,
);

$signature = WebhookSignature::fromHeaderValue(
    $request->getHeaderLine('X-Webhook-Signature'),
);

$eventId = $request->getHeaderLine('X-Webhook-Id');

$valid = $verifier->verify(
    payload: (string) $request->getBody(),
    secret: 'whsec_...',
    signature: $signature,
    eventId: $eventId,
);
```

### Replay protection

[](#replay-protection)

Use the event ID (from the `X-Webhook-Id` header) as the nonce — it uniquely identifies the delivery and allows replay detection independently of signature verification.

```
use Rasuvaeff\Yii3Webhooks\InMemoryNonceStorage;
use Rasuvaeff\Yii3Webhooks\ReplayGuard;

$guard = new ReplayGuard(new InMemoryNonceStorage());

// $eventId = $request->getHeaderLine('X-Webhook-Id');
if ($valid) {
    $guard->accept($eventId); // throws RuntimeException if already seen
    // process the webhook...
}
```

### Tracking deliveries

[](#tracking-deliveries)

```
use Rasuvaeff\Yii3Webhooks\InMemoryDeliveryStorage;
use Rasuvaeff\Yii3Webhooks\WebhookDelivery;
use Rasuvaeff\Yii3Webhooks\WebhookRetryPolicy;

$storage = new InMemoryDeliveryStorage();
$policy = WebhookRetryPolicy::fixed(maxAttempts: 3, delaySeconds: 60);
// or: WebhookRetryPolicy::exponential(maxAttempts: 5, baseSeconds: 10, cap: 3600)

$delivery = WebhookDelivery::create(event: $event, endpoint: $endpoint);
$storage->save($delivery);

// After attempt:
$delivery = $delivery->withAttempt($clock->now(), error: 'Connection refused');
$storage->save($delivery);

if ($policy->isReadyForRetry($delivery, $clock->now())) {
    // retry...
}
```

API reference
-------------

[](#api-reference)

### WebhookEvent

[](#webhookevent)

MethodDescription`create(type, payload, occurredAt?)`Factory with auto-generated ID`getId()`32-char hex ID`getType()`Event type string`getPayload()`Raw payload bytes to sign and deliver`getOccurredAt()``DateTimeImmutable`### WebhookEndpoint

[](#webhookendpoint)

MethodDescription`__construct(url, secret, headers?)`URL must use http/https; secret non-empty`getUrl()`Endpoint URL`getSecret()`Shared secret (not stored in delivery)`getHeaders()`Additional request headers### WebhookSignature

[](#webhooksignature)

MethodDescription`__construct(timestamp, value)`Positive timestamp, non-empty value`fromHeaderValue(header)`Parse `t=...,v1=...` format`toHeaderValue()`Serialize to `t=...,v1=...` format`getTimestamp()`Unix timestamp`getValue()`HMAC hex string### WebhookSigner

[](#webhooksigner)

Interface for outbound signature implementations. Custom signers must sign the exact payload bytes and return a `WebhookSignature`.

MethodDescription`sign(payload, secret, timestamp, eventId)`Returns `WebhookSignature`### HmacSha256Signer

[](#hmacsha256signer)

Signs `"{eventId}.{timestamp}.{payload}"` with the secret using HMAC-SHA256. `payload` is the exact HTTP body string, not a re-encoded JSON value.

MethodDescription`sign(payload, secret, timestamp, eventId)`Returns `WebhookSignature`### WebhookVerifier

[](#webhookverifier)

MethodDescription`__construct(signer, clock, toleranceSeconds?)`Default tolerance: 300s`verify(payload, secret, signature, eventId)`Returns `bool`; uses `hash_equals`### WebhookRetryPolicy

[](#webhookretrypolicy)

MethodDescription`fixed(maxAttempts?, delaySeconds?)`Constant delay; default: 3 attempts, 60s`exponential(maxAttempts?, baseSeconds?, cap?, multiplier?)`Doubling delay; default: 5 attempts, 10s base, 3600s cap`getMaxAttempts()`Max retry attempts`nextDelaySeconds(attempts)`Delay before next attempt; `attempts` = current attempt count`shouldRetry(delivery)`Returns `true` when status is Pending and attempts &lt; maxAttempts`isReadyForRetry(delivery, now)`Returns `true` when delay has elapsed### WebhookDelivery

[](#webhookdelivery)

MethodDescription`create(event, endpoint, createdAt?)`Factory; stores URL only (no secret)`getId()`32-char hex ID`getEventId()`Source event ID`getEventType()`Source event type`getEndpointUrl()`Endpoint URL`getStatus()``WebhookDeliveryStatus` enum`getCreatedAt()``DateTimeImmutable` creation time`getAttempts()`Attempt count`getLastAttemptAt()``?DateTimeImmutable``getLastError()``?string``withAttempt(at, error?)`Returns new instance with incremented attempts`withStatus(status)`Returns new instance with updated status### WebhookDeliveryStorage

[](#webhookdeliverystorage)

Interface for persistence backends. Core ships `InMemoryDeliveryStorage` for tests; use a persistent backend in production.

MethodDescription`save(delivery)`Stores a delivery attempt record`findPending(limit)`Returns pending deliveries`markDelivered(delivery)`Marks a delivery as delivered`markFailed(delivery)`Marks a delivery as failed`getById(id)`Loads a delivery by ID### ReplayGuard

[](#replayguard)

MethodDescription`__construct(NonceStorage)`Storage must atomically reject duplicate nonces`isReplayed(nonce)`Returns `bool``accept(nonce)`Marks as seen; throws `RuntimeException` if duplicate### WebhookDeliveryStatus

[](#webhookdeliverystatus)

Backed string enum with three cases:

CaseValue`Pending``'pending'``Delivered``'delivered'``Failed``'failed'`### WebhookDispatcher

[](#webhookdispatcher)

Interface for HTTP transport implementations. The package ships no concrete dispatcher — bring your own (Guzzle, PSR-18, etc.).

MethodDescription`dispatch(event, endpoint)`Sends signed webhook; returns `WebhookDelivery`### NonceStorage

[](#noncestorage)

Interface for replay-protection storage backends. Implementations must reject duplicate nonces atomically.

MethodDescription`has(nonce)`Returns `true` if nonce was already seen`add(nonce)`Stores nonce; returns `false` if duplicate### InMemoryNonceStorage

[](#inmemorynoncestorage)

Test-only `NonceStorage` implementation. Not safe for production use.

MethodDescription`has(nonce)`Returns `bool``add(nonce)`Returns `false` on duplicate`clear()`Removes all stored nonces### InMemoryDeliveryStorage

[](#inmemorydeliverystorage)

Test-only `WebhookDeliveryStorage` implementation. Implements `IteratorAggregate` and `Countable` for easy inspection.

MethodDescription`save(delivery)`Stores a delivery record`findPending(limit)`Returns pending deliveries`markDelivered(delivery)`Sets status to `Delivered``markFailed(delivery)`Sets status to `Failed``getById(id)`Loads a delivery by ID`clear()`Removes all recordsSecurity
--------

[](#security)

- Signature comparison uses `hash_equals()` — safe against timing attacks.
- `WebhookDelivery` stores only the endpoint URL, never the secret.
- All secret parameters are marked `#[\SensitiveParameter]` — they do not appear in stack traces.
- Always validate timestamps (tolerance) to prevent replay of old signatures.
- Use `ReplayGuard` with a persistent `NonceStorage` in production; storage implementations must reject duplicates atomically.

Examples
--------

[](#examples)

See [examples/](examples/) for complete usage examples.

Development
-----------

[](#development)

```
make install
make build
make cs-fix
make test
make test-coverage
make mutation
make release-check
```

`make test-coverage` and `make mutation` bootstrap `pcov` inside the `composer:2` container because the base image has no coverage driver.

License
-------

[](#license)

BSD-3-Clause. See [LICENSE.md](LICENSE.md).

###  Health Score

46

—

FairBetter than 92% of packages

Maintenance95

Actively maintained with recent releases

Popularity16

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity53

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

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 ~5 days

Total

3

Last Release

34d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/b0812d5572a7041dfe36e222d295b2e6dc55833a605350fcde58a51a5965ed30?d=identicon)[rasuvaeff](/maintainers/rasuvaeff)

---

Top Contributors

[![rasuvaeff](https://avatars.githubusercontent.com/u/1352718?v=4)](https://github.com/rasuvaeff "rasuvaeff (9 commits)")

---

Tags

hmacphppsr-15replay-protectionwebhooksyii3signaturewebhookhmacyii3replay-protection

###  Code Quality

TestsPHPUnit

Static AnalysisPsalm, Rector

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/rasuvaeff-yii3-webhooks/health.svg)

```
[![Health](https://phpackages.com/badges/rasuvaeff-yii3-webhooks/health.svg)](https://phpackages.com/packages/rasuvaeff-yii3-webhooks)
```

###  Alternatives

[symfony/symfony

The Symfony PHP framework

31.4k87.4M2.2k](/packages/symfony-symfony)[tempest/framework

The PHP framework that gets out of your way.

2.3k37.6k19](/packages/tempest-framework)[standard-webhooks/standard-webhooks

Standard Webhooks - Open source tools and guidelines for sending webhooks easily, securely, and reliably

1.7k474.4k31](/packages/standard-webhooks-standard-webhooks)[ecotone/ecotone

Enterprise architecture layer for Laravel and Symfony — CQRS, Event Sourcing, Durable Workflows (Sagas, Orchestrators), Projections, and Outbox messaging via PHP attributes.

569591.1k60](/packages/ecotone-ecotone)[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

86337.5k](/packages/flow-php-flow)[flow-php/etl

PHP ETL - Extract Transform Load - Abstraction

379637.6k118](/packages/flow-php-etl)

PHPackages © 2026

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