Agent skill

Oyster Webhooks Skill

Receive and authenticate Oyster (OysterHR) global HR and EOR webhooks. Use when setting up an Oyster webhook endpoint, checking the Authorization bearer shared secret, debugging 401s, or handling events such as hiring.employment_engaged, expenses.expense_approved, contract.change_applied and benefits.enrollment_enrolled.

Install this skill

npx skills add hookdeck/webhook-skills --skill oyster-webhooks


Oyster (global HR and employer-of-record platform) pushes events such as hires, expenses, contract changes and benefits enrollments to an endpoint you register with POST /v1/webhooks. Payloads are not signed. The only authentication is the sharedSecret you chose, which Oyster sends back as an Authorization bearer header.

When to Use This Skill

  • How do I receive Oyster webhooks?
  • How do I verify the Oyster Authorization: Bearer <sharedSecret> header?
  • Does Oyster sign webhook payloads? (No: shared secret in the Authorization header only)
  • Why does my Oyster webhook endpoint return 401?
  • How do I handle hiring.employment_engaged or expenses.expense_approved events?

Verification (core)

When you register the endpoint you send {"url": "...", "sharedSecret": "12345"}. Oyster then sends Authorization: Bearer 12345 on every webhook. Treat the whole header value, scheme prefix included, as the credential and compare it in full. Don't strip or parse the Bearer prefix:

const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyOysterRequest(headers, expected = process.env.OYSTER_AUTHORIZATION) {
  if (!expected) return false;                 // never accept when unconfigured
  const received = headers['authorization'];   // full value, e.g. "Bearer 12345"
  return Boolean(received) && safeEqual(received, expected);
}

Return 401 when this fails, 400 for a body that isn't a JSON object with an eventName, and 200 once handled (Oyster needs a 200 within 5 seconds). Read the body raw (express.raw({ type: '*/*' })) so any content type works, and parse JSON only after the check. Python and Next.js equivalents are in references/verification.md.

What this does and doesn't prove: a static shared credential authenticates the sender, not the body. There is no signature and no timestamp header, so nothing protects the payload from tampering by anything that can see the request, and a captured request can be replayed. Use HTTPS only, dedupe on the event id, and rotate the shared secret (PATCH the webhook endpoint) if it leaks.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Types

Every payload has the same envelope: eventName, eventTimestamp (ISO 8601 UTC), id (globally unique, prefixed wh_), companyId and an event-specific data object. Names are namespace.event_name.

eventNameFires whendata keys
hiring.employment_engagedAn EOR employment is engagedengagementId
expenses.expense_approvedAn expense is approvedexpenseId, reviewState, previousReviewState
contract.change_appliedA contract change is appliedengagementChangeId, changeType, state
benefits.enrollment_enrolledA benefits enrollment is enrolledengagementId, enrollmentId

Oyster documents many more (hiring, expenses, invoicing, payments, offboarding, time off, time tracking and more). See references/overview.md.

Delivery

Respond 200 within 5 seconds. Otherwise Oyster retries up to 9 total attempts over about 48 hours (30s, 5m, 30m, 1h, 3h, 6h, 12h, 1d, plus jitter), so handlers must be idempotent: dedupe on id.

Environment Variables

OYSTER_AUTHORIZATION=Bearer 12345   # full header value: "Bearer " + the sharedSecret you registered

Local Development

npx hookdeck-cli listen 3000 oyster --path /webhooks/oyster

No account required. Register the printed URL as the webhook url with a sharedSecret of your choice, and set OYSTER_AUTHORIZATION to Bearer <that secret>.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 5, 2026

View on GitHub →