REST API

The Formward REST API lets you read and manage your forms and their submissions programmatically. It is a JSON REST API scoped to all organizations the key's user belongs to: a key can reach every form and submission in any organization where its user is a member, not just forms they personally created. Access is governed by per-key scopes — reads need the matching :read scope (forms:read, submissions:read) and writes need a :write scope. Create, update, and delete form operations require forms:write, and managing triage (tags, notes, assignee) requires triage:write. The API requires the Professional plan or above.

Authentication

Create an API key in the dashboard under API keys. The full key is shown once at creation; only a hash is stored, so copy it immediately. Send the key on every request with either header:

Authorization: Bearer fwk_live_<your-key>
# or
X-Api-Key: fwk_live_<your-key>

Requests without a valid, non-revoked key return 401. Requests whose user is below the Professional plan return 403. A key inherits its data scope from its user's current organization memberships, so treat a key as granting access to every organization that user belongs to.

Base URL

https://app.formward.eu/api/v1

OpenAPI spec

A machine-readable OpenAPI 3.1 description of every REST endpoint, parameter, and response is served at /api/v1/openapi.json. Use it to generate clients or import the API into tools like Postman or Insomnia.

A combined spec covering both this REST API and the public submission endpoint (POST /f/{formId} on forms.formward.eu) is published as a static file at /openapi.json.

Rate limits

Each API key is limited to 120 requests per minute (fixed window). Every response carries the current limit state:

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 1736500860

X-RateLimit-Reset is the epoch time in seconds at which the window resets. When you exceed the limit the API returns 429 with a Retry-After header (seconds to wait) and the standard error body. Limits are enforced per node; on a multi-instance deploy the effective ceiling scales with the number of instances.

Filtering

The submissions endpoint accepts the following query parameters, which can be combined:

  • limit - number of rows, 1 to 100 (default 50)
  • before - cursor, an ISO timestamp; returns rows strictly older than it (use nextBefore from the previous page)
  • status - one of received, processed, spam, held, failed
  • since - an ISO timestamp; returns rows at or after it

List forms

GET /api/v1/forms returns every form across all organizations the key's user belongs to.

curl https://app.formward.eu/api/v1/forms \
  -H "Authorization: Bearer fwk_live_<your-key>"
{
  "data": [
    {
      "id": "<formId>",
      "name": "Contact form",
      "createdAt": "2026-01-01T12:00:00.000Z",
      "endpoint": "https://forms.formward.eu/f/<formId>"
    }
  ]
}

Get one form

GET /api/v1/forms/<formId> returns a single form's public info, including its submission count. A form your account cannot access returns 404.

curl https://app.formward.eu/api/v1/forms/<formId> \
  -H "Authorization: Bearer fwk_live_<your-key>"
{
  "data": {
    "id": "<formId>",
    "name": "Contact form",
    "createdAt": "2026-01-01T12:00:00.000Z",
    "endpoint": "https://forms.formward.eu/f/<formId>",
    "submissionCount": 42
  }
}

List a form's submissions

GET /api/v1/forms/<formId>/submissions returns the form's submissions, newest first. A form that does not belong to your account returns 404.

Pagination uses a cursor. Pass ?limit= (1 to 100, default 50) and ?before= set to the nextBefore value from the previous response to fetch older rows. You can also filter with ?status= and ?since= (see the Filtering section above).

curl "https://app.formward.eu/api/v1/forms/<formId>/submissions?limit=50" \
  -H "X-Api-Key: fwk_live_<your-key>"

Filtered example, processed submissions since a date:

curl "https://app.formward.eu/api/v1/forms/<formId>/submissions?status=processed&since=2026-01-01" \
  -H "X-Api-Key: fwk_live_<your-key>"
{
  "data": [
    {
      "id": "<submissionId>",
      "payload": { "email": "a@example.com", "message": "Hi" },
      "status": "processed",
      "createdAt": "2026-01-02T09:30:00.000Z",
      "spamScore": 3,
      "aiSummary": "New enquiry from a@example.com"
    }
  ],
  "nextBefore": "2026-01-02T09:30:00.000Z"
}

Get one submission

GET /api/v1/submissions/<id> returns a single submission. A submission that does not belong to your account returns 404.

curl https://app.formward.eu/api/v1/submissions/<submissionId> \
  -H "Authorization: Bearer fwk_live_<your-key>"

Status codes

  • 200 - success
  • 400 - invalid query parameter (limit, before, status, or since)
  • 401 - missing or invalid API key
  • 403 - plan does not include the API
  • 404 - resource not found or not owned by your account
  • 429 - rate limit exceeded (see Retry-After)

All errors share one JSON shape so they are easy to handle programmatically:

{ "error": { "code": "rate_limited", "message": "Rate limit exceeded. Retry after the window resets." } }

TypeScript SDK

A tiny zero-dependency TypeScript client. Copy it into your project.

export interface FormwardClientOptions { apiKey: string; baseUrl?: string }
export class FormwardClient {
  private base: string;
  constructor(private opts: FormwardClientOptions) {
    this.base = (opts.baseUrl ?? "https://app.formward.eu").replace(/\/+$/, "");
  }
  private async get<T>(path: string, query?: Record<string, string>): Promise<T> {
    const url = new URL(this.base + path);
    if (query) for (const [k, v] of Object.entries(query)) url.searchParams.set(k, v);
    const res = await fetch(url, { headers: { Authorization: `Bearer ${this.opts.apiKey}`, Accept: "application/json" } });
    if (!res.ok) throw new Error(`Formward API ${res.status}: ${await res.text()}`);
    return (await res.json()).data as T;
  }
  listForms() { return this.get<Array<{ id: string; name: string }>>("/api/v1/forms"); }
  getForm(id: string) { return this.get<{ id: string; name: string }>(`/api/v1/forms/${id}`); }
  listSubmissions(formId: string, opts?: { since?: string }) {
    return this.get<unknown[]>(`/api/v1/forms/${formId}/submissions`, opts?.since ? { since: opts.since } : undefined);
  }
}

Usage:

const fw = new FormwardClient({ apiKey: "fk_..." });
const forms = await fw.listForms();
REST API, read and manage forms and submissions | Formward Docs