├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── config.yml └── workflows │ ├── update-changelog.yml │ ├── php-cs-fixer.yml │ └── run-tests.yml ├── src ├── Events │ ├── WebhookCallFailedEvent.php │ ├── WebhookCallSucceededEvent.php │ ├── FinalWebhookCallFailedEvent.php │ ├── DispatchingWebhookCallEvent.php │ └── WebhookCallEvent.php ├── BackoffStrategy │ ├── BackoffStrategy.php │ └── ExponentialBackoffStrategy.php ├── Signer │ ├── Signer.php │ └── DefaultSigner.php ├── WebhookServerServiceProvider.php ├── Exceptions │ ├── InvalidSigner.php │ ├── CouldNotCallWebhook.php │ ├── InvalidWebhookJob.php │ └── InvalidBackoffStrategy.php ├── CallWebhookJob.php └── WebhookCall.php ├── LICENSE.md ├── .php-cs-fixer.php ├── composer.json ├── config └── webhook-server.php ├── CHANGELOG.md └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://spatie.be/open-source/support-us 2 | -------------------------------------------------------------------------------- /src/Events/WebhookCallFailedEvent.php: -------------------------------------------------------------------------------- 1 | 4) { 10 | return 100000; 11 | } 12 | 13 | return 10 ** $attempt; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Feature Request 4 | url: https://github.com/spatie/laravel-webhook-server/discussions/new?category=ideas 5 | about: Share ideas for new features 6 | - name: Ask a Question 7 | url: https://github.com/spatie/laravel-webhook-server/discussions/new?category=q-a 8 | about: Ask the community for help 9 | -------------------------------------------------------------------------------- /src/Events/DispatchingWebhookCallEvent.php: -------------------------------------------------------------------------------- 1 | name('laravel-webhook-server') 14 | ->hasConfigFile(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidSigner.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | notPath('bootstrap/*') 5 | ->notPath('storage/*') 6 | ->notPath('vendor') 7 | ->in([ 8 | __DIR__ . '/src', 9 | __DIR__ . '/tests', 10 | ]) 11 | ->name('*.php') 12 | ->notName('*.blade.php') 13 | ->ignoreDotFiles(true) 14 | ->ignoreVCS(true); 15 | 16 | return (new PhpCsFixer\Config()) 17 | ->setRules([ 18 | '@PSR2' => true, 19 | 'array_syntax' => ['syntax' => 'short'], 20 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 21 | 'no_unused_imports' => true, 22 | 'not_operator_with_successor_space' => true, 23 | 'trailing_comma_in_multiline' => true, 24 | 'phpdoc_scalar' => true, 25 | 'unary_operator_spaces' => true, 26 | 'binary_operator_spaces' => true, 27 | 'blank_line_before_statement' => [ 28 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 29 | ], 30 | 'phpdoc_single_line_var_spacing' => true, 31 | 'phpdoc_var_without_name' => true, 32 | 'class_attributes_separation' => [ 33 | 'elements' => [ 34 | 'method' => 'one', 35 | 'property' => 'one', 36 | ], 37 | ], 38 | 'method_argument_space' => [ 39 | 'on_multiline' => 'ensure_fully_multiline', 40 | 'keep_multiple_spaces_after_comma' => true, 41 | ] 42 | ]) 43 | ->setFinder($finder); 44 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | runs-on: ${{ matrix.os }} 10 | 11 | strategy: 12 | fail-fast: true 13 | matrix: 14 | php: [8.3, 8.2, 8.1, 8.0] 15 | laravel: ['8.*', '9.*', '10.*', '11.*', '12.*'] 16 | os: [ubuntu-latest] 17 | dependency-version: [prefer-stable] 18 | include: 19 | - laravel: 11.* 20 | testbench: 9.* 21 | - laravel: 10.* 22 | testbench: 8.* 23 | - laravel: 9.* 24 | testbench: 7.* 25 | - laravel: 8.* 26 | testbench: ^6.23 27 | - laravel: 12.* 28 | testbench: 10.* 29 | exclude: 30 | - laravel: 11.* 31 | php: 8.1 32 | - laravel: 11.* 33 | php: 8.0 34 | - laravel: 10.* 35 | php: 8.0 36 | - laravel: 12.* 37 | php: 8.1 38 | - laravel: 12.* 39 | php: 8.0 40 | 41 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ${{ matrix.os }} 42 | 43 | steps: 44 | - name: Checkout code 45 | uses: actions/checkout@v3 46 | 47 | - name: Setup PHP 48 | uses: shivammathur/setup-php@v2 49 | with: 50 | php-version: ${{ matrix.php }} 51 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick 52 | coverage: none 53 | 54 | - name: Install dependencies 55 | run: | 56 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 57 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest 58 | 59 | - name: Execute tests 60 | run: vendor/bin/pest 61 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/laravel-webhook-server", 3 | "description": "Send webhooks in Laravel apps", 4 | "keywords": [ 5 | "spatie", 6 | "laravel-webhook-server", 7 | "webhook", 8 | "server" 9 | ], 10 | "homepage": "https://github.com/spatie/laravel-webhook-server", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Freek Van der Herten", 15 | "email": "freek@spatie.be", 16 | "homepage": "https://spatie.be", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.0", 22 | "ext-json": "*", 23 | "guzzlehttp/guzzle": "^6.3|^7.3", 24 | "illuminate/bus": "^8.50|^9.0|^10.0|^11.0|^12.0", 25 | "illuminate/queue": "^8.50|^9.0|^10.0|^11.0|^12.0", 26 | "illuminate/support": "^8.50|^9.0|^10.0|^11.0|^12.0", 27 | "spatie/laravel-package-tools": "^1.11" 28 | }, 29 | "require-dev": { 30 | "mockery/mockery": "^1.4.3", 31 | "orchestra/testbench": "^6.19|^7.0|^8.0|^10.0", 32 | "pestphp/pest": "^1.22|^2.0|^3.0", 33 | "pestphp/pest-plugin-laravel": "^1.3|^2.0|^3.0", 34 | "spatie/test-time": "^1.2.2" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Spatie\\WebhookServer\\": "src" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Spatie\\WebhookServer\\Tests\\": "tests" 44 | } 45 | }, 46 | "scripts": { 47 | "test": "vendor/bin/pest", 48 | "test-coverage": "vendor/bin/pest" 49 | }, 50 | "config": { 51 | "sort-packages": true, 52 | "allow-plugins": { 53 | "pestphp/pest-plugin": true 54 | } 55 | }, 56 | "extra": { 57 | "laravel": { 58 | "providers": [ 59 | "Spatie\\WebhookServer\\WebhookServerServiceProvider" 60 | ] 61 | } 62 | }, 63 | "minimum-stability": "dev", 64 | "prefer-stable": true 65 | } 66 | -------------------------------------------------------------------------------- /config/webhook-server.php: -------------------------------------------------------------------------------- 1 | 'default', 9 | 10 | /* 11 | * The default queue connection that should be used to send webhook requests. 12 | */ 13 | 'connection' => null, 14 | 15 | /* 16 | * The default http verb to use. 17 | */ 18 | 'http_verb' => 'post', 19 | 20 | /* 21 | * Proxies to use for request. 22 | * 23 | * See https://docs.guzzlephp.org/en/stable/request-options.html#proxy 24 | */ 25 | 'proxy' => null, 26 | 27 | /* 28 | * This class is responsible for calculating the signature that will be added to 29 | * the headers of the webhook request. A webhook client can use the signature 30 | * to verify the request hasn't been tampered with. 31 | */ 32 | 'signer' => \Spatie\WebhookServer\Signer\DefaultSigner::class, 33 | 34 | /* 35 | * This is the name of the header where the signature will be added. 36 | */ 37 | 'signature_header_name' => 'Signature', 38 | 39 | /* 40 | * These are the headers that will be added to all webhook requests. 41 | */ 42 | 'headers' => [ 43 | 'Content-Type' => 'application/json', 44 | ], 45 | 46 | /* 47 | * If a call to a webhook takes longer this amount of seconds 48 | * the attempt will be considered failed. 49 | */ 50 | 'timeout_in_seconds' => 3, 51 | 52 | /* 53 | * The amount of times the webhook should be called before we give up. 54 | */ 55 | 'tries' => 3, 56 | 57 | /* 58 | * This class determines how many seconds there should be between attempts. 59 | */ 60 | 'backoff_strategy' => \Spatie\WebhookServer\BackoffStrategy\ExponentialBackoffStrategy::class, 61 | 62 | /* 63 | * This class is used to dispatch webhooks onto the queue. 64 | */ 65 | 'webhook_job' => \Spatie\WebhookServer\CallWebhookJob::class, 66 | 67 | /* 68 | * By default we will verify that the ssl certificate of the destination 69 | * of the webhook is valid. 70 | */ 71 | 'verify_ssl' => true, 72 | 73 | /* 74 | * When set to true, an exception will be thrown when the last attempt fails 75 | */ 76 | 'throw_exception_on_failure' => false, 77 | 78 | /* 79 | * When using Laravel Horizon you can specify tags that should be used on the 80 | * underlying job that performs the webhook request. 81 | */ 82 | 'tags' => [], 83 | ]; 84 | -------------------------------------------------------------------------------- /src/CallWebhookJob.php: -------------------------------------------------------------------------------- 1 | attempts() >= $this->tries; 79 | 80 | try { 81 | $body = strtoupper($this->httpVerb) === 'GET' 82 | ? ['query' => $this->payload] 83 | : ['body' => $this->generateBody()]; 84 | 85 | $this->response = $this->createRequest($body); 86 | 87 | if (! Str::startsWith($this->response->getStatusCode(), 2)) { 88 | throw new Exception('Webhook call failed'); 89 | } 90 | 91 | $this->dispatchEvent(WebhookCallSucceededEvent::class); 92 | 93 | return; 94 | } catch (Exception $exception) { 95 | if ($exception instanceof RequestException) { 96 | $this->response = $exception->getResponse(); 97 | $this->errorType = get_class($exception); 98 | $this->errorMessage = $exception->getMessage(); 99 | } 100 | 101 | if ($exception instanceof ConnectException) { 102 | $this->errorType = get_class($exception); 103 | $this->errorMessage = $exception->getMessage(); 104 | } 105 | 106 | if (! $lastAttempt) { 107 | /** @var \Spatie\WebhookServer\BackoffStrategy\BackoffStrategy $backoffStrategy */ 108 | $backoffStrategy = app($this->backoffStrategyClass); 109 | 110 | $waitInSeconds = $backoffStrategy->waitInSecondsAfterAttempt($this->attempts()); 111 | 112 | $this->release($waitInSeconds); 113 | } 114 | 115 | $this->dispatchEvent(WebhookCallFailedEvent::class); 116 | 117 | if ($lastAttempt || $this->shouldBeRemovedFromQueue()) { 118 | $this->dispatchEvent(FinalWebhookCallFailedEvent::class); 119 | 120 | $this->throwExceptionOnFailure ? $this->fail($exception) : $this->delete(); 121 | } 122 | } 123 | } 124 | 125 | public function tags(): array 126 | { 127 | return $this->tags; 128 | } 129 | 130 | public function getResponse(): ?Response 131 | { 132 | return $this->response; 133 | } 134 | 135 | protected function getClient(): ClientInterface 136 | { 137 | return app(Client::class); 138 | } 139 | 140 | protected function createRequest(array $body): Response 141 | { 142 | $client = $this->getClient(); 143 | 144 | return $client->request($this->httpVerb, $this->webhookUrl, array_merge( 145 | [ 146 | 'timeout' => $this->requestTimeout, 147 | 'verify' => $this->verifySsl, 148 | 'headers' => $this->headers, 149 | 'on_stats' => function (TransferStats $stats) { 150 | $this->transferStats = $stats; 151 | }, 152 | ], 153 | $body, 154 | is_null($this->proxy) ? [] : ['proxy' => $this->proxy], 155 | is_null($this->cert) ? [] : ['cert' => [$this->cert, $this->certPassphrase]], 156 | is_null($this->sslKey) ? [] : ['ssl_key' => [$this->sslKey, $this->sslKeyPassphrase]] 157 | )); 158 | } 159 | 160 | protected function shouldBeRemovedFromQueue(): bool 161 | { 162 | return false; 163 | } 164 | 165 | private function dispatchEvent(string $eventClass) 166 | { 167 | event(new $eventClass( 168 | $this->httpVerb, 169 | $this->webhookUrl, 170 | $this->payload, 171 | $this->headers, 172 | $this->meta, 173 | $this->tags, 174 | $this->attempts(), 175 | $this->response, 176 | $this->errorType, 177 | $this->errorMessage, 178 | $this->uuid, 179 | $this->transferStats 180 | )); 181 | } 182 | 183 | private function generateBody(): string 184 | { 185 | return match ($this->outputType) { 186 | "RAW" => $this->payload, 187 | default => json_encode($this->payload), 188 | }; 189 | } 190 | 191 | public function failed(Throwable $e) 192 | { 193 | if ($this->throwExceptionOnFailure) { 194 | throw $e; 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/WebhookCall.php: -------------------------------------------------------------------------------- 1 | useJob($config['webhook_job']) 37 | ->uuid(Str::uuid()) 38 | ->onQueue($config['queue']) 39 | ->onConnection($config['connection'] ?? null) 40 | ->useHttpVerb($config['http_verb']) 41 | ->maximumTries($config['tries']) 42 | ->useBackoffStrategy($config['backoff_strategy']) 43 | ->timeoutInSeconds($config['timeout_in_seconds']) 44 | ->signUsing($config['signer']) 45 | ->withHeaders($config['headers']) 46 | ->withTags($config['tags']) 47 | ->verifySsl($config['verify_ssl']) 48 | ->throwExceptionOnFailure($config['throw_exception_on_failure']) 49 | ->useProxy($config['proxy']); 50 | } 51 | 52 | public function __construct() 53 | { 54 | } 55 | 56 | public function url(string $url): self 57 | { 58 | $this->callWebhookJob->webhookUrl = $url; 59 | 60 | return $this; 61 | } 62 | 63 | public function payload(array $payload): self 64 | { 65 | $this->payload = $payload; 66 | 67 | $this->callWebhookJob->payload = $payload; 68 | 69 | return $this; 70 | } 71 | 72 | public function uuid(string $uuid): self 73 | { 74 | $this->uuid = $uuid; 75 | 76 | $this->callWebhookJob->uuid = $uuid; 77 | 78 | return $this; 79 | } 80 | 81 | public function getUuid(): string 82 | { 83 | return $this->uuid; 84 | } 85 | 86 | public function onQueue(?string $queue): self 87 | { 88 | $this->callWebhookJob->queue = $queue; 89 | 90 | return $this; 91 | } 92 | 93 | public function onConnection(?string $connection): self 94 | { 95 | $this->callWebhookJob->connection = $connection; 96 | 97 | return $this; 98 | } 99 | 100 | public function useSecret(string $secret): self 101 | { 102 | $this->secret = $secret; 103 | 104 | return $this; 105 | } 106 | 107 | public function useHttpVerb(string $verb): self 108 | { 109 | $this->callWebhookJob->httpVerb = $verb; 110 | 111 | return $this; 112 | } 113 | 114 | public function maximumTries(int $tries): self 115 | { 116 | $this->callWebhookJob->tries = $tries; 117 | 118 | return $this; 119 | } 120 | 121 | public function mutualTls(string $certPath, string $sslKeyPath, ?string $certPassphrase = null, ?string $sslKeyPassphrase = null): self 122 | { 123 | $this->callWebhookJob->cert = $certPath; 124 | $this->callWebhookJob->certPassphrase = $certPassphrase; 125 | $this->callWebhookJob->sslKey = $sslKeyPath; 126 | $this->callWebhookJob->sslKeyPassphrase = $sslKeyPassphrase; 127 | 128 | return $this; 129 | } 130 | 131 | public function useBackoffStrategy(string $backoffStrategyClass): self 132 | { 133 | if (! is_subclass_of($backoffStrategyClass, BackoffStrategy::class)) { 134 | throw InvalidBackoffStrategy::doesNotExtendBackoffStrategy($backoffStrategyClass); 135 | } 136 | 137 | $this->callWebhookJob->backoffStrategyClass = $backoffStrategyClass; 138 | 139 | return $this; 140 | } 141 | 142 | public function timeoutInSeconds(int $timeoutInSeconds): self 143 | { 144 | $this->callWebhookJob->requestTimeout = $timeoutInSeconds; 145 | 146 | return $this; 147 | } 148 | 149 | public function signUsing(string $signerClass): self 150 | { 151 | if (! is_subclass_of($signerClass, Signer::class)) { 152 | throw InvalidSigner::doesNotImplementSigner($signerClass); 153 | } 154 | 155 | $this->signer = app($signerClass); 156 | 157 | return $this; 158 | } 159 | 160 | public function doNotSign(): self 161 | { 162 | $this->signWebhook = false; 163 | 164 | return $this; 165 | } 166 | 167 | public function withHeaders(array $headers): self 168 | { 169 | $this->headers = array_merge($this->headers, $headers); 170 | 171 | return $this; 172 | } 173 | 174 | public function verifySsl(bool|string $verifySsl = true): self 175 | { 176 | $this->callWebhookJob->verifySsl = $verifySsl; 177 | 178 | return $this; 179 | } 180 | 181 | public function doNotVerifySsl(): self 182 | { 183 | $this->verifySsl(false); 184 | 185 | return $this; 186 | } 187 | 188 | public function throwExceptionOnFailure(bool $throwExceptionOnFailure = true): self 189 | { 190 | $this->callWebhookJob->throwExceptionOnFailure = $throwExceptionOnFailure; 191 | 192 | return $this; 193 | } 194 | 195 | public function useProxy(array|string|null $proxy = null): self 196 | { 197 | $this->callWebhookJob->proxy = $proxy; 198 | 199 | return $this; 200 | } 201 | 202 | public function meta(array $meta): self 203 | { 204 | $this->callWebhookJob->meta = $meta; 205 | 206 | return $this; 207 | } 208 | 209 | public function withTags(array $tags): self 210 | { 211 | $this->callWebhookJob->tags = $tags; 212 | 213 | return $this; 214 | } 215 | 216 | public function useJob(string $webhookJobClass): self 217 | { 218 | $job = app($webhookJobClass); 219 | 220 | if (! $job instanceof CallWebhookJob) { 221 | throw InvalidWebhookJob::doesNotExtendCallWebhookJob($webhookJobClass); 222 | } 223 | 224 | $this->callWebhookJob = $job; 225 | 226 | return $this; 227 | } 228 | 229 | public function dispatch(): PendingDispatch 230 | { 231 | $this->prepareForDispatch(); 232 | 233 | event(new DispatchingWebhookCallEvent( 234 | $this->callWebhookJob->httpVerb, 235 | $this->callWebhookJob->webhookUrl, 236 | $this->callWebhookJob->payload, 237 | $this->callWebhookJob->headers, 238 | $this->callWebhookJob->meta, 239 | $this->callWebhookJob->tags, 240 | $this->callWebhookJob->uuid, 241 | )); 242 | 243 | return dispatch($this->callWebhookJob); 244 | } 245 | 246 | public function dispatchIf($condition): PendingDispatch|null 247 | { 248 | if ($condition) { 249 | return $this->dispatch(); 250 | } 251 | 252 | return null; 253 | } 254 | 255 | public function dispatchUnless($condition): PendingDispatch|null 256 | { 257 | return $this->dispatchIf(! $condition); 258 | } 259 | 260 | public function dispatchSync(): void 261 | { 262 | $this->prepareForDispatch(); 263 | 264 | dispatch_sync($this->callWebhookJob); 265 | } 266 | 267 | public function dispatchSyncIf($condition): void 268 | { 269 | if ($condition) { 270 | $this->dispatchSync(); 271 | } 272 | } 273 | 274 | public function dispatchSyncUnless($condition): void 275 | { 276 | $this->dispatchSyncIf(! $condition); 277 | } 278 | 279 | public function sendRawBody(string $body): self 280 | { 281 | $this->callWebhookJob->payload = $body; 282 | $this->callWebhookJob->outputType = "RAW"; 283 | 284 | return $this; 285 | } 286 | 287 | protected function prepareForDispatch(): void 288 | { 289 | if (! $this->callWebhookJob->webhookUrl) { 290 | throw CouldNotCallWebhook::urlNotSet(); 291 | } 292 | 293 | if ($this->signWebhook && empty($this->secret)) { 294 | throw CouldNotCallWebhook::secretNotSet(); 295 | } 296 | 297 | $this->callWebhookJob->headers = $this->getAllHeaders(); 298 | } 299 | 300 | protected function getAllHeaders(): array 301 | { 302 | $headers = $this->headers; 303 | 304 | if (! $this->signWebhook) { 305 | return $headers; 306 | } 307 | 308 | $signature = $this->signer->calculateSignature($this->callWebhookJob->webhookUrl, $this->payload, $this->secret); 309 | 310 | $headers[$this->signer->signatureHeaderName()] = $signature; 311 | 312 | return $headers; 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-webhook-server` will be documented in this file 4 | 5 | ## 3.8.3 - 2025-02-14 6 | 7 | ### What's Changed 8 | 9 | * Laravel 12.x Compatibility by @laravel-shift in https://github.com/spatie/laravel-webhook-server/pull/162 10 | 11 | ### New Contributors 12 | 13 | * @laravel-shift made their first contribution in https://github.com/spatie/laravel-webhook-server/pull/162 14 | 15 | **Full Changelog**: https://github.com/spatie/laravel-webhook-server/compare/3.8.2...3.8.3 16 | 17 | ## 3.8.2 - 2024-12-16 18 | 19 | ### What's Changed 20 | 21 | * Fix CallWebhookJob behaviour when throwExceptionOnFailure is true by @cristian-fleischer in https://github.com/spatie/laravel-webhook-server/pull/161 22 | 23 | ### New Contributors 24 | 25 | * @cristian-fleischer made their first contribution in https://github.com/spatie/laravel-webhook-server/pull/161 26 | 27 | **Full Changelog**: https://github.com/spatie/laravel-webhook-server/compare/3.8.1...3.8.2 28 | 29 | ## 3.8.1 - 2024-02-12 30 | 31 | ### What's Changed 32 | 33 | * Add support for laravel 11 by @shuvroroy in https://github.com/spatie/laravel-webhook-server/pull/154 34 | * Corrected typos in README.md and webhook-server.php by @OussamaMater in https://github.com/spatie/laravel-webhook-server/pull/151 35 | 36 | ### New Contributors 37 | 38 | * @OussamaMater made their first contribution in https://github.com/spatie/laravel-webhook-server/pull/151 39 | 40 | **Full Changelog**: https://github.com/spatie/laravel-webhook-server/compare/3.8.0...3.8.1 41 | 42 | ## 3.8.0 - 2023-11-27 43 | 44 | 3.8.0 45 | 46 | ### What's Changed 47 | 48 | * Add event that is being fired upon the webhook's dispatch by @thannaske in https://github.com/spatie/laravel-webhook-server/pull/150 49 | 50 | ### New Contributors 51 | 52 | * @thannaske made their first contribution in https://github.com/spatie/laravel-webhook-server/pull/150 53 | 54 | **Full Changelog**: https://github.com/spatie/laravel-webhook-server/compare/3.7.0...3.8.0 55 | 56 | ## 3.7.0 - 2023-11-20 57 | 58 | ### What's Changed 59 | 60 | - Adds support for mutual TLS by @JonErickson in https://github.com/spatie/laravel-webhook-server/pull/149 61 | 62 | ### New Contributors 63 | 64 | - @JonErickson made their first contribution in https://github.com/spatie/laravel-webhook-server/pull/149 65 | 66 | **Full Changelog**: https://github.com/spatie/laravel-webhook-server/compare/3.6.0...3.7.0 67 | 68 | ## 3.6.0 - 2023-09-25 69 | 70 | ### What's Changed 71 | 72 | - Fix webhook event type for raw body by @DotNetSimon in https://github.com/spatie/laravel-webhook-server/pull/147 73 | 74 | **Full Changelog**: https://github.com/spatie/laravel-webhook-server/compare/3.5.0...3.6.0 75 | 76 | ## 3.5.0 - 2023-09-12 77 | 78 | ### What's Changed 79 | 80 | - Webhook option to allow sending a raw body instead of array -> json. by @DotNetSimon in https://github.com/spatie/laravel-webhook-server/pull/146 81 | 82 | ### New Contributors 83 | 84 | - @DotNetSimon made their first contribution in https://github.com/spatie/laravel-webhook-server/pull/146 85 | 86 | **Full Changelog**: https://github.com/spatie/laravel-webhook-server/compare/3.4.3...3.5.0 87 | 88 | ## 3.4.3 - 2023-03-17 89 | 90 | ### What's Changed 91 | 92 | - Change to protected properties `$response`, `$errorType` and `$errorMessage` in `CallWebhookJob` by @Kazuto in https://github.com/spatie/laravel-webhook-server/pull/143 93 | 94 | **Full Changelog**: https://github.com/spatie/laravel-webhook-server/compare/3.4.2...3.4.3 95 | 96 | ## 3.4.2 - 2023-01-25 97 | 98 | ### What's Changed 99 | 100 | - Fixes a couple of minor typographical errors. by @cxj in https://github.com/spatie/laravel-webhook-server/pull/141 101 | - support Laravel 10.0 by @hihuangwei in https://github.com/spatie/laravel-webhook-server/pull/142 102 | 103 | ### New Contributors 104 | 105 | - @cxj made their first contribution in https://github.com/spatie/laravel-webhook-server/pull/141 106 | - @hihuangwei made their first contribution in https://github.com/spatie/laravel-webhook-server/pull/142 107 | 108 | **Full Changelog**: https://github.com/spatie/laravel-webhook-server/compare/3.4.1...3.4.2 109 | 110 | ## 3.4.1 - 2023-01-10 111 | 112 | ### What's Changed 113 | 114 | - Add PHP 8.2 Support by @patinthehat in https://github.com/spatie/laravel-webhook-server/pull/138 115 | - Convert all tests to pest by @alexmanase in https://github.com/spatie/laravel-webhook-server/pull/139 116 | - Refactored Request to Method by @JamesFreeman in https://github.com/spatie/laravel-webhook-server/pull/140 117 | 118 | ### New Contributors 119 | 120 | - @patinthehat made their first contribution in https://github.com/spatie/laravel-webhook-server/pull/138 121 | - @alexmanase made their first contribution in https://github.com/spatie/laravel-webhook-server/pull/139 122 | - @JamesFreeman made their first contribution in https://github.com/spatie/laravel-webhook-server/pull/140 123 | 124 | **Full Changelog**: https://github.com/spatie/laravel-webhook-server/compare/3.4.0...3.4.1 125 | 126 | ## 3.4.0 - 2022-11-16 127 | 128 | ### What's Changed 129 | 130 | - Add proxy option by @andycowan in https://github.com/spatie/laravel-webhook-server/pull/136 131 | 132 | ### New Contributors 133 | 134 | - @andycowan made their first contribution in https://github.com/spatie/laravel-webhook-server/pull/136 135 | 136 | **Full Changelog**: https://github.com/spatie/laravel-webhook-server/compare/3.3.0...3.4.0 137 | 138 | ## 3.3.0 - 2022-11-09 139 | 140 | ### What's Changed 141 | 142 | - Add missing config documentation to readme by @Kazuto in https://github.com/spatie/laravel-webhook-server/pull/134 143 | - Add option for configurable Webhook Job by @Kazuto in https://github.com/spatie/laravel-webhook-server/pull/135 144 | 145 | ### New Contributors 146 | 147 | - @Kazuto made their first contribution in https://github.com/spatie/laravel-webhook-server/pull/134 148 | 149 | **Full Changelog**: https://github.com/spatie/laravel-webhook-server/compare/3.2.1...3.3.0 150 | 151 | ## 3.2.1 - 2022-07-29 152 | 153 | ### What's Changed 154 | 155 | - Allow sub-classes of `CallWebhookJob` to use a `GuzzleHttp\Client` specific for outgoing webhooks by @bezhermoso in https://github.com/spatie/laravel-webhook-server/pull/125 156 | 157 | **Full Changelog**: https://github.com/spatie/laravel-webhook-server/compare/3.2.0...3.2.1 158 | 159 | ## 3.2.0 - 2022-06-24 160 | 161 | ### What's Changed 162 | 163 | - feat: add dispatchIf, dispatchUnless, dispatchSyncIf and dispatchSync… by @regnerisch in https://github.com/spatie/laravel-webhook-server/pull/124 164 | 165 | ### New Contributors 166 | 167 | - @regnerisch made their first contribution in https://github.com/spatie/laravel-webhook-server/pull/124 168 | 169 | **Full Changelog**: https://github.com/spatie/laravel-webhook-server/compare/3.1.2...3.2.0 170 | 171 | ## 3.1.2 - 2022-01-26 172 | 173 | - support Laravel 9 174 | 175 | ## 3.1.1 - 2021-12-10 176 | 177 | ## What's Changed 178 | 179 | - Include Exception with Laravel queue failures by @awarrenlove in https://github.com/spatie/laravel-webhook-server/pull/114 180 | 181 | **Full Changelog**: https://github.com/spatie/laravel-webhook-server/compare/3.1.0...3.1.1 182 | 183 | ## 3.0.0 - 2021-09-10 184 | 185 | - support using webhook URLs as part of webhook signatures (#98) 186 | 187 | The only breaking change in this release is the addidation of `string $webhookUrl` to the `calculateSignature` method of the `Signer` interface. 188 | If you have a custom `Signer` in your project, add that `$webhookUrl` to the `calculateSignature` method. 189 | 190 | ## 2.1.1 - 2021-08-27 191 | 192 | - add ability to use default queue of connection (#94) 193 | 194 | ## 2.1.0 - 2021-08-01 195 | 196 | - allow setting queue connection (#92) 197 | 198 | ## 2.0.1 - 2021-07-23 199 | 200 | - fix tests 201 | 202 | ## 2.0.0 - 2021-07-23 203 | 204 | - require Laravel 8 205 | - require PHP 8 206 | 207 | No changes to the API were made, so you can safely upgrade from v1 to v2 208 | 209 | ## 1.13.0 - 2021-04-28 210 | 211 | - add `dispatchSync` 212 | 213 | ## 1.12.0 - 2021-04-20 214 | 215 | - pass Guzzle TransferStats into resulting Event (#81) 216 | 217 | ## 1.11.3 - 2021-04-02 218 | 219 | - fix for missing default headers when using withHeaders (#79) 220 | 221 | ## 1.11.2 - 2021-03-17 222 | 223 | - dispatch should return the PendingDispatch (#74) 224 | 225 | ## 1.11.1 - 2020-12-15 226 | 227 | - fix exception name for invalid signers (#67) 228 | 229 | ## 1.11.0 - 2020-11-28 230 | 231 | - add support for PHP 8 232 | 233 | ## 1.10.0 - 2020-10-04 234 | 235 | - add `getUuid` 236 | 237 | ## 1.9.3 - 2020-09-09 238 | 239 | - support Guzzle 7 240 | 241 | ## 1.9.2 - 2020-09-09 242 | 243 | - support Laravel 8 244 | 245 | ## 1.9.1 - 2020-04-10 246 | 247 | - do not use body in GET request (#43) 248 | 249 | ## 1.9.0 - 2020-03-19 250 | 251 | - add `doNotSign` 252 | 253 | ## 1.8.1 - 2020-03-19 254 | 255 | - fix `uuid` 256 | 257 | ## 1.8.0 - 2020-03-18 258 | 259 | - add `uuid` 260 | 261 | ## 1.7.0 - 2020-03-05 262 | 263 | - add `dispatchNow` (#39) 264 | 265 | ## 1.6.0 - 2020-03-02 266 | 267 | - add support for Laravel 7 268 | 269 | ## 1.5.0 - 2019-12-08 270 | 271 | - drop support for PHP 7.3 272 | 273 | ## 1.4.0 - 2019-09-05 274 | 275 | - add error info to the dispatched event 276 | 277 | ## 1.3.1 - 2019-09-05 278 | 279 | - remove duplicate line 280 | 281 | ## 1.3.0 - 2019-09-04 282 | 283 | - do not release job on last attempt 284 | 285 | ## 1.2.0 - 2019-09-04 286 | 287 | - add `getResponse` 288 | 289 | ## 1.1.0 - 2019-09-04 290 | 291 | - Add Laravel 6 support 292 | 293 | ## 1.0.5 - 2019-07-24 294 | 295 | - avoid sending unsuccessfull event when the final try of a job succeeds 296 | 297 | ## 1.0.4 - 2019-06-22 298 | 299 | - remove constructor on `WebhookCallFailedEvent` so it inherits properties 300 | 301 | ## 1.0.3 - 2019-06-19 302 | 303 | - add `ContentType` header with value `application/json` by default 304 | 305 | ## 1.0.2 - 2019-06-16 306 | 307 | - move `test-time` to dev dependencies 308 | 309 | ## 1.0.1 - 2019-06-15 310 | 311 | - fixed method names 312 | 313 | ## 1.0.0 - 2019-06-15 314 | 315 | **contains bug, do not use** 316 | 317 | - initial release 318 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
6 |
](https://spatie.be/github-ad-click/laravel-webhook-server)
25 |
26 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us).
27 |
28 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards).
29 |
30 | ## Installation
31 |
32 | You can install the package via composer:
33 |
34 | ```bash
35 | composer require spatie/laravel-webhook-server
36 | ```
37 |
38 | You can publish the config file with:
39 | ```bash
40 | php artisan vendor:publish --provider="Spatie\WebhookServer\WebhookServerServiceProvider"
41 | ```
42 |
43 | This is the contents of the file that will be published at `config/webhook-server.php`:
44 |
45 | ```php
46 | return [
47 |
48 | /*
49 | * The default queue that should be used to send webhook requests.
50 | */
51 | 'queue' => 'default',
52 |
53 | /*
54 | * The default http verb to use.
55 | */
56 | 'http_verb' => 'post',
57 |
58 | /*
59 | * This class is responsible for calculating the signature that will be added to
60 | * the headers of the webhook request. A webhook client can use the signature
61 | * to verify the request hasn't been tampered with.
62 | */
63 | 'signer' => \Spatie\WebhookServer\Signer\DefaultSigner::class,
64 |
65 | /*
66 | * This is the name of the header where the signature will be added.
67 | */
68 | 'signature_header_name' => 'Signature',
69 |
70 | /*
71 | * These are the headers that will be added to all webhook requests.
72 | */
73 | 'headers' => [],
74 |
75 | /*
76 | * If a call to a webhook takes longer this amount of seconds
77 | * the attempt will be considered failed.
78 | */
79 | 'timeout_in_seconds' => 3,
80 |
81 | /*
82 | * The amount of times the webhook should be called before we give up.
83 | */
84 | 'tries' => 3,
85 |
86 | /*
87 | * This class determines how many seconds there should be between attempts.
88 | */
89 | 'backoff_strategy' => \Spatie\WebhookServer\BackoffStrategy\ExponentialBackoffStrategy::class,
90 |
91 | /*
92 | * This class is used to dispatch webhooks onto the queue.
93 | */
94 | 'webhook_job' => \Spatie\WebhookServer\CallWebhookJob::class,
95 |
96 | /*
97 | * By default we will verify that the ssl certificate of the destination
98 | * of the webhook is valid.
99 | */
100 | 'verify_ssl' => true,
101 |
102 | /*
103 | * When set to true, an exception will be thrown when the last attempt fails
104 | */
105 | 'throw_exception_on_failure' => false,
106 |
107 | /*
108 | * When using Laravel Horizon you can specify tags that should be used on the
109 | * underlying job that performs the webhook request.
110 | */
111 | 'tags' => [],
112 | ];
113 | ```
114 |
115 | By default, the package uses queues to retry failed webhook requests. Be sure to set up a real queue other than `sync` in non-local environments.
116 |
117 | ## Usage
118 |
119 | This is the simplest way to call a webhook:
120 |
121 | ```php
122 | WebhookCall::create()
123 | ->url('https://other-app.com/webhooks')
124 | ->payload(['key' => 'value'])
125 | ->useSecret('sign-using-this-secret')
126 | ->dispatch();
127 | ```
128 |
129 | This will send a post request to `https://other-app.com/webhooks`. The body of the request will be JSON encoded version of the array passed to `payload`. The request will have a header called `Signature` that will contain a signature the receiving app can use [to verify](https://github.com/spatie/laravel-webhook-server#how-signing-requests-works) the payload hasn't been tampered with. Dispatching a webhook call will also fire a `DispatchingWebhookCallEvent`.
130 |
131 | If the receiving app doesn't respond with a response code starting with `2`, the package will retry calling the webhook after 10 seconds. If that second attempt fails, the package will attempt to call the webhook a final time after 100 seconds. Should that attempt fail, the `FinalWebhookCallFailedEvent` will be raised.
132 |
133 | ### Send webhook synchronously
134 |
135 | If you would like to call the webhook immediately (synchronously), you may use the dispatchSync method. When using this method, the webhook will not be queued and will be run immediately. This can be helpful in situations where sending the webhook is part of a bigger job that already has been queued.
136 |
137 | ```php
138 | WebhookCall::create()
139 | ...
140 | ->dispatchSync();
141 | ```
142 |
143 | ### Conditionally sending webhooks
144 |
145 | If you would like to conditionally dispatch a webhook, you may use the `dispatchIf`, `dispatchUnless`, `dispatchSyncIf`, and `dispatchSyncUnless` methods:
146 |
147 | ```php
148 | WebhookCall::create()
149 | ...
150 | ->dispatchIf($condition);
151 |
152 | WebhookCall::create()
153 | ...
154 | ->dispatchUnless($condition);
155 |
156 | WebhookCall::create()
157 | ...
158 | ->dispatchSyncIf($condition);
159 |
160 | WebhookCall::create()
161 | ...
162 | ->dispatchSyncUnless($condition);
163 | ```
164 |
165 | ### How signing requests work
166 |
167 | When setting up, it's common to generate, store, and share a secret between your app and the app that wants to receive webhooks. Generating the secret could be done with `Illuminate\Support\Str::random()`, but it's entirely up to you. The package will use the secret to sign a webhook call.
168 |
169 | By default, the package will add a header called `Signature` that will contain a signature the receiving app can use if the payload hasn't been tampered with. This is how that signature is calculated:
170 |
171 | ```php
172 | // payload is the array passed to the `payload` method of the webhook
173 | // secret is the string given to the `signUsingSecret` method on the webhook.
174 |
175 | $payloadJson = json_encode($payload);
176 |
177 | $signature = hash_hmac('sha256', $payloadJson, $secret);
178 | ```
179 |
180 | ### Skip signing request
181 |
182 | We don't recommend this, but if you don't want the webhook request to be signed call the `doNotSign` method.
183 |
184 | ```php
185 | WebhookCall::create()
186 | ->doNotSign()
187 | ...
188 | ```
189 |
190 | By calling this method, the `Signature` header will not be set.
191 |
192 | ### Customizing signing requests
193 |
194 | If you want to customize the signing process, you can create your own custom signer. A signer is any class that implements `Spatie\WebhookServer\Signer`.
195 |
196 | This is what that interface looks like.
197 |
198 | ```php
199 | namespace Spatie\WebhookServer\Signer;
200 |
201 | interface Signer
202 | {
203 | public function signatureHeaderName(): string;
204 |
205 | public function calculateSignature(array $payload, string $secret): string;
206 | }
207 | ```
208 |
209 | After creating your signer, you can specify its class name in the `signer` key of the `webhook-server` config file. Your signer will then be used by default in all webhook calls.
210 |
211 | You can also specify a signer for a specific webhook call:
212 |
213 | ```php
214 | WebhookCall::create()
215 | ->signUsing(YourCustomSigner::class)
216 | ...
217 | ->dispatch();
218 | ```
219 |
220 | If you want to customize the name of the header, you don't need to use a custom signer, but you can change the value in the `signature_header_name` in the `webhook-server` config file.
221 |
222 | ### Retrying failed webhooks
223 |
224 | When the app to which we're sending the webhook fails to send a response with a `2xx` status code the package will consider the call as failed. The call will also be considered failed if the remote app doesn't respond within 3 seconds.
225 |
226 | You can configure that default timeout in the `timeout_in_seconds` key of the `webhook-server` config file. Alternatively, you can override the timeout for a specific webhook like this:
227 |
228 | ```php
229 | WebhookCall::create()
230 | ->timeoutInSeconds(5)
231 | ...
232 | ->dispatch();
233 | ```
234 |
235 | When a webhook call fails, we'll retry the call two more times. You can set the default amount of times we retry the webhook call in the `tries` key of the config file. Alternatively, you can specify the number of tries for a specific webhook like this:
236 |
237 | ```php
238 | WebhookCall::create()
239 | ->maximumTries(5)
240 | ...
241 | ->dispatch();
242 | ```
243 |
244 | To not hammer the remote app we'll wait some time between each attempt. By default, we wait 10 seconds between the first and second attempts, 100 seconds between the third and the fourth, 1000 between the fourth and the fifth, and so on. The maximum amount of seconds that we'll wait is 100 000, which is about 27 hours. This behavior is implemented in the default `ExponentialBackoffStrategy`.
245 |
246 | You can define your own backoff strategy by creating a class that implements `Spatie\WebhookServer\BackoffStrategy\BackoffStrategy`. This is what that interface looks like:
247 |
248 | ```php
249 | namespace Spatie\WebhookServer\BackoffStrategy;
250 |
251 | interface BackoffStrategy
252 | {
253 | public function waitInSecondsAfterAttempt(int $attempt): int;
254 | }
255 | ```
256 |
257 | You can make your custom strategy the default strategy by specifying its fully qualified class name in the `backoff_strategy` of the `webhook-server` config file. Alternatively, you can specify a strategy for a specific webhook like this.
258 |
259 | ```php
260 | WebhookCall::create()
261 | ->useBackoffStrategy(YourBackoffStrategy::class)
262 | ...
263 | ->dispatch();
264 | ```
265 |
266 | Under the hood, the retrying of the webhook calls is implemented using [delayed dispatching](https://laravel.com/docs/master/queues#delayed-dispatching). Amazon SQS only has support for a small maximum delay. If you're using Amazon SQS for your queues, make sure you do not configure the package in a way so there are more than 15 minutes between each attempt.
267 |
268 | ### Customizing the HTTP verb
269 |
270 | By default, all webhooks will use the `post` method. You can customize that by specifying the HTTP verb you want in the `http_verb` key of the `webhook-server` config file.
271 |
272 | You can also override the default for a specific call by using the `useHttpVerb` method.
273 |
274 | ```php
275 | WebhookCall::create()
276 | ->useHttpVerb('get')
277 | ...
278 | ->dispatch();
279 | ```
280 |
281 | ### Adding extra headers
282 |
283 | You can use extra headers by adding them to the `headers` key in the `webhook-server` config file. If you want to add additional headers for a specific webhook, you can use the `withHeaders` call.
284 |
285 | ```php
286 | WebhookCall::create()
287 | ->withHeaders([
288 | 'Another Header' => 'Value of Another Header'
289 | ])
290 | ...
291 | ->dispatch();
292 | ```
293 |
294 | ### Using a proxy
295 |
296 | You can direct webhooks through a proxy by specifying the `proxy` key in the `webhook-server` config file. To set a proxy for a specific
297 | request, you can use the `useProxy` call.
298 |
299 | ```php
300 | WebhookCall::create()
301 | ->useProxy('http://proxy.server:3128')
302 | ...
303 | ```
304 |
305 | ### Using mutual TLS authentication
306 |
307 | To safeguard the integrity of webhook data transmission, it's critical to authenticate the intended recipient of your webhook payload.
308 | Mutual TLS authentication serves as a robust method for this purpose. Contrary to standard TLS, where only the client verifies the server,
309 | mutual TLS requires both the webhook endpoint (acting as the client) and the webhook provider (acting as the server) to authenticate each other.
310 | This is achieved through an exchange of certificates during the TLS handshake, ensuring that both parties confirm each other's identity.
311 |
312 | > Note: If you need to include your own certificate authority, pass the certificate path to the `verifySsl()` method.
313 |
314 | ```php
315 | WebhookCall::create()
316 | ->mutualTls(
317 | certPath: storage_path('path/to/cert.pem'),
318 | certPassphrase: 'optional_cert_passphrase',
319 | sslKeyPath: storage_path('path/to/key.pem'),
320 | sslKeyPassphrase: 'optional_key_passphrase'
321 | )
322 | ```
323 |
324 | The proxy specification follows the [guzzlehttp proxy format](https://docs.guzzlephp.org/en/stable/request-options.html#proxy)
325 |
326 | ### Verifying the SSL certificate of the receiving app
327 |
328 | When using a URL that starts with `https://` the package will verify if the SSL certificate of the receiving party is valid. If it is not, we will consider the webhook call failed. We don't recommend this, but you can turn off this verification by setting the `verify_ssl` key in the `webhook-server` config file to `false`.
329 |
330 | You can also disable the verification per webhook call with the `doNotVerifySsl` method.
331 |
332 | ```php
333 | WebhookCall::create()
334 | ->doNotVerifySsl()
335 | ...
336 | ->dispatch();
337 | ```
338 |
339 | ### Adding meta information
340 |
341 | You can add extra meta information to the webhook. This meta information will not be transmitted, and it will only be used to pass to [the events this package fires](#events).
342 |
343 | This is how you can add meta information:
344 |
345 | ```php
346 | WebhookCall::create()
347 | ->meta($arrayWithMetaInformation)
348 | ...
349 | ->dispatch();
350 | ```
351 |
352 | ### Adding tags
353 |
354 | If you're using [Laravel Horizon](https://laravel.com/docs/5.8/horizon) for your queues, you'll be happy to know that we support [tags](https://laravel.com/docs/5.8/horizon#tags).
355 |
356 | To add tags to the underlying job that'll perform the webhook call, simply specify them in the `tags` key of the `webhook-server` config file or use the `withTags` method:
357 |
358 | ```php
359 | WebhookCall::create()
360 | ->withTags($tags)
361 | ...
362 | ->dispatch();
363 | ```
364 |
365 | ### Exception handling
366 | By default, the package will not log any exceptions that are thrown when sending a webhook.
367 |
368 | To handle exceptions you need to create listeners for the `Spatie\WebhookServer\Events\WebhookCallFailedEvent` and/or `Spatie\WebhookServer\Events\FinalWebhookCallFailedEvent` events.
369 |
370 | #### Retry failed execution
371 | By default, failing jobs will be ignored. To throw an exception when the last attempt of a job fails, you can call `throwExceptionOnFailure` :
372 | ```php
373 | WebhookCall::create()
374 | ->throwExceptionOnFailure()
375 | ...
376 | ->dispatch();
377 | ```
378 | or activate the `throw_exception_on_failure` global option of the `webhook-server` config file.
379 |
380 | ### Sending raw string body instead of JSON
381 |
382 | By default, all webhooks will transform the payload into JSON. Instead of sending JSON, you can send any string by using the `sendRawBody(string $body)` option instead.
383 |
384 | Due to a type mismatch in the Signer API, it is currently not supported to sign raw data requests.
385 | When using the _sendRawBody_ option, you will receive a _string_ payload in the WebhookEvents.
386 | ```php
387 | WebhookCall::create()
388 | ->sendRawBody("