Receive results with a webhook
A production-ready webhook receiver in Express or FastAPI that authenticates SimplyParse, verifies the signature and stores each entry exactly once.
This receiver does everything the webhooks guide recommends:
- Rejects requests without your secret header.
- Verifies
X-SimplyParse-Signatureagainst the raw body. - Upserts by
entry_id, so redeliveries never create duplicates. - Responds
204immediately; slow work happens after the response.
Configure
Set three environment variables on your server:
SIMPLYPARSE_PARSER_ID="1679091c-5a88-4faf-afb5-e6087eb1b2dc" # from app.simplyparse.com/parsers/<id>
SIMPLYPARSE_WEBHOOK_SECRET="$(openssl rand -hex 32)" # any long random value
PORT=8080In the dashboard, open the parser → Integrations → add a webhook:
- URL:
https://your-domain.com/hooks/simplyparse - Method:
POST - Environment: the value your API calls send (
defaultif none) - Headers:
X-Webhook-Secret= the same value asSIMPLYPARSE_WEBHOOK_SECRET
The receiver
npm install express
node server.mjsimport crypto from "node:crypto";
import express from "express";
const PARSER_ID = process.env.SIMPLYPARSE_PARSER_ID;
const SECRET = process.env.SIMPLYPARSE_WEBHOOK_SECRET;
// --- Verification -----------------------------------------------------------
function safeEqual(a, b) {
const x = Buffer.from(a ?? "");
const y = Buffer.from(b ?? "");
return x.length === y.length && crypto.timingSafeEqual(x, y);
}
// The signature covers the compact JSON form of the body (no spaces after
// "," and ":"). Strip whitespace outside strings to rebuild it exactly.
function compactJson(raw) {
let out = "";
let inString = false;
let escaped = false;
for (const ch of raw) {
if (inString) {
out += ch;
if (escaped) escaped = false;
else if (ch === "\\") escaped = true;
else if (ch === '"') inString = false;
} else if (ch === '"') {
inString = true;
out += ch;
} else if (ch !== " " && ch !== "\n" && ch !== "\r" && ch !== "\t") {
out += ch;
}
}
return out;
}
function verifySignature(raw, signature) {
const expected = crypto.createHmac("sha256", PARSER_ID).update(compactJson(raw), "utf8").digest("hex");
return safeEqual(expected, signature);
}
// --- Storage (replace with your database) ------------------------------------
const entries = new Map(); // entry_id -> record
async function saveEntry(data) {
// Upsert: a redelivery of the same entry_id overwrites, never duplicates.
entries.set(data.entry_id, {
documentId: data.document_id,
isValid: data.is_valid,
parsedData: data.parsed_data,
validationErrors: data.validation_errors,
receivedAt: new Date().toISOString(),
});
}
async function processEntry(data) {
// Slow work goes here: push to your ERP, notify a reviewer, etc.
console.log(`entry ${data.entry_id} valid=${data.is_valid}`);
}
// --- HTTP ---------------------------------------------------------------------
const app = express();
app.post("/hooks/simplyparse", express.raw({ type: "*/*", limit: "5mb" }), async (req, res) => {
if (!safeEqual(req.get("X-Webhook-Secret"), SECRET)) return res.sendStatus(401);
const raw = req.body.toString("utf8");
if (!verifySignature(raw, req.get("X-SimplyParse-Signature"))) return res.sendStatus(401);
const { data } = JSON.parse(raw);
await saveEntry(data); // persist before acknowledging
res.sendStatus(204);
processEntry(data).catch((err) => console.error("processing failed", data.entry_id, err));
});
app.listen(process.env.PORT ?? 8080, () => console.log("listening"));Test it locally
- Start the receiver.
- Expose it with a tunnel, for example
ngrok http 8080orcloudflared tunnel --url http://localhost:8080, and use the HTTPS URL it prints as the webhook URL. - Upload a document in the dashboard, or send one through the async endpoint with the webhook's environment.
- You should see
entry … valid=truein your logs.
If nothing arrives, check that the webhook is active and that its environment matches the document's. Failed deliveries can be resent from View data → Integrations → Redeliver Failed.
Going further
- Use a real queue for
processEntry, such as SQS, Cloud Tasks, Celery or BullMQ, so work survives restarts. - Several parsers on one URL: keep a list of parser IDs and accept the request if any of them verifies, or give each parser its own path.
- Route invalid entries to people: see Build a review queue.