API Events¶
Overview¶
The Gold Standard Module API provides event notifications through webhooks, allowing external systems to react to changes in real-time. This document covers the webhook system, event payloads, and integration patterns.
Webhook Architecture¶
flowchart TB
subgraph "Event Flow"
A[Domain Event] --> B[Event Handler]
B --> C[Webhook Dispatcher]
C --> D{Queue}
D --> E[HTTP POST to Webhook URL]
E --> F{Response}
F -->|Success| G[Mark Delivered]
F -->|Failure| H[Retry Queue]
H --> D
end Webhook Configuration¶
Registering Webhooks¶
Webhooks can be registered via the Admin panel or API:
POST /api/v1/webhooks
Content-Type: application/json
Authorization: Bearer {token}
{
"url": "https://your-domain.com/webhook/goldstandard",
"events": [
"article.created",
"article.published",
"article.updated",
"article.deleted",
"comment.created",
"comment.approved"
],
"secret": "your-webhook-secret",
"active": true
}
Response¶
{
"data": {
"id": "01HXK5PWGM3QZXN8VBCD9EF2GH",
"type": "webhook",
"attributes": {
"url": "https://your-domain.com/webhook/goldstandard",
"events": [
"article.created",
"article.published"
],
"active": true,
"created_at": "2026-01-15T10:30:00Z"
}
}
}
Event Types¶
Article Events¶
| Event | Description | Trigger |
|---|---|---|
article.created | New article created (draft) | Article saved for first time |
article.published | Article made public | Status changed to published |
article.updated | Article content modified | Any field updated |
article.deleted | Article removed | Article deleted (soft or hard) |
article.archived | Article moved to archive | Status changed to archived |
article.featured | Article marked as featured | Featured flag toggled |
Category Events¶
| Event | Description | Trigger |
|---|---|---|
category.created | New category added | Category created |
category.updated | Category modified | Category details changed |
category.deleted | Category removed | Category deleted |
category.reordered | Category order changed | Sort order updated |
Comment Events¶
| Event | Description | Trigger |
|---|---|---|
comment.created | New comment posted | Comment submitted |
comment.approved | Comment approved by moderator | Status changed to approved |
comment.rejected | Comment rejected | Status changed to rejected |
comment.deleted | Comment removed | Comment deleted |
User Events¶
| Event | Description | Trigger |
|---|---|---|
user.subscribed | User subscribed to notifications | Subscription created |
user.unsubscribed | User unsubscribed | Subscription removed |
Event Payloads¶
Common Payload Structure¶
All webhook payloads follow this structure:
{
"id": "evt_01HXK5PWGM3QZXN8VBCD9EF2GH",
"type": "article.published",
"created_at": "2026-01-15T10:30:00Z",
"data": {
// Event-specific data
},
"metadata": {
"module": "goldstandard",
"version": "1.0",
"site_url": "https://your-site.com"
}
}
Article Published Event¶
{
"id": "evt_01HXK5PWGM3QZXN8VBCD9EF2GH",
"type": "article.published",
"created_at": "2026-01-15T10:30:00Z",
"data": {
"article": {
"id": "01HXK5MWDJ6QZXN8VBCD9EF2GH",
"title": "Getting Started with XOOPS 2026",
"slug": "getting-started-xoops-2026",
"excerpt": "Learn how to set up and configure XOOPS 2026...",
"url": "https://your-site.com/modules/goldstandard/article/getting-started-xoops-2026-01HXK5MWDJ6QZXN8VBCD9EF2GH.html",
"author": {
"id": 1,
"username": "admin",
"display_name": "Site Admin"
},
"categories": [
{
"id": "01HXK5NRKS2QZXN8VBCD9EF2GH",
"name": "Tutorials",
"slug": "tutorials"
}
],
"tags": ["xoops", "tutorial", "getting-started"],
"published_at": "2026-01-15T10:30:00Z",
"featured_image": "https://your-site.com/uploads/goldstandard/featured-image.jpg"
},
"previous_status": "draft"
},
"metadata": {
"module": "goldstandard",
"version": "1.0",
"site_url": "https://your-site.com"
}
}
Article Updated Event¶
{
"id": "evt_01HXK5RWGM3QZXN8VBCD9EF2GH",
"type": "article.updated",
"created_at": "2026-01-15T11:00:00Z",
"data": {
"article": {
"id": "01HXK5MWDJ6QZXN8VBCD9EF2GH",
"title": "Getting Started with XOOPS 2026 (Updated)",
"url": "https://your-site.com/modules/goldstandard/article/..."
},
"changes": {
"title": {
"old": "Getting Started with XOOPS 2026",
"new": "Getting Started with XOOPS 2026 (Updated)"
},
"content": {
"changed": true
}
},
"updated_by": {
"id": 1,
"username": "admin"
}
}
}
Comment Created Event¶
{
"id": "evt_01HXK5SWGM3QZXN8VBCD9EF2GH",
"type": "comment.created",
"created_at": "2026-01-15T12:00:00Z",
"data": {
"comment": {
"id": "01HXK5TWGM3QZXN8VBCD9EF2GH",
"content": "Great article! Very helpful.",
"author": {
"id": 5,
"username": "johndoe",
"display_name": "John Doe"
},
"status": "pending",
"created_at": "2026-01-15T12:00:00Z"
},
"article": {
"id": "01HXK5MWDJ6QZXN8VBCD9EF2GH",
"title": "Getting Started with XOOPS 2026",
"url": "https://your-site.com/modules/goldstandard/article/..."
},
"parent_comment": null
}
}
Webhook Security¶
Signature Verification¶
All webhook requests include a signature header for verification:
Verifying Signatures (PHP)¶
function verifyWebhookSignature(
string $payload,
string $signature,
string $secret
): bool {
$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
return hash_equals($expected, $signature);
}
// In your webhook handler
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_GOLDSTANDARD_SIGNATURE'] ?? '';
$secret = 'your-webhook-secret';
if (!verifyWebhookSignature($payload, $signature, $secret)) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($payload, true);
// Process event...
Verifying Signatures (Node.js)¶
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
Retry Policy¶
Failed webhook deliveries are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 8 hours |
| 7 | 24 hours |
After 7 failed attempts, the webhook is marked as failed and an admin notification is sent.
Webhook Responses¶
Your endpoint should respond with a 2xx status code to indicate successful receipt:
Responses with 4xx or 5xx status codes trigger retries.
Event Subscription API¶
List Webhooks¶
Get Webhook¶
Update Webhook¶
PATCH /api/v1/webhooks/{webhook_id}
Content-Type: application/json
Authorization: Bearer {token}
{
"events": ["article.published", "article.updated"],
"active": true
}
Delete Webhook¶
List Webhook Deliveries¶
Retry Failed Delivery¶
PHP Event Dispatching¶
Dispatching Webhook Events¶
namespace Xoops\Modules\GoldStandard\Infrastructure\Webhook;
class WebhookDispatcher
{
public function __construct(
private readonly WebhookRepository $webhooks,
private readonly HttpClient $client,
private readonly QueueInterface $queue
) {}
public function dispatch(string $eventType, array $data): void
{
$webhooks = $this->webhooks->findByEvent($eventType);
foreach ($webhooks as $webhook) {
if (!$webhook->isActive()) {
continue;
}
$payload = $this->buildPayload($eventType, $data);
// Queue for async delivery
$this->queue->push(new DeliverWebhookJob(
webhookId: $webhook->getId(),
payload: $payload
));
}
}
private function buildPayload(string $eventType, array $data): array
{
return [
'id' => 'evt_' . Ulid::generate(),
'type' => $eventType,
'created_at' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM),
'data' => $data,
'metadata' => [
'module' => 'goldstandard',
'version' => '1.0',
'site_url' => XOOPS_URL
]
];
}
}
Webhook Delivery Job¶
namespace Xoops\Modules\GoldStandard\Infrastructure\Webhook;
class DeliverWebhookJob
{
public function __construct(
public readonly string $webhookId,
public readonly array $payload,
public readonly int $attempt = 1
) {}
public function handle(
WebhookRepository $webhooks,
HttpClient $client,
QueueInterface $queue
): void {
$webhook = $webhooks->findById($this->webhookId);
if (!$webhook) {
return;
}
$signature = $this->generateSignature(
json_encode($this->payload),
$webhook->getSecret()
);
try {
$response = $client->post($webhook->getUrl(), [
'json' => $this->payload,
'headers' => [
'Content-Type' => 'application/json',
'X-GoldStandard-Signature' => $signature,
'X-GoldStandard-Event' => $this->payload['type'],
'X-GoldStandard-Delivery' => $this->payload['id']
],
'timeout' => 30
]);
$this->logDelivery($webhook, $response, 'success');
} catch (\Exception $e) {
$this->logDelivery($webhook, null, 'failed', $e->getMessage());
if ($this->attempt < 7) {
$delay = $this->getRetryDelay($this->attempt);
$queue->later($delay, new self(
webhookId: $this->webhookId,
payload: $this->payload,
attempt: $this->attempt + 1
));
}
}
}
private function generateSignature(string $payload, string $secret): string
{
return 'sha256=' . hash_hmac('sha256', $payload, $secret);
}
private function getRetryDelay(int $attempt): int
{
return match($attempt) {
1 => 60, // 1 minute
2 => 300, // 5 minutes
3 => 1800, // 30 minutes
4 => 7200, // 2 hours
5 => 28800, // 8 hours
6 => 86400, // 24 hours
default => 86400
};
}
}
Testing Webhooks¶
Test Endpoint¶
Use the test endpoint to verify your webhook configuration:
This sends a test event to your webhook URL:
{
"id": "evt_test_01HXK5UWGM3QZXN8VBCD9EF2GH",
"type": "webhook.test",
"created_at": "2026-01-15T10:30:00Z",
"data": {
"message": "This is a test webhook delivery"
},
"metadata": {
"module": "goldstandard",
"version": "1.0",
"site_url": "https://your-site.com"
}
}
Best Practices¶
- Respond Quickly: Return 2xx within 30 seconds
- Process Asynchronously: Queue webhook data for processing
- Verify Signatures: Always verify webhook signatures
- Handle Duplicates: Events may be delivered more than once
- Log Everything: Keep records for debugging
- Monitor Failures: Set up alerts for failed deliveries