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.
- HTTPS only. Non-https URLs are rejected at save and at delivery. Embedded credentials (
https://user:pass@host) are also rejected. - HMAC-SHA256 signature on every request — verify before you trust the body.
- Retry on failure: 1s, 5s, 30s. Three attempts then Failed terminal state.
- Circuit breaker: 50 consecutive failures auto-disable the subscription.
- Replay-safe: every payload has a UUID
event_id. Dedupe on it. - Event age cutoff: deliveries older than 24h auto-fail rather than re-firing stale. The cutoff uses the database server’s clock, not the worker’s — one drifting workstation can’t kill the feature for the rest of the shop.
- Payload size cap 1 MB. Oversized events still fire as a slim envelope (
payload_truncated: true). - Exactly-once delivery across multiple GlyphFex installs sharing the same SQL Server project. Per-row claim mechanism (atomic
UPDATE…OUTPUTwithREADPAST/UPDLOCKhints) means events fire once even when 4 workstations are running. - Audit trail. Every config change to a subscription (URL, events, signing secret, enable/disable) stamps
LastModifiedAtandLastModifiedBy. Secret rotation gets its ownSecretRotatedAt/SecretRotatedBy. Visible in-app on the subscription card so an auditor can answer “who configured this and when?” without leaving Settings.
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.
| Event | Trigger |
|---|---|
entry_created | A new job/quote was saved. Fires after attachments are persisted, so the payload's attachments[] array is complete. |
stage_changed | Pipeline stage moved. Fires from Dashboard quick-status, Dashboard Board drag-and-drop, Entry Detail Edit/Advance, Workstation Terminal Done / Pause-Advance / Undo. |
quote_won | Quote 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_lost | Quote 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_opened | A new non-conformance record (quality issue) was added to an entry's quality_notes array. |
time_entry_submitted | A 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:
| Header | Example value | Purpose |
|---|---|---|
Content-Type | application/json; charset=utf-8 | Body is JSON. |
User-Agent | GlyphFex-Webhook/1.0 | Useful for filtering in your reverse proxy / WAF. |
X-GlyphFex-Event | entry_created | Same as event_type in the body. Lets your endpoint route without parsing JSON first. |
X-GlyphFex-Event-Id | 9c8d8b80-... | UUID. Use as the dedupe key. |
X-GlyphFex-Timestamp | 2026-04-30T15:42:18.5821000Z | When the source event happened (NOT when the POST was sent — that may be later on a retry). |
X-GlyphFex-Delivery-Attempt | 1 | 1, 2, or 3. |
X-GlyphFex-Schema-Version | 1.0 | Same as schema_version in the body. |
X-GlyphFex-Signature | sha256=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.
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:
- Copy curl — one-liner that replays the exact POST in Bash, WSL, or PowerShell. Same URL, same headers, same body, same signature. Paste into a terminal and re-run on demand.
- Copy body — the JSON bytes the signature was computed over. Paste into your verifier code to reproduce the HMAC and compare against the header value byte-by-byte.
- Copy signature — the
X-GlyphFex-Signaturevalue (sha256=<hex>).
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.
- Attempt 1 fails → retry in 1 second
- Attempt 2 fails → retry in 5 seconds
- Attempt 3 fails → delivery marked Failed, no further retries
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:
- UUID per event —
event_idin the body and theX-GlyphFex-Event-Idheader. Dedupe on it. - Event age cutoff — if a pending delivery has been queued for more than 24 hours (e.g., the project was closed mid-retry days ago), GlyphFex marks it Failed instead of firing it now. Your consumer doesn't have to defend against week-old events arriving fresh. The cutoff uses the database server’s clock, so a single drifting workstation can’t age out the rest of the shop’s events.
- Per-row claim — in multi-install team mode (multiple GlyphFex
workstations sharing one SQL Server project), each delivery is claimed by exactly
one worker via an atomic
UPDATE…OUTPUT. You receive each event once even when 4 workstations are running. No producer-side fanout to worry about; the consumer-side dedup onevent_idis your safety net for the rare retry-after-network-blip case.
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
{ "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:
- No deliveries listed — the events you registered for haven't
fired yet, or the subscription is disabled. Try the action manually
(e.g., create a new entry to fire
entry_created). - Failed with HTTP 0 — network/DNS error. Check your URL is reachable from the GlyphFex machine. Note: GlyphFex is a desktop app, so the deliveries originate from the user's machine, not a cloud IP — firewall allowlists keyed on a GlyphFex public IP won't work.
- Failed with HTTP 401 — your endpoint rejected the signature. Double-check the signing secret matches between Settings and your environment.
- Failed with HTTP 5xx — your endpoint threw. Check its logs. GlyphFex retries 3 times then gives up; circuit-breaker triggers after 50 consecutive failures.
"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.