Webhooks

Webhooks are SEEK’s preferred method of notifying your software when events occur on SEEK. They deliver events by 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.

Processing webhooks

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 identifier 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: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"
}
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.

Retries & debugging

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:
QueryVariables
query ($filter: WebhookRequestFilterInput!, $subscriptionId: String!) {
  webhookRequestsForSubscription(
    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

You self-manage your webhook subscriptions using GraphQL operations or the Developer Dashboard’s webhooks page.Subscriptions are scoped to a scheme ID and event type. To handle multiple event types with the same endpoint you can create multiple subscriptions pointing to the same 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.

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
    }
  }
}

Replaying a subscription’s events

When an event repeatedly fails delivery, the SEEK API will eventually stop trying to send it to your webhook endpoint.You can configure notifications for delivery failures on the Developer Dashboard’s notifications page, and view the undelivered events for a given webhook subscription through the Developer Dashboard’s webhooks page or GraphQL query:
QueryVariables
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
  }
}
The 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.

Replaying events by date range

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:
  1. Replay events that were accepted by your webhook endpoint but failed subsequent processing
  2. Restore the state of your software during disaster recovery
  3. Backfill historical events for a new subscription or hirer relationship configuration
MutationVariables
mutation ($input: ReplayWebhookSubscriptionInput!) {
  replayWebhookSubscription(input: $input) {
    webhookSubscription {
      id {
        value
      }
    }
  }
}

Replaying events by ID

As an alternative to filtering events by date range, the eventIds argument can be passed to the replayWebhookSubscription mutation to replay a list of events by their IDs.
MutationVariables
mutation ($input: ReplayWebhookSubscriptionInput!) {
  replayWebhookSubscription(input: $input) {
    webhookSubscription {
      id {
        value
      }
    }
  }
}
All the replay functionality described above is packaged into a convenient user interface on the Developer Dashboard’s webhooks page.

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 software 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:
GraphQLJSONSuccess resultConflict result
mutation ($input: UpdateWebhookSubscriptionDeliveryConfigurationInput!) {
  updateWebhookSubscriptionDeliveryConfiguration(input: $input) {
    ... on UpdateWebhookSubscriptionDeliveryConfigurationPayload_Success {
      webhookSubscription {
        url
        maxEventsPerAttempt
      }
    }

    ... on UpdateWebhookSubscriptionDeliveryConfigurationPayload_Conflict {
      conflictingWebhookSubscription {
        id {
          value
        }
      }
    }
  }
}
You can also update webhook signing using the updateWebhookSubscriptionSigningConfiguration mutation.
MutationVariables
mutation ($input: UpdateWebhookSubscriptionSigningConfigurationInput!) {
  updateWebhookSubscriptionSigningConfiguration(input: $input) {
    webhookSubscription {
      signingAlgorithmCode
    }
  }
}

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. You can also cross-match the 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.

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:
JavaScript
Copy
// 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');
  }
};
Tips:
  • Some languages may produce a hex digest differently, for example C#’s 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.
  • Use a constant-time algorithm to validate the signature. Regular comparison algorithms like the == operator may be vulnerable to timing attacks .