Agent skill

Greenshades Webhooks Skill

Receive and verify Greenshades webhooks. Use when setting up Greenshades payroll or HR webhook handlers, debugging X-GS-SIGNATURE verification, or handling payRun.completed, employee.updated and other subscription events.

Install this skill

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


When to Use This Skill

  • How do I receive Greenshades webhooks?
  • How do I verify the X-GS-SIGNATURE header on a Greenshades webhook?
  • Why does my Greenshades webhook signature verification fail?
  • How do I handle payRun.completed or employee.updated events?
  • How do I create a Greenshades webhook subscription with an HMAC key?

How Greenshades Signs Webhooks

Greenshades signs each delivery with HMAC-SHA256 and the hmacKey you chose when creating the subscription (POST /webhooks/subscriptions). Two headers arrive on every request:

HeaderValue
X-GS-TIMESTAMPThe timestamp used for the hash (format not documented)
X-GS-SIGNATUREBare lowercase hex HMAC-SHA256 digest, no prefix

The signed message is the X-GS-TIMESTAMP value, one newline (a real LF, 0x0A), then the raw request body bytes. The key is the secret's UTF-8 bytes, used as-is.

Verification (core)

const crypto = require('crypto');

function verifyGreenshades(rawBody /* Buffer */, timestamp, signature, secret) {
  if (!timestamp || !signature || !secret) return false;
  if (!/^[0-9a-fA-F]{64}$/.test(signature)) return false; // validate before decoding
  const message = Buffer.concat([Buffer.from(timestamp, 'latin1'), Buffer.from('\n'), rawBody]);
  const expected = crypto.createHmac('sha256', Buffer.from(secret, 'utf8')).update(message).digest();
  const received = Buffer.from(signature.toLowerCase(), 'hex');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
import hashlib, hmac, re

def verify_greenshades(raw_body: bytes, timestamp, signature, secret) -> bool:
    if not timestamp or not signature or not secret:
        return False
    if not re.fullmatch(r"[0-9a-fA-F]{64}", signature):
        return False
    message = timestamp.encode("latin-1") + b"\n" + raw_body
    expected = hmac.new(secret.encode(), message, hashlib.sha256).digest()
    return hmac.compare_digest(bytes.fromhex(signature), expected)

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

Gotchas:

  • Build the message as bytes. Never decode the body to a string and re-encode it, and never JSON.parse/JSON.stringify before verifying. In Express use express.raw({ type: '*/*' }); in Next.js await request.arrayBuffer(); in FastAPI await request.body().
  • The separator is one real newline byte, not the two characters backslash and n.
  • Greenshades' C# recipe encodes key and content as ASCII; Hookdeck and these examples sign the UTF-8 body bytes. The two only differ for a non-ASCII body.
  • The recipe's "Response Example" hash uses placeholder inputs and is not a test vector.
  • Missing or empty header, malformed digest or mismatch: respond 401.
  • No timestamp window by default. Hookdeck retries and replays resend the original headers and timestamp, and Hookdeck applies no tolerance, so a fixed window would reject legitimate retries. The examples offer an opt-in GREENSHADES_WEBHOOK_TOLERANCE_SECONDS (unset or 0 = off); because the X-GS-TIMESTAMP format is undocumented, it is parsed only when you opt in.

HTTP Method

Greenshades' docs don't name the HTTP method used for deliveries. Hookdeck's Greenshades source pre-selects POST without managing the method, so if the sender uses another method, allow it on the source too. The examples accept GET, POST, PUT, PATCH and DELETE and verify the signature first for every method, treating a missing body as empty bytes.

Events

Subscribe to these event names (from the WebhookEvent list of the create-subscription API):

EventArea
payRun.completedPay run
payRun.costReportingTransactions.completedPay run
payRun.payRecord.voidedPay run
payRun.payRecords.completedPay run
paySchedule.updatedPay schedule
employee.addedEmployee
employee.updatedEmployee
employee.directDeposit.updatedEmployee
employee.paySchedule.updatedEmployee
employee.earningCode.addedEmployee earning code
employee.earningCode.updatedEmployee earning code
employee.earningCode.deletedEmployee earning code

No delivery body is documented, so which field carries the event name is unknown. The examples read it defensively (eventName, event, type or name) and mark that code as illustrative; check a real delivery. No delivery, retry or timeout policy is documented either.

Environment Variables

GREENSHADES_WEBHOOK_SECRET=your_hmac_key_here        # the hmacKey set on the subscription
GREENSHADES_WEBHOOK_TOLERANCE_SECONDS=300            # optional; unset or 0 = no timestamp check

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 greenshades --path /webhooks/greenshades

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 10, 2026

View on GitHub →