Gareth Wilson Gareth Wilson

Guide to Greenshades Webhooks: Features and Best Practices

Published


Greenshades webhooks tell your systems when payroll and HR data changes: a pay run completes, a pay record is voided, an employee is added or edited, direct deposit details change, or an earning code is added. If you build integrations on top of Greenshades payroll, such as a general ledger sync, a benefits feed or an HRIS, webhooks save you from polling the API for each of those changes.

Greenshades documents its webhooks briefly: the subscription API, the event names and the signature. This guide covers those, points out what isn't documented, and suggests how to build a receiver that copes with the gaps.

What are Greenshades webhooks?

Greenshades webhooks are HTTP callbacks to a URL you choose. You create a subscription per workspace through the API, list the events you want, and give Greenshades an HMAC key of your choosing. Greenshades signs each delivery with that key and sends two headers, X-GS-TIMESTAMP and X-GS-SIGNATURE.

The signature is a lowercase hex HMAC-SHA256 over the timestamp value, one newline character and the raw request body.

Greenshades webhook features

FeatureDetails
ConfigurationAPI only: POST https://api.greenshadesonline.com/webhooks/subscriptions, per workspace
API scopesGO.Api.WEBHOOK.write or GO.Api.WEBHOOK.full_access
Signature headerX-GS-SIGNATURE, bare lowercase hex
Timestamp headerX-GS-TIMESTAMP, format not documented
Signed content<timestamp>, a newline (LF), then the raw body
SecretThe hmacKey you set on the subscription
Events12 payroll, pay schedule and employee events
BodyNot documented. JSON, going by the signature recipe
HTTP methodNot documented
Retries and timeoutNot documented

Common events

EventFires when
payRun.completedA pay run is completed
payRun.payRecords.completedA pay run's pay records have been generated
payRun.costReportingTransactions.completedA pay run's cost reporting transactions have been generated
payRun.payRecord.voidedA pay record is voided (listed in the API enum only)
paySchedule.updatedA pay schedule is updated
employee.addedAn employee is added
employee.updatedAn employee is edited
employee.directDeposit.updatedAn employee's payment information is updated
employee.paySchedule.updatedAn employee's pay schedule is updated
employee.earningCode.added, .updated, .deletedAn employee earning code is added, updated or deleted

Greenshades doesn't publish a delivery body for any event, so the field that carries the event name isn't known from the docs. Capture a real delivery of each event you subscribe to before writing the parser.

Setting up Greenshades webhooks

  1. Generate a long random string to use as the HMAC key and store it server-side, for example as GREENSHADES_WEBHOOK_SECRET. Greenshades uses it exactly as written.
  2. Get an OAuth2 access token with the GO.Api.WEBHOOK.write or GO.Api.WEBHOOK.full_access scope.
  3. Create a subscription for the workspace with POST https://api.greenshadesonline.com/webhooks/subscriptions:
{
  "url": "https://your-domain.example/webhooks/greenshades",
  "hmacKey": "your-long-random-key",
  "subscribedEvents": ["payRun.completed", "employee.updated"]
}
  1. Trigger a test event, or change a test employee, and confirm the delivery verifies. Note the HTTP method, the body and the timestamp format it arrives with.

If you receive Greenshades webhooks through Hookdeck, use the Hookdeck source URL as the subscription url, create a source with the Greenshades type and paste your hmacKey.

Greenshades' docs don't name the HTTP method its deliveries use, so Hookdeck's Greenshades source pre-selects POST without managing the method. If Greenshades sends deliveries with another method, add it to the source's allowed methods yourself.

Securing Greenshades webhooks

Read X-GS-TIMESTAMP and X-GS-SIGNATURE. Build the message from the timestamp value, one newline byte (0x0A, not the two characters \ and n) and the raw body bytes. Compute an HMAC-SHA256 with your hmacKey and compare the hex digest with the header in constant time.

const crypto = require('crypto');
const express = require('express');

function verifyGreenshadesSignature(rawBody, signatureHeader, timestampHeader, hmacKey) {
  if (!signatureHeader || !timestampHeader || !hmacKey) return false;
  if (!/^[0-9a-fA-F]{64}$/.test(signatureHeader)) return false;

  // The timestamp, ONE real newline, then the raw body.
  const message = Buffer.concat([Buffer.from(timestampHeader, 'latin1'), Buffer.from('\n'), rawBody]);
  const expected = crypto.createHmac('sha256', hmacKey).update(message).digest();
  const received = Buffer.from(signatureHeader, 'hex');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

// Greenshades doesn't document the HTTP method, so verify every method the same way.
app.all('/webhooks/greenshades', express.raw({ type: '*/*' }), (req, res) => {
  const hmacKey = process.env.GREENSHADES_WEBHOOK_SECRET;
  if (!hmacKey) return res.sendStatus(500); // fail closed

  const rawBody = Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
  const valid = verifyGreenshadesSignature(
    rawBody,
    req.get('x-gs-signature'),
    req.get('x-gs-timestamp'),
    hmacKey
  );
  if (!valid) return res.sendStatus(401);

  // The body format isn't documented: store it, log its shape, then process asynchronously.
  res.sendStatus(200);
});

The same check in Python:

import hashlib
import hmac
import re


def verify_greenshades_signature(raw_body: bytes, signature_header, timestamp_header, hmac_key: str) -> bool:
    if not signature_header or not timestamp_header or not hmac_key:
        return False
    if not re.fullmatch(r"[0-9a-fA-F]{64}", signature_header):
        return False
    # The timestamp, ONE real newline, then the raw body.
    message = timestamp_header.encode("latin-1") + b"\n" + raw_body
    expected = hmac.new(hmac_key.encode("utf-8"), message, hashlib.sha256).digest()
    return hmac.compare_digest(bytes.fromhex(signature_header), expected)


# In FastAPI: raw_body = await request.body()
# verify_greenshades_signature(raw_body, request.headers.get("x-gs-signature"),
#                              request.headers.get("x-gs-timestamp"),
#                              os.environ["GREENSHADES_WEBHOOK_SECRET"])

Greenshades' C# recipe encodes the key and the content as ASCII. The code above, like Hookdeck, signs the UTF-8 body bytes. The two give the same result for an ASCII body and differ only if a body contains non-ASCII characters, such as an accented employee name. The recipe's "Response Example" hash is computed from placeholder values, so don't use it as a test vector.

Greenshades doesn't document the format of X-GS-TIMESTAMP or a tolerance for it, so the code above doesn't check its age. Hookdeck applies no timestamp tolerance, and its retries and replays resend the original headers. If you add a window, confirm the format from a real delivery first and leave enough room for late retries.

To check your verifier, use the key greenshades-guide-key, an X-GS-TIMESTAMP of 1736937000 and this exact body:

{"event":"employee.updated"}

The header should be:

X-GS-SIGNATURE: eed67ae2e9deb4f4ea16edadb0330f2988b28e30bf110402b16201412b9fb53a

Make Greenshades webhooks production-ready. Hookdeck Event Gateway verifies the X-GS-SIGNATURE header on every delivery, stores each request so you can see exactly what Greenshades sends, and queues payroll events until your service has processed them.

Greenshades webhook limitations and pain points

No documented payload

The Problem: You can't write a parser or a router before the first delivery arrives.

Why It Happens: Greenshades documents the subscription API and the signature, but no delivery body and no header that names the event.

Workarounds:

  • Subscribe a test workspace first and capture one delivery of each event.
  • Keep the parser defensive until you've seen real bodies, and log anything it doesn't recognise.
  • Re-read the changed record from the Greenshades API rather than relying on fields you haven't confirmed.

How Hookdeck Can Help: Hookdeck stores the headers and body of every request it receives, so the first real delivery becomes your reference payload, and you can replay it against your handler while you build.

No documented retries or timeout

The Problem: You don't know whether a failed delivery is retried, or for how long.

Why It Happens: Greenshades publishes no delivery, retry, timeout or ordering policy.

Workarounds:

  • Return 2xx quickly and do the work asynchronously.
  • Reconcile pay runs and employee changes against the API on a schedule, so a missed delivery is caught.

How Hookdeck Can Help: Hookdeck acknowledges each delivery as soon as it arrives and stores it, then delivers to your service with retries you control, so your own downtime doesn't depend on whatever retry policy Greenshades applies.

The HTTP method isn't stated

The Problem: A receiver that only accepts POST might turn deliveries away.

Why It Happens: Greenshades' webhook pages call deliveries HTTP callbacks without naming a method.

Workarounds:

  • Verify the signature on every method at first, as the code above does, and log the method.
  • Once you've seen real deliveries, restrict the route to that method.

How Hookdeck Can Help: Hookdeck's Greenshades source pre-selects POST and lets you allow other methods if Greenshades turns out to send them.

Best practices

Choose a strong key

Greenshades says "The key can be anything, but we recommend using a strong password of your choosing." Generate at least 32 random bytes, encode them as a string, and use a different key per subscription or environment.

Verify against the raw body

Build the signed message from bytes. Don't decode the body and re-encode it, and don't verify a re-serialised JSON object. In Express, use express.raw({ type: '*/*' }). In Next.js, use Buffer.from(await request.arrayBuffer()). In FastAPI, use await request.body().

Treat payroll events as triggers to re-read

With no documented body, the safest pattern is to treat each delivery as a signal and fetch the pay run or employee from the API before acting. That also protects you from duplicates and out-of-order deliveries. See our guide to webhook idempotency.

Conclusion

Greenshades gives you a clear signature and an API to subscribe with, and little else in writing. Sign the timestamp, a real newline and the raw body with your own key, capture real deliveries before you build parsers, and reconcile against the API because the retry behaviour is unknown.

Hookdeck Event Gateway sits in front of your receiver, checks every Greenshades signature, and keeps a record of each delivery until your service has processed it.

Get started with Hookdeck for free and handle Greenshades webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.