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.
- 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.
- Payload size cap 1 MB. Oversized events still fire as a slim envelope (
payload_truncated: true).
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.
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.
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:
- 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.