v1 API

HTTP API

Forms

A form is a public endpoint your own website posts into: a contact form, a newsletter signup, a lead capture box. It is the only part of the Sedgemark API that accepts a write without a key.

How it works

You define a form in the dashboard the same way you define a collection: a name, a slug, and a list of typed fields. Sedgemark gives you back two endpoints and an inbox. Your site posts to the endpoints; you read what arrives in the inbox.

GET /api/v1/forms/{slug}

No authentication. Returns the form’s fields and an anti-spam token.

POST /api/v1/forms/{slug}/submit

No authentication. Never send an API key here.

No key, ever

These endpoints run in your visitors’ browsers. They are unauthenticated by design, and attaching an API key to a request from a browser publishes that key to everyone who loads the page.

Fetching a form’s descriptor

The descriptor tells you what the form currently expects. Building your markup from it, rather than hardcoding field names, means adding a field in the dashboard does not require a code change on the site.

json
{
  "slug": "contact",
  "name": "Contact us",
  "fields": [
    {
      "name": "Email",
      "slug": "email",
      "field_type": "email",
      "required": true,
      "config": {},
      "constraints": {
        "control": "input",
        "input_type": "email",
        "input_mode": "email",
        "autocomplete": "email",
        "max_length": 320,
        "min": null,
        "max": null,
        "step": null,
        "values": []
      }
    }
  ],
  "honeypot_field": "website",
  "min_submit_seconds": 3,
  "token": "eyJmIjoiYzE0Zi4uLiIsImlhdCI6MTc1NDMxNDI0NX0.9Qk3..."
}
KeyNotes
slugThe form’s slug, echoing what you asked for.
nameDisplay name from the dashboard.
fieldsEach with name, slug, field_type, required, config, and a constraints block. Post values keyed by slug.
honeypot_fieldThe name of the trap input to render hidden, or null if the form has none.
min_submit_secondsHow long a submission must take. 0 means no timing requirement.
tokenThe signed anti-spam token, or null when min_submit_seconds is 0.

The constraints block on each field carries everything needed to render the right control and validate the value in the browser: control, input_type, input_mode, autocomplete, max_length, min, max, step, and values for an enum. You never have to infer an input type from a field type yourself.

Submitting

Post JSON, keyed by field slug. No key, no session, no CSRF token, just the values.

javascript
const res = await fetch(
  'https://acme.sedgemark.app/api/v1/forms/contact/submit',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: 'visitor@example.com',
      message: 'Hello!',
    }),
  }
)

const result = await res.json()
// { ok: true, id: "…", submitted_at: "2026-08-04T15:04:05.000Z" }

Or from a server, where no Origin header means the allowlist never applies:

bash
curl -X POST https://acme.sedgemark.app/api/v1/forms/contact/submit \
  -H "Content-Type: application/json" \
  -d '{"email":"visitor@example.com","message":"Hello!"}'
http
HTTP/1.1 201 Created

{
  "ok": true,
  "id": "5a1f3c88-2b47-4e19-90d6-7c8e9f0a1b2c",
  "submitted_at": "2026-08-04T15:04:05.000Z",
  "ignored": ["utm_source"]
}
KeyNotes
okAlways true on a 201.
idThe stored submission’s id, or null if the honeypot was tripped. See below.
submitted_atISO 8601 timestamp. Absent from the honeypot response, which is exactly { ok: true, id: null } and nothing else.
ignoredPresent only when the payload carried keys the form does not define, and truncated to the first 20. See Unknown keys.

Unknown keys are dropped, not rejected

A payload key that matches no field is ignored and echoed back in the ignored array. It is not an error. This is deliberate on two counts: real browser posts routinely carry tracking parameters like utm_source or framework fields your form did not ask for, and rejecting unknown keys would turn a public endpoint into a way for a stranger to enumerate your field names one guess at a time.

Missing required fields still fail. You define which fields are required, so that is not a disclosure.

Three caps bound all of this:

CapBehavior
64 KB bodyA larger body is rejected with 413 while being read, before it is parsed.
100 keysA payload with more than 100 keys is rejected, not trimmed: 400 with a single error whose field is _payload. That is not a real field slug, so a handler mapping errors onto inputs will find nothing to attach it to: render it as a form-level message.
20 ignored keysThe `ignored` array echoes at most the first 20 unknown keys. The rest are still dropped silently; a short `ignored` array is not proof there were no others.

Field types

Forms have their own list of 12 field types, overlapping with but not identical to a collection’s. There is deliberately no rich_text, relation, media or json: none of them make sense on a surface a stranger can post to.

Field typeRender asInput typeMax lengthServer rule
texttext1,000Single-line: control characters stripped, newlines/tabs collapsed to spaces, then trimmed.
long_text