@agility/management-sdk 0.1.39 → 0.2.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/apiClient.d.ts +2 -0
- package/dist/apiClient.js +2 -0
- package/dist/apiMethods/searchMethods.d.ts +16 -0
- package/dist/apiMethods/searchMethods.js +81 -0
- package/dist/apiMethods/webhookMethods.d.ts +11 -2
- package/dist/apiMethods/webhookMethods.js +58 -7
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/models/contentItem.d.ts +26 -0
- package/dist/models/contentItem.js +7 -1
- package/dist/models/geo.d.ts +38 -0
- package/dist/models/geo.js +31 -0
- package/dist/models/pageItem.d.ts +11 -0
- package/dist/models/pagedResult.d.ts +4 -0
- package/dist/models/search.d.ts +114 -0
- package/dist/models/search.js +45 -0
- package/dist/models/searchNotAvailableException.d.ts +12 -0
- package/dist/models/searchNotAvailableException.js +41 -0
- package/dist/models/webhook.d.ts +17 -0
- package/dist/models/webhookHistory.d.ts +32 -0
- package/dist/models/webhookHistory.js +9 -0
- package/docs/content-methods.md +10 -0
- package/docs/page-methods.md +32 -0
- package/docs/search-methods.md +129 -0
- package/docs/webhook-methods.md +253 -354
- package/package.json +2 -2
package/docs/webhook-methods.md
CHANGED
|
@@ -2,437 +2,337 @@
|
|
|
2
2
|
|
|
3
3
|
## WebhookMethods
|
|
4
4
|
|
|
5
|
-
This class provides
|
|
5
|
+
This class provides webhook management operations for Agility CMS. Webhooks deliver real-time HTTP POST notifications to your endpoints when content is saved, published, or moved through workflow, enabling integration with external systems and automated builds.
|
|
6
6
|
|
|
7
7
|
**Important Notes:**
|
|
8
|
-
- Webhooks
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
8
|
+
- Webhooks fire on three event categories: content save, content publish, and content workflow events — each can be toggled per webhook
|
|
9
|
+
- Webhook IDs are string GUIDs (the `rowKey` of the stored record), not numbers
|
|
10
|
+
- Signed delivery is **opt-in per webhook** (`secureDeliveryEnabled`, off by default): when enabled, deliveries carry [Standard Webhooks](https://www.standardwebhooks.com/) signature headers — see [Verifying deliveries](#verifying-deliveries-signing) below. Webhooks with it off (including every webhook created before the feature shipped) are delivered unsigned, exactly as before
|
|
11
|
+
- List responses never include signing secrets; `getWebhook` masks the signing secret unless the caller has *FullControl* permission on webhooks
|
|
12
|
+
- Rotating a signing secret requires *FullControl* permission; reading delivery history requires *Manage* permission
|
|
13
|
+
- Failed deliveries are retried automatically when retries are enabled — see [Retry behavior](#retry-behavior)
|
|
14
14
|
|
|
15
15
|
### Function List
|
|
16
|
+
- [webhookList](#webhooklist) - Retrieves a paged list of webhooks
|
|
16
17
|
- [getWebhook](#getwebhook) - Retrieves a specific webhook by ID
|
|
17
|
-
- [webhookList](#webhooklist) - Retrieves list of all webhooks
|
|
18
18
|
- [saveWebhook](#savewebhook) - Creates or updates a webhook
|
|
19
19
|
- [deleteWebhook](#deletewebhook) - Deletes a webhook by ID
|
|
20
|
+
- [getWebhookHistory](#getwebhookhistory) - Retrieves the delivery history for a webhook
|
|
21
|
+
- [rotateWebhookSecret](#rotatewebhooksecret) - Rotates a webhook's signing secret
|
|
22
|
+
|
|
23
|
+
### Reference
|
|
24
|
+
- [The Webhook object](#the-webhook-object)
|
|
25
|
+
- [Verifying deliveries (signing)](#verifying-deliveries-signing)
|
|
26
|
+
- [Retry behavior](#retry-behavior)
|
|
20
27
|
|
|
21
28
|
---
|
|
22
29
|
|
|
23
|
-
###
|
|
30
|
+
### webhookList
|
|
24
31
|
|
|
25
|
-
Retrieves a
|
|
32
|
+
Retrieves a paged list of webhooks for the instance. Uses continuation-token paging: pass the `token` from a previous response to fetch the next page. Signing secrets are always stripped from list responses.
|
|
26
33
|
|
|
27
34
|
| Parameter | Type | Required | Description |
|
|
28
35
|
|-----------|------|----------|-------------|
|
|
29
|
-
| `webhookID` | `number` | Yes | The webhook ID to retrieve |
|
|
30
36
|
| `guid` | `string` | Yes | Current website GUID |
|
|
37
|
+
| `take` | `number` | No | Number of records to take (default: 20) |
|
|
38
|
+
| `token` | `string` | No | Continuation token from a previous response (default: null) |
|
|
31
39
|
|
|
32
|
-
**Returns:** `Webhook
|
|
40
|
+
**Returns:** `TokenPagedResult<Webhook>` - `{ token, items }` where `token` is the continuation token for the next page (or null when there are no more results) and `items` is the array of webhooks
|
|
33
41
|
|
|
34
42
|
**Usage Example:**
|
|
35
43
|
```typescript
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
console.log(
|
|
40
|
-
|
|
41
|
-
console.log('Created:', webhook.createdDate);
|
|
42
|
-
console.log('Last triggered:', webhook.lastTriggered);
|
|
43
|
-
|
|
44
|
-
// Display webhook statistics
|
|
45
|
-
if (webhook.statistics) {
|
|
46
|
-
console.log('Statistics:');
|
|
47
|
-
console.log(` Total deliveries: ${webhook.statistics.totalDeliveries}`);
|
|
48
|
-
console.log(` Successful deliveries: ${webhook.statistics.successfulDeliveries}`);
|
|
49
|
-
console.log(` Failed deliveries: ${webhook.statistics.failedDeliveries}`);
|
|
50
|
-
console.log(` Success rate: ${webhook.statistics.successRate}%`);
|
|
51
|
-
}
|
|
44
|
+
// First page
|
|
45
|
+
let page = await apiClient.webhookMethods.webhookList(guid, 20);
|
|
46
|
+
page.items.forEach(webhook => {
|
|
47
|
+
console.log(`${webhook.name} -> ${webhook.url} (enabled: ${webhook.enabled})`);
|
|
48
|
+
});
|
|
52
49
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
console.log(` Retry count: ${webhook.retryCount}`);
|
|
59
|
-
|
|
60
|
-
// Check authentication
|
|
61
|
-
if (webhook.authentication) {
|
|
62
|
-
console.log('Authentication enabled:');
|
|
63
|
-
console.log(` Type: ${webhook.authentication.type}`);
|
|
64
|
-
console.log(` Headers: ${Object.keys(webhook.authentication.headers).join(', ')}`);
|
|
65
|
-
} else {
|
|
66
|
-
console.log('No authentication configured');
|
|
50
|
+
// Fetch all pages
|
|
51
|
+
const allWebhooks = [...page.items];
|
|
52
|
+
while (page.token) {
|
|
53
|
+
page = await apiClient.webhookMethods.webhookList(guid, 20, page.token);
|
|
54
|
+
allWebhooks.push(...page.items);
|
|
67
55
|
}
|
|
56
|
+
console.log(`Total webhooks: ${allWebhooks.length}`);
|
|
68
57
|
```
|
|
69
58
|
|
|
70
|
-
**Response Properties:**
|
|
71
|
-
The `Webhook` object includes:
|
|
72
|
-
- `webhookID`: Unique identifier for the webhook
|
|
73
|
-
- `name`: Human-readable name for the webhook
|
|
74
|
-
- `url`: Target URL for webhook deliveries
|
|
75
|
-
- `events`: Array of events that trigger the webhook
|
|
76
|
-
- `isActive`: Whether the webhook is active
|
|
77
|
-
- `method`: HTTP method (GET, POST, PUT, etc.)
|
|
78
|
-
- `contentType`: Content type for webhook payload
|
|
79
|
-
- `timeout`: Request timeout in milliseconds
|
|
80
|
-
- `retryCount`: Number of retry attempts for failed deliveries
|
|
81
|
-
- `authentication`: Authentication configuration
|
|
82
|
-
- `statistics`: Delivery statistics and metrics
|
|
83
|
-
|
|
84
59
|
**Error Handling:**
|
|
85
|
-
- Throws `Exception` when
|
|
60
|
+
- Throws `Exception` when retrieval fails
|
|
86
61
|
|
|
87
|
-
###
|
|
62
|
+
### getWebhook
|
|
88
63
|
|
|
89
|
-
Retrieves a
|
|
64
|
+
Retrieves a specific webhook by its ID (the `rowKey` GUID).
|
|
90
65
|
|
|
91
66
|
| Parameter | Type | Required | Description |
|
|
92
67
|
|-----------|------|----------|-------------|
|
|
93
68
|
| `guid` | `string` | Yes | Current website GUID |
|
|
94
|
-
| `
|
|
95
|
-
| `token` | `string` | No | Token parameter (default: null) |
|
|
69
|
+
| `webhookID` | `string` | Yes | The webhook ID (`rowKey`) to retrieve |
|
|
96
70
|
|
|
97
|
-
**Returns:** `
|
|
71
|
+
**Returns:** `Webhook` - The webhook object. The `signingSecret` is masked unless the caller has *FullControl* permission on webhooks — the same bar as rotating it.
|
|
98
72
|
|
|
99
73
|
**Usage Example:**
|
|
100
74
|
```typescript
|
|
101
|
-
const
|
|
102
|
-
console.log(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
console.log(` Active: ${webhook.isActive ? 'Yes' : 'No'}`);
|
|
110
|
-
console.log(` Last triggered: ${webhook.lastTriggered || 'Never'}`);
|
|
111
|
-
console.log('---');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// Filter active webhooks
|
|
115
|
-
const activeWebhooks = webhooks.filter(w => w.isActive);
|
|
116
|
-
const inactiveWebhooks = webhooks.filter(w => !w.isActive);
|
|
117
|
-
console.log(`Active webhooks: ${activeWebhooks.length}`);
|
|
118
|
-
console.log(`Inactive webhooks: ${inactiveWebhooks.length}`);
|
|
119
|
-
|
|
120
|
-
// Group webhooks by event type
|
|
121
|
-
const webhooksByEvent = webhooks.reduce((acc, webhook) => {
|
|
122
|
-
webhook.events.forEach(event => {
|
|
123
|
-
if (!acc[event]) acc[event] = [];
|
|
124
|
-
acc[event].push(webhook);
|
|
125
|
-
});
|
|
126
|
-
return acc;
|
|
127
|
-
}, {});
|
|
128
|
-
|
|
129
|
-
console.log('Webhooks by event:');
|
|
130
|
-
Object.entries(webhooksByEvent).forEach(([event, eventWebhooks]) => {
|
|
131
|
-
console.log(` ${event}: ${eventWebhooks.length} webhooks`);
|
|
75
|
+
const webhook = await apiClient.webhookMethods.getWebhook(guid, webhookID);
|
|
76
|
+
console.log('Webhook name:', webhook.name);
|
|
77
|
+
console.log('Webhook URL:', webhook.url);
|
|
78
|
+
console.log('Enabled:', webhook.enabled);
|
|
79
|
+
console.log('Events:', {
|
|
80
|
+
save: webhook.contentSaveEvents,
|
|
81
|
+
publish: webhook.contentPublishEvents,
|
|
82
|
+
workflow: webhook.contentWorkflowEvents
|
|
132
83
|
});
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
webhook.statistics && webhook.statistics.successRate < 90
|
|
137
|
-
);
|
|
138
|
-
console.log(`Webhooks with high failure rate: ${failingWebhooks.length}`);
|
|
139
|
-
|
|
140
|
-
// Create webhook dropdown options
|
|
141
|
-
const webhookOptions = webhooks.map(webhook => ({
|
|
142
|
-
value: webhook.webhookID,
|
|
143
|
-
label: webhook.name,
|
|
144
|
-
url: webhook.url,
|
|
145
|
-
active: webhook.isActive
|
|
146
|
-
}));
|
|
84
|
+
console.log('Retries:', webhook.retriesEnabled
|
|
85
|
+
? `${webhook.retryCount} retries at ${webhook.retrySpeed} speed`
|
|
86
|
+
: 'disabled');
|
|
147
87
|
```
|
|
148
88
|
|
|
149
|
-
**Common Webhook Events:**
|
|
150
|
-
- `content.created`: New content item created
|
|
151
|
-
- `content.updated`: Content item updated
|
|
152
|
-
- `content.deleted`: Content item deleted
|
|
153
|
-
- `content.published`: Content item published
|
|
154
|
-
- `content.unpublished`: Content item unpublished
|
|
155
|
-
- `page.created`: New page created
|
|
156
|
-
- `page.updated`: Page updated
|
|
157
|
-
- `page.deleted`: Page deleted
|
|
158
|
-
- `page.published`: Page published
|
|
159
|
-
- `page.unpublished`: Page unpublished
|
|
160
|
-
- `model.created`: Content model created
|
|
161
|
-
- `model.updated`: Content model updated
|
|
162
|
-
- `asset.uploaded`: Asset uploaded
|
|
163
|
-
- `asset.updated`: Asset updated
|
|
164
|
-
|
|
165
89
|
**Error Handling:**
|
|
166
|
-
- Throws `Exception` when retrieval fails
|
|
90
|
+
- Throws `Exception` when the webhook is not found or retrieval fails
|
|
167
91
|
|
|
168
92
|
### saveWebhook
|
|
169
93
|
|
|
170
94
|
Creates a new webhook or updates an existing one.
|
|
171
95
|
|
|
96
|
+
**To update an existing webhook, the `partitionKey` and `rowKey` from a previously retrieved webhook must be present on the object** — without them the API treats the save as a create and mints a new webhook. The easiest pattern is get → modify → save.
|
|
97
|
+
|
|
98
|
+
Signed delivery is opt-in per webhook: set `secureDeliveryEnabled: true` and the server mints a signing secret on that save (webhooks with it off — including every webhook created before this feature — are delivered unsigned, exactly as before). Any `signingSecret` value you send is ignored; secrets are always server-controlled. Use [rotateWebhookSecret](#rotatewebhooksecret) to change one.
|
|
99
|
+
|
|
100
|
+
The response sets **`signingSecretJustCreated: true`** on exactly the save that mints the secret, so you can tell a newly created secret from one that was already there. It is a property of the response, never of the stored webhook — `getWebhook` never returns it. Note that flipping `secureDeliveryEnabled` off and back on does **not** mint a new secret: the original is retained, and the flag stays `false`.
|
|
101
|
+
|
|
172
102
|
| Parameter | Type | Required | Description |
|
|
173
103
|
|-----------|------|----------|-------------|
|
|
174
|
-
| `webhook` | `Webhook` | Yes | Webhook object to save |
|
|
175
104
|
| `guid` | `string` | Yes | Current website GUID |
|
|
105
|
+
| `webhook` | `Webhook` | Yes | Webhook object to save |
|
|
176
106
|
|
|
177
|
-
**Returns:** `Webhook` -
|
|
107
|
+
**Returns:** `Webhook` - The saved webhook object
|
|
178
108
|
|
|
179
109
|
**Usage Example:**
|
|
180
110
|
```typescript
|
|
111
|
+
import { Webhook } from '@agility/management-sdk';
|
|
112
|
+
|
|
181
113
|
// Create a new webhook
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const savedWebhook = await apiClient.webhookMethods.saveWebhook(newWebhook, guid);
|
|
205
|
-
console.log('Webhook created with ID:', savedWebhook.webhookID);
|
|
206
|
-
|
|
207
|
-
// Update existing webhook
|
|
208
|
-
const existingWebhook = await apiClient.webhookMethods.getWebhook(123, guid);
|
|
209
|
-
existingWebhook.name = 'Updated Webhook Name';
|
|
210
|
-
existingWebhook.url = 'https://api.example.com/webhooks/updated-endpoint';
|
|
211
|
-
existingWebhook.events.push('page.published');
|
|
212
|
-
|
|
213
|
-
const updatedWebhook = await apiClient.webhookMethods.saveWebhook(existingWebhook, guid);
|
|
214
|
-
console.log('Webhook updated:', updatedWebhook.name);
|
|
215
|
-
|
|
216
|
-
// Create webhook with custom payload template
|
|
217
|
-
const customWebhook = {
|
|
218
|
-
name: 'Custom Payload Webhook',
|
|
219
|
-
url: 'https://api.example.com/webhooks/custom',
|
|
220
|
-
events: ['content.published'],
|
|
221
|
-
isActive: true,
|
|
222
|
-
method: 'POST',
|
|
223
|
-
contentType: 'application/json',
|
|
224
|
-
timeout: 15000,
|
|
225
|
-
retryCount: 2,
|
|
226
|
-
payloadTemplate: {
|
|
227
|
-
eventType: '{{event}}',
|
|
228
|
-
timestamp: '{{timestamp}}',
|
|
229
|
-
contentData: {
|
|
230
|
-
id: '{{content.id}}',
|
|
231
|
-
title: '{{content.title}}',
|
|
232
|
-
referenceName: '{{content.referenceName}}'
|
|
233
|
-
},
|
|
234
|
-
customField: 'custom value'
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
// Create webhook with signature verification
|
|
239
|
-
const secureWebhook = {
|
|
240
|
-
name: 'Secure Webhook',
|
|
241
|
-
url: 'https://api.example.com/webhooks/secure',
|
|
242
|
-
events: ['content.created', 'content.updated'],
|
|
243
|
-
isActive: true,
|
|
244
|
-
method: 'POST',
|
|
245
|
-
contentType: 'application/json',
|
|
246
|
-
timeout: 30000,
|
|
247
|
-
retryCount: 3,
|
|
248
|
-
authentication: {
|
|
249
|
-
type: 'signature',
|
|
250
|
-
secret: 'your-webhook-secret',
|
|
251
|
-
algorithm: 'sha256',
|
|
252
|
-
headerName: 'X-Agility-Signature'
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
// Bulk webhook operations
|
|
257
|
-
const createContentWebhooks = async (endpoints) => {
|
|
258
|
-
const webhooks = [];
|
|
259
|
-
for (const endpoint of endpoints) {
|
|
260
|
-
const webhook = {
|
|
261
|
-
name: `Content Webhook - ${endpoint.name}`,
|
|
262
|
-
url: endpoint.url,
|
|
263
|
-
events: ['content.created', 'content.updated', 'content.published'],
|
|
264
|
-
isActive: true,
|
|
265
|
-
method: 'POST',
|
|
266
|
-
contentType: 'application/json',
|
|
267
|
-
timeout: 30000,
|
|
268
|
-
retryCount: 3
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
const saved = await apiClient.webhookMethods.saveWebhook(webhook, guid);
|
|
272
|
-
webhooks.push(saved);
|
|
273
|
-
}
|
|
274
|
-
return webhooks;
|
|
275
|
-
};
|
|
114
|
+
const webhook = new Webhook();
|
|
115
|
+
webhook.name = 'Content Publish Notification';
|
|
116
|
+
webhook.url = 'https://api.example.com/webhooks/agility';
|
|
117
|
+
webhook.enabled = true;
|
|
118
|
+
webhook.contentSaveEvents = false;
|
|
119
|
+
webhook.contentPublishEvents = true;
|
|
120
|
+
webhook.contentWorkflowEvents = false;
|
|
121
|
+
webhook.retriesEnabled = true;
|
|
122
|
+
webhook.retryCount = 5;
|
|
123
|
+
webhook.retrySpeed = 'standard';
|
|
124
|
+
|
|
125
|
+
const saved = await apiClient.webhookMethods.saveWebhook(guid, webhook);
|
|
126
|
+
console.log('Webhook created with ID:', saved.rowKey);
|
|
127
|
+
|
|
128
|
+
// Update an existing webhook (partitionKey/rowKey carry through from the get)
|
|
129
|
+
const existing = await apiClient.webhookMethods.getWebhook(guid, saved.rowKey);
|
|
130
|
+
existing.name = 'Updated Webhook Name';
|
|
131
|
+
existing.contentWorkflowEvents = true;
|
|
132
|
+
|
|
133
|
+
const updated = await apiClient.webhookMethods.saveWebhook(guid, existing);
|
|
134
|
+
console.log('Webhook updated:', updated.name);
|
|
276
135
|
```
|
|
277
136
|
|
|
278
|
-
**
|
|
137
|
+
**Error Handling:**
|
|
138
|
+
- Throws `Exception` when validation or the save operation fails
|
|
139
|
+
|
|
140
|
+
### deleteWebhook
|
|
141
|
+
|
|
142
|
+
Deletes a webhook by its ID. Deletion immediately stops future deliveries for that webhook, and also removes that webhook's delivery history — history is keyed by webhook, so it is not retrievable once the webhook is gone. Export anything you need first.
|
|
143
|
+
|
|
144
|
+
| Parameter | Type | Required | Description |
|
|
145
|
+
|-----------|------|----------|-------------|
|
|
146
|
+
| `guid` | `string` | Yes | Current website GUID |
|
|
147
|
+
| `webhookID` | `string` | Yes | The webhook ID (`rowKey`) to delete |
|
|
148
|
+
|
|
149
|
+
**Returns:** `void`
|
|
150
|
+
|
|
151
|
+
**Usage Example:**
|
|
279
152
|
```typescript
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
url: string; // Target URL (required)
|
|
284
|
-
events: string[]; // Array of event types (required)
|
|
285
|
-
isActive: boolean; // Whether webhook is active
|
|
286
|
-
method: string; // HTTP method (GET, POST, PUT, etc.)
|
|
287
|
-
contentType: string; // Content type for payload
|
|
288
|
-
timeout: number; // Request timeout in milliseconds
|
|
289
|
-
retryCount: number; // Number of retry attempts
|
|
290
|
-
authentication?: WebhookAuth; // Authentication configuration
|
|
291
|
-
payloadTemplate?: any; // Custom payload template
|
|
292
|
-
statistics?: WebhookStats; // Delivery statistics (read-only)
|
|
293
|
-
createdDate?: string; // Auto-generated
|
|
294
|
-
lastTriggered?: string; // Auto-generated
|
|
295
|
-
}
|
|
153
|
+
await apiClient.webhookMethods.deleteWebhook(guid, webhookID);
|
|
154
|
+
console.log('Webhook deleted');
|
|
155
|
+
```
|
|
296
156
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
algorithm?: string; // Algorithm for signature (sha256, etc.)
|
|
302
|
-
headerName?: string; // Header name for signature
|
|
303
|
-
}
|
|
157
|
+
**Error Handling:**
|
|
158
|
+
- Throws `Exception` when the webhook is not found or deletion fails
|
|
159
|
+
|
|
160
|
+
### getWebhookHistory
|
|
304
161
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
162
|
+
Retrieves the delivery history for a webhook — one record per event delivery, including the payload, HTTP response, and retry state. Requires *Manage* permission on webhooks.
|
|
163
|
+
|
|
164
|
+
Dates are UTC dates. The date range may span at most **366 days**; when no range is given, the **last 7 days** are returned. `take` is capped at **100** per page; use the continuation `token` for more.
|
|
165
|
+
|
|
166
|
+
Records come back **newest first**, and that ordering holds *across* pages — history is stored newest-first, so paging walks it in order rather than sorting each page in isolation. Treat `token` as opaque: it identifies a position in the sequence and is only meaningful for the same `webhookID` and date range.
|
|
167
|
+
|
|
168
|
+
| Parameter | Type | Required | Description |
|
|
169
|
+
|-----------|------|----------|-------------|
|
|
170
|
+
| `guid` | `string` | Yes | Current website GUID |
|
|
171
|
+
| `webhookID` | `string` | Yes | The webhook ID (`rowKey`) whose history to retrieve |
|
|
172
|
+
| `options` | `object` | No | Optional query options (below) |
|
|
173
|
+
| `options.fromDate` | `string` | No | UTC start date (e.g. `2026-08-01`) |
|
|
174
|
+
| `options.toDate` | `string` | No | UTC end date; range max 366 days |
|
|
175
|
+
| `options.take` | `number` | No | Records per page, max 100 |
|
|
176
|
+
| `options.token` | `string` | No | Continuation token from a previous response |
|
|
177
|
+
|
|
178
|
+
**Returns:** `TokenPagedResult<WebhookHistory>` - `{ token, items }` with the continuation token and the history records
|
|
179
|
+
|
|
180
|
+
**Usage Example:**
|
|
181
|
+
```typescript
|
|
182
|
+
// Last 7 days (default)
|
|
183
|
+
const history = await apiClient.webhookMethods.getWebhookHistory(guid, webhookID);
|
|
184
|
+
history.items.forEach(entry => {
|
|
185
|
+
console.log(`${entry.sendDate} -> HTTP ${entry.httpResponseCode} (success: ${entry.success})`);
|
|
186
|
+
if (!entry.success) {
|
|
187
|
+
console.log(` attempts: ${entry.attemptCount}, next attempt: ${entry.nextAttemptUtc}`);
|
|
188
|
+
console.log(` last error: ${entry.lastError}`);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// A specific window, paging through all records
|
|
193
|
+
let page = await apiClient.webhookMethods.getWebhookHistory(guid, webhookID, {
|
|
194
|
+
fromDate: '2026-08-01',
|
|
195
|
+
toDate: '2026-08-15',
|
|
196
|
+
take: 100
|
|
197
|
+
});
|
|
198
|
+
const failures = page.items.filter(e => !e.success);
|
|
199
|
+
while (page.token) {
|
|
200
|
+
page = await apiClient.webhookMethods.getWebhookHistory(guid, webhookID, {
|
|
201
|
+
fromDate: '2026-08-01',
|
|
202
|
+
toDate: '2026-08-15',
|
|
203
|
+
take: 100,
|
|
204
|
+
token: page.token
|
|
205
|
+
});
|
|
206
|
+
failures.push(...page.items.filter(e => !e.success));
|
|
312
207
|
}
|
|
208
|
+
console.log(`Failed deliveries in window: ${failures.length}`);
|
|
313
209
|
```
|
|
314
210
|
|
|
315
|
-
**
|
|
316
|
-
- `
|
|
317
|
-
- `
|
|
318
|
-
- `
|
|
319
|
-
- `
|
|
320
|
-
- `
|
|
211
|
+
**WebhookHistory Properties:**
|
|
212
|
+
- `partitionKey` / `rowKey`: Storage identity of the history record. **`rowKey` is the value sent as the `webhook-id` header**, so use it to correlate a history record with what your endpoint received. Both are opaque — treat them as strings and don't parse them; the internal format is not part of this contract
|
|
213
|
+
- `webhookRowKey`: The ID of the webhook this delivery belongs to
|
|
214
|
+
- `eventKey`: Identifier of the triggering *event*, shared by every webhook that received it (used server-side for de-duplication). This is **not** the `webhook-id` header — correlate with `rowKey` instead
|
|
215
|
+
- `queuedDate` / `sendDate`: When the delivery was queued and sent
|
|
216
|
+
- `url`: The URL the payload was POSTed to
|
|
217
|
+
- `instanceGuid`: The instance the event originated from
|
|
218
|
+
- `contentSaveEvent` / `contentPublishEvent` / `contentWorkflowEvent`: Which event category fired
|
|
219
|
+
- `payload`: The JSON payload that was sent
|
|
220
|
+
- `httpResponseCode` / `success` / `responseText`: The endpoint's response (any 2xx counts as success)
|
|
221
|
+
- `attemptCount` / `lastAttemptDate` / `nextAttemptUtc` / `lastError`: Retry state for failed deliveries. `attemptCount > 1` means the record is a retry; a failed record with no `nextAttemptUtc` will not be attempted again
|
|
222
|
+
- `signed` / `signatureKeyCount`: Whether the last attempt went out with signature headers, and how many signatures it carried (0 unsigned, 1 normally, 2 inside the 24-hour window after a secret roll). Recorded at send time, so it reports what *that* delivery did — it is not the webhook's current `secureDeliveryEnabled`, which can change at any time. `signed` is `null` when there is no record: the delivery has not been attempted yet, or the record predates the field
|
|
321
223
|
|
|
322
224
|
**Error Handling:**
|
|
323
|
-
- Throws `Exception` when
|
|
324
|
-
- Throws `Exception` when save operation fails
|
|
325
|
-
- Throws `Exception` when authentication configuration is invalid
|
|
225
|
+
- Throws `Exception` when retrieval fails (including insufficient permissions or an invalid date range)
|
|
326
226
|
|
|
327
|
-
###
|
|
227
|
+
### rotateWebhookSecret
|
|
228
|
+
|
|
229
|
+
Rotates the webhook's signing secret. Requires *FullControl* permission on webhooks.
|
|
328
230
|
|
|
329
|
-
|
|
231
|
+
The response is the only place the new secret is returned in full — store it immediately. The previous secret is kept as `previousSigningSecret` so deliveries signed during the rollover window can still be verified; update your endpoint to the new secret promptly.
|
|
330
232
|
|
|
331
233
|
| Parameter | Type | Required | Description |
|
|
332
234
|
|-----------|------|----------|-------------|
|
|
333
|
-
| `webhookID` | `number` | Yes | The webhook ID to delete |
|
|
334
235
|
| `guid` | `string` | Yes | Current website GUID |
|
|
236
|
+
| `webhookID` | `string` | Yes | The webhook ID (`rowKey`) whose secret to rotate |
|
|
335
237
|
|
|
336
|
-
**Returns:** `
|
|
238
|
+
**Returns:** `Webhook` - The webhook with the new, unmasked `signingSecret`, the old secret in `previousSigningSecret`, and `secretRolledUtc` set to the rotation time
|
|
337
239
|
|
|
338
240
|
**Usage Example:**
|
|
339
241
|
```typescript
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
242
|
+
const rotated = await apiClient.webhookMethods.rotateWebhookSecret(guid, webhookID);
|
|
243
|
+
console.log('New signing secret:', rotated.signingSecret); // whsec_...
|
|
244
|
+
console.log('Rotated at:', rotated.secretRolledUtc);
|
|
245
|
+
|
|
246
|
+
// Store the new secret in your secret manager, then update your endpoint's verifier.
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Error Handling:**
|
|
250
|
+
- Throws `Exception` when rotation fails (including insufficient permissions)
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## The Webhook object
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
class Webhook {
|
|
258
|
+
partitionKey: string | null; // Storage identity — required (with rowKey) to update an existing webhook
|
|
259
|
+
rowKey: string | null; // The webhook's ID (GUID); used as webhookID in the other methods
|
|
260
|
+
timestamp?: string | null; // Storage timestamp (read-only)
|
|
261
|
+
eTag?: string | null; // Storage ETag (read-only)
|
|
262
|
+
name: string | null; // Human-readable name
|
|
263
|
+
url: string | null; // Target URL for deliveries
|
|
264
|
+
instanceGuid: string | null; // Instance the webhook belongs to
|
|
265
|
+
enabled: boolean; // Whether the webhook fires at all
|
|
266
|
+
contentWorkflowEvents: boolean; // Fire on workflow events (request approval, approve, decline)
|
|
267
|
+
contentPublishEvents: boolean; // Fire on publish/unpublish events
|
|
268
|
+
contentSaveEvents: boolean; // Fire on save/delete events
|
|
269
|
+
secureDeliveryEnabled: boolean; // Opt-in for signed delivery (default false; existing webhooks stay unsigned)
|
|
270
|
+
signingSecret: string | null; // Standard Webhooks secret (whsec_...) — server-controlled, masked without FullControl permission
|
|
271
|
+
previousSigningSecret: string | null; // Prior secret, kept for the rotation rollover window
|
|
272
|
+
secretRolledUtc: string | null; // When the secret was last rotated
|
|
273
|
+
retriesEnabled: boolean; // Whether failed deliveries are retried
|
|
274
|
+
retryCount: number; // Retries after the initial attempt (1-8)
|
|
275
|
+
retrySpeed: "fast" | "standard" | "slow" | null; // Base retry delay tier
|
|
345
276
|
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Verifying deliveries (signing)
|
|
280
|
+
|
|
281
|
+
Deliveries for a webhook with `secureDeliveryEnabled: true` are signed per the [Standard Webhooks](https://www.standardwebhooks.com/) specification; those carry three extra headers. Deliveries for webhooks without it carry no signature headers at all, so verify signatures only for endpoints you have enabled secure delivery on:
|
|
282
|
+
|
|
283
|
+
| Header | Description |
|
|
284
|
+
|--------|-------------|
|
|
285
|
+
| `webhook-id` | Unique ID of this delivery (the history record's `rowKey`) — reuse it as an **idempotency key**, since retries resend the same `webhook-id` |
|
|
286
|
+
| `webhook-timestamp` | Unix timestamp (seconds) of the delivery attempt |
|
|
287
|
+
| `webhook-signature` | `v1,<base64 signature>` — HMAC-SHA256 over `{id}.{timestamp}.{body}` keyed with the decoded secret |
|
|
346
288
|
|
|
347
|
-
|
|
348
|
-
|
|
289
|
+
The signing secret is the webhook's `signingSecret` (`whsec_` followed by the base64-encoded key). Verify with any Standard Webhooks library rather than hand-rolling the HMAC:
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
import { Webhook as StandardWebhook } from 'standardwebhooks';
|
|
293
|
+
|
|
294
|
+
const verifier = new StandardWebhook(process.env.AGILITY_WEBHOOK_SECRET); // whsec_...
|
|
295
|
+
|
|
296
|
+
app.post('/webhooks/agility', (req, res) => {
|
|
349
297
|
try {
|
|
350
|
-
//
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
// Check if webhook is active
|
|
357
|
-
if (webhook.isActive) {
|
|
358
|
-
console.log('Warning: Deleting active webhook');
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Perform deletion
|
|
362
|
-
const result = await apiClient.webhookMethods.deleteWebhook(webhookID, guid);
|
|
363
|
-
console.log('Webhook deleted successfully');
|
|
364
|
-
return result;
|
|
365
|
-
} catch (error) {
|
|
366
|
-
console.error('Failed to delete webhook:', error.message);
|
|
367
|
-
throw error;
|
|
368
|
-
}
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
// Bulk cleanup of inactive webhooks
|
|
372
|
-
const cleanupInactiveWebhooks = async () => {
|
|
373
|
-
const webhooks = await apiClient.webhookMethods.getWebhookList(guid);
|
|
374
|
-
const inactiveWebhooks = webhooks.filter(w => !w.isActive);
|
|
375
|
-
|
|
376
|
-
console.log(`Found ${inactiveWebhooks.length} inactive webhooks`);
|
|
377
|
-
|
|
378
|
-
for (const webhook of inactiveWebhooks) {
|
|
379
|
-
const shouldDelete = confirm(
|
|
380
|
-
`Delete inactive webhook "${webhook.name}"? (${webhook.url})`
|
|
381
|
-
);
|
|
382
|
-
|
|
383
|
-
if (shouldDelete) {
|
|
384
|
-
await apiClient.webhookMethods.deleteWebhook(webhook.webhookID, guid);
|
|
385
|
-
console.log(`Deleted webhook: ${webhook.name}`);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
// Delete webhooks with high failure rates
|
|
391
|
-
const cleanupFailingWebhooks = async (maxFailureRate = 50) => {
|
|
392
|
-
const webhooks = await apiClient.webhookMethods.getWebhookList(guid);
|
|
393
|
-
|
|
394
|
-
const failingWebhooks = webhooks.filter(webhook =>
|
|
395
|
-
webhook.statistics &&
|
|
396
|
-
webhook.statistics.successRate < maxFailureRate &&
|
|
397
|
-
webhook.statistics.totalDeliveries > 10 // Only consider webhooks with sufficient data
|
|
398
|
-
);
|
|
399
|
-
|
|
400
|
-
console.log(`Found ${failingWebhooks.length} webhooks with high failure rate`);
|
|
401
|
-
|
|
402
|
-
for (const webhook of failingWebhooks) {
|
|
403
|
-
console.log(`Webhook "${webhook.name}" has ${webhook.statistics.successRate}% success rate`);
|
|
404
|
-
|
|
405
|
-
const shouldDelete = confirm(
|
|
406
|
-
`Delete failing webhook "${webhook.name}"? (Success rate: ${webhook.statistics.successRate}%)`
|
|
407
|
-
);
|
|
408
|
-
|
|
409
|
-
if (shouldDelete) {
|
|
410
|
-
await apiClient.webhookMethods.deleteWebhook(webhook.webhookID, guid);
|
|
411
|
-
console.log(`Deleted failing webhook: ${webhook.name}`);
|
|
298
|
+
// verify() checks the signature AND enforces the timestamp tolerance
|
|
299
|
+
const payload = verifier.verify(req.rawBody, req.headers);
|
|
300
|
+
|
|
301
|
+
// Use webhook-id for idempotency — retries resend the same id
|
|
302
|
+
if (alreadyProcessed(req.headers['webhook-id'])) {
|
|
303
|
+
return res.sendStatus(200);
|
|
412
304
|
}
|
|
305
|
+
|
|
306
|
+
processEvent(payload);
|
|
307
|
+
res.sendStatus(200); // any 2xx marks the delivery successful
|
|
308
|
+
} catch {
|
|
309
|
+
res.sendStatus(401);
|
|
413
310
|
}
|
|
414
|
-
};
|
|
311
|
+
});
|
|
415
312
|
```
|
|
416
313
|
|
|
417
|
-
**
|
|
418
|
-
-
|
|
419
|
-
-
|
|
420
|
-
-
|
|
421
|
-
- Consider deactivating instead of deleting for temporary suspension
|
|
422
|
-
|
|
423
|
-
**Best Practices:**
|
|
424
|
-
- Test webhook endpoints before creating webhooks
|
|
425
|
-
- Use appropriate authentication to secure webhook endpoints
|
|
426
|
-
- Monitor webhook statistics and success rates
|
|
427
|
-
- Implement proper error handling in webhook receivers
|
|
428
|
-
- Use webhook signatures to verify authenticity
|
|
429
|
-
- Set reasonable timeout values to avoid blocking
|
|
430
|
-
- Consider implementing idempotency in webhook receivers
|
|
314
|
+
**Verification rules:**
|
|
315
|
+
- Reject deliveries whose `webhook-timestamp` is more than **5 minutes** from now (standard libraries enforce this) — this bounds replay attacks
|
|
316
|
+
- During a secret rotation, accept signatures from either the new secret or `previousSigningSecret`
|
|
317
|
+
- Respond with a **2xx** status to acknowledge; anything else (or a timeout) counts as a failure and triggers retries when enabled
|
|
431
318
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
319
|
+
## Retry behavior
|
|
320
|
+
|
|
321
|
+
Retries apply per delivery and are configured on the webhook:
|
|
322
|
+
|
|
323
|
+
- `retriesEnabled` — master switch; when false, one attempt is made and failures are recorded but never retried
|
|
324
|
+
- `retryCount` — how many retries are made **after the initial attempt**, from 1 to 8
|
|
325
|
+
- `retrySpeed` — the base delay before the first retry:
|
|
326
|
+
|
|
327
|
+
| Speed | Base delay |
|
|
328
|
+
|-------|-----------|
|
|
329
|
+
| `fast` | 30 seconds |
|
|
330
|
+
| `standard` | 5 minutes |
|
|
331
|
+
| `slow` | 30 minutes |
|
|
332
|
+
|
|
333
|
+
Each subsequent retry backs off exponentially at **×4** the previous delay, with jitter, capped at **24 hours** between attempts. For example, `standard` retries land at roughly 5m, 20m, 80m, ... after the failure.
|
|
334
|
+
|
|
335
|
+
A delivery is successful as soon as the endpoint returns any **2xx** response; success stops the retry chain. Retry state for a delivery is visible in [getWebhookHistory](#getwebhookhistory) via `attemptCount`, `lastAttemptDate`, `nextAttemptUtc`, and `lastError`.
|
|
436
336
|
|
|
437
337
|
---
|
|
438
338
|
|
|
@@ -448,5 +348,4 @@ const cleanupFailingWebhooks = async (maxFailureRate = 50) => {
|
|
|
448
348
|
- [ModelMethods](./model-methods.md)
|
|
449
349
|
- [PageMethods](./page-methods.md)
|
|
450
350
|
- [ServerUserMethods](./server-user-methods.md)
|
|
451
|
-
-
|
|
452
|
-
- **WebhookMethods** *(current)*
|
|
351
|
+
- **WebhookMethods** *(current)*
|