Webhooks

Webhooks let you receive real-time HTTP notifications when events happen in GeckoGuard — license authorizations, revocations, team changes, and more.

Event Catalog

Every delivery has the same envelope:

{
  "event": "<event-name>",
  "data": { /* event-specific fields, see below */ },
  "timestamp": "2026-04-29T14:32:11.000Z"
}

License events

license.created

A license was created (single or as part of a bulk create).

{
  "event": "license.created",
  "data": {
    "licenseId": "9c1a8b2e-…"
  },
  "timestamp": "2026-04-29T14:32:11.000Z"
}

Fetch the full license with GET /v1/dashboard/licenses/:licenseId if you need its key, expiry, policy, or product details.

license.authorized

A license was successfully validated against your product policy.

{
  "event": "license.authorized",
  "data": {
    "licenseId": "9c1a8b2e-…",
    "productId": "abc-123-…",
    "wasActivated": true,
    "effectiveExpiresAt": "2026-12-31T23:59:59.000Z"
  },
  "timestamp": "2026-04-29T14:32:11.000Z"
}

wasActivated: true means this call flipped activatedAt from null — the very first successful authorization. You'll only see this once per license.

license.authorization_denied

A license validation was rejected by policy. The reasonCode is one of the license reason codes.

{
  "event": "license.authorization_denied",
  "data": {
    "licenseId": "9c1a8b2e-…",
    "productId": "abc-123-…",
    "reasonCode": "HWID_MISMATCH",
    "message": "Hardware ID does not match bound device"
  },
  "timestamp": "2026-04-29T14:32:11.000Z"
}

license.expired

A license tried to authorize past its expiry.

{
  "event": "license.expired",
  "data": {
    "licenseId": "9c1a8b2e-…",
    "productId": "abc-123-…",
    "effectiveExpiresAt": "2026-04-28T00:00:00.000Z"
  },
  "timestamp": "2026-04-29T14:32:11.000Z"
}

license.revoked / license.unrevoked

{
  "event": "license.revoked",
  "data": { "licenseId": "9c1a8b2e-…" },
  "timestamp": "2026-04-29T14:32:11.000Z"
}

license.unrevoked fires when a previously-revoked license is restored. Same payload shape.

license.frozen / license.unfrozen

{
  "event": "license.frozen",
  "data": {
    "licenseId": "9c1a8b2e-…",
    "productId": "abc-123-…",
    "frozenAt": "2026-04-29T14:32:11.000Z",
    "frozenDaysRemaining": 7
  },
  "timestamp": "2026-04-29T14:32:11.000Z"
}

license.unfrozen omits frozenAt and frozenDaysRemaining.

license.hwid_reset / license.ip_reset

Fires whether the reset came from the dashboard or the end-user portal.

{
  "event": "license.hwid_reset",
  "data": {
    "licenseId": "9c1a8b2e-…",
    "productId": "abc-123-…",
    "resetCount": 3
  },
  "timestamp": "2026-04-29T14:32:11.000Z"
}

resetCount is the new total for that license.

Other events

EventTriggerPayload
api_key.createdAPI key issued{ apiKeyId, productId, name, last4 }
api_key.revokedAPI key revoked{ apiKeyId, productId }
api_key.rotatedAPI key secret rotated{ apiKeyId, productId, last4 }
team.member_addedNew org member{ orgId, userId, role }
team.member_removedMember removed{ orgId, userId }
test.pingYou called POST /v1/dashboard/webhooks/products/:productId/:endpointId/test{ message: "pong" }

Managing Webhook Endpoints

Create an Endpoint

const response = await fetch('/v1/dashboard/webhooks/products/PRODUCT_ID', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_TOKEN',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    url: 'https://your-server.com/webhooks/geckoguard',
    events: ['license.authorized', 'license.revoked', 'license.expired'],
    enabled: true
  })
});

const { data } = await response.json();
// data.endpoint.secret = 'whsec_...' — save this for signature verification!

The response includes a secret — store it securely. You'll use it to verify webhook signatures.

Update an Endpoint

PATCH /v1/dashboard/webhooks/products/:productId/:endpointId

Update the URL, events list, or enabled state.

Delete an Endpoint

DELETE /v1/dashboard/webhooks/products/:productId/:endpointId

View Delivery History

GET /v1/dashboard/webhooks/products/:productId/:endpointId/deliveries?limit=50

Returns recent delivery attempts with status, success flag, attempts count, error messages, and nextRetryAt. Same data is rendered in the dashboard under Settings → Webhooks → [endpoint] → Deliveries.

Send a Test Event

POST /v1/dashboard/webhooks/products/:productId/:endpointId/test

Sends a test.ping event to verify your endpoint is working.

Delivery Semantics

The contract you can rely on:

  • At-least-once delivery. A successful delivery means at least one HTTP 2xx response from your endpoint. You may receive the same event more than once — design handlers to be idempotent. The standard pattern: keep a small dedupe set of (event, licenseId, timestamp) or (event, licenseId, deliveryId) tuples for a few minutes and skip duplicates.
  • Per-endpoint fan-out. If an org has multiple endpoints subscribed to the same event, each one gets its own delivery + retry timeline. One slow or failing endpoint does not delay the others.
  • Per-event subscription. An endpoint only receives events it explicitly subscribed to (via the events array on create). Events not in the subscription list are dropped silently — no delivery row, no retry.
  • Disabled or deleted endpoints. Disabled endpoints receive no new deliveries and any in-flight retries stop (the row is updated with lastError: "Endpoint disabled or removed" and nextRetryAt: null).

Retry policy

Trigger for retryNetwork error or HTTP response not in the 2xx range
Max attempts6 total (1 initial + 5 retries)
Retry delays30s → 1m → 2m → 4m → 8m → 16m (exponential, base 30s)
Total window~32 minutes from first attempt to last retry
Per-attempt timeout10 seconds
Permanent failuresSSRF / private-IP / redirect-refusal responses are not retried — your endpoint resolved to a private IP or refused, and that won't change

After 6 failed attempts the delivery is marked permanently failed. You can replay any delivery manually:

POST /v1/dashboard/webhooks/products/:productId/deliveries/:deliveryId/replay

(Requires the endpoint to be enabled — disabled endpoints return 400 ENDPOINT_DISABLED.)

Ordering

Deliveries are not ordered. Two events fired in quick succession may arrive in either order. If your handler depends on order (e.g. "license.created must arrive before license.authorized"), don't rely on delivery sequence — fetch fresh state with GET /v1/dashboard/licenses/:id when you need a consistent snapshot.

Best practices

  • Respond 2xx immediately, then process asynchronously. Your endpoint has 10 seconds before the request is considered failed and retried. Hand off to a queue, ack with 200, do the work after.
  • Verify the signature before parsing. A failed signature is a stronger signal than parser exceptions. See Signature Verification below.
  • Log the X-GeckoGuard-Timestamp and deliveryId. The delivery row in the dashboard shares the same ID — useful for cross-referencing when something goes wrong.

Webhook Payload Format

{
  "event": "license.authorized",
  "data": {
    "licenseId": "license-uuid",
    "productId": "product-uuid",
    "wasActivated": false,
    "effectiveExpiresAt": "2025-12-31T23:59:59.000Z"
  },
  "timestamp": "2025-01-15T10:30:00.000Z"
}

Signature Verification

Every webhook request includes an HMAC-SHA256 signature in the X-GeckoGuard-Signature header. Verify it to ensure the request is authentic:

const crypto = require('crypto');

function verifyWebhook(body, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// Express example
app.post('/webhooks/geckoguard', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-geckoguard-signature'];
  const body = req.body.toString();

  if (!verifyWebhook(body, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(body);
  console.log('Received:', event.event, event.data);

  res.status(200).send('OK');
});

Request Headers

HeaderDescription
Content-Typeapplication/json
X-GeckoGuard-SignatureHMAC-SHA256 signature of the body
X-GeckoGuard-EventEvent type (e.g., license.authorized)
User-AgentGeckoGuard-Webhooks/1.0

Delivery Behavior

  • Webhook requests have a 10-second timeout
  • A 2xx response is considered successful
  • Failed deliveries are recorded with the error message
  • Delivery history is available via the dashboard API

Best Practices

  1. Always verify signatures — reject unsigned or invalid requests
  2. Respond quickly — return 200 immediately, process asynchronously if needed
  3. Handle duplicates — use idempotent processing in case of retries
  4. Subscribe only to events you need — reduces noise and load
  5. Monitor delivery history — check for failing endpoints regularly
  6. Use the test endpoint — verify your handler works before going live