Webhooks
Affinda has implemented webhooks to allow the data extracted by Affinda to be 'pushed' to you when an event occurs (e.g. document parsed or document validated), instead of you having to constantly poll our API to 'pull' the data to your system.
Webhooks can be created at an Organization or Workspace level
RESThooks
We have implemented a slightly extended version of webhook called “RESTful webhook”, or “resthook”. It allows you to programmatically manage webhook subscriptions like you would with any RESTful resources.
For end-users, it means setting up a webhook subscription is a matter of clicking buttons, and no copy-pasting of URLs and cryptic tokens.
Create a webhook subscription
There are 2 steps to creating a webhook:
- Subscribe to an event
- The receiver confirms its intention to subscribe
Subscribe to an event
Request
URL: Create a resthook subscriptions
Method: POST
Body:
{
"targetUrl": "https://your-receiver-domain.com/receive/",
"event": "resume.parse.completed",
"organization": "{{organization_identifier}}"
}
Available events:
Event Name | Description |
---|---|
document.parse.completed | Document has completed parsing, the parsing may have succeeded or failed |
document.parse.succeeded | Document parsing has succeeded |
document.parse.failed | Document parsing has failed |
document.validate.completed | Document has been validated |
document.classify.completed | Document has completed classifying, the classification may have succeeded or failed |
document.classify.succeeded | Document classification has succeeded |
document.classify.failed | Document classification has failed |
document.rejected | Document has been rejected (automatically or by a user) |
resume.parse.completed | Resume has completed parsing, the parsing may have succeeded or failed |
resume.parse.succeeded | Resume parsing has succeeded |
resume.parse.failed | Resume parsing has failed |
invoice.parse.completed | Invoice has completed parsing, the parsing may have succeeded or failed |
invoice.parse.succeeded | Invoice parsing has succeeded |
invoice.parse.failed | Invoice parsing has failed |
invoice.validate.completed | Invoice has been validated |
Response
Status code: 201
Body:
{
"id": 1,
"event": "document.parse.completed",
"targetUrl": "https://your-receiver-domain.com/receive/",
"organization": {
"identifier": "{{organization_identifier}}",
"name": "{{organization_name}}",
"userRole": "admin",
"avatar": null,
"resthookSignatureKey": "{{resthook_signature_key}}",
"isTrial": false
},
"active": false,
"version": "v3",
"autoDeactivated": false,
"autoDeactivateReason": ""
}
Receiver confirms intention to subscribe
After the last step, we’ll POST to the receiver at https://your-receiver-domain.com/receive/ with a X-Hook-Secret
header. The receiver should respond to this request with a 200 status code, and then activate the subscription using the X-Hook-Secret
as below
Request
URL: /v3/resthook_subscriptions/activate
Method: POST
Headers:
X-Hook-Secret: <The X-Hook-Secret you received>
Response
{
"id": 1,
"event": "document.parse.completed",
"targetUrl": "https://your-receiver-domain.com/receive/",
"organization": {
"identifier": "{{organization_identifier}}",
"name": "{{organization_name}}",
"userRole": "admin",
"avatar": null,
"resthookSignatureKey": "{{resthook_signature_key}}",
"isTrial": false
},
"active": true,
"version": "v3",
"autoDeactivated": false,
"autoDeactivateReason": ""
}
Example code
import requests
from some_framework import make_response
def receiver(request):
if "X-Hook-Secret" in request.headers:
# Confirm subscibe intention (you can confirm now or save the "X-Hook-Secret" and confirm later)
requests.post(
'https://api.affinda.com/v3/resthook_subscriptions/activate',
headers={
'Authorization': 'Bearer <your-api-key>',
'X-Hook-Secret': request.headers['X-Hook-Secret'],
}
)
return make_response(status_code=200)
else:
# Handle webhook notification
...
You have confirmed your intention to subscribe and are now ready to start receiving webhook notifications!
Receive a webhook notification
Verify webhook payload integrity
Enable webhook payload signing
Enable webhook payload signing
It’s highly recommended to enable webhook payload signing, so that you can verify that the payload indeed comes from Affinda and has not been tampered with.
To enable webhook payload signing, setup a resthook signature key for your account
- Go to Affinda
- Click on the “Settings” tab of your organization page
- In the section “Webhook Signature Key”, copy the generated key (or click “Regenerate” to get a new key)
Verify the webhook payload when you receive it
In the webhook notification that Affinda sends, there’s a header called X-Hook-Signature
of the following format: <timestamp>.<signature>
.
To verify the webhook payload, sign the request body with the Signature Key you set up in the previous step using SHA256, then compare the resulting signature with the signature found in the X-Hook-Signature header. Only process the data if the signatures match!
Confirm payload timestamp is within a short range of current time to prevent replay attacks.
Example code
import hashlib
import hmac
import json
import time
from some_framework import make_response
def receiver(request):
if "X-Hook-Secret" in request.headers:
# Confirm subscribe intention
...
else:
# Handle webhook notification
sig_header = request.headers['X-Hook-Signature']
timestamp, sig_received = sig_header.split('.')
sig_key = b'your-signature-key'
sig_calculated = hmac.new(sig_key, msg=request.body, digestmod=hashlib.sha256).hexdigest()
# Verify signature
sig_verified = hmac.compare_digest(sig_received, sig_calculated)
# Verify timestamp to prevent replay attack
body = json.loads(request.body)
now = time.time()
max_timestamp_diff = 10 * 60 # 10 minutes
timestamp_verified = (
body["timestamp"] == int(timestamp) and
now - body["timestamp"] < max_timestamp_diff
)
if sig_verified and timestamp_verified:
print('Data is safe to process')
process_data(body)
else:
print("Spies detected! Don't process the data")
return make_response(status_code=200)
Respond to the webhook notification
If we receive status code 200, the webhook notification is considered successfully received.
If we receive status code 410, the receiver is considered “gone” and we'll automatically delete your webhook subscription.
If we receive any other 4xx, 5xx status code, we’ll retry sending the webhook notification in increasingly wider intervals (exponential backoff retry strategy), and eventually stop retrying after about 1 day.
Webhook payload reference
The webhook payload is the metadata of the document. You can use it to retrieve the full parsed data if you need to.
{
"id": "e3bd1942-635b-4971-b8f1-59543b0b2f1f",
"event": "document.parse.completed",
"timestamp": 1665637107,
"payload": {
"identifier": "abcdXYZ",
"ready": true,
"failed": false,
"readyDt": "2023-02-10T08:05:30.775110Z",
"fileName": "resume.pdf",
"expiryTime": null,
"language": "en",
"pdf": "{{url_to_pdf}}",
"parentDocument": null,
"childDocuments": [],
"pages": [
{
"id": 123,
"pageIndex": 0,
"image": null,
"height": 841.0,
"width": 595.0,
"rotation": 0
}
],
"ocrConfidence": null,
"reviewUrl": null,
"createdDt": "2023-02-10T08:05:28.871650Z",
"errorDetail": null,
"file": "{{url_to_file}}",
"collection": {
"identifier": "abcdXYZ",
"name": "Resume Parses",
"extractor": {
"id": 6,
"identifier": "resume",
"name": "Resume Parse",
"baseExtractor": null,
"validatable": false
}
},
"workspace": {
"identifier": "abcdXYZ",
"name": "Recruitment"
},
"tags": [],
"isOcrd": false,
"isConfirmed": true,
"confirmedDt": "2023-02-10T08:05:30.775110Z",
"isRejected": false,
"rejectedDt": null,
"isArchived": false,
"archivedDt": null,
"errorCode": null
}
}
Update webhook subscription
Request
URL: Update a resthook subscription
Method: PATCH
Body:
{
"event": "document.parse.failed"
}
Response
Status code: 200
Body:
{
"id": 1,
"event": "document.parse.failed",
"targetUrl": "https://your-receiver-domain.com/receive/",
"organization": {
"identifier": "abcdXYZ",
"name": "Affinda",
"userRole": "admin",
"avatar": null,
"resthookSignatureKey": "KEY",
"isTrial": false
},
"active": true,
"version": "v3",
"autoDeactivated": false,
"autoDeactivateReason": ""
}
Delete webhook subscription
Request
URL: Delete a resthook subscription
Method: DELETE
Response
Status code: 204
List webhook subscriptions
Request
URL: Get list of all resthook subscriptions
Method: GET
Response
Status code: 200
Body:
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"event": "document.parse.completed",
"targetUrl": "https://your-receiver-domain.com",
"organization": {
"identifier": "abcdXYZ",
"name": "Affinda",
"userRole": "admin",
"avatar": null,
"resthookSignatureKey": "KEY",
"isTrial": false
},
"active": true,
"version": "v3",
"autoDeactivated": false,
"autoDeactivateReason": ""
},
]
}
Retrieve webhook subscription
Request
URL: Get specific resthook subscription
Method: GET
Response
Status code: 200
Body:
{
"id": 1,
"event": "document.parse.completed",
"targetUrl": "https://your-receiver-domain.com/receive/",
"organization": {
"identifier": "abcdXYZ",
"name": "Affinda",
"userRole": "admin",
"avatar": null,
"resthookSignatureKey": "KEY",
"isTrial": false
},
"active": true,
"version": "v3",
"autoDeactivated": false,
"autoDeactivateReason": ""
}
Updated 3 months ago