Agent skill

Remote Webhooks Skill

Receive and verify Remote (remote.com) webhooks. Use when setting up a Remote webhook callback, debugging X-Remote-Signature verification, or handling employment.onboarding.completed, employment.onboarding_task.completed, timeoff.requested, payslip.released and contract.termination_date_reached events.

Install this skill

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


Remote (global employment, contractor management and payroll) sends API webhooks to the callback URL you register with POST /v1/webhook-callbacks. Each delivery carries two headers, X-Remote-Timestamp (Unix milliseconds) and X-Remote-Signature (a bare lowercase hex HMAC-SHA256 digest), and a JSON body whose event_type field names the event.

When to Use This Skill

  • How do I receive Remote webhooks?
  • How do I verify the X-Remote-Signature header from Remote?
  • Why does my Remote webhook signature verification fail?
  • Where do I get the Remote webhook signing key? (the signing_key returned when you create the callback)
  • How do I handle employment.onboarding.completed, timeoff.requested or payslip.released events?

Verification (core)

const crypto = require('crypto');

// signature = hex HMAC-SHA256(secret, raw_body + ":" + X-Remote-Timestamp)
function verifyRemote(rawBody, headers, secret = process.env.REMOTE_WEBHOOK_SECRET) {
  const sig = headers['x-remote-signature'];
  const ts = headers['x-remote-timestamp'];
  if (!secret || typeof sig !== 'string' || typeof ts !== 'string' || !ts) return false;
  if (!/^[0-9a-fA-F]{64}$/.test(sig)) return false; // validate before decoding

  const message = Buffer.concat([rawBody, Buffer.from(':'), Buffer.from(ts, 'latin1')]);
  const expected = crypto.createHmac('sha256', Buffer.from(secret, 'utf8')).update(message).digest();
  const received = Buffer.from(sig.toLowerCase(), 'hex');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
  • The key is the callback's signing_key, used as plain UTF-8 text (not hex- or base64-decoded).
  • Sign the raw body bytes, then a literal :, then the timestamp header exactly as sent. Never parse and re-serialise the body first.
  • Return 401 for a missing, malformed or wrong signature. Remote treats any 4xx or 5xx as delivered and does not retry it (only transport failures are retried), so there is no retry to wait for.
  • No timestamp window by default. Hookdeck retries and replays resend the original timestamp, so a fixed window would reject legitimate deliveries. If you want one, make it opt-in with REMOTE_WEBHOOK_TOLERANCE_SECONDS (unset or 0 = off) and compare against milliseconds, as the examples do.
  • Express: read the body with express.raw({ type: '*/*' }). Next.js: await request.arrayBuffer(). FastAPI: await request.body().

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

Event Types

Dispatch on the body's event_type. Remote documents 112 event types; these are the ones the examples handle.

EventSent When
employment.onboarding.completedAn employee's onboarding is fully completed
employment.onboarding_task.completedAn employee completes an onboarding task
timeoff.requestedAn employee submits a time off request
timeoff.approvedA time off request is approved
payslip.releasedA payslip becomes available
contract.termination_date_reachedA contract reaches its termination date

See references/overview.md for more, and log unknown types rather than failing.

Environment Variables

REMOTE_WEBHOOK_SECRET=your_signing_key_here        # signing_key from POST /v1/webhook-callbacks
# REMOTE_WEBHOOK_TOLERANCE_SECONDS=300             # optional, off by default

Local Development

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

No account required: the CLI creates a guest account on first run and gives you a tunnel and a web UI. Register the printed URL as your callback URL.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 10, 2026

View on GitHub →