Webhooks

Webhooks

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.

Processing webhooks

Partnerauth.seek.comgraphql.seek.comWebhook with event & object IDSend client credentialsIssue partner tokenRequest object via GraphQLReturn objectPartnerauth.seek.comgraphql.seek.com
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 processed 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.

Retries & debugging

If a webhook endpoint is not reachable or returns a non-2xx status code the delivery will be considered a failure. SEEK will automatically retry delivery after a delay. If the retries continue to fail the delivery attempts will stop and the missing events must be recovered using polling.
Delivery attempts can be queried using the webhookAttemptsForSubscription query :
QueryVariables
query($filter: WebhookAttemptsFilterInput!, $subscriptionId: String!) {
  webhookAttemptsForSubscription(
    filter: $filter
    subscriptionId: $subscriptionId
  ) {
    edges {
      node {
        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.

Duplicates

The SEEK API may send an event more than once to your endpoint. This occurs when:
  1. Events fail delivery and are automatically retried, as described in retries & debugging above.
  2. Requests are incidentally duplicated; we don’t guarantee exactly-once delivery.
  3. Events are resent to backfill historical data.
    This may be requested for onboarding or disaster recovery purposes.
  4. 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.

Subscription management

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.

Creating a 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.
MutationVariablesSuccess ResultConflict Result
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
query($filter: EventsFilterInput!, $schemeId: String!) {
  events(filter: $filter, schemeId: $schemeId) {
    edges {
      cursor
    }
  }
}

Listing subscriptions

The webhookSubscriptions query can be used to page through your subscriptions:
QueryVariables
query($schemeId: String!) {
  webhookSubscriptions(schemeId: $schemeId) {
    edges {
      node {
        id {
          value
        }
        schemeId
        eventTypeCode
        url
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Updating a subscription’s delivery configuration

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
mutation($input: UpdateWebhookSubscriptionDeliveryConfigurationInput!) {
  updateWebhookSubscriptionDeliveryConfiguration(input: $input) {
    webhookSubscription {
      url
      signingAlgorithmCode
      maxEventsPerAttempt
    }
  }
}

Deleting a subscription

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
mutation($input: DeleteWebhookSubscriptionInput!) {
  deleteWebhookSubscription(input: $input) {
    webhookSubscription {
      id {
        value
      }
      schemeId
      eventTypeCode
      url
    }
  }
}

Security

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.

Enable webhook signatures

Create a webhook subscription with the following input fields:
MutationVariables
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.

Validate webhook signatures

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 .
Events
Events
Events