diff --git a/docs/Webhooks.md b/docs/Webhooks.md index 3583d37..6aa392b 100644 --- a/docs/Webhooks.md +++ b/docs/Webhooks.md @@ -25,6 +25,16 @@ The `eventName` field on a delivery identifies what happened. See [`Vatly\API\Ty | `checkout.failed` | Checkout payment failed. | | `checkout.canceled` | Checkout was canceled. | | `checkout.expired` | Checkout session expired. | +| `webhook.setup` | Verification call sent when an endpoint is registered or its URL is updated. `entityType` is `webhook`; `object` is the (secret-free) endpoint config. | + +### The `webhook.setup` event + +When you register a webhook endpoint (or change its URL), Vatly sends a signed +`webhook.setup` event to confirm the endpoint is reachable. It is delivered as a normal +webhook — same envelope, same signature, same `Vatly-Event-Id` header — so there is +**nothing special to parse**: `Webhook::parse()` returns an ordinary `WebhookPayload`. +Just acknowledge it with a `2xx` and take no action; the `default` arm of an event +`match` already does this. --- diff --git a/src/API/Types/WebhookEventName.php b/src/API/Types/WebhookEventName.php index d325302..f2f8ff0 100644 --- a/src/API/Types/WebhookEventName.php +++ b/src/API/Types/WebhookEventName.php @@ -21,4 +21,11 @@ class WebhookEventName public const CHECKOUT_FAILED = 'checkout.failed'; public const CHECKOUT_CANCELED = 'checkout.canceled'; public const CHECKOUT_EXPIRED = 'checkout.expired'; + + /** + * Verification call sent when a webhook endpoint is registered or its URL is + * updated. Delivered as a normal webhook event with `entityType` `webhook`; + * receivers can acknowledge it (2xx) without running event-specific handling. + */ + public const WEBHOOK_SETUP = 'webhook.setup'; } diff --git a/tests/Webhooks/WebhookTest.php b/tests/Webhooks/WebhookTest.php index ee7138f..4c8a42e 100644 --- a/tests/Webhooks/WebhookTest.php +++ b/tests/Webhooks/WebhookTest.php @@ -7,6 +7,7 @@ use InvalidArgumentException; use PHPUnit\Framework\TestCase; use Vatly\API\Exceptions\InvalidSignatureException; +use Vatly\API\Types\WebhookEventName; use Vatly\API\Webhooks\Webhook; use Vatly\API\Webhooks\WebhookPayload; @@ -78,6 +79,36 @@ public function test_it_parses_a_webhook_without_object(): void $this->assertNull($event->object); } + public function test_it_parses_a_webhook_setup_event(): void + { + $payload = $this->makePayload([ + 'id' => 'webhook_event_St9uVwXyZa1BcDeFgHiJ', + 'eventName' => WebhookEventName::WEBHOOK_SETUP, + 'entityType' => 'webhook', + 'entityId' => 'webhook_QdEpFhdSrG4Y3DnfsdqsH', + 'object' => [ + 'resource' => 'webhook', + 'id' => 'webhook_QdEpFhdSrG4Y3DnfsdqsH', + 'url' => 'https://merchant.example/webhooks/vatly', + 'testmode' => true, + ], + ]); + $signature = $this->sign($payload); + + $event = Webhook::parse($payload, $signature, $this->secret); + + // The setup call is a normal WebhookPayload — no special-casing on parse. + $this->assertInstanceOf(WebhookPayload::class, $event); + $this->assertSame('webhook.setup', WebhookEventName::WEBHOOK_SETUP); + $this->assertSame(WebhookEventName::WEBHOOK_SETUP, $event->eventName); + $this->assertSame('webhook', $event->entityType); + $this->assertSame('webhook_QdEpFhdSrG4Y3DnfsdqsH', $event->entityId); + $this->assertSame('webhook_event', $event->resource); + $this->assertSame('webhook_event_St9uVwXyZa1BcDeFgHiJ', $event->id); + $this->assertIsObject($event->object); + $this->assertSame('webhook', $event->object->resource); + } + public function test_it_throws_for_invalid_signature(): void { $this->expectException(InvalidSignatureException::class);