# Form Webhooks: Secure Delivery, Retries, and Signatures

> Send Reuily form responses to your application with signed webhooks, automatic retries, idempotency keys, and 30-day delivery activity.

Canonical: https://reuily.com/help/integrations/webhooks
Published: 2026-08-01
Last updated: 2026-09-15

## How do Reuily webhooks deliver form responses?

Reuily sends an HTTPS POST request with a signed JSON event for each new response. Your endpoint has 10 seconds to accept it; retryable failures receive up to five additional delivery attempts.

## Delivery behavior

- Request timeout: 10 seconds
- Delivery guarantee: At least once
- Authentication: Signed requests

## How do I set up a webhook?

1. Open the form's **Integrations** tab and select **Webhooks**.
2. Select **Add endpoint** and enter a public HTTPS URL.
3. Copy the signing secret and store it with your application secrets.
4. Select **Send a test**, then confirm success in **View activity**.

## What does Reuily send?

Reuily sends an HTTPS POST request containing the form, submission details, and labeled answers.

```json
{
  "id": "evt_4f5f9c...",
  "type": "form.submission.created",
  "api_version": "2026-07-01",
  "created_at": "2026-07-29T12:00:00Z",
  "data": {
    "form": {
      "id": "Ab12Cd34",
      "name": "Customer feedback"
    },
    "submission": {
      "id": "Ef56Gh78",
      "submitted_at": "2026-07-29T11:59:58Z",
      "time_to_complete_ms": 42000,
      "answers": [
        {
          "field": {
            "id": "rating-field",
            "label": "Rating",
            "type": "rating"
          },
          "value": 5
        }
      ]
    }
  }
}
```

## How do I verify each request?

- **Reuily-Event:** What happened.
- **Reuily-Delivery:** The unique delivery ID.
- **Reuily-Timestamp:** When the request was signed.
- **Reuily-Signature:** The signature to verify.

Verify the exact raw request body before parsing JSON. Reject timestamps outside your allowed window and compare signatures with a constant-time function.

### JavaScript

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";

const secret = process.env.REUILY_WEBHOOK_SECRET;

export function verifyWebhook(rawBody, timestamp, signature) {
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const match = /^v1=([a-f0-9]{64})$/.exec(signature);
  if (!match) return false;

  const expected = createHmac("sha256", secret)
    .update(timestamp + ".")
    .update(rawBody)
    .digest();
  const received = Buffer.from(match[1], "hex");

  return timingSafeEqual(received, expected);
}
```

### Python

```python
import hashlib
import hmac
import os
import time

secret = os.environ["REUILY_WEBHOOK_SECRET"].encode()


def verify_webhook(raw_body: bytes, timestamp: str, signature: str) -> bool:
    try:
        if abs(time.time() - int(timestamp)) > 300:
            return False
    except ValueError:
        return False

    if not signature.startswith("v1="):
        return False

    expected = hmac.new(
        secret,
        timestamp.encode() + b"." + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(signature.removeprefix("v1="), expected)
```

## Which response should my endpoint return?

Return any 2xx status after safely accepting the event. Timeouts, connection failures, 408, 409, 425, 429, and 5xx responses are retried.

Reuily can make five additional attempts after approximately 5 minutes, 30 minutes, 1 hour, 6 hours, and 24 hours. A valid Retry-After header can delay an attempt for up to 24 hours. Redirects are not followed.

## How do I avoid processing an event twice?

Store the Reuily-Delivery ID before processing work and skip IDs already handled. Use event and submission timestamps when ordering matters.

## Where can I check delivery activity?

Open the form, choose **Integrations**, select **Webhooks**, and open **View activity**. Delivery activity is retained for 30 days; form responses are stored separately.

Delivery activity shows safe failure explanations and retry information.

Raw response bodies and headers are hidden from every workspace user, including owners and admins, because destination responses may contain private data.

This does not change the submission payload sent to your destination. Limited diagnostics may still be retained internally with delivery history.

### How do I troubleshoot delivery failures?

- **401 / 403:** The destination rejected authentication or access. Check its credentials and permissions.
- **404:** The webhook address was not found. Check the endpoint URL.
- **429:** The destination is rate-limiting requests. Check its request limits.
- **408 or no response:** The destination timed out or could not be reached. Check that it is online and publicly reachable, and accept or queue events promptly.
- **409 / 425:** The destination could not accept the event yet. Check its availability and event handling.
- **5xx:** The destination encountered a server error. Check its service status or server logs.
- **3xx:** The destination returned a redirect. Use the final HTTPS endpoint URL; Reuily does not follow redirects.
- **Other 4xx:** The destination rejected the request. Check its expected payload and endpoint configuration.

This status appears only while an automatic retry is pending. If automatic attempts are exhausted, fix the destination and then use Send a test, Retry, or Add previous responses as appropriate.

## Common questions

### How long does my endpoint have to respond?

Ten seconds. Queue longer work and return a 2xx response after accepting the event.

### Can the same event arrive more than once?

Yes. Delivery is at least once, so deduplicate requests using Reuily-Delivery.
