Affinda have 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.
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 urls and cryptic tokens.
For more information about resthook: REST Hooks
Create a webhook subscription
There are 2 steps to creating an webhook:
-
Subscribe to an event
-
The receiver confirms its subscribe intention
Subscribe to an event
Request
URL:/v3/resthook_subscriptions
Method: POST
Body:
{
"targetUrl": "https://your-receiver-domain.com/receive/",
"event": "resume.parse.completed"
"organization":"{{organization_identifier}}"
}
Available events:
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 |
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 subscribe intention
After the last step, Affinda will POST to the receiver at https://your-receiver-domain.com/receive/
with a X-Hook-Secret
header. The receiver should response 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/v2/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 now ready to start receiving webhook notifications!
Receive a webhook notification
Verify webhook payload integrity
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 (after all, your receiver is a public URL, you don’t want to blindly process any data posted to it!).
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)
In the webhook notification that Affinda sends, there’s a header called X-Hook-Signature
of the following format: <timestamp>.<signature>
.
Sign the request body with the Signature Key you setup 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!
Example code
import hashlib
import hmac
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()
if hmac.compare_digest(sig_received, sig_calculated):
print('Data is safe to process')
process_data(request.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
{
"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
}
}
Request
URL: /v3/resthook_subscriptions/<id>
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: /v3/resthook_subscriptions/<id>
Method: DELETE
Response
Status code: 204
List webhook subscriptions
Request
URL: /v3/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: /v3/resthook_subscriptions/<id>
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": ""
}