Getting started
Core resources
Other

Verification result via Webhooks

SEEK Pass can deliver verification status through POST requests to partner-registered webhook endpoints. Partners can manage their webhooks by creating and updating them, as well as rotating webhook secrets:
POST /api/partner/v2/webhooks
GET /api/partner/v2/webhooks
GET /api/partner/v2/webhooks/:id
PUT /api/partner/v2/webhooks
PUT /api/partner/v2/webhooks/:id/secret
DELETE /api/partner/v2/webhooks/:id

Registering a webhook

Sample webhook resource request:
# POST /api/partner/v2/webhooks

{
  "name": "SEEK Pass Webhook",          // Optional
  "description": "SEEK Pass Webhook",   // Optional
  "url": "https://example.com/webhook"  // Required
}
Sample webhook resource response:
{
  "id": "03a7c560-93cd-4579-a26a-a3b98efa8a49",
  "name": "SEEK Pass Webhook",
  "description": "SEEK Pass Webhook",
  "url": "https://example.com/webhook",
  "secret": "23a8db7b1b368dd0e30eff95b048b994e892000e69a4fbb3ca78ff92e13ad1f7"
}
You can refer to the Open API specification for more details on updating your webhook endpoints, and request and response formats.

Webhook events

SEEK Pass can provide updates for various events at different stages of a verification workflow. Currently we support the following verification status events that are sent to registered webhooks:
  • canceled
  • verified
Caution
Webhook events only include some simple high-level information such as status. Partners should call the verification status API with the request ID to retrieve the complete verification result such as full name and date of birth.
Sample webhook event:
{
  "event_id": "5c4ac58b-5cf9-40a0-b60a-28c0137663ed",
  "request_id": "7cbc7e51-82f5-43ec-8d63-b7cdf1170425",
  "type": "vwsp_request.verified",
  "created_at": "2025-04-15T02:14:01.307391Z"
}

Verifying webhook signature (HMAC-SHA256)

Each webhook is associated with an automatically generated secret that serves as a shared key between partners and SEEK Pass to ensure secure communication. SEEK Pass uses this secret to sign every outgoing event payload using HMAC-SHA256. You must validate this signature on every incoming request to confirm it genuinely came from SEEK Pass and has not been tampered with.SEEK Pass includes two headers with every webhook delivery:
HeaderDescription
X-SignatureHMAC-SHA256 hex digest of the signed message
X-TimestampUnix timestamp of when the event was sent
The HMAC is computed over a message that incorporates both the timestamp and the raw request body, typically concatenated as <timestamp>.<raw_body>. Including the timestamp is a deliberate design choice to protect against replay attacks, where a valid request is captured and re-sent maliciously at a later time.To validate an incoming webhook, your endpoint should:
  • 1. Extract the X-Signature and X-Timestamp values from the request headers.
  • 2. Reconstruct the signed message by concatenating the timestamp and raw body as <timestamp>.<raw_body>.
  • 3. Recompute the HMAC-SHA256 of that message using your webhook secret as the key.
  • 4. Compare your computed HMAC against the X-Signature header using a constant-time comparison to prevent timing attacks.
  • 5. Check the timestamp to reject requests older than a reasonable tolerance window (e.g. ±15 minutes) to mitigate replay attacks.
Caution
Always use the raw, unparsed request body when computing the HMAC. Parsing the JSON and re-serialising it may alter the byte sequence and cause signature validation to fail.
Ruby example for secure HMAC verification
require "openssl"
require "active_support/security_utils"

TOLERANCE_SECONDS = 900

def verify_webhook(raw_body, secret, given_hmac, timestamp, tolerance_seconds = TOLERANCE_SECONDS)
  # Step 1: Reject stale requests to prevent replay attacks
  age = Time.now.to_i - timestamp.to_i
  return false if age.abs > tolerance_seconds

  # Step 2: Reconstruct the signed message
  signed_message = "#{timestamp}.#{raw_body}"

  # Step 3: Recompute the HMAC-SHA256
  calculated_hmac = OpenSSL::HMAC.hexdigest("SHA256", secret, signed_message)

  # Step 4: Constant-time comparison to prevent timing attacks
  return false unless calculated_hmac.bytesize == given_hmac.bytesize
  ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, given_hmac)
end

def handle_event(raw_body, secret, given_hmac, timestamp)
  return if !verify_webhook(raw_body, secret, given_hmac, timestamp)

  event = JSON.parse(raw_body)

  # Step 5: Idempotency check — discard duplicate events within the tolerance window
  return if event_already_processed?(event["event_id"])
  mark_event_as_processed(event["event_id"])

  # ... handle event
end
Node.js example for secure HMAC verification
const crypto = require("crypto");

const TOLERANCE_SECONDS = 900;

function verifyWebhook(rawBody, secret, givenHmac, timestamp, toleranceSeconds = TOLERANCE_SECONDS) {
  // Step 1: Reject stale requests to prevent replay attacks
  const age = Math.abs(Math.floor(Date.now() / 1000) - parseInt(timestamp, 10));
  if (age > toleranceSeconds) return false;

  // Step 2: Reconstruct the signed message
  const signedMessage = `${timestamp}.${rawBody}`;

  // Step 3: Recompute the HMAC-SHA256
  const calculatedHmac = crypto
    .createHmac("sha256", secret)
    .update(signedMessage, "utf8")
    .digest(); // Returns a Buffer

  const givenHmacBuffer = Buffer.from(givenHmac, "hex");

  // Step 4: Constant-time comparison to prevent timing attacks
  if (calculatedHmac.length !== givenHmacBuffer.length) return false;
  return crypto.timingSafeEqual(calculatedHmac, givenHmacBuffer);
}

async function handleEvent(rawBody, secret, givenHmac, timestamp) {
  if (!verifyWebhook(rawBody, secret, givenHmac, timestamp)) return;

  const event = JSON.parse(rawBody);

  // Step 5: Idempotency check — discard duplicate events within the tolerance window
  if (await isEventAlreadyProcessed(event.event_id)) return;
  await markEventAsProcessed(event.event_id);

  // ... handle event
}

Rotating webhook secret

Partner can rotate a webhook secret using this endpoint: PUT /api/partner/v2/webhooks//secret.During the transition period, webhook events may be signed using the previous secret. Partners' webhook endpoints should be able to verify events signed with both the old and new secrets. A safe approach is to try the new secret first, then fall back to the old one.Sample response for secret rotation:
{
  "id": "03a7c560-93cd-4579-a26a-a3b98efa8a49",
  "name": "SEEK Pass Webhook",
  "description": "SEEK Pass Webhook",
  "url": "https://example.com/webhook",
  "secret": "NEW_SECRET"
}

Notes

Caution
Webhook events may be delivered out of order due to factors like network latency. To help you determine the correct sequence, each event payload includes a created_at timestamp. We recommend implementing idempotent event processing since duplicate events may occur. Each event includes an event_id that can be used to identify and skip repeated events.
Info
If your webhook endpoint does not respond successfully (HTTP 200) within 5 seconds, SEEK Pass will retry sending the event up to 5 more times.
Info
SEEK Pass sends verification events from designated IP addresses that are published here, allowing partners to allowlist these IPs for secure connectivity.

SEEK Pass IP addresses

The following IP addresses are used by SEEK Pass to send webhook events. Partners should allowlist these IPs to ensure reliable delivery of webhook notifications.
Caution
Action Required: A new IP range (18.98.198.160/28) will be added in the near future. Please ensure this range is added to your allowlist ahead of time to avoid any disruption to webhook delivery.
EnvironmentIP addresses
Staging
  • 13.210.155.65
  • 52.64.188.102
  • 54.79.229.97
  • 18.98.198.160/28
Production
  • 13.210.238.239
  • 52.63.199.197
  • 54.252.19.190
  • 18.98.198.160/28