Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/Webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Every delivery carries a [`WebhookEvent`](../src/API/Resources/WebhookEvent.php)
| `entityType` | `string` | Type of the related resource (e.g. `order`, `refund`, `subscription`). |
| `entityId` | `string` | ID of the related resource (e.g. `order_Hn5xWqVfKm8RjTgYbUcP`). |
| `object` | `object\|null` | The full resource payload at the time of the event. Shape depends on `entityType`. |
| `createdAt` | `string` | When the event occurred, ISO 8601 (e.g. `2024-01-15T10:30:00+00:00`). |
| `testmode` | `bool` | `true` when the event was produced by a test-mode resource. |
| `links` | `object` | HATEOAS links — `links.self.href` points to this webhook event. |

### Example payload
Expand All @@ -54,6 +56,8 @@ Every delivery carries a [`WebhookEvent`](../src/API/Resources/WebhookEvent.php)
"status": "paid",
"total": { "value": "29.99", "currency": "EUR" }
},
"createdAt": "2024-01-15T10:30:00+00:00",
"testmode": false,
"links": {
"self": {
"href": "https://api.vatly.com/v1/webhook-events/webhook_event_Qk8pRtSvWm2NjLhYcZaE",
Expand Down
13 changes: 13 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2050,6 +2050,8 @@ paths:
subtotal:
value: '24.79'
currency: EUR
createdAt: '2024-01-15T10:30:00Z'
testmode: false
links:
self:
href: https://api.vatly.com/v1/webhook-events/webhook_event_Qk8pRtSvWm2NjLhYcZaE
Expand Down Expand Up @@ -3947,6 +3949,8 @@ components:
- entityType
- entityId
- object
- createdAt
- testmode
- links
properties:
id:
Expand Down Expand Up @@ -3988,6 +3992,15 @@ components:
description: |
The full resource payload at the time of the event.
The shape depends on the `entityType` (e.g., Order, Refund, Chargeback, Subscription, Checkout).
createdAt:
type: string
format: date-time
description: When this event occurred (ISO 8601 format)
example: '2024-01-15T10:30:00Z'
testmode:
type: boolean
description: Whether this event was produced in test mode
example: false
links:
type: object
description: HATEOAS links to related resources
Expand Down
12 changes: 12 additions & 0 deletions src/API/Resources/WebhookEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,17 @@ class WebhookEvent extends BaseResource
*/
public $object = null;

/**
* When this event occurred (ISO 8601 format).
*
* @example 2023-08-11T10:48:51+02:00
*/
public string $createdAt;

/**
* Whether this event was produced in test mode.
*/
public bool $testmode;

public WebhookEventLinks $links;
}
4 changes: 3 additions & 1 deletion src/API/Webhooks/Webhook.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public static function parse(string $payload, string $signature, string $secret)
);
}

foreach (['id', 'resource', 'eventName', 'entityType', 'entityId'] as $field) {
foreach (['id', 'resource', 'eventName', 'entityType', 'entityId', 'createdAt', 'testmode'] as $field) {
if (! isset($decoded->{$field})) {
throw new \InvalidArgumentException(
"Webhook payload is missing required field: {$field}"
Expand All @@ -56,6 +56,8 @@ public static function parse(string $payload, string $signature, string $secret)
$decoded->eventName,
$decoded->entityType,
$decoded->entityId,
$decoded->createdAt,
$decoded->testmode,
$object,
);
}
Expand Down
16 changes: 16 additions & 0 deletions src/API/Webhooks/WebhookPayload.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,35 @@ class WebhookPayload
*/
public ?object $object;

/**
* When this event occurred (ISO 8601 format).
*
* @example 2023-08-11T10:48:51+02:00
*/
public string $createdAt;

/**
* Whether this event was produced in test mode.
*/
public bool $testmode;

public function __construct(
string $id,
string $resource,
string $eventName,
string $entityType,
string $entityId,
string $createdAt,
bool $testmode,
?object $object = null,
) {
$this->id = $id;
$this->resource = $resource;
$this->eventName = $eventName;
$this->entityType = $entityType;
$this->entityId = $entityId;
$this->createdAt = $createdAt;
$this->testmode = $testmode;
$this->object = $object;
}
}
4 changes: 4 additions & 0 deletions tests/Endpoints/WebhookEventEndpointTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public function can_get_webhook_event(): void
'total' => ['value' => '29.99', 'currency' => 'EUR'],
'subtotal' => ['value' => '24.79', 'currency' => 'EUR'],
],
'createdAt' => '2024-01-15T10:30:00+00:00',
'testmode' => false,
'links' => [
'self' => [
'href' => self::API_ENDPOINT_URL.'/webhook-events/'.$webhookEventId,
Expand All @@ -56,6 +58,8 @@ public function can_get_webhook_event(): void
$this->assertEquals(WebhookEventType::ORDER_PAID, $event->eventName);
$this->assertEquals('order', $event->entityType);
$this->assertEquals($orderId, $event->entityId);
$this->assertEquals('2024-01-15T10:30:00+00:00', $event->createdAt);
$this->assertFalse($event->testmode);
$this->assertIsObject($event->object);
$this->assertEquals($orderId, $event->object->id);
$this->assertEquals('paid', $event->object->status);
Expand Down
20 changes: 20 additions & 0 deletions tests/Webhooks/WebhookTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ private function makePayload(array $overrides = []): string
'entityType' => 'order',
'entityId' => 'order_Hn5xWqVfKm8RjTgYbUcP',
'object' => ['id' => 'order_Hn5xWqVfKm8RjTgYbUcP', 'resource' => 'order'],
'createdAt' => '2024-01-15T10:30:00+00:00',
'testmode' => false,
], $overrides);

return json_encode($data);
Expand Down Expand Up @@ -50,10 +52,22 @@ public function test_it_parses_a_valid_webhook(): void
$this->assertSame('order.paid', $event->eventName);
$this->assertSame('order', $event->entityType);
$this->assertSame('order_Hn5xWqVfKm8RjTgYbUcP', $event->entityId);
$this->assertSame('2024-01-15T10:30:00+00:00', $event->createdAt);
$this->assertFalse($event->testmode);
$this->assertIsObject($event->object);
$this->assertSame('order_Hn5xWqVfKm8RjTgYbUcP', $event->object->id);
}

public function test_it_parses_testmode_true(): void
{
$payload = $this->makePayload(['testmode' => true]);
$signature = $this->sign($payload);

$event = Webhook::parse($payload, $signature, $this->secret);

$this->assertTrue($event->testmode);
}

public function test_it_parses_a_webhook_without_object(): void
{
$data = [
Expand All @@ -62,6 +76,8 @@ public function test_it_parses_a_webhook_without_object(): void
'eventName' => 'order.paid',
'entityType' => 'order',
'entityId' => 'order_Hn5xWqVfKm8RjTgYbUcP',
'createdAt' => '2024-01-15T10:30:00+00:00',
'testmode' => false,
];
$payload = json_encode($data);
$signature = $this->sign($payload);
Expand Down Expand Up @@ -112,6 +128,8 @@ public function test_it_throws_when_required_field_is_missing(string $field): vo
'eventName' => 'order.paid',
'entityType' => 'order',
'entityId' => 'order_Hn5xWqVfKm8RjTgYbUcP',
'createdAt' => '2024-01-15T10:30:00+00:00',
'testmode' => false,
];
unset($data[$field]);

Expand All @@ -128,6 +146,8 @@ public function requiredFieldProvider(): array
'eventName' => ['eventName'],
'entityType' => ['entityType'],
'entityId' => ['entityId'],
'createdAt' => ['createdAt'],
'testmode' => ['testmode'],
];
}

Expand Down