send.webforms.toRead the docs →

A zero-JavaScript Astro form that delivers signed JSON to your API

Keep the form as plain HTML in a .astro file, and add a webhook destination pointing at your own API. The site ships no JavaScript and needs no SSR adapter, and each submission reaches your API as signed JSON you can verify. Webhook delivery is on Pro and Agency.

Setting it up

  1. 01A plain form, fully static

    This works on a fully static build, with no island and no hydration.

    src/pages/contact.astro
    <form action="https://send.webforms.to/wft_your_key" method="POST">
      <p hidden><input name="_gotcha" tabindex="-1" autocomplete="off" /></p>
      <input type="hidden" name="_redirect" value="https://example.com/thanks" />
      <input name="email" type="email" required />
      <textarea name="message" required></textarea>
      <button type="submit">Send</button>
    </form>
  2. 02Verify the signature on your API

    Each delivery carries an x-webforms-timestamp and an x-webforms-signature header. Recompute the HMAC over the timestamp and the raw body, and compare in constant time.

    verify.ts
    import { createHmac, timingSafeEqual } from "node:crypto";
    
    export function verify(headers: Headers, rawBody: string, secret: string): boolean {
      const ts = headers.get("x-webforms-timestamp") ?? "";
      const sig = headers.get("x-webforms-signature") ?? "";
      // Reject anything older than five minutes so a captured request
      // cannot be replayed later.
      if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
      const expected =
        "sha256=" + createHmac("sha256", secret).update(ts + "." + rawBody).digest("hex");
      const a = Buffer.from(sig);
      const b = Buffer.from(expected);
      return a.length === b.length && timingSafeEqual(a, b);
    }

What is different about Astro and Webhooks

Hash the raw body, not the parsed JSON

Framework body parsers hand you an object. Re-serialising it changes key order and whitespace, so the signature never matches and people disable the check to ship. Read the raw text first, verify, then parse.

Retries mean your endpoint must be idempotent

A slow response is retried, so the same submission can arrive twice. The x-webforms-delivery-id header is stable across attempts; store it and ignore repeats.

No adapter, no island

The browser does the POST, so the Astro site stays static. You only need an SSR adapter if you want Astro Actions, and this does not.

The full guides cover each side on its own: Astro setup and Webhooks as a destination.