Developers

    Local test examples

    Run these scripts on your laptop to receive test webhooks and call the REST API. No extra dependencies beyond Node.js 18+.

    Before you start

    Open Dashboard → API Keys, create an API key, save webhook URL http://127.0.0.1:4242/webhook, and copy your whsec_… signing secret. See the API reference for the full workflow.

    1. Project setup

    Create a folder anywhere on your machine.

    mkdir prostnow-api-test && cd prostnow-api-test

    .env

    # From Dashboard → API Keys
    PROSTNOW_API_KEY=pn_live_your_key_here
    PROSTNOW_WEBHOOK_SECRET=whsec_your_signing_secret_here
    
    # Your Prostnow site (local dev or production)
    PROSTNOW_BASE_URL=https://www.prostnow.com
    
    # Local webhook listener port
    WEBHOOK_PORT=4242

    Load env vars in your shell: export $(grep -v '^#' .env | xargs) (macOS/Linux).

    2. Local webhook listener

    Receives POSTs from Prostnow, verifies the signature, and prints the payload.

    Save as webhook-server.mjs and run with node webhook-server.mjs.

    // webhook-server.mjs — Node 18+ (no npm install required)
    import crypto from "crypto";
    import http from "http";
    
    const PORT = Number(process.env.WEBHOOK_PORT || 4242);
    const SECRET = process.env.PROSTNOW_WEBHOOK_SECRET;
    
    if (!SECRET) {
      console.error("Set PROSTNOW_WEBHOOK_SECRET in your environment");
      process.exit(1);
    }
    
    function verifySignature(rawBody, signatureHeader, secret) {
      const parts = Object.fromEntries(
        signatureHeader.split(",").map((p) => p.trim().split("="))
      );
      const timestamp = parts.t;
      const expected = parts.v1;
      if (!timestamp || !expected) return false;
    
      const signed = `${timestamp}.${rawBody}`;
      const digest = crypto.createHmac("sha256", secret).update(signed).digest("hex");
    
      try {
        return crypto.timingSafeEqual(
          Buffer.from(digest, "hex"),
          Buffer.from(expected, "hex")
        );
      } catch {
        return false;
      }
    }
    
    const server = http.createServer((req, res) => {
      if (req.method === "GET" && req.url === "/health") {
        res.writeHead(200, { "Content-Type": "text/plain" });
        res.end("ok");
        return;
      }
    
      if (req.method !== "POST" || req.url !== "/webhook") {
        res.writeHead(404).end();
        return;
      }
    
      const chunks = [];
      req.on("data", (c) => chunks.push(c));
      req.on("end", () => {
        const rawBody = Buffer.concat(chunks).toString("utf8");
        const sig = req.headers["x-prostnow-signature"];
        const ts = req.headers["x-prostnow-timestamp"];
    
        if (typeof sig !== "string" || !verifySignature(rawBody, sig, SECRET)) {
          console.error("✗ Invalid signature");
          res.writeHead(401).end("invalid signature");
          return;
        }
    
        let payload;
        try {
          payload = JSON.parse(rawBody);
        } catch {
          res.writeHead(400).end("invalid json");
          return;
        }
    
        const label = payload.test ? "TEST" : "LIVE";
        console.log(`\n✓ [${label}] post.sent received`);
        console.log("  timestamp:", ts);
        console.log("  post id:", payload.post?.id);
        console.log("  sent:", payload.post?.sent);
        if (payload.test) {
          console.log("  (dashboard test event — safe to ignore in prod logs)\n");
        }
    
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ received: true }));
      });
    });
    
    server.listen(PORT, "127.0.0.1", () => {
      console.log(`Webhook listener → http://127.0.0.1:${PORT}/webhook`);
      console.log("Health check     → http://127.0.0.1:" + PORT + "/health");
      console.log("\nIn Dashboard → API Keys, set endpoint URL to:");
      console.log(`  http://127.0.0.1:${PORT}/webhook\n`);
    });
    1. 1Start the server: node webhook-server.mjs
    2. 2In Dashboard → API Keys, set endpoint to http://127.0.0.1:4242/webhook and Save.
    3. 3Click Send test webhook — your terminal should print ✓ [TEST] post.sent received.

    3. Test the REST API

    Confirm your API key works with curl or a small script.

    List recent posts

    # macOS / Linux — load PROSTNOW_API_KEY and PROSTNOW_BASE_URL first
    curl -s \
      -H "Authorization: Bearer $PROSTNOW_API_KEY" \
      "$PROSTNOW_BASE_URL/api/v1/posts?limit=5" | jq .

    Get one post by id

    POST_ID="paste_post_id_from_dashboard_or_list_response"
    
    curl -s \
      -H "Authorization: Bearer $PROSTNOW_API_KEY" \
      "$PROSTNOW_BASE_URL/api/v1/posts/$POST_ID" | jq .

    Optional: test-rest.mjs

    // test-rest.mjs
    const base = process.env.PROSTNOW_BASE_URL?.replace(/\/$/, "");
    const key = process.env.PROSTNOW_API_KEY;
    
    if (!base || !key) {
      console.error("Set PROSTNOW_BASE_URL and PROSTNOW_API_KEY");
      process.exit(1);
    }
    
    const res = await fetch(`${base}/api/v1/posts?limit=5`, {
      headers: { Authorization: `Bearer ${key}` },
    });
    
    console.log("HTTP", res.status);
    console.log(await res.json());
    node test-rest.mjs

    4. End-to-end on your laptop

    Combine webhook listener + REST in one session.

    1. Terminal 1Run node webhook-server.mjs and leave it running.
    2. Terminal 2Run node test-rest.mjs to confirm REST auth.
    3. DashboardSend test webhook, then publish a real post and watch for a [LIVE] event (no test: true flag).

    5. Using a tunnel (optional)

    When you need a public https URL instead of localhost.

    Tools like ngrok or Cloudflare Tunnel can expose your local listener:

    # Example with ngrok (after installing ngrok)
    ngrok http 4242
    
    # Copy the https URL, e.g. https://abc123.ngrok-free.app/webhook
    # Paste it in Dashboard → API Keys → Endpoint URL → Save → Send test webhook
    Production webhooks require https://. Local testing accepts http://127.0.0.1 and http://localhost only.

    Troubleshooting

    • Invalid signature — use the raw request body for HMAC, not re-parsed JSON. Confirm PROSTNOW_WEBHOOK_SECRET matches the dashboard.
    • 401 on REST — key must start with pn_live_ and be sent as Authorization: Bearer ….
    • No webhook received — server must listen on 127.0.0.1:4242, URL in dashboard must match exactly, and return HTTP 2xx.
    • Connection refused — start webhook-server.mjs before clicking Send test webhook.