Webhooks

Forward form submissions to your own infrastructure in real time. RizzForms POSTs JSON to your URL every time a new submission arrives.

Setup

Add a webhook via the dashboard or the API:

POST https://www.rizzness.com/api/forms/:form_id/plugins
Authorization: Bearer frk_your_api_key
Content-Type: application/json

{
  "plugin_type": "webhook",
  "config": {
    "url": "https://your-app.com/webhooks/rizzforms"
  }
}

The response includes a signing_secret — save it immediately. This is the only time the secret is returned in plaintext.

Payload format

Every webhook delivery sends a JSON body with this shape:

{
  "id": 12345,
  "created_at": "2026-03-22T21:13:49Z",
  "form_id": "cBirQjTv",
  "form_name": "Contact Form",
  "ip": "203.0.113.42",
  "user_agent": "Mozilla/5.0...",
  "referrer": "https://example.com/contact",
  "data": {
    "email": "[email protected]",
    "message": "Hello world"
  }
}
Field Description
id Unique submission ID.
created_at ISO 8601 timestamp.
form_id The form's endpoint token.
form_name The form's display name.
ip Submitter's IP address.
user_agent Submitter's User-Agent string.
referrer Page the form was submitted from.
data All submitted fields (from payload_json).

HTTP details

  • Method: POST
  • Content-Type: application/json
  • Connect timeout: 3 seconds
  • Read timeout: 5 seconds

If your server does not respond within these timeouts, the delivery is marked as failed and retried.

Webhook signing

Every webhook request includes an X-RizzForms-Signature header containing an HMAC-SHA256 hex digest of the request body, signed with your webhook's signing secret.

Verify the signature to ensure the request came from RizzForms and was not tampered with:

Ruby

expected = OpenSSL::HMAC.hexdigest("SHA256", signing_secret, request.body.read)
if Rack::Utils.secure_compare(expected, request.headers["X-RizzForms-Signature"])
  # Valid
end

Node.js

const crypto = require("crypto");

const expected = crypto
  .createHmac("sha256", signingSecret)
  .update(rawBody)
  .digest("hex");

if (crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader))) {
  // Valid
}

Python

import hmac
import hashlib

expected = hmac.new(
    signing_secret.encode(),
    request.data,
    hashlib.sha256
).hexdigest()

if hmac.compare_digest(expected, request.headers["X-RizzForms-Signature"]):
    # Valid

Always use a constant-time comparison function to prevent timing attacks.

Retries

Failed deliveries are retried up to 3 times with exponential backoff. A delivery is considered failed if:

  • Your server returns a non-2xx status code.
  • The connection times out.
  • A network error occurs.

Response bodies from your server are stored (truncated at 4 KB) for debugging. You can view delivery status and replay any submission from the dashboard.

Security

  • HTTPS required. Webhook URLs must use https://. HTTP URLs are rejected.
  • SSRF protection. Private and reserved IP addresses (e.g., 127.0.0.1, 10.x.x.x, 192.168.x.x) are blocked. Webhooks can only reach public internet endpoints.

Rotate signing secret

If your signing secret is compromised, generate a new one immediately:

POST https://www.rizzness.com/api/forms/:form_id/plugins/:id/rotate_secret
Authorization: Bearer frk_your_api_key

The response returns the new signing secret. Update your server's verification code with the new value. The old secret stops working immediately.