hookbase 3.1.0__tar.gz → 3.2.0__tar.gz

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.
Files changed (88) hide show
  1. {hookbase-3.1.0 → hookbase-3.2.0}/PKG-INFO +27 -5
  2. {hookbase-3.1.0 → hookbase-3.2.0}/README.md +26 -4
  3. hookbase-3.2.0/src/hookbase/_version.py +1 -0
  4. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/client.py +8 -0
  5. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/__init__.py +27 -0
  6. hookbase-3.2.0/src/hookbase/models/catalog.py +120 -0
  7. hookbase-3.2.0/src/hookbase/models/sources.py +283 -0
  8. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/__init__.py +2 -0
  9. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/events.py +20 -0
  10. hookbase-3.2.0/src/hookbase/resources/providers.py +54 -0
  11. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/sources.py +82 -2
  12. hookbase-3.2.0/tests/resources/test_source_contract.py +302 -0
  13. {hookbase-3.1.0 → hookbase-3.2.0}/tests/resources/test_source_fields.py +55 -2
  14. hookbase-3.1.0/src/hookbase/_version.py +0 -1
  15. hookbase-3.1.0/src/hookbase/models/sources.py +0 -180
  16. {hookbase-3.1.0 → hookbase-3.2.0}/.github/workflows/python-publish.yml +0 -0
  17. {hookbase-3.1.0 → hookbase-3.2.0}/.gitignore +0 -0
  18. {hookbase-3.1.0 → hookbase-3.2.0}/LICENSE +0 -0
  19. {hookbase-3.1.0 → hookbase-3.2.0}/examples/async_usage.py +0 -0
  20. {hookbase-3.1.0 → hookbase-3.2.0}/examples/basic_inbound.py +0 -0
  21. {hookbase-3.1.0 → hookbase-3.2.0}/examples/error_handling.py +0 -0
  22. {hookbase-3.1.0 → hookbase-3.2.0}/examples/send_webhooks.py +0 -0
  23. {hookbase-3.1.0 → hookbase-3.2.0}/examples/verify_webhooks.py +0 -0
  24. {hookbase-3.1.0 → hookbase-3.2.0}/pyproject.toml +0 -0
  25. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/__init__.py +0 -0
  26. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/_client.py +0 -0
  27. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/_constants.py +0 -0
  28. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/_pagination.py +0 -0
  29. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/errors.py +0 -0
  30. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/_base.py +0 -0
  31. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/_wire.py +0 -0
  32. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/analytics.py +0 -0
  33. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/api_keys.py +0 -0
  34. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/applications.py +0 -0
  35. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/common.py +0 -0
  36. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/cron_jobs.py +0 -0
  37. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/deliveries.py +0 -0
  38. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/destinations.py +0 -0
  39. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/dlq.py +0 -0
  40. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/endpoints.py +0 -0
  41. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/event_types.py +0 -0
  42. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/events.py +0 -0
  43. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/filters.py +0 -0
  44. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/messages.py +0 -0
  45. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/organizations.py +0 -0
  46. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/portal_tokens.py +0 -0
  47. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/routes.py +0 -0
  48. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/schemas.py +0 -0
  49. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/subscriptions.py +0 -0
  50. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/transforms.py +0 -0
  51. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/models/tunnels.py +0 -0
  52. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/py.typed +0 -0
  53. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/_base.py +0 -0
  54. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/analytics.py +0 -0
  55. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/api_keys.py +0 -0
  56. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/applications.py +0 -0
  57. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/cron_jobs.py +0 -0
  58. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/deliveries.py +0 -0
  59. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/destinations.py +0 -0
  60. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/dlq.py +0 -0
  61. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/endpoints.py +0 -0
  62. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/event_types.py +0 -0
  63. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/filters.py +0 -0
  64. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/message_log.py +0 -0
  65. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/messages.py +0 -0
  66. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/organizations.py +0 -0
  67. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/portal_tokens.py +0 -0
  68. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/routes.py +0 -0
  69. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/schemas.py +0 -0
  70. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/subscriptions.py +0 -0
  71. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/transforms.py +0 -0
  72. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/resources/tunnels.py +0 -0
  73. {hookbase-3.1.0 → hookbase-3.2.0}/src/hookbase/webhook.py +0 -0
  74. {hookbase-3.1.0 → hookbase-3.2.0}/tests/__init__.py +0 -0
  75. {hookbase-3.1.0 → hookbase-3.2.0}/tests/conftest.py +0 -0
  76. {hookbase-3.1.0 → hookbase-3.2.0}/tests/resources/__init__.py +0 -0
  77. {hookbase-3.1.0 → hookbase-3.2.0}/tests/resources/test_applications.py +0 -0
  78. {hookbase-3.1.0 → hookbase-3.2.0}/tests/resources/test_cron_jobs.py +0 -0
  79. {hookbase-3.1.0 → hookbase-3.2.0}/tests/resources/test_destinations.py +0 -0
  80. {hookbase-3.1.0 → hookbase-3.2.0}/tests/resources/test_dlq.py +0 -0
  81. {hookbase-3.1.0 → hookbase-3.2.0}/tests/resources/test_endpoints.py +0 -0
  82. {hookbase-3.1.0 → hookbase-3.2.0}/tests/resources/test_messages.py +0 -0
  83. {hookbase-3.1.0 → hookbase-3.2.0}/tests/resources/test_sources.py +0 -0
  84. {hookbase-3.1.0 → hookbase-3.2.0}/tests/test_client.py +0 -0
  85. {hookbase-3.1.0 → hookbase-3.2.0}/tests/test_errors.py +0 -0
  86. {hookbase-3.1.0 → hookbase-3.2.0}/tests/test_pagination.py +0 -0
  87. {hookbase-3.1.0 → hookbase-3.2.0}/tests/test_slug_contract.py +0 -0
  88. {hookbase-3.1.0 → hookbase-3.2.0}/tests/test_webhook.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hookbase
3
- Version: 3.1.0
3
+ Version: 3.2.0
4
4
  Summary: Official Python SDK for the Hookbase webhook management platform
5
5
  Project-URL: Homepage, https://hookbase.app
6
6
  Project-URL: Documentation, https://www.hookbase.app/docs/send/sdk
@@ -98,7 +98,26 @@ client.sources.create({"name": "GitHub", "slug": "github", "provider": "github"}
98
98
  client.sources.get("src_id")
99
99
  client.sources.update("src_id", {"name": "Updated"})
100
100
  client.sources.delete("src_id")
101
- client.sources.rotate_secret("src_id")
101
+ client.sources.rotate_secret("src_id") # 400 for vendor-issued/public-key/built-in secrets
102
+ client.sources.reveal_secret("src_id")
103
+
104
+ # A provider that verifies with no secret: signing_secret comes back None
105
+ wise = client.sources.create({"name": "Wise", "slug": "wise", "provider": "wise"})
106
+ assert wise.verifies_without_secret and wise.signing_secret is None
107
+
108
+ # WhatsApp (Meta) sources also carry a verify token
109
+ wa = client.sources.create({"name": "WhatsApp", "slug": "whatsapp", "provider": "whatsapp"})
110
+ print(wa.verify_token)
111
+ client.sources.rotate_verify_token(wa.id)
112
+
113
+ # Notion parks its verification token until you adopt it
114
+ pending = client.sources.get_pending_verification("notion")
115
+ client.sources.adopt_pending_verification("notion")
116
+
117
+ # Provider catalog: which secret each provider needs (public, no auth needed)
118
+ for p in client.providers.list(category="payments").providers:
119
+ print(p.hookbase_provider, p.signature_verification and p.signature_verification.secret_source)
120
+ client.providers.get("stripe").signature_verification
102
121
 
103
122
  # Destinations
104
123
  client.destinations.list()
@@ -111,7 +130,7 @@ client.routes.get_circuit_status("route_id")
111
130
  client.routes.reset_circuit("route_id")
112
131
 
113
132
  # Events & Deliveries
114
- client.events.list(source_id="src_id", from_date="2024-01-01")
133
+ client.events.list(source_id="src_id", from_date="2024-01-01") # id or slug; unknown -> 404
115
134
  client.events.get("evt_id") # Includes payload and deliveries
116
135
  client.deliveries.replay("del_id")
117
136
  client.deliveries.bulk_replay(["del_1", "del_2"])
@@ -252,11 +271,14 @@ All responses are typed Pydantic models with camelCase alias support:
252
271
  from hookbase.models import Source, CreateSourceParams
253
272
 
254
273
  # Use Pydantic models for type safety
255
- params = CreateSourceParams(name="GitHub", provider="github")
274
+ params = CreateSourceParams(name="GitHub", slug="github", provider="github")
256
275
  source = client.sources.create(params)
257
276
 
258
277
  # Or use plain dicts
259
- source = client.sources.create({"name": "GitHub", "provider": "github"})
278
+ source = client.sources.create({"name": "GitHub", "slug": "github", "provider": "github"})
279
+
280
+ # `provider` is any string (hookbase.models.KNOWN_SOURCE_PROVIDERS lists today's values), so a
281
+ # source with a provider newer than this package still parses.
260
282
  ```
261
283
 
262
284
  ## Development
@@ -63,7 +63,26 @@ client.sources.create({"name": "GitHub", "slug": "github", "provider": "github"}
63
63
  client.sources.get("src_id")
64
64
  client.sources.update("src_id", {"name": "Updated"})
65
65
  client.sources.delete("src_id")
66
- client.sources.rotate_secret("src_id")
66
+ client.sources.rotate_secret("src_id") # 400 for vendor-issued/public-key/built-in secrets
67
+ client.sources.reveal_secret("src_id")
68
+
69
+ # A provider that verifies with no secret: signing_secret comes back None
70
+ wise = client.sources.create({"name": "Wise", "slug": "wise", "provider": "wise"})
71
+ assert wise.verifies_without_secret and wise.signing_secret is None
72
+
73
+ # WhatsApp (Meta) sources also carry a verify token
74
+ wa = client.sources.create({"name": "WhatsApp", "slug": "whatsapp", "provider": "whatsapp"})
75
+ print(wa.verify_token)
76
+ client.sources.rotate_verify_token(wa.id)
77
+
78
+ # Notion parks its verification token until you adopt it
79
+ pending = client.sources.get_pending_verification("notion")
80
+ client.sources.adopt_pending_verification("notion")
81
+
82
+ # Provider catalog: which secret each provider needs (public, no auth needed)
83
+ for p in client.providers.list(category="payments").providers:
84
+ print(p.hookbase_provider, p.signature_verification and p.signature_verification.secret_source)
85
+ client.providers.get("stripe").signature_verification
67
86
 
68
87
  # Destinations
69
88
  client.destinations.list()
@@ -76,7 +95,7 @@ client.routes.get_circuit_status("route_id")
76
95
  client.routes.reset_circuit("route_id")
77
96
 
78
97
  # Events & Deliveries
79
- client.events.list(source_id="src_id", from_date="2024-01-01")
98
+ client.events.list(source_id="src_id", from_date="2024-01-01") # id or slug; unknown -> 404
80
99
  client.events.get("evt_id") # Includes payload and deliveries
81
100
  client.deliveries.replay("del_id")
82
101
  client.deliveries.bulk_replay(["del_1", "del_2"])
@@ -217,11 +236,14 @@ All responses are typed Pydantic models with camelCase alias support:
217
236
  from hookbase.models import Source, CreateSourceParams
218
237
 
219
238
  # Use Pydantic models for type safety
220
- params = CreateSourceParams(name="GitHub", provider="github")
239
+ params = CreateSourceParams(name="GitHub", slug="github", provider="github")
221
240
  source = client.sources.create(params)
222
241
 
223
242
  # Or use plain dicts
224
- source = client.sources.create({"name": "GitHub", "provider": "github"})
243
+ source = client.sources.create({"name": "GitHub", "slug": "github", "provider": "github"})
244
+
245
+ # `provider` is any string (hookbase.models.KNOWN_SOURCE_PROVIDERS lists today's values), so a
246
+ # source with a provider newer than this package still parses.
225
247
  ```
226
248
 
227
249
  ## Development
@@ -0,0 +1 @@
1
+ __version__ = "3.2.0"
@@ -26,6 +26,7 @@ from .resources import (
26
26
  AsyncMessages,
27
27
  AsyncOrganizations,
28
28
  AsyncPortalTokens,
29
+ AsyncProviders,
29
30
  AsyncRoutes,
30
31
  AsyncSchemas,
31
32
  AsyncSources,
@@ -43,6 +44,7 @@ from .resources import (
43
44
  Messages,
44
45
  Organizations,
45
46
  PortalTokens,
47
+ Providers,
46
48
  Routes,
47
49
  Schemas,
48
50
  Sources,
@@ -118,6 +120,8 @@ class Hookbase:
118
120
  transforms: Transforms
119
121
  filters: Filters
120
122
  schemas: Schemas
123
+ #: The public provider catalog (what each provider signs with, which secret it needs).
124
+ providers: Providers
121
125
 
122
126
  # Outbound
123
127
  outbound: _OutboundNamespace
@@ -160,6 +164,7 @@ class Hookbase:
160
164
  self.transforms = Transforms(self._transport)
161
165
  self.filters = Filters(self._transport)
162
166
  self.schemas = Schemas(self._transport)
167
+ self.providers = Providers(self._transport)
163
168
 
164
169
  # Outbound namespace
165
170
  self.outbound = _OutboundNamespace(self._transport)
@@ -210,6 +215,8 @@ class AsyncHookbase:
210
215
  transforms: AsyncTransforms
211
216
  filters: AsyncFilters
212
217
  schemas: AsyncSchemas
218
+ #: The public provider catalog (what each provider signs with, which secret it needs).
219
+ providers: AsyncProviders
213
220
 
214
221
  # Outbound
215
222
  outbound: _AsyncOutboundNamespace
@@ -252,6 +259,7 @@ class AsyncHookbase:
252
259
  self.transforms = AsyncTransforms(self._transport)
253
260
  self.filters = AsyncFilters(self._transport)
254
261
  self.schemas = AsyncSchemas(self._transport)
262
+ self.providers = AsyncProviders(self._transport)
255
263
 
256
264
  # Outbound namespace
257
265
  self.outbound = _AsyncOutboundNamespace(self._transport)
@@ -1,6 +1,16 @@
1
1
  from .analytics import AnalyticsTimeline, DashboardData
2
2
  from .api_keys import ApiKey, ApiKeyWithSecret, CreateApiKeyParams
3
3
  from .applications import Application, CreateApplicationParams, UpdateApplicationParams
4
+ from .catalog import (
5
+ CatalogProvider,
6
+ CatalogProviderDetail,
7
+ ProviderCatalogList,
8
+ ProviderEvents,
9
+ ProviderEventType,
10
+ ProviderSignatureVerification,
11
+ ProviderSignatureVerificationSummary,
12
+ ProviderVerifyTokenInfo,
13
+ )
4
14
  from .common import BulkDeleteResult, ImportResult, ImportResultItem
5
15
  from .cron_jobs import (
6
16
  CreateCronGroupParams,
@@ -89,9 +99,13 @@ from .routes import (
89
99
  )
90
100
  from .schemas import CreateSchemaParams, Schema, SchemaValidationResult, UpdateSchemaParams
91
101
  from .sources import (
102
+ CLEARABLE_SOURCE_FIELDS,
103
+ KNOWN_SOURCE_PROVIDERS,
104
+ ClearableSourceField,
92
105
  CreateSourceParams,
93
106
  DedupStrategy,
94
107
  IpFilterMode,
108
+ PendingVerification,
95
109
  Source,
96
110
  SourceProvider,
97
111
  SourceWithSecret,
@@ -119,6 +133,15 @@ __all__ = [
119
133
  "Application",
120
134
  "CreateApplicationParams",
121
135
  "UpdateApplicationParams",
136
+ # Catalog
137
+ "CatalogProvider",
138
+ "CatalogProviderDetail",
139
+ "ProviderCatalogList",
140
+ "ProviderEventType",
141
+ "ProviderEvents",
142
+ "ProviderSignatureVerification",
143
+ "ProviderSignatureVerificationSummary",
144
+ "ProviderVerifyTokenInfo",
122
145
  # Common
123
146
  "BulkDeleteResult",
124
147
  "ImportResult",
@@ -211,9 +234,13 @@ __all__ = [
211
234
  "SchemaValidationResult",
212
235
  "UpdateSchemaParams",
213
236
  # Sources
237
+ "CLEARABLE_SOURCE_FIELDS",
238
+ "KNOWN_SOURCE_PROVIDERS",
239
+ "ClearableSourceField",
214
240
  "CreateSourceParams",
215
241
  "DedupStrategy",
216
242
  "IpFilterMode",
243
+ "PendingVerification",
217
244
  "Source",
218
245
  "SourceProvider",
219
246
  "SourceWithSecret",
@@ -0,0 +1,120 @@
1
+ """The public provider catalog: GET /api/catalog/providers (no authentication needed).
2
+
3
+ Every enumerated value here (category, kind, secret_source, secret_format) is a plain string
4
+ rather than a Literal, for the reason ``SourceProvider`` is: the catalog grows server-side, and a
5
+ value this package has not heard of must not make the whole response fail to parse.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from pydantic import field_validator
13
+
14
+ from ._base import HookbaseModel
15
+
16
+
17
+ class ProviderEventType(HookbaseModel):
18
+ type: str
19
+ description: str = ""
20
+
21
+
22
+ class ProviderVerifyTokenInfo(HookbaseModel):
23
+ """Meta's second, Hookbase-generated credential (see SourceWithSecret.verify_token)."""
24
+
25
+ label: str
26
+ instruction: str = ""
27
+
28
+
29
+ class ProviderSignatureVerificationSummary(HookbaseModel):
30
+ """The secret fields a create flow needs, as the catalog list carries them.
31
+
32
+ ``secret_source`` says who produces the credential: "provider" (the vendor issues it; paste
33
+ it as signing_secret), "operator" (Hookbase generates it when omitted; paste it into the
34
+ vendor), "public_key" (paste the vendor's public key), "built_in" (nothing to enter; events
35
+ are still verified) or "none" (the vendor does not sign).
36
+ """
37
+
38
+ supported: bool = False
39
+ #: The scheme kind: "hmac", "shared_secret", "bespoke" or "asymmetric". None when no scheme.
40
+ kind: str | None = None
41
+ secret_source: str | None = None
42
+ secret_label: str | None = None
43
+ secret_placeholder: str | None = None
44
+ #: The vendor issues the secret only after the source's URL is registered with it.
45
+ secret_after_registration: bool = False
46
+
47
+
48
+ class ProviderSignatureVerification(ProviderSignatureVerificationSummary):
49
+ """The full block from the provider detail call."""
50
+
51
+ algorithm: str | None = None
52
+ header_name: str | None = None
53
+ secret_location_hint: str | None = None
54
+ #: One sentence on where the value comes from or goes, in the vendor's own UI wording.
55
+ secret_instruction: str | None = None
56
+ #: "whsec" or "basic_auth" when the value has a required shape.
57
+ secret_format: str | None = None
58
+ #: Present for Meta providers, which also use a verify token.
59
+ verify_token: ProviderVerifyTokenInfo | None = None
60
+
61
+
62
+ class CatalogProvider(HookbaseModel):
63
+ """A provider as the catalog list returns it.
64
+
65
+ ``id`` is the catalog's id; ``hookbase_provider`` is the value to use as a source's
66
+ ``provider``. They differ for some entries (catalog "mollie-nextgen" is provider "mollie",
67
+ and several catalog entries map to "custom").
68
+ """
69
+
70
+ id: str
71
+ name: str
72
+ category: str = ""
73
+ description: str = ""
74
+ icon: str | None = None
75
+ event_type_count: int = 0
76
+ hookbase_provider: str = ""
77
+ has_signature_verification: bool = False
78
+ has_ip_ranges: bool = False
79
+ #: None when the provider has no verification block, and also from API versions that
80
+ #: predate this summary on list items: fall back to client.providers.get(id) for it.
81
+ signature_verification: ProviderSignatureVerificationSummary | None = None
82
+
83
+
84
+ class ProviderCatalogList(HookbaseModel):
85
+ providers: list[CatalogProvider] = []
86
+ categories: list[str] = []
87
+ total: int = 0
88
+
89
+
90
+ class CatalogProviderDetail(HookbaseModel):
91
+ """One provider in full, from GET /api/catalog/providers/:id."""
92
+
93
+ id: str
94
+ name: str
95
+ category: str = ""
96
+ description: str = ""
97
+ website: str | None = None
98
+ docs_url: str | None = None
99
+ icon: str | None = None
100
+ hookbase_provider: str = ""
101
+ signature_verification: ProviderSignatureVerification | None = None
102
+ event_types: list[ProviderEventType] = []
103
+ #: Example payloads keyed by event type.
104
+ sample_payloads: dict[str, Any] = {}
105
+ ip_ranges: list[str] | None = None
106
+ setup_guide: list[str] = []
107
+
108
+ @field_validator("event_types", "setup_guide", mode="before")
109
+ @classmethod
110
+ def _none_to_list(cls, v: Any) -> Any:
111
+ return [] if v is None else v
112
+
113
+
114
+ class ProviderEvents(HookbaseModel):
115
+ """GET /api/catalog/providers/:id/events."""
116
+
117
+ provider_id: str
118
+ provider_name: str = ""
119
+ event_types: list[ProviderEventType] = []
120
+ sample_payloads: dict[str, Any] = {}
@@ -0,0 +1,283 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, Literal
5
+
6
+ from pydantic import field_validator
7
+
8
+ from . import _wire
9
+ from ._base import HookbaseModel
10
+
11
+ # Providers the API accepts for a source today.
12
+ #
13
+ # Mirrors SUPPORTED_SIGNATURE_PROVIDERS in the API (api/src/utils/signature-schemes.ts): 71
14
+ # signature schemes plus the aliases svix, clerk, resend, render, etsy and stytch (all of
15
+ # standard-webhooks) and generic (of custom). Regenerate with
16
+ # `npx tsx scripts/print-source-enums.ts` in the api package.
17
+ #
18
+ # This is a list of suggestions, not a constraint. `provider` is typed as a plain string on every
19
+ # model, request and response alike, because the API adds providers faster than this package
20
+ # ships: when it was a Literal, a single source created with a newer provider (wise, whatsapp...)
21
+ # made list(), get() and create() raise a ValidationError for the whole call. The API validates
22
+ # the value on create and update and answers an unknown one with a 400 naming it.
23
+ KNOWN_SOURCE_PROVIDERS: tuple[str, ...] = (
24
+ "adyen", "airtable", "asana", "auth0", "aws-sns", "bigcommerce", "bitbucket", "braintree",
25
+ "calendly", "chargebee", "circleci", "clerk", "clickup", "cloudflare", "coinbase-commerce",
26
+ "custom", "datadog", "discord", "docusign", "etsy", "freshdesk", "generic", "github",
27
+ "gitlab", "gitlab-signing-token", "gocardless", "grafana", "grafana-hmac",
28
+ "grafana-hmac-timestamped", "heroku", "hubspot", "intercom", "jira", "klaviyo",
29
+ "launchdarkly", "lemonsqueezy", "linear", "mailchimp", "mailgun", "mollie", "netlify",
30
+ "newrelic", "notion", "okta", "paddle", "pagerduty", "paypal", "pipedrive", "postmark",
31
+ "railway", "razorpay", "render", "resend", "segment", "sendgrid", "sentry", "shopify",
32
+ "slack", "square", "standard-webhooks", "stripe", "stytch", "supabase",
33
+ "supabase-auth-hooks", "svix", "telegram", "terraform-cloud", "trello", "twilio",
34
+ "typeform", "vercel", "whatsapp", "wise", "wise-sandbox", "woocommerce", "workos",
35
+ "zendesk", "zoom",
36
+ )
37
+ #: A source provider id. Any string: see KNOWN_SOURCE_PROVIDERS for the values accepted today.
38
+ SourceProvider = str
39
+
40
+ # Corrected in 2026-09: "header" and "event_id" were never accepted, and the three values the
41
+ # API actually defaults to and documents were missing. "auto" is the default.
42
+ DedupStrategy = Literal["auto", "provider_id", "payload_hash", "idempotency_key", "none"]
43
+ IpFilterMode = Literal["none", "allowlist", "denylist", "both"]
44
+ # HTTP verbs an ingest endpoint can be restricted to. OPTIONS is excluded: CORS preflight is
45
+ # answered before ingest runs, so it is never gateable.
46
+ IngestMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]
47
+
48
+ # The settings PATCH /api/sources/:id accepts an explicit null for (`.nullable()` in
49
+ # updateSourceSchema). Naming one in `UpdateSourceParams.clear` sends that null. Null signingSecret
50
+ # removes the secret; null verifyToken generates a fresh one. `clear` also takes snake_case.
51
+ CLEARABLE_SOURCE_FIELDS: tuple[str, ...] = (
52
+ "description",
53
+ "provider",
54
+ "signingSecret",
55
+ "verifyToken",
56
+ "rateLimitPerMinute",
57
+ "ipAllowlist",
58
+ "ipDenylist",
59
+ "encryptFields",
60
+ "maskFields",
61
+ "dedupStrategy",
62
+ "dedupWindowHours",
63
+ "dedupCustomHeader",
64
+ "allowedMethods",
65
+ )
66
+ ClearableSourceField = Literal[
67
+ "description", "provider", "signingSecret", "verifyToken", "rateLimitPerMinute",
68
+ "ipAllowlist", "ipDenylist", "encryptFields", "maskFields", "dedupStrategy",
69
+ "dedupWindowHours", "dedupCustomHeader", "allowedMethods",
70
+ "signing_secret", "verify_token", "rate_limit_per_minute", "ip_allowlist", "ip_denylist",
71
+ "encrypt_fields", "mask_fields", "dedup_strategy", "dedup_window_hours",
72
+ "dedup_custom_header", "allowed_methods",
73
+ ]
74
+
75
+
76
+ class Source(HookbaseModel):
77
+ """A source as the API returns it.
78
+
79
+ The signing secret is not included. Responses carry ``has_signing_secret`` and the last four
80
+ characters; the full value comes back exactly twice, from :meth:`Sources.create` (which
81
+ returns a :class:`SourceWithSecret`) and from :meth:`Sources.reveal_secret`.
82
+
83
+ ``has_signing_secret`` being False is not always a problem. Check the two flags beside it:
84
+ ``verifies_without_secret`` (the provider signs with a key Hookbase already holds, as Wise
85
+ and AWS SNS do, so events are verified with nothing set) and ``awaiting_signing_secret``
86
+ (the vendor issues the secret only after the URL is registered with it, as Asana and Notion
87
+ do, and it has not been set yet).
88
+ """
89
+
90
+ id: str
91
+ organization_id: str | None = None
92
+ name: str
93
+ slug: str
94
+ #: Any string; see KNOWN_SOURCE_PROVIDERS. Never validated client-side.
95
+ provider: SourceProvider | None = None
96
+ description: str | None = None
97
+ #: Whether a signing secret is set. The secret itself is not returned here.
98
+ has_signing_secret: bool = False
99
+ #: Last four characters of the signing secret, prefixed with "...", or None if unset.
100
+ signing_secret_last4: str | None = None
101
+ #: The provider verifies with a key Hookbase already holds (wise, wise-sandbox, aws-sns), so
102
+ #: there is no secret to set and has_signing_secret stays False on a fully verified source.
103
+ verifies_without_secret: bool = False
104
+ #: The vendor issues the secret only once this source's URL is registered with it (asana,
105
+ #: notion) and it has not been set yet. Events are not verified until it is. For Notion, see
106
+ #: Sources.get_pending_verification.
107
+ awaiting_signing_secret: bool = False
108
+ #: Reject events whose signature fails verification. Defaults to False, in which case a
109
+ #: failing event is flagged with signature_valid=False and delivered anyway.
110
+ reject_invalid_signatures: bool = False
111
+ rate_limit_per_minute: int | None = None
112
+ is_active: bool = True
113
+ custom_domain_id: str | None = None
114
+ ip_filter_mode: IpFilterMode = "none"
115
+ ip_allowlist: list[str] = []
116
+ ip_denylist: list[str] = []
117
+ #: JSONPath expressions whose values are encrypted at rest.
118
+ encrypt_fields: list[str] = []
119
+ #: JSONPath expressions whose values are masked in stored payloads.
120
+ mask_fields: list[str] = []
121
+ dedup_enabled: bool = False
122
+ dedup_strategy: DedupStrategy = "auto"
123
+ dedup_window_hours: int = 24
124
+ dedup_custom_header: str | None = None
125
+ transient_mode: bool = False
126
+ #: HTTP verbs the ingest endpoint accepts. Empty list means any method.
127
+ allowed_methods: list[str] = []
128
+ event_count: int = 0
129
+ route_count: int = 0
130
+ #: Returned by get() and create(). Not included in list() responses.
131
+ ingest_url: str | None = None
132
+ created_at: str = ""
133
+ updated_at: str = ""
134
+
135
+ @field_validator(
136
+ "is_active",
137
+ "reject_invalid_signatures",
138
+ "has_signing_secret",
139
+ "verifies_without_secret",
140
+ "awaiting_signing_secret",
141
+ "dedup_enabled",
142
+ "transient_mode",
143
+ mode="before",
144
+ )
145
+ @classmethod
146
+ def parse_bool_from_int(cls, v: Any) -> Any:
147
+ if isinstance(v, int):
148
+ return bool(v)
149
+ return v
150
+
151
+ @field_validator(
152
+ "ip_allowlist", "ip_denylist", "encrypt_fields", "mask_fields", mode="before"
153
+ )
154
+ @classmethod
155
+ def parse_json_list(cls, v: Any) -> Any:
156
+ if v is None:
157
+ return []
158
+ if isinstance(v, str):
159
+ try:
160
+ parsed = json.loads(v)
161
+ return parsed if isinstance(parsed, list) else []
162
+ except (json.JSONDecodeError, ValueError):
163
+ return []
164
+ return v
165
+
166
+ @field_validator("allowed_methods", mode="before")
167
+ @classmethod
168
+ def parse_allowed_methods(cls, v: Any) -> Any:
169
+ """Coerce null/JSON-string forms to a list; both mean "any method" when empty."""
170
+ if v is None:
171
+ return []
172
+ if isinstance(v, str):
173
+ try:
174
+ parsed = json.loads(v)
175
+ return parsed if isinstance(parsed, list) else []
176
+ except (json.JSONDecodeError, ValueError):
177
+ return []
178
+ return v
179
+
180
+
181
+ class SourceWithSecret(Source):
182
+ """What create() returns: a source plus the one look at its signing secret you get."""
183
+
184
+ #: The signing secret, in full, this once. None when the source has none to show: the
185
+ #: provider verifies without one (verifies_without_secret), the vendor issues it after
186
+ #: registration (awaiting_signing_secret), or the provider does not sign.
187
+ signing_secret: str | None = None
188
+ #: Meta providers (whatsapp) only: the verify token to paste into Meta's webhook setup, in
189
+ #: full, this once. Sources.verify_token reveals it later.
190
+ verify_token: str | None = None
191
+
192
+
193
+ class PendingVerification(HookbaseModel):
194
+ """A verification token a vendor delivered to the ingest URL during registration (Notion).
195
+
196
+ The request that carried it was unauthenticated, so it is held rather than adopted: paste
197
+ ``token`` into the vendor's verify form, and once the vendor accepts it, call
198
+ :meth:`Sources.adopt_pending_verification` to make it the source's signing secret.
199
+ """
200
+
201
+ provider: str
202
+ token: str
203
+ received_at: str
204
+ client_ip: str | None = None
205
+
206
+
207
+ class CreateSourceParams(HookbaseModel):
208
+ name: str
209
+ #: Required: it forms the ingest URL, /ingest/<org>/<slug>, and cannot be changed later.
210
+ slug: str
211
+ provider: SourceProvider | None = None
212
+ description: str | None = None
213
+ #: What to supply depends on the provider (the catalog's signatureVerification.secretSource,
214
+ #: see client.providers.get):
215
+ #: - "provider": the vendor issues it. Required for several (standard-webhooks and its
216
+ #: aliases, airtable, gitlab-signing-token, supabase-auth-hooks, jira, discord, sendgrid);
217
+ #: a missing or malformed one is a 400.
218
+ #: - "operator": omit it and the API generates one, returned once on the created source.
219
+ #: - "public_key": the vendor's public key (PEM or base64).
220
+ #: - "built_in" (wise, wise-sandbox, aws-sns): send nothing; any secret is a 400.
221
+ signing_secret: str | None = None
222
+ #: Meta providers (whatsapp) only; a 400 for any other. Omit it and the API generates one.
223
+ verify_token: str | None = None
224
+ #: Reject events whose signature fails verification. Defaults to False.
225
+ reject_invalid_signatures: bool | None = None
226
+ rate_limit_per_minute: int | None = None
227
+ ip_filter_mode: IpFilterMode | None = None
228
+ ip_allowlist: list[str] | None = None
229
+ ip_denylist: list[str] | None = None
230
+ encrypt_fields: list[str] | None = None
231
+ mask_fields: list[str] | None = None
232
+ dedup_enabled: bool | None = None
233
+ dedup_strategy: DedupStrategy | None = None
234
+ #: Deduplication window in hours, 1 to 168.
235
+ dedup_window_hours: int | None = None
236
+ #: Header to deduplicate on when dedup_strategy is "idempotency_key".
237
+ dedup_custom_header: str | None = None
238
+ transient_mode: bool | None = None
239
+ #: Restrict the ingest endpoint to these verbs. None/[] accepts any method.
240
+ allowed_methods: list[IngestMethod] | None = None
241
+
242
+
243
+ class UpdateSourceParams(HookbaseModel):
244
+ name: str | None = None
245
+ description: str | None = None
246
+ provider: SourceProvider | None = None
247
+ is_active: bool | None = None
248
+ signing_secret: str | None = None
249
+ #: Meta providers (whatsapp) only. "" regenerates it, as does clear=["verifyToken"].
250
+ verify_token: str | None = None
251
+ reject_invalid_signatures: bool | None = None
252
+ rate_limit_per_minute: int | None = None
253
+ ip_filter_mode: IpFilterMode | None = None
254
+ ip_allowlist: list[str] | None = None
255
+ ip_denylist: list[str] | None = None
256
+ encrypt_fields: list[str] | None = None
257
+ mask_fields: list[str] | None = None
258
+ dedup_enabled: bool | None = None
259
+ dedup_strategy: DedupStrategy | None = None
260
+ dedup_window_hours: int | None = None
261
+ dedup_custom_header: str | None = None
262
+ transient_mode: bool | None = None
263
+ #: Restrict the ingest endpoint to these verbs. Pass [] to accept any method again.
264
+ allowed_methods: list[IngestMethod] | None = None
265
+
266
+ clear: list[ClearableSourceField] | None = None
267
+ """Fields to send as an explicit JSON null.
268
+
269
+ ``None`` on any field above means "leave it alone", so it cannot also mean "remove it".
270
+ Naming a field here sends null for it, which PATCH /api/sources/:id accepts for the fields in
271
+ ``CLEARABLE_SOURCE_FIELDS``::
272
+
273
+ client.sources.update("src_1", UpdateSourceParams(clear=["signingSecret"]))
274
+ client.sources.update("src_1", UpdateSourceParams(clear=["verifyToken"])) # regenerate
275
+
276
+ A field that is also set on this model raises rather than the SDK choosing one silently."""
277
+
278
+
279
+ def update_source_body(params: UpdateSourceParams | dict[str, Any]) -> dict[str, Any]:
280
+ """Build the PATCH /api/sources/:id body, turning ``clear`` into explicit nulls."""
281
+ body = _wire.dump(params)
282
+ _wire.apply_clear(body, CLEARABLE_SOURCE_FIELDS, "source")
283
+ return body
@@ -13,6 +13,7 @@ from .message_log import AsyncMessageLog, MessageLog
13
13
  from .messages import AsyncMessages, Messages
14
14
  from .organizations import AsyncOrganizations, Organizations
15
15
  from .portal_tokens import AsyncPortalTokens, PortalTokens
16
+ from .providers import AsyncProviders, Providers
16
17
  from .routes import AsyncRoutes, Routes
17
18
  from .schemas import AsyncSchemas, Schemas
18
19
  from .sources import AsyncSources, Sources
@@ -36,6 +37,7 @@ __all__ = [
36
37
  "Messages", "AsyncMessages",
37
38
  "Organizations", "AsyncOrganizations",
38
39
  "PortalTokens", "AsyncPortalTokens",
40
+ "Providers", "AsyncProviders",
39
41
  "Routes", "AsyncRoutes",
40
42
  "Schemas", "AsyncSchemas",
41
43
  "Sources", "AsyncSources",