Main content

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.

Testing your endpoint

Every subscription card in Settings → Webhooks has a Test button. Click it to fire a synthetic event right now — real signature, real headers, real envelope shape. Use this to verify your endpoint receives the POST and your HMAC verifier accepts it before you go live.

Test events have "event_type": "test" and "is_test": true at the top of the envelope so your downstream automation can ignore them in production logic. They bypass the persistent delivery queue — no retry, no circuit-breaker impact, no row in Recent Deliveries. The result (HTTP status, latency, or error) shows inline on the subscription card.

Below the Test result, three Copy links appear:

Together these mean a customer debugging a 401 has everything needed to replay locally and find the mismatch. No screen-scraping, no log mining.

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:

Operator tools

The Settings → Webhooks panel surfaces health and recovery controls for each subscription. These are the buttons your admin reaches for when something’s off.

At-a-glance health

Each subscription card shows a one-line summary of the last 50 deliveries: “Last 47: 45 delivered, 2 failed (95.7% delivered).” Tinted green when everything’s green; red when there are any failures. No need to open the Recent Deliveries dialog to know if the integration is working.

Recent Deliveries

Click View recent deliveries on a subscription to see the last 50 POST attempts. Filter dropdown narrows by status (All, Failed only, Pending only, Delivered only). Each row shows status, event type, attempt count, HTTP code, error message, and the event_id for cross-referencing with your own logs.

Retry & bulk Retry

Each Failed delivery has a per-row Retry button that re-queues the delivery for another attempt with a fresh 3-retry budget. The dialog header also has a Retry all N failed button for the dominant recovery flow: customer’s endpoint was down for an hour, 12 deliveries failed, customer fixes endpoint, clicks one button instead of clicking Retry 12 times.

Retried deliveries get a fresh event_timestamp (so they don’t age out before the worker tick fires) but keep the original event_id — your consumer-side dedup catches the case where you already received the payload before the network blip.

Reset failures

When a subscription hits 50 consecutive failures the circuit breaker auto-disables it. A Reset failures button appears on the card — clears the failure counter, clears the last-failure reason, re-enables the subscription. Use this after fixing your endpoint to take the subscription back out of circuit-broken state.

Audit footer

Each subscription card shows a compliance footer: “Created 2026-04-12 14:23 by Philippe · Last modified 2026-05-01 09:14 by Tyler · ⟳ Signing secret last rotated 2026-04-30 17:02 by Philippe.” ISO 9001 / SOC 2 / regulated-shop audits answer “who configured this data-exfil endpoint and when?” without leaving the dialog. Secret rotation gets its own timestamp because rotation breaks every downstream consumer’s verifier — that event is worth surfacing separately from generic edits.

Consumer examples

Slack and Microsoft Teams are not direct endpoints. Both services accept their own message envelope ({ "text": "..." } for Slack incoming webhooks, an Adaptive Card JSON for Teams) — not raw HTTP POSTs from arbitrary services. To get GlyphFex events into Slack or Teams, point your subscription at a Zapier / n8n / Power Automate trigger and let the workflow translate the payload into the format Slack or Teams expects. The Zapier and n8n examples below cover this pattern. A future GlyphFex update may add a built-in “Slack mode” payload adapter; for now, route through your no-code platform.

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.