Skip to content

Commit e193a3e

Browse files
Webhook settings and changelog
1 parent 9fbadd6 commit e193a3e

14 files changed

Lines changed: 951 additions & 7 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Mapping
4+
5+
from linkedapi import (
6+
WEBHOOK_SIGNATURE_HEADER,
7+
WEBHOOK_SIGNATURE_TIMESTAMP_HEADER,
8+
verify_webhook_signature,
9+
)
10+
11+
12+
def verify_request(raw_body: bytes, headers: Mapping[str, str], *, secret: str | None) -> bool:
13+
"""Pass Flask's request.get_data() or Django's request.body and request.headers."""
14+
return verify_webhook_signature(
15+
raw_body=raw_body,
16+
timestamp=headers.get(WEBHOOK_SIGNATURE_TIMESTAMP_HEADER),
17+
signature=headers.get(WEBHOOK_SIGNATURE_HEADER),
18+
secret=secret,
19+
)

‎linkedapi/__init__.py‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
AdminWebhooks,
77
LinkedApiAdmin,
88
)
9+
from linkedapi.changelog import get_changelog
910
from linkedapi.client import LinkedApi
1011
from linkedapi.config import LinkedApiConfig
1112
from linkedapi.core import Operation, poll_workflow_result
@@ -79,9 +80,14 @@
7980
)
8081
from linkedapi.types import * # noqa: F403
8182
from linkedapi.types import __all__ as _types_all
82-
from linkedapi.webhooks import parse_webhook_event
83+
from linkedapi.webhooks import (
84+
WEBHOOK_SIGNATURE_HEADER,
85+
WEBHOOK_SIGNATURE_TIMESTAMP_HEADER,
86+
parse_webhook_event,
87+
verify_webhook_signature,
88+
)
8389

84-
__version__ = "1.4.2"
90+
__version__ = "1.4.3"
8591
PredefinedOperation = Operation
8692

8793
__all__ = [
@@ -158,8 +164,12 @@
158164
"SyncNetwork",
159165
"ThenWorkflowMapper",
160166
"VoidWorkflowMapper",
167+
"WEBHOOK_SIGNATURE_HEADER",
168+
"WEBHOOK_SIGNATURE_TIMESTAMP_HEADER",
161169
"WithdrawConnectionRequest",
162170
"__version__",
171+
"get_changelog",
163172
"parse_webhook_event",
164173
"poll_workflow_result",
174+
"verify_webhook_signature",
165175
]

‎linkedapi/admin/webhooks.py‎

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,17 @@
55
from linkedapi.errors import LinkedApiError
66
from linkedapi.http import HttpClient
77
from linkedapi.types.webhooks import (
8+
DeleteWebhookHeaderParams,
89
DeleteWebhookParams,
910
ReplayWebhookDeliveryParams,
11+
RevealWebhookSecretParams,
12+
RotateWebhookSecretParams,
13+
SetWebhookEventsParams,
14+
SetWebhookHeaderParams,
15+
SetWebhookHeadersParams,
1016
SetWebhookParams,
1117
SetWebhookPayloadModeParams,
18+
SetWebhookSigningParams,
1219
WebhookDelivery,
1320
WebhookSubscription,
1421
)
@@ -17,8 +24,9 @@
1724
class AdminWebhooks:
1825
"""Manage the client's registered outbound webhook and inspect recent deliveries.
1926
20-
A client may hold at most one active webhook. It receives every event Linked API emits
21-
(workflow lifecycle + account status changes); filter by the event ``type`` on your receiver.
27+
A client may hold at most one active webhook. Its event selectors control which workflow,
28+
account, inbox and network events it receives; None selects all events. Test events are
29+
always delivered. Signing and custom headers are configured per subscription.
2230
"""
2331

2432
def __init__(self, http_client: HttpClient[Any]) -> None:
@@ -40,6 +48,78 @@ def set_payload_mode(self, params: SetWebhookPayloadModeParams) -> WebhookSubscr
4048
)
4149
return WebhookSubscription.model_validate(result["webhook"])
4250

51+
def set_signing(self, params: SetWebhookSigningParams) -> WebhookSubscription:
52+
"""Enable or disable signing. Enabling returns the stored secret."""
53+
result = self._post_result(
54+
"/admin/webhook.setSigning",
55+
"Failed to set webhook signing",
56+
params,
57+
)
58+
return WebhookSubscription.model_validate(result["webhook"])
59+
60+
def set_headers(self, params: SetWebhookHeadersParams) -> WebhookSubscription:
61+
"""Replace all custom headers. None or an empty dict clears them."""
62+
result = self._post_result(
63+
"/admin/webhook.setHeaders",
64+
"Failed to set webhook headers",
65+
params,
66+
)
67+
return WebhookSubscription.model_validate(result["webhook"])
68+
69+
def set_header(self, params: SetWebhookHeaderParams) -> WebhookSubscription:
70+
"""Set one custom header; its name is matched case-insensitively."""
71+
result = self._post_result(
72+
"/admin/webhook.setHeader",
73+
"Failed to set webhook header",
74+
params,
75+
)
76+
return WebhookSubscription.model_validate(result["webhook"])
77+
78+
def delete_header(self, params: DeleteWebhookHeaderParams) -> WebhookSubscription:
79+
"""Remove one custom header; its name is matched case-insensitively."""
80+
result = self._post_result(
81+
"/admin/webhook.deleteHeader",
82+
"Failed to delete webhook header",
83+
params,
84+
)
85+
return WebhookSubscription.model_validate(result["webhook"])
86+
87+
def reveal_secret(self, params: RevealWebhookSecretParams) -> WebhookSubscription:
88+
"""Return the current signing secret without changing it."""
89+
result = self._post_result(
90+
"/admin/webhook.revealSecret",
91+
"Failed to reveal webhook secret",
92+
params,
93+
)
94+
return WebhookSubscription.model_validate(result["webhook"])
95+
96+
def rotate_secret(self, params: RotateWebhookSecretParams) -> WebhookSubscription:
97+
"""Replace the signing secret and return its new value."""
98+
result = self._post_result(
99+
"/admin/webhook.rotateSecret",
100+
"Failed to rotate webhook secret",
101+
params,
102+
)
103+
return WebhookSubscription.model_validate(result["webhook"])
104+
105+
def set_events(self, params: SetWebhookEventsParams) -> WebhookSubscription:
106+
"""Select event types or namespace wildcards; None selects all. Tests always deliver."""
107+
result = self._post_result(
108+
"/admin/webhook.setEvents",
109+
"Failed to set webhook events",
110+
params,
111+
)
112+
return WebhookSubscription.model_validate(result["webhook"])
113+
114+
def event_types(self) -> list[str]:
115+
"""Return selectable event types in the server's order.
116+
117+
The catalog excludes webhook.test, which is always delivered.
118+
"""
119+
result = self._post_result("/admin/webhook.eventTypes", "Failed to get webhook event types")
120+
event_types: list[str] = result["eventTypes"]
121+
return event_types
122+
43123
def delete(self, params: DeleteWebhookParams) -> None:
44124
self._post_void("/admin/webhook.delete", "Failed to delete webhook", params)
45125

‎linkedapi/changelog.py‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from __future__ import annotations
2+
3+
from datetime import date, datetime
4+
5+
import requests
6+
7+
from linkedapi.errors import LinkedApiError
8+
from linkedapi.types.changelog import ChangelogResponse
9+
10+
11+
def get_changelog(
12+
since: str | date | datetime | None = None,
13+
*,
14+
url: str = "https://linkedapi.io/api/changelog",
15+
) -> ChangelogResponse:
16+
"""Read the public Linked API changelog.
17+
18+
A release counts as shipped at the end of its week, so callers deduplicate by date.
19+
20+
Raises:
21+
LinkedApiError: on a non-2xx response or network failure.
22+
"""
23+
since_value = since.isoformat() if isinstance(since, date) else since
24+
try:
25+
response = requests.get(
26+
url, params={"since": since_value} if since_value is not None else None
27+
)
28+
except requests.RequestException as error:
29+
raise LinkedApiError("httpError", f"Request error: {error}", {"error": error}) from error
30+
31+
if not 200 <= response.status_code < 300:
32+
try:
33+
error_data = response.json()
34+
except ValueError:
35+
error_data = None
36+
message = f"HTTP {response.status_code}: {response.reason}"
37+
if isinstance(error_data, dict) and isinstance(error_data.get("error"), str):
38+
message = error_data["error"]
39+
raise LinkedApiError(
40+
"httpError",
41+
message,
42+
{"status": response.status_code, "statusText": response.reason, "url": response.url},
43+
)
44+
45+
return ChangelogResponse.model_validate(response.json())

‎linkedapi/types/__init__.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
serialize_model,
4848
serialize_value,
4949
)
50+
from linkedapi.types.changelog import ChangelogItem, ChangelogRelease, ChangelogResponse
5051
from linkedapi.types.company import (
5152
BaseFetchCompanyParams,
5253
BaseFetchCompanyParamsWide,
@@ -242,17 +243,25 @@
242243
AccountWebhookEvent,
243244
AccountWebhookEventData,
244245
AccountWebhookStatus,
246+
DeleteWebhookHeaderParams,
245247
DeleteWebhookParams,
246248
MessageWebhookEvent,
247249
MessageWebhookEventData,
248250
NetworkWebhookEvent,
249251
NetworkWebhookEventData,
250252
ReplayWebhookDeliveryParams,
253+
RevealWebhookSecretParams,
254+
RotateWebhookSecretParams,
255+
SetWebhookEventsParams,
256+
SetWebhookHeaderParams,
257+
SetWebhookHeadersParams,
251258
SetWebhookParams,
252259
SetWebhookPayloadModeParams,
260+
SetWebhookSigningParams,
253261
WebhookDelivery,
254262
WebhookDeliveryStatus,
255263
WebhookEvent,
264+
WebhookEventSelector,
256265
WebhookEventType,
257266
WebhookPayloadMode,
258267
WebhookSubscription,
@@ -300,6 +309,9 @@
300309
"BaseFetchPostParamsWide",
301310
"BillingPeriod",
302311
"CancelConnectionSessionParams",
312+
"ChangelogItem",
313+
"ChangelogRelease",
314+
"ChangelogResponse",
303315
"CheckConnectionStatusParams",
304316
"CheckConnectionStatusResult",
305317
"Comment",
@@ -327,6 +339,7 @@
327339
"CreateRepostResult",
328340
"DeleteLimitEntry",
329341
"DeleteLimitsParams",
342+
"DeleteWebhookHeaderParams",
330343
"DeleteWebhookParams",
331344
"DisconnectParams",
332345
"EmploymentType",
@@ -456,6 +469,8 @@
456469
"RetrievePerformanceResult",
457470
"RetrieveProfileViewersParams",
458471
"RetrieveSSIResult",
472+
"RevealWebhookSecretParams",
473+
"RotateWebhookSecretParams",
459474
"SalaryPeriod",
460475
"SearchCompaniesFilter",
461476
"SearchCompaniesParams",
@@ -485,8 +500,12 @@
485500
"SetSeatsParams",
486501
"SetSeatsResult",
487502
"SetSeatsStatus",
503+
"SetWebhookEventsParams",
504+
"SetWebhookHeaderParams",
505+
"SetWebhookHeadersParams",
488506
"SetWebhookParams",
489507
"SetWebhookPayloadModeParams",
508+
"SetWebhookSigningParams",
490509
"StCompanyDm",
491510
"StCompanyEmployee",
492511
"StCompanyEmployeesFilter",
@@ -502,6 +521,7 @@
502521
"WebhookDelivery",
503522
"WebhookDeliveryStatus",
504523
"WebhookEvent",
524+
"WebhookEventSelector",
505525
"WebhookEventType",
506526
"WebhookPayloadMode",
507527
"WebhookSubscription",

‎linkedapi/types/changelog.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from __future__ import annotations
2+
3+
from linkedapi.types.base import LinkedApiModel
4+
5+
6+
class ChangelogItem(LinkedApiModel):
7+
title: str
8+
body: str
9+
docs: str
10+
11+
12+
class ChangelogRelease(LinkedApiModel):
13+
date: str
14+
items: list[ChangelogItem]
15+
16+
17+
class ChangelogResponse(LinkedApiModel):
18+
since: str | None
19+
count: int
20+
entries: list[ChangelogRelease]

0 commit comments

Comments
 (0)