This content is still in draft.
Webhooks are SEEK’s preferred method of notifying your software when events occur on SEEK.
They deliver events by posting JSON to an HTTPS endpoint you configure within the SEEK API.
SEEK keeps track of which events have been successfully delivered and automatically retries those that haven’t.
See the why webhooks? section for comparison between webhooks and polling for event delivery.
Whenever an event occurs within the SEEK API, the relevant webhook subscriptions are selected based on the
schemeId
, eventTypeCode
and the hirer’s relationships with their integration partners.Each matching subscription’s
url
endpoint will be called with a JSON body containing up to 10 events.
The details of the event JSON vary depending on the event type, but will typically contain an object ID to be queried via GraphQL.For example, a JSON body containing two
CandidateApplicationCreated
events might look like this:JSON
Copy
{
"events": [
{
"id": "seekAnzPublicTest:event:events:PKCrbdMA7Z99Dvtfo94WTL",
"type": "CandidateApplicationCreated",
"createDateTime": "2019-09-18T10:18:16.050Z",
"candidateApplicationProfileId": "seekAnzPublicTest:candidateProfile:apply:4QM5fWQbdekL9gPtPZrzex",
"candidateId": "seekAnzPublicTest:candidate:feed:5PGXAHysjZdkQYwZghfL4bRCqvZ7ZM"
},
{
"id": "seekAnzPublicTest:event:events:3QgcY4aFZcc1eu5gjBNCtc",
"type": "CandidateApplicationCreated",
"createDateTime": "2019-09-18T10:18:32.150Z",
"candidateApplicationProfileId": "seekAnzPublicTest:candidateProfile:apply:7DtW6Q68gk2R4okNGhvg1Y",
"candidateId": "seekAnzPublicTest:candidate:feed:5PGXAHysiXhtA9JUqhyM8hhzMuWMPA"
}
],
"subscriptionId": "seekAnzPublicTest:webhookSubscription:events:BoJiJ9ZWFVgejLXLJxUnvL"
}
To indicate the events were successfully accepted the endpoint should return a
2xx
status code (for example, 200 OK
or 204 No Content
).
The endpoint’s response body is not used by SEEK.SEEK will time out the HTTPS request after 10 seconds and treat it as a failure.
If your software requires more time to process events it should queue the event internally instead of processing it synchronously.
Once the webhook endpoint has enqueued the events it should return a
2xx
status code (conventionally 202 Accepted
).You should consider the time required to process multiple events, especially if your webhook endpoint processes events sequentially.
For example, if a subscription is configured with
maxEventsPerAttempt: 10
your endpoint would have 1 second to process each event in sequence.
You can alleviate this by processing events concurrently or reducing maxEventsPerAttempt
to fit within the 10 second time budget.If a webhook endpoint is not reachable, times out or returns a non-
2xx
status code the delivery will be considered a failure.
SEEK will automatically retry delivery after a short delay.SEEK will continue to retry for least a day with a maximum of 15 minutes between delivery attempts.
If the retries continue to fail, the delivery attempts will stop and the missed events must be recovered using replays or polling.
Delivery attempts can be queried using the
webhookAttemptsForSubscription
query :QueryVariables
Copy Playground
query($filter: WebhookAttemptsFilterInput!, $subscriptionId: String!) {
webhookAttemptsForSubscription(
filter: $filter
subscriptionId: $subscriptionId
) {
edges {
node {
request {
descriptionCode
requestId
statusCode
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
The
requestId
field corresponds to a unique X-Request-Id
HTTP header sent to the webhook endpoint.
This is useful for correlating delivery failures with your application logs.The SEEK API may send an event more than once to your endpoint.
This occurs when:
- Events fail delivery and are automatically retried, as described in retries & debugging above.
- Requests are incidentally duplicated; we don’t guarantee exactly-once delivery.
- Events are resent to backfill historical data.This may be requested for onboarding or disaster recovery purposes.
- Events are resent to remediate a data quality issue with the underlying objects.For example, if we find and fix a data quality issue with candidate applications, we may resend
CandidateApplicationCreated
events to prompt your software to re-retrieve the affected applications.
We encourage you to reprocess duplicate events where possible.
Webhook subscriptions are self-managed by the partner using GraphQL queries and mutations.
Subscriptions are scoped to a scheme ID, event type and an optional hirer ID.
To handle multiple event types with the same endpoint you can create multiple subscriptions pointing to the same
url
.If you don’t specify a hirer ID then the endpoint will receive events for all SEEK hirers you have a relationship with.
Filtering on a hirer ID can be useful for testing or if your software is partitioned by hirer.
However, it becomes your responsibility to create the appropriate webhook subscription as part of onboarding a new hirer.
To receive webhooks from SEEK you need to register an HTTPS endpoint using the
createWebhookSubscription
mutation .The
eventTypeCode
and schemeId
indicate which event types the url
should receive.MutationVariablesSuccess ResultConflict Result
Copy Playground
mutation($input: CreateWebhookSubscriptionInput!) {
createWebhookSubscription(input: $input) {
... on CreateWebhookSubscriptionPayload_Success {
webhookSubscription {
id {
value
}
}
}
... on CreateWebhookSubscriptionPayload_Conflict {
conflictingWebhookSubscription {
id {
value
}
}
}
}
}
To reduce per-event overhead up to 10 events may be delivered in the same HTTP request to
url
.
This can be limited using the maxEventsPerAttempt
field on the webhook subscription.Subscriptions are compared by their
eventTypeCode
, hirerId
, schemeId
and url
fields.
A request to create an existing subscription will receive a CreateWebhookSubscriptionPayload_Conflict
result.
The existing subscription will not be updated; you can select its details from the conflict result.Each webhook subscription maintains its own event stream under the hood to allow you to reprocess historical events.
Events will remain for 90 days after they occur.
Your software must store any data that it needs to access after the 90 day period.
QueryVariables
Copy Playground
query($filter: EventsFilterInput!, $schemeId: String!) {
events(filter: $filter, schemeId: $schemeId) {
edges {
cursor
}
}
}
The
replayWebhookSubscription
mutation can be used to requeue events for delivery.You can only replay events that were generated after the subscription was created.
The polling backfill flow can be used to backfill a new subscription with historical events.
By default all undelivered events from the past 90 days will be requeued.
MutationVariables
Copy Playground
mutation($input: ReplayWebhookSubscriptionInput!) {
replayWebhookSubscription(input: $input) {
webhookSubscription {
id {
value
}
}
}
}
All events for a subscription may be replayed by setting
replayDeliveredEventsIndicator
to true and specifying a date range.MutationVariables
Copy Playground
mutation($input: ReplayWebhookSubscriptionInput!) {
replayWebhookSubscription(input: $input) {
webhookSubscription {
id {
value
}
}
}
}
The
webhookSubscriptions
query can be used to page through your subscriptions:QueryVariables
Copy Playground
query($schemeId: String!) {
webhookSubscriptions(schemeId: $schemeId) {
edges {
node {
id {
value
}
schemeId
eventTypeCode
url
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
You can’t modify which events are associated with an existing subscription.
Create a new subscription if you need to subscribe to a different set of events.
However fields related to the URL, payload & signature of an existing webhook subscription are mutable.
When these fields are updated, the changes take some time to propagate through SEEK’s internal systems.
When making a change to an existing webhook subscription your system should handle events being sent using either the old or the new configuration for up to half an hour.
Once the changes are applied they are applied atomically for any given event.
For example, an event will never be signed with an old secret but sent to a new URL or vice versa.
The
updateWebhookSubscriptionDeliveryConfiguration
mutation lets you modify how and where the events are delivered:MutationVariables
Copy Playground
mutation($input: UpdateWebhookSubscriptionDeliveryConfigurationInput!) {
updateWebhookSubscriptionDeliveryConfiguration(input: $input) {
webhookSubscription {
url
signingAlgorithmCode
maxEventsPerAttempt
}
}
}
When a subscription is deleted, the SEEK API will stop delivering events to its URL.
You may continue to retrieve historical events from its event stream for up to 90 days after they’ve occurred.
Deleting and recreating a subscription will issue a fresh ID and event stream.
If you need to maintain continuity, you should rather update the delivery configuration of the subscription.
Webhook subscriptions can be deleted using the
deleteWebhookSubscription
mutation :MutationVariables
Copy Playground
mutation($input: DeleteWebhookSubscriptionInput!) {
deleteWebhookSubscription(input: $input) {
webhookSubscription {
id {
value
}
schemeId
eventTypeCode
url
}
}
}
To receive webhooks, you’ll need to expose an HTTPS endpoint over the public Internet.
This allows anyone to make requests to your server.
For security and reliability purposes, SEEK can sign its requests.
You can validate the authenticity and integrity of requests through their signatures,
and filter out those that were not sent by SEEK.
The SEEK API does not send webhooks from a fixed list of specific IP addresses suitable for allowlisting.
Its IP addresses are dynamically allocated and may change at any point.
Create a webhook subscription with the following input fields:
MutationVariables
Copy Playground
mutation($input: CreateWebhookSubscriptionInput!) {
createWebhookSubscription(input: $input) {
... on CreateWebhookSubscriptionPayload_Success {
webhookSubscription {
id {
value
}
}
}
... on CreateWebhookSubscriptionPayload_Conflict {
conflictingWebhookSubscription {
id {
value
}
}
}
}
}
The secret should be a random string with high entropy that is not reused for any other purpose.
For
SeekHmacSha512
, a 128-byte secret is recommended.Requests will be sent to your configured URL with a
Seek-Signature
header.Compute your own HMAC-SHA-512 hex digest of the request body and compare it against the received header value:
TypeScript
Copy
// Koa + Node.js example
import crypto from 'crypto';
const validateRequest = async (ctx) => {
const receivedSignature = ctx.get('Seek-Signature');
const secret = await securelyRetrieveSecret();
// HMAC key is your secret
const hmac = crypto.createHmac('sha512', secret);
const computedSignature = hmac.update(ctx.request.rawBody).digest('hex');
if (
receivedSignature.length !== computedSignature.length ||
// constant-time algorithm
!crypto.timingSafeEqual(
Buffer.from(receivedSignature),
Buffer.from(computedSignature),
)
) {
ctx.throw(400, 'Invalid request signature');
}
};
Use a constant-time algorithm to validate the signature.
Regular comparison algorithms like the
==
operator may be vulnerable to timing attacks .