Schema: Webhooks v1 (envelope) · data.entry follows v1 entry schema  ·  First shipped: Wave 14AF / 14AG

Webhooks v1

GlyphFex POSTs an HMAC-signed JSON event to your URL when something interesting happens. You point Zapier, n8n, Power Automate, AWS Lambda, or your own endpoint at it — we never integrate per-vendor connectors, so anything that accepts an HTTP POST works out of the box. The payload data.entry is the same shape as your entries.json file, so the consumer code is identical for both paths.

Operational guarantees
On this page

Event types

Subscribe to any combination of these per URL. The event key (e.g. entry_created) is what you select in the Settings panel and what arrives in the event_type field of the payload.

EventTrigger
entry_createdA new job/quote was saved. Fires after attachments are persisted, so the payload's attachments[] array is complete.
stage_changedPipeline stage moved. Fires from Dashboard quick-status, Dashboard Board drag-and-drop, Entry Detail Edit/Advance, Workstation Terminal Done / Pause-Advance / Undo.
quote_wonQuote outcome set to Won via the Entry Detail "Mark as Won" flow. Fires alongside stage_changed if the win moves the entry to a quoting-completed stage.
quote_lostQuote outcome set to Lost via Entry Detail "Quick Close." Fires alongside stage_changed. The payload's data.entry.key_fields.lost_reason field carries the reason code if the user picked one.
ncr_openedA new non-conformance record (quality issue) was added to an entry's quality_notes array.
time_entry_submittedA worker's time clock stopped (Done, Pause, Switch job, Break, Split, Travel toggle, etc.). Payload's data.entry reflects the entry's state at the moment the timer stopped.

Multiple events can fire from a single user action — e.g., a Done click on the Workstation Terminal fires both time_entry_submitted (the timer stopped) and stage_changed (the entry advanced). Each has its own event_id.

Payload envelope

{
  "event_id": "9c8d8b80-13ad-4f48-9c5e-2b8a3e83a6e1",
  "event_type": "entry_created",
  "event_timestamp": "2026-04-30T15:42:18.5821000Z",
  "schema_version": "1.0",
  "data": {
    "entry": { ...full v1 entry shape... }
  }
}

data.entry is the same shape documented at data-export-schema-v1.html: snake_case keys, the full key_fields object with all 17 built-in fields, tags as a category-keyed object, attachments[] with file metadata, audit_summary, work_state, etc.

Two extra fields appear ONLY in the oversize/slim case (see Oversized payloads): payload_truncated: true and drop_reason. Neither is present on normal-size deliveries.

HTTP headers

Every POST sets these:

HeaderExample valuePurpose
Content-Typeapplication/json; charset=utf-8Body is JSON.
User-AgentGlyphFex-Webhook/1.0Useful for filtering in your reverse proxy / WAF.
X-GlyphFex-Evententry_createdSame as event_type in the body. Lets your endpoint route without parsing JSON first.
X-GlyphFex-Event-Id9c8d8b80-...UUID. Use as the dedupe key.
X-GlyphFex-Timestamp2026-04-30T15:42:18.5821000ZWhen the source event happened (NOT when the POST was sent — that may be later on a retry).
X-GlyphFex-Delivery-Attempt11, 2, or 3.
X-GlyphFex-Schema-Version1.0Same as schema_version in the body.
X-GlyphFex-Signaturesha256=4e1b2c3d...HMAC-SHA256 of the raw body bytes, hex-encoded. Verify before trusting the body.

Signature verification

Compute HMAC-SHA256(signing_secret_utf8_bytes, raw_body_bytes) as lowercase hex and compare to the value after sha256= in the X-GlyphFex-Signature header. Always use a constant-time compare (e.g., crypto.timingSafeEqual in Node, hmac.compare_digest in Python) so a malicious caller can't probe for the secret one byte at a time.

The signing secret is per-subscription. Find it in Settings → Webhooks → Edit → Signing secret. The "Regenerate secret" button rolls a new 32-byte random key; the old one is invalid the moment you save. Store the secret in your endpoint's environment variables — never check it into source.

Always verify before trusting

If the signature doesn't match, return 401. Don't process the body. The same signature header is what proves the request came from GlyphFex and that the body wasn't tampered with in transit.

Retry behavior

Your endpoint should respond with HTTP 2xx for any case where you consider the event successfully received — even if you decide internally to ignore it. Non-2xx (or no response within 30 seconds) triggers a retry.

After 50 consecutive failed deliveries on the same subscription, GlyphFex auto-disables the subscription (circuit breaker). The customer sees a yellow warning in the Settings panel; they re-enable when ready. This protects a permanently-broken endpoint from accumulating millions of zombie retries.

Idempotency requirement: because of retries, your endpoint must handle the same event_id arriving twice. Dedupe on X-GlyphFex-Event-Id — the cheapest path is a small cache (Redis, your DB, even a hash set) of recently-seen event_ids.

Oversized payloads

Hard limit: 1 MB per payload (UTF-8 bytes). Most cloud-function platforms (AWS Lambda, Azure Functions, Cloud Run) and API gateways have similar or tighter caps; we set ours conservatively so deliveries don't fail at your end.

When an event would exceed 1 MB — typically a job with hundreds of attachments or a long audit history — we still fire, but as a slim envelope:

{
  "event_id": "9c8d8b80-...",
  "event_type": "entry_created",
  "event_timestamp": "2026-04-30T15:42:18.5821000Z",
  "schema_version": "1.0",
  "payload_truncated": true,
  "drop_reason": "Payload exceeded 1024 KB limit; full entry data omitted.",
  "data": {
    "entry": {
      "id": 1234,
      "ref_number": "Q-2026-001"
    }
  }
}

Your consumer should branch on payload_truncated. If true, you got the event but not the data — fetch the full entry through your entries.json Data Feed file (cheaper) or a database read.

Replay protection

Two layers:

Consumer examples

Node.js / Express

import express from "express";
import crypto from "crypto";

const app = express();
const SECRET = process.env.GLYPHFEX_SECRET;
const seen = new Set(); // replace with Redis / DB in production

app.post("/glyphfex-webhook",
  express.raw({ type: "application/json" }), // raw body for signature
  (req, res) => {
    const sig = (req.header("x-glyphfex-signature") || "").replace(/^sha256=/, "");
    const expected = crypto.createHmac("sha256", SECRET)
                           .update(req.body)
                           .digest("hex");
    if (sig.length !== expected.length ||
        !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
      return res.sendStatus(401);
    }

    const body = JSON.parse(req.body.toString("utf8"));
    if (seen.has(body.event_id)) return res.sendStatus(200); // replay
    seen.add(body.event_id);

    console.log(`${body.event_type}: ${body.data.entry.ref_number}`);
    if (body.payload_truncated) {
      // fetch full entry from your Data Feed if needed
    }
    res.sendStatus(200);
});

app.listen(3000);

Python / Flask

from flask import Flask, request, abort
import hmac, hashlib, os

app = Flask(__name__)
SECRET = os.environ["GLYPHFEX_SECRET"].encode("utf-8")
seen = set()  # replace with Redis / DB in production

@app.post("/glyphfex-webhook")
def hook():
    raw = request.get_data()
    sig = request.headers.get("X-GlyphFex-Signature", "").removeprefix("sha256=")
    expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(sig, expected):
        abort(401)

    body = request.get_json()
    if body["event_id"] in seen:
        return ("", 200)
    seen.add(body["event_id"])

    print(body["event_type"], body["data"]["entry"]["ref_number"])
    return ("", 200)

Zapier

Use the Webhooks by Zapier trigger → Catch Hook. Paste the URL Zapier gives you into a new GlyphFex subscription. Zapier doesn't verify HMAC for you, so for sensitive integrations add a Code by Zapier step that recomputes the signature and aborts the Zap on mismatch. Dedupe on event_id using a Storage by Zapier lookup.

n8n

Use the Webhook node configured for POST. Verify the signature with a Function node:

const crypto = require('crypto');
const sig = $input.first().headers['x-glyphfex-signature'].replace('sha256=', '');
const expected = crypto.createHmac('sha256', $env.GLYPHFEX_SECRET)
                       .update(JSON.stringify($input.first().body))
                       .digest('hex');
if (sig !== expected) throw new Error('Invalid signature');
return $input.all();

Note: n8n re-serializes the JSON, which can break HMAC if your secret was computed against the original byte sequence. For strict signature checks, expose the webhook node with "Binary Data" enabled and verify against the raw bytes.

Power Automate

Use the When a HTTP request is received trigger. Power Automate doesn't expose the raw request body in a HMAC-friendly way out of the box, so for production use either run an Azure Function as a verifier-proxy or accept the tradeoff that anyone who knows your URL can post to it. For low-stakes flows (Slack notifications, internal logs) this is usually fine; for anything that mutates external state, put a Function in front.

Troubleshooting

"My endpoint isn't getting any deliveries."

In GlyphFex: Settings → Webhooks → click View recent deliveries on the subscription. You'll see one of:

"I'm getting duplicate events."

Either: (a) your endpoint returned non-2xx for a delivery that succeeded internally (the retry then re-delivered), or (b) two distinct user actions both fire the same event type. Dedupe on event_id; that handles both cases.

"My signature verification fails sometimes but not always."

Almost always a body-byte mismatch — your framework re-serialized the JSON before you computed the HMAC. Verify against raw request bytes, not against a parsed-and-restringified version. The HMAC is computed by GlyphFex against the exact UTF-8 byte sequence we send.

"I subscribed to quote_won but I'm getting stage_changed too."

They're separate subscriptions per event type, but the same user action (marking a quote Won) can fire both events: quote_won for the outcome change, and stage_changed if the win moved the entry to a quoting- completed stage. If you only want one, uncheck the other in the Settings panel.


Questions? Email support@glyphfex.com. See also: v1 entry schema.