Reference

FORX documentation

Create an endpoint, point a form at it, and FORX validates, stores, and forwards every submission. No SDK.

01 / Start hereShip your first formCreate an endpoint and make a real request in four steps.02 / Go furtherDeliver events anywhereConnect a webhook and verify every delivery with a signature.

Quickstart

Four steps. Replace YOUR_ENDPOINT with the id from the dashboard.

  1. Create an account.
  2. Create an endpoint in the dashboard.
  3. Copy that URL. It looks like https://forx.voryn.xyz/f/YOUR_ENDPOINT.
  4. Paste it into your form, fetch call, or cURL command.
HTML
<!-- Replace YOUR_ENDPOINT with the id from your dashboard -->
<form action="https://forx.voryn.xyz/f/YOUR_ENDPOINT" method="POST">
  <input name="email" type="email" required />
  <textarea name="message"></textarea>
  <input type="hidden" name="_gotcha" value="" />
  <button type="submit">Send</button>
</form>
fetch
await fetch("https://forx.voryn.xyz/f/YOUR_ENDPOINT", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "dev@example.com",
    message: "Hello"
  })
})
cURL
curl -X POST "https://forx.voryn.xyz/f/YOUR_ENDPOINT" \
  -H "Content-Type: application/json" \
  -d '{"email":"dev@example.com","message":"Hello"}'

Demo

https://forx.voryn.xyz/f/demo is a reserved public endpoint. It accepts email and message, returns a real 201 payload, and does not write into a workspace or send email.

POST /f/demo
await fetch("https://forx.voryn.xyz/f/demo", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "you@example.com",
    message: "Hello from the docs"
  })
})

demo is reserved. You cannot create an endpoint with that id. It is rate limited like any other public endpoint.

Endpoints

Every endpoint follows the same shape: {origin}/f/{id}. The id is generated when you create the endpoint and never changes.

MethodPathPurpose
POST/f/:idSubmit a form or JSON payload
GET/f/:idEndpoint health and accepted fields

HTML forms, JSON, URL-encoded fields, and multipart form data are accepted. Browser requests are CORS-enabled, including preflight.

Add a hidden honeypot field named _gotcha and leave it empty. Bots that fill it get 202 and the submission is not stored or delivered.

Public endpoints are rate limited. Treat 429 rate_limited as a signal to retry later.

Optionally lock an endpoint to one domain in endpoint settings. Browser requests are checked against Origin or Referer. Leave it blank to allow any origin. Server-to-server requests without those headers still work.

Payloads

Successful submissions return the stored event so you can confirm the shape immediately.

201 Created
{
  "id": "evt_01H8XA",
  "event": "user.submit",
  "endpoint": "contact-form",
  "created_at": "2024-03-12T14:22:01Z",
  "data": {
    "email": "dev@example.com",
    "message": "Hello from FORX"
  }
}

Custom redirects

After a successful HTML form submission, add a hidden _next field to send the visitor to your thank-you page instead of the JSON response.

HTML form
<form action="https://forx.voryn.xyz/f/YOUR_ENDPOINT" method="POST">
  <input name="email" type="email" required />
  <textarea name="message"></textarea>
  <input type="hidden" name="_next" value="https://yoursite.com/thank-you" />
  <button type="submit">Send</button>
</form>

Use a full https:// URL, or a root-relative path such as /thank-you. Root-relative paths redirect on the FORX domain. Redirects apply to browser form submissions only. JSON requests still get 201 Created.

Action URL option
<form action="https://forx.voryn.xyz/f/YOUR_ENDPOINT?_next=https%3A%2F%2Fyoursite.com%2Fthank-you" method="POST">
  <input name="email" type="email" required />
  <button type="submit">Send</button>
</form>

If both the hidden field and the query param are present, the hidden field wins.

Webhooks

FORX POSTs a signed JSON body to each destination. The header is X-Forx-Signature: sha256=<hex>. Verify it with the secret shown when you create the destination. Compare against the raw body, not a re-serialized object.

Verification
import { createHmac, timingSafeEqual } from "node:crypto"

export function verifyForxSignature(rawBody: string, header: string | null) {
  if (!header?.startsWith("sha256=")) return false

  const expected = createHmac("sha256", process.env.FORX_WEBHOOK_SECRET!)
    .update(rawBody)
    .digest("hex")

  const actual = header.slice("sha256=".length)
  if (actual.length !== expected.length) return false
  return timingSafeEqual(Buffer.from(actual), Buffer.from(expected))
}
Next.js route handler
export async function POST(request: Request) {
  const rawBody = await request.text()
  const signature = request.headers.get("X-Forx-Signature")

  if (!verifyForxSignature(rawBody, signature)) {
    return Response.json({ error: "invalid_signature" }, { status: 401 })
  }

  const event = JSON.parse(rawBody)
  if (event.event === "user.submit") {
    await saveSubmission(event.data)
  }

  return Response.json({ received: true })
}

Failed deliveries retry up to 3 times. Check X-Forx-Event-Id if you need to ignore duplicates.

Billing

Free is for testing. Starter is the plan for a live form. Pro adds capacity and priority support.

LimitFree — $0Starter — $4Pro — $10
Submissions / month501,00010,000
Active endpoints21050
Data retention30 days1 yearUnlimited
Email notificationsIncludedIncludedIncluded
Webhooks + retries—IncludedIncluded
Webhook destinations1550

Plan limits are enforced before new resources or submissions are accepted.

Troubleshooting

Submission accepted but no email: check notification settings, the verified sending domain, and the delivery status on that submission. Webhook URLs must be public HTTPS and should return 2xx quickly. FORX retries transient failures and stores the last HTTP status.

Browser forms can use _next for a thank-you redirect. JSON requests always get the event response.

Homepage demo failing: POST to /f/demo with both email and message. That id is reserved and will not appear in your dashboard.

Errors

StatusCodeMeaning
403domain_not_allowedOrigin does not match the allowed domain
403endpoint_pausedEndpoint is paused
404endpoint_not_foundNo endpoint exists for that id
413payload_too_largeBody is over 1 MB
422invalid_payloadRequired fields missing or malformed
422invalid_redirect_next is not an http(s) URL or root-relative path
429rate_limitedToo many submissions; retry later
503storage_unavailableSubmission could not be saved