Skip to content

Commit 483ffef

Browse files
feat: add webhook verification methods (#89)
1 parent 77b9914 commit 483ffef

File tree

6 files changed

+494
-1
lines changed

6 files changed

+494
-1
lines changed

README.md

+23
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,29 @@ client.hris.directory.list_individuals(
141141
)
142142
```
143143

144+
## Webhook Verification
145+
146+
We provide helper methods for verifying that a webhook request came from Finch, and not a malicious third party.
147+
148+
You can use `finch.webhooks.verify_signature(body: string, headers, secret?) -> None` or `finch.webhooks.unwrap(body: string, headers, secret?) -> Payload`,
149+
both of which will raise an error if the signature is invalid.
150+
151+
Note that the "body" parameter must be the raw JSON string sent from the server (do not parse it first).
152+
The `.unwrap()` method can parse this JSON for you into a `Payload` object.
153+
154+
For example, in [FastAPI](https://fastapi.tiangolo.com/):
155+
156+
```py
157+
@app.post('/my-webhook-handler')
158+
async def handler(request: Request):
159+
body = await request.body()
160+
secret = os.environ['FINCH_WEBHOOK_SECRET'] # env var used by default; explicit here.
161+
payload = client.webhooks.unwrap(body, request.headers, secret)
162+
print(payload)
163+
164+
return {'ok': True}
165+
```
166+
144167
## Handling errors
145168

146169
When the library is unable to connect to the API (e.g., due to network connection problems or a timeout), a subclass of `finch.APIConnectionError` is raised.

api.md

+7
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,10 @@ Methods:
223223

224224
- <code title="post /disconnect">client.account.<a href="./src/finch/resources/account.py">disconnect</a>() -> <a href="./src/finch/types/disconnect_response.py">DisconnectResponse</a></code>
225225
- <code title="get /introspect">client.account.<a href="./src/finch/resources/account.py">introspect</a>() -> <a href="./src/finch/types/introspection.py">Introspection</a></code>
226+
227+
# Webhooks
228+
229+
Methods:
230+
231+
- <code>client.webhooks.<a href="./src/finch/resources/webhooks.py">unwrap</a>(\*args) -> object</code>
232+
- <code>client.webhooks.<a href="./src/finch/resources/webhooks.py">verify_signature</a>(\*args) -> None</code>

src/finch/_client.py

+20
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,20 @@ class Finch(SyncAPIClient):
4949
ats: resources.ATS
5050
providers: resources.Providers
5151
account: resources.Account
52+
webhooks: resources.Webhooks
5253

5354
# client options
5455
access_token: str | None
5556
client_id: str | None
5657
client_secret: str | None
58+
webhook_secret: str | None
5759

5860
def __init__(
5961
self,
6062
*,
6163
client_id: str | None = None,
6264
client_secret: str | None = None,
65+
webhook_secret: str | None = None,
6366
base_url: Optional[str] = None,
6467
access_token: Optional[str] = None,
6568
timeout: Union[float, Timeout, None] = DEFAULT_TIMEOUT,
@@ -87,6 +90,7 @@ def __init__(
8790
This automatically infers the following arguments from their corresponding environment variables if they are not provided:
8891
- `client_id` from `FINCH_CLIENT_ID`
8992
- `client_secret` from `FINCH_CLIENT_SECRET`
93+
- `webhook_secret` from `FINCH_WEBHOOK_SECRET`
9094
"""
9195
self.access_token = access_token
9296

@@ -96,6 +100,9 @@ def __init__(
96100
client_secret_envvar = os.environ.get("FINCH_CLIENT_SECRET", None)
97101
self.client_secret = client_secret or client_secret_envvar or None
98102

103+
webhook_secret_envvar = os.environ.get("FINCH_WEBHOOK_SECRET", None)
104+
self.webhook_secret = webhook_secret or webhook_secret_envvar or None
105+
99106
if base_url is None:
100107
base_url = f"https://api.tryfinch.com"
101108

@@ -116,6 +123,7 @@ def __init__(
116123
self.ats = resources.ATS(self)
117124
self.providers = resources.Providers(self)
118125
self.account = resources.Account(self)
126+
self.webhooks = resources.Webhooks(self)
119127

120128
@property
121129
def qs(self) -> Querystring:
@@ -151,6 +159,7 @@ def copy(
151159
*,
152160
client_id: str | None = None,
153161
client_secret: str | None = None,
162+
webhook_secret: str | None = None,
154163
access_token: str | None = None,
155164
base_url: str | None = None,
156165
timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
@@ -189,6 +198,7 @@ def copy(
189198
return self.__class__(
190199
client_id=client_id or self.client_id,
191200
client_secret=client_secret or self.client_secret,
201+
webhook_secret=webhook_secret or self.webhook_secret,
192202
base_url=base_url or str(self.base_url),
193203
access_token=access_token or self.access_token,
194204
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
@@ -272,17 +282,20 @@ class AsyncFinch(AsyncAPIClient):
272282
ats: resources.AsyncATS
273283
providers: resources.AsyncProviders
274284
account: resources.AsyncAccount
285+
webhooks: resources.AsyncWebhooks
275286

276287
# client options
277288
access_token: str | None
278289
client_id: str | None
279290
client_secret: str | None
291+
webhook_secret: str | None
280292

281293
def __init__(
282294
self,
283295
*,
284296
client_id: str | None = None,
285297
client_secret: str | None = None,
298+
webhook_secret: str | None = None,
286299
base_url: Optional[str] = None,
287300
access_token: Optional[str] = None,
288301
timeout: Union[float, Timeout, None] = DEFAULT_TIMEOUT,
@@ -310,6 +323,7 @@ def __init__(
310323
This automatically infers the following arguments from their corresponding environment variables if they are not provided:
311324
- `client_id` from `FINCH_CLIENT_ID`
312325
- `client_secret` from `FINCH_CLIENT_SECRET`
326+
- `webhook_secret` from `FINCH_WEBHOOK_SECRET`
313327
"""
314328
self.access_token = access_token
315329

@@ -319,6 +333,9 @@ def __init__(
319333
client_secret_envvar = os.environ.get("FINCH_CLIENT_SECRET", None)
320334
self.client_secret = client_secret or client_secret_envvar or None
321335

336+
webhook_secret_envvar = os.environ.get("FINCH_WEBHOOK_SECRET", None)
337+
self.webhook_secret = webhook_secret or webhook_secret_envvar or None
338+
322339
if base_url is None:
323340
base_url = f"https://api.tryfinch.com"
324341

@@ -339,6 +356,7 @@ def __init__(
339356
self.ats = resources.AsyncATS(self)
340357
self.providers = resources.AsyncProviders(self)
341358
self.account = resources.AsyncAccount(self)
359+
self.webhooks = resources.AsyncWebhooks(self)
342360

343361
@property
344362
def qs(self) -> Querystring:
@@ -374,6 +392,7 @@ def copy(
374392
*,
375393
client_id: str | None = None,
376394
client_secret: str | None = None,
395+
webhook_secret: str | None = None,
377396
access_token: str | None = None,
378397
base_url: str | None = None,
379398
timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
@@ -412,6 +431,7 @@ def copy(
412431
return self.__class__(
413432
client_id=client_id or self.client_id,
414433
client_secret=client_secret or self.client_secret,
434+
webhook_secret=webhook_secret or self.webhook_secret,
415435
base_url=base_url or str(self.base_url),
416436
access_token=access_token or self.access_token,
417437
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,

src/finch/resources/__init__.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
from .ats import ATS, AsyncATS
44
from .hris import HRIS, AsyncHRIS
55
from .account import Account, AsyncAccount
6+
from .webhooks import Webhooks, AsyncWebhooks
67
from .providers import Providers, AsyncProviders
78

8-
__all__ = ["HRIS", "AsyncHRIS", "ATS", "AsyncATS", "Providers", "AsyncProviders", "Account", "AsyncAccount"]
9+
__all__ = [
10+
"HRIS",
11+
"AsyncHRIS",
12+
"ATS",
13+
"AsyncATS",
14+
"Providers",
15+
"AsyncProviders",
16+
"Account",
17+
"AsyncAccount",
18+
"Webhooks",
19+
"AsyncWebhooks",
20+
]

0 commit comments

Comments
 (0)