Skip to main content
Webhooks let you receive HTTP notifications whenever something happens in FormFlows.ai — a form is published, a submission is created, or a submission is updated. When an event occurs, FormFlows.ai sends a POST request with a JSON payload to your registered URL.

Register a webhook

POST /webhooks Registers a new webhook endpoint.

Body parameters

url
string
required
The HTTPS URL that FormFlows.ai will send event payloads to. Must be publicly accessible.
events
string[]
required
Array of event types to subscribe to. Supported events:
  • submission.created — A new submission is received on a form.
  • submission.updated — An existing submission is modified.
  • form.published — A form’s status changes to published.
form_id
string
Restrict this webhook to events from a specific form. Omit to receive events from all forms in your account.
secret
string
A signing secret you choose. If provided, FormFlows.ai signs every payload with HMAC-SHA256 using this secret and includes the signature in the X-FormFlows-Signature header. Use this to verify that payloads genuinely come from FormFlows.ai.

Response

id
string
Unique webhook identifier, prefixed with whk_.
url
string
The registered endpoint URL.
events
string[]
List of event types this webhook is subscribed to.
form_id
string
The form this webhook is scoped to. null if it receives events from all forms.
created_at
string
ISO 8601 creation timestamp.
curl --request POST \
  --url "https://api.formflows.ai/v1/webhooks" \
  --header "Authorization: Bearer sk_live_your_api_key" \
  --header "Content-Type: application/json" \
  --data '{
    "url": "https://hooks.yourapp.com/formflows",
    "events": ["submission.created", "form.published"],
    "form_id": "frm_8kQmP2xNvL",
    "secret": "whsec_your_signing_secret"
  }'
Example response:
{
  "id": "whk_6mNoP8qRsT",
  "url": "https://hooks.yourapp.com/formflows",
  "events": ["submission.created", "form.published"],
  "form_id": "frm_8kQmP2xNvL",
  "secret_set": true,
  "created_at": "2025-01-15T12:00:00Z"
}
The secret value you provide is never returned in API responses. The secret_set boolean confirms whether a signing secret is configured.

List webhooks

GET /webhooks Returns all registered webhooks in your account.

Query parameters

limit
integer
default:"20"
Number of webhooks to return per page. Maximum 100.
cursor
string
Pagination cursor from a previous response’s next_cursor.
curl --request GET \
  --url "https://api.formflows.ai/v1/webhooks" \
  --header "Authorization: Bearer sk_live_your_api_key"

Delete a webhook

DELETE /webhooks/{id} Removes a registered webhook. FormFlows.ai will stop sending events to that endpoint immediately.

Path parameters

id
string
required
The webhook ID to delete, e.g. whk_6mNoP8qRsT.

Response

Returns a 204 No Content response on success.
curl --request DELETE \
  --url "https://api.formflows.ai/v1/webhooks/whk_6mNoP8qRsT" \
  --header "Authorization: Bearer sk_live_your_api_key"

Webhook payload format

When a subscribed event occurs, FormFlows.ai sends a POST request to your URL with the following JSON body:
{
  "event": "submission.created",
  "form_id": "frm_8kQmP2xNvL",
  "timestamp": "2025-01-15T14:32:00Z",
  "data": {
    "submission_id": "sub_7vHnM4rKpQ",
    "fields": {
      "fld_a1B2c3D4": "Jane Smith",
      "fld_e5F6g7H8": "[email protected]",
      "fld_i9J0k1L2": ["Keynote", "Workshop A"]
    }
  }
}

Payload fields

event
string
The event type that fired. One of submission.created, submission.updated, or form.published.
form_id
string
The ID of the form that the event is associated with.
timestamp
string
ISO 8601 timestamp of when the event occurred.
data
object
Event-specific data.

Signature verification

If you set a secret when registering your webhook, every request includes an X-FormFlows-Signature header:
X-FormFlows-Signature: sha256=3d2f8c1e9b4a...
The value is sha256= followed by the hex-encoded HMAC-SHA256 of the raw request body, computed using your secret as the key. Always verify this signature before processing a payload to confirm it came from FormFlows.ai.
Compare signatures using a constant-time comparison function. Using a regular string equality check (==) is vulnerable to timing attacks.
import hashlib
import hmac

def verify_signature(payload_body: bytes, signature_header: str, secret: str) -> bool:
    """Verify the X-FormFlows-Signature header against the request body."""
    expected_sig = hmac.new(
        secret.encode("utf-8"),
        payload_body,
        hashlib.sha256,
    ).hexdigest()

    received_sig = signature_header.removeprefix("sha256=")

    return hmac.compare_digest(expected_sig, received_sig)


# Example usage in a Flask handler
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = "whsec_your_signing_secret"

@app.route("/formflows", methods=["POST"])
def handle_webhook():
    signature = request.headers.get("X-FormFlows-Signature", "")

    if not verify_signature(request.get_data(), signature, WEBHOOK_SECRET):
        abort(400, "Invalid signature")

    event = request.json
    print(f"Received event: {event['event']}")
    return "", 200

Retry policy

FormFlows.ai considers a delivery successful when your endpoint returns any 2xx HTTP status code within 10 seconds. If the request times out or returns a non-2xx status, FormFlows.ai retries the delivery with exponential backoff:
AttemptDelay
1 (initial)Immediate
230 seconds
35 minutes
430 minutes
52 hours
After 5 failed attempts, the event is marked as undeliverable and no further retries occur.
Your endpoint must respond within 10 seconds. If your processing logic takes longer than that, return a 200 immediately and handle the payload asynchronously (e.g., push it to a queue).
Because retries can deliver the same event more than once, make your handler idempotent. Use data.submission_id or timestamp to detect and skip duplicate deliveries.