SimplyParseDocs

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:

  1. Rejects requests without your secret header.
  2. Verifies X-SimplyParse-Signature against the raw body.
  3. Upserts by entry_id, so redeliveries never create duplicates.
  4. Responds 204 immediately; 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=8080

In 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 (default if none)
  • Headers: X-Webhook-Secret = the same value as SIMPLYPARSE_WEBHOOK_SECRET

The receiver

npm install express
node server.mjs
server.mjs
import 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

  1. Start the receiver.
  2. Expose it with a tunnel, for example ngrok http 8080 or cloudflared tunnel --url http://localhost:8080, and use the HTTPS URL it prints as the webhook URL.
  3. Upload a document in the dashboard, or send one through the async endpoint with the webhook's environment.
  4. You should see entry … valid=true in 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.

On this page