POST
ing JSON to an HTTPS endpoint you configure with SEEK.
SEEK keeps track of which events have been successfully delivered and automatically retries those that haven’t.The Developer Dashboard’s webhooks page is the easiest way to configure and monitor webhooks.
It wraps the SEEK API’s webhook queries & mutations in a friendly user interface.See the why webhooks? section for comparison between webhooks and polling for event delivery.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 identifier to be queried via GraphQL.For example, a JSON body containing two CandidateApplicationCreated
events might look like this:{
"events": [
{
"id": "seekAnzPublicTest:event:events:5cKZnRzXas97fyUAhpiAV1",
"type": "CandidateApplicationCreated",
"createDateTime": "2019-09-13T22:16:10.593Z",
"candidateApplicationProfileId": "seekAnzPublicTest:candidateProfile:apply:7DtW6Q68gk2R4okNGhvg1Y",
"candidateId": "seekAnzPublicTest:candidate:feed:5PGXAHysiXhtA9JUqhyM8hhzMuWMPA",
// This is only available for signed webhook subscriptions
"hirerId": "seekAnzPublicTest:organization:seek:93WyyF1h"
},
{
"id": "seekAnzPublicTest:event:events:WBmJMt4uEbj72ZVMhGi2hS",
"type": "CandidateApplicationCreated",
"createDateTime": "2019-09-08T20:32:12.479Z",
"candidateApplicationProfileId": "seekAnzPublicTest:candidateProfile:apply:4QM5fWQbdekL9gPtPZrzex",
"candidateId": "seekAnzPublicTest:candidate:feed:5PGXAHysjZdkQYwZghfL4bRCqvZ7ZM",
// This is only available for signed webhook subscriptions
"hirerId": "seekAnzPublicTest:organization:seek:93WyyF1h"
}
],
"subscriptionId": "seekAnzPublicTest:webhookSubscription:events:BoJiJ9ZWFVgejLXLJxUnvL",
"url": "https://example.com/webhook"
}
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 request will be considered a failure.
SEEK will automatically retry delivery of each event in the request 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.Webhook requests can be queried using the webhookRequestsForSubscription
query:query ($filter: WebhookRequestFilterInput!, $subscriptionId: String!) {
webhookRequestsForSubscription(
filter: $filter
subscriptionId: $subscriptionId
) {
edges {
node {
descriptionCode
requestId
statusCode
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
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:CandidateApplicationCreated
events to prompt your software to re-retrieve the affected applications.url
.By default, your endpoint will receive events for all SEEK hirers you have a relationship with.
This partner-scoped configuration is recommended for production usage, as it reduces per-request overhead and avoids additional steps when onboarding or offboarding a hirer.You can optionally scope a webhook subscription to a specific hirer ID.
This hirer-scoped configuration is useful for exploratory testing or piloting features with a subset of your hirers,
but you should switch over to a partner-scoped webhook subscription before you scale your build to all SEEK hirers.Creating a hirer-scoped subscription requires a configured relationship with the SEEK hirer.
For that reason, you cannot create a hirer-scoped HirerRelationshipChanged
subscription.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.mutation ($input: CreateWebhookSubscriptionInput!) {
createWebhookSubscription(input: $input) {
... on CreateWebhookSubscriptionPayload_Success {
webhookSubscription {
id {
value
}
}
}
... on CreateWebhookSubscriptionPayload_Conflict {
conflictingWebhookSubscription {
id {
value
}
}
}
}
}
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.query ($filter: EventsFilterInput!, $schemeId: String!) {
events(filter: $filter, schemeId: $schemeId) {
edges {
cursor
}
}
}
query ($id: String!) {
webhookSubscription(id: $id) {
webhookRequests(
filter: {
descriptionCodes: ["BadResponse", "InvalidUrl", "RequestTimeout"]
}
first: 1
) {
edges {
node {
createDateTime
descriptionCode
latencyMs
requestId
statusCode
}
}
pageInfo {
endCursor
hasNextPage
}
}
url
}
}
replayWebhookSubscription
mutation can then be used to requeue events for delivery.Replaying events should not form part of your standard event processing flow.
It is intended for exceptional circumstances where events have failed delivery and you need to reprocess them, or for one-off backfilling of historical events and incident recovery.
Regular failed delivery should be fixed in your software to avoid the need for replays.All events relevant to a subscription may be reprocessed by setting filter.replayDeliveredEventsIndicator
to true, specifying a date range and a filter.hirerId
.
This extends the replay to queue events that were previously delivered and events that were emitted prior to a subscription or hirer relationship being created,
and can be used to:mutation ($input: ReplayWebhookSubscriptionInput!) {
replayWebhookSubscription(input: $input) {
webhookSubscription {
id {
value
}
}
}
}
filter
ing events by date range, the eventIds
argument can be passed to the replayWebhookSubscription
mutation to replay a list of events by their IDs.mutation ($input: ReplayWebhookSubscriptionInput!) {
replayWebhookSubscription(input: $input) {
webhookSubscription {
id {
value
}
}
}
}
webhookSubscriptions
query can be used to page through your subscriptions:query ($schemeId: String!) {
webhookSubscriptions(schemeId: $schemeId) {
edges {
node {
id {
value
}
schemeId
eventTypeCode
url
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
updateWebhookSubscriptionDeliveryConfiguration
mutation lets you modify how and where the events are delivered:mutation ($input: UpdateWebhookSubscriptionDeliveryConfigurationInput!) {
updateWebhookSubscriptionDeliveryConfiguration(input: $input) {
... on UpdateWebhookSubscriptionDeliveryConfigurationPayload_Success {
webhookSubscription {
url
maxEventsPerAttempt
}
}
... on UpdateWebhookSubscriptionDeliveryConfigurationPayload_Conflict {
conflictingWebhookSubscription {
id {
value
}
}
}
}
}
updateWebhookSubscriptionSigningConfiguration
mutation.mutation ($input: UpdateWebhookSubscriptionSigningConfigurationInput!) {
updateWebhookSubscriptionSigningConfiguration(input: $input) {
webhookSubscription {
signingAlgorithmCode
}
}
}
deleteWebhookSubscription
mutation:mutation ($input: DeleteWebhookSubscriptionInput!) {
deleteWebhookSubscription(input: $input) {
webhookSubscription {
id {
value
}
schemeId
eventTypeCode
url
}
}
}
url
in the webhook event payload for hirer-scoped subscriptions.
This will ensure your software is associating the correct events for the intended hirer.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:mutation ($input: CreateWebhookSubscriptionInput!) {
createWebhookSubscription(input: $input) {
... on CreateWebhookSubscriptionPayload_Success {
webhookSubscription {
id {
value
}
}
}
... on CreateWebhookSubscriptionPayload_Conflict {
conflictingWebhookSubscription {
id {
value
}
}
}
}
}
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:// Koa + Node.js example
import crypto from 'crypto';
const validateRequest = async (ctx) => {
const receivedSignature = ctx.get('Seek-Signature');
// Reject request early if it does not contain the expected signature header.
// This can also be done on a network / WAF level.
if (!receivedSignature) {
ctx.throw(400, 'Invalid request 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');
}
};
BitConverter.ToString
produces pairs of uppercase hex digits separated by dashes.
You should normalise your computed signature to lowercase hex digits without dashes before comparing it to the received signature.
This should look like: 1194fd636c8077
.==
operator may be vulnerable to timing attacks .