# 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:

| Header | Value |
| --- | --- |
| `X-GS-TIMESTAMP` | The timestamp used for the hash (format not documented) |
| `X-GS-SIGNATURE` | Bare 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)

```javascript
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);
}

```

```python
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/greenshades-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/greenshades-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/greenshades-webhooks/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):

| Event | Area |
| --- | --- |
| `payRun.completed` | Pay run |
| `payRun.costReportingTransactions.completed` | Pay run |
| `payRun.payRecord.voided` | Pay run |
| `payRun.payRecords.completed` | Pay run |
| `paySchedule.updated` | Pay schedule |
| `employee.added` | Employee |
| `employee.updated` | Employee |
| `employee.directDeposit.updated` | Employee |
| `employee.paySchedule.updated` | Employee |
| `employee.earningCode.added` | Employee earning code |
| `employee.earningCode.updated` | Employee earning code |
| `employee.earningCode.deleted` | Employee 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

```bash
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

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

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/greenshades-webhooks/references/overview.md) - What Greenshades webhooks are, events, payloads
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/greenshades-webhooks/references/setup.md) - Create a subscription, choose the HMAC key, point it at your endpoint
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/greenshades-webhooks/references/verification.md) - Signature details, byte handling and debugging