Developer guide

Bring your own form

Build the form in your own markup, with your own design and validation, and post it straight to Origami. You keep the front end; Origami keeps the submissions, notifications, file storage, exports and webhooks.

POST https://origami.arcticleaf.com/f/your-form-slug

What you need

Three things, all of them in the form builder.

The form slug

Open your form and look at the Share & embed panel. The hosted link ends in the slug: in origami.arcticleaf.com/f/w8ne32s3n the slug is w8ne32s3n. That same path accepts your posts, and /embed/w8ne32s3n behaves identically on POST, so integrations already pointed at the embed path keep working.

The data keys

Every field has a data key, and that key, not the label, is the name you post. You'll find it on each field card in the builder and again on the field's edit page as Data key. A field labelled "What are you hoping to solve?" has the key what_are_you_hoping_to_solve.

Keys don't follow labels

A key is set once, when the field is created, and renaming the label afterwards leaves it alone. Copy the key from the builder rather than deriving it from the current label.

An encoding Origami accepts

Post application/x-www-form-urlencoded or multipart/form-data, whatever a normal browser form sends. File uploads must be multipart. A JSON request body is not accepted; ask for a JSON response with an Accept header instead.

Pick a path

Three ways in. They're alternatives, not steps: choose on how much control you need over what the visitor sees next, and whether you have a secret to protect.

No JavaScript

Plain HTML

The browser posts the form the way forms have always worked.

  • Nothing to host
  • Visitor leaves your page
  • A Redirect URL brings them back
Recommended

JavaScript & JSON

Post from the page, read a real answer, never navigate away.

  • Cross-origin, no backend
  • Status codes and error text
  • You control the whole experience
For secrets

Your own server

Your backend receives the submission and forwards it.

  • Room for Turnstile or reCAPTCHA
  • Keeps keys off the client
  • Your own logging and retries

Plain HTML

Point a normal form at the endpoint. No JavaScript, nothing to deploy.

your-page.html
<form method="post" action="https://origami.arcticleaf.com/f/w8ne32s3n">

  <label for="full_name">Full name</label>
  <input id="full_name" name="full_name" type="text" required>

  <label for="work_email">Work email</label>
  <input id="work_email" name="work_email" type="email" required>

  <label for="message">What are you hoping to solve?</label>
  <textarea id="message" name="what_are_you_hoping_to_solve"></textarea>

  <button type="submit">Send</button>
</form>

The id is yours to choose; the name has to be the Origami key. Add enctype="multipart/form-data" to the form tag if any field is a file upload.

Send the visitor back to your site

By default a successful post lands on Origami's thank-you page. Under Form settings → Redirect URL after submit, set a page on your own site and Origami sends them there instead.

Where validation errors land

If a required field is missing or a value is malformed, the visitor lands on an Origami-hosted copy of your form showing the error, not on your page. Mirror the form's required fields and types in your markup so that never happens.

JavaScript & JSON

Add one header and the endpoint answers JSON instead of a page. Cross-origin requests are allowed, so this works from any domain with no backend of your own.

app.js
const res = await fetch("https://origami.arcticleaf.com/f/w8ne32s3n", {
  method: "POST",
  headers: { Accept: "application/json" },
  body: new FormData(form),   // or a URLSearchParams
});

const result = await res.json();

if (result.ok) {
  // Origami stored it. Follow the form's Redirect URL if it has one.
  if (result.redirect) location.href = result.redirect;
  else showThanks();
} else {
  // result.field is the data key of the offending input, when there is one.
  showError(result.error, result.field);
}

Every JSON response is one of two shapes:

// stored
{ "ok": true, "redirect": "https://yoursite.com/thanks" | null }

// rejected
{ "ok": false, "error": "\"Budget\" has an invalid value.", "field": "budget" }
Why JSON mode never redirects

A form with a Redirect URL answers 303 to a browser, but JSON callers get ok: true and the target in redirect instead. A cross-origin 303 would send fetch() chasing a page it isn't allowed to read.

Send the body as FormData or URLSearchParams and don't set a Content-Type yourself. Accept and a form content-type are CORS-safelisted, so the request goes straight through without a preflight; if you do add a custom header, the endpoint answers OPTIONS correctly anyway.

Through your own server

Worth the extra hop when you have something to protect: a bot-verification secret, an API key, your own audit log.

Cloudflare Worker · also works as-is in Node 18+
// 1. Your own checks first — Turnstile, reCAPTCHA, rate limits.
const submitted = await request.json();

// 2. Build a normal form body, keyed by Origami's data keys.
const body = new FormData();
body.append("full_name", submitted.fullName);
body.append("work_email", submitted.workEmail);
body.append("what_are_you_hoping_to_solve", submitted.message);

// Deliberately no _ts and no _website — see "The two spam fields" below.

const res = await fetch("https://origami.arcticleaf.com/f/w8ne32s3n", {
  method: "POST",
  headers: { Accept: "application/json" },
  body,
});

const result = await res.json();
if (!result.ok) {
  console.error("Origami rejected the submission:", result.error, result.field);
  return Response.json({ error: result.error }, { status: 502 });
}
return Response.json({ success: true });

Ask for JSON here too. Without the Accept header you get HTML back, a form with a Redirect URL answers 303, and fetch follows it to your own thank-you page and reports whatever that returns. If you must stay on HTML, set redirect: "manual" and treat a 3xx as success.

Skip what you don't have

Leave optional fields out rather than appending empty strings, and drop a value the field type can't accept instead of letting one optional answer fail the whole submission.

Field reference

What each field type expects, and what ends up stored. Anything posted under a key Origami doesn't recognise is dropped without a word.

Field typeWhat to sendWatch for
Short text, Long text, URL, PhoneThe stringStored as typed
EmailA valid addressThe first email field on the form becomes the submitter: it's the reply-to on your notification and where the auto-reply goes
NumberSomething Number() can parse(204) 555-0134 is rejected outright. Strip the formatting, or use a Phone field
DateYYYY-MM-DDThe same format a native date input posts
Dropdown, Multiple choiceExactly one configured optionCharacter for character, including case and spacing. Editing the options invalidates values your form still sends
Checkboxes (with options)Repeat the key once per selectionArrives as one value: Design, SEO
Single checkboxAny non-empty value, or omit itPresent becomes yes, absent becomes no
File uploadMultipart; repeat the key for several files25 MB per file, counted against the workspace's storage
Heading, Paragraph, Page breakNothingLayout only, they hold no data

Conditional logic still applies

A field set to appear only when another answer matches is checked on the server too. Send a value for a field whose condition isn't met and it's discarded, so replicate the same show and hide rules in your form or those answers quietly go missing.

Multi-page forms are one post

Page breaks are a front-end nicety. The endpoint takes every field in a single request, however many pages the Origami version shows.

The two spam fields

Origami's hosted form ships two anti-bot fields. Both are optional for you, but sending them wrongly makes submissions vanish.

FieldWhat it doesWhat you should do
_website A honeypot. If it arrives with anything in it, the submission is dropped and the sender is shown success anyway. Leave it out, or include it as a hidden field kept empty.
_ts Unix seconds from when the page rendered. If the gap to submit is under the form's minimum fill time, the submission is dropped, again with a fake success. Leave it out entirely. Only send it if you set it when the page loaded.
Silent by design

Both traps answer exactly like a success, in HTML and in JSON, because telling a bot it was caught teaches it how to get through. That's precisely why a server-side integration should omit _ts: a fresh timestamp from your backend looks like an instant fill, and you'd drop real submissions with no error anywhere to find.

Responses

With Accept: application/json you get the JSON shapes above. Without it, every response is HTML meant for a browser and you read the status code.

StatusJSONMeaning
200ok: trueStored. redirect carries the form's Redirect URL, or null.
303HTML only: stored, and the Redirect URL is in the Location header.
400ok: falseRejected. error says why and field names the input. Also what a JSON request body gets.
403closed: trueThe form is closed or over its monthly limit. In HTML this is a 200 with a friendly page instead.
404ok: falseNo form with that slug, or it's archived.
405Wrong method. The endpoint takes GET, POST and OPTIONS.
500ok: falseAn uploaded file couldn't be saved. Nothing was stored; safe to retry.
503ok: falseThe workspace is out of file storage. Retrying won't help until space is freed.
In HTML mode, 200 isn't proof

A closed form, a form at its monthly limit and both spam traps all answer 200 with a friendly page. JSON mode separates the first two out as a 403; the traps stay indistinguishable on purpose. If a submission matters, confirm it with a webhook.

Getting the data out

Once a submission is stored, four ways to reach it, one of them built for your code.

Webhooks

Add an endpoint under Webhooks in the builder and Origami posts JSON to it on every submission. Each endpoint gets its own secret and the body is signed, so you can prove the call came from Origami.

POST to your endpoint
x-origami-event: submission.created
x-origami-signature: sha256=<hmac-sha256 of the raw body, lowercase hex>

{
  "event": "submission.created",
  "form": { "id": "frm_…", "slug": "w8ne32s3n", "title": "New Sales Request" },
  "submission": {
    "id": "sub_…",
    "submitted_at": 1786980689,
    "data": { "full_name": "Ada Lovelace", "work_email": "ada@example.com" },
    "files": [{ "field": "attachments", "filename": "brief.pdf",
               "size": 18244, "url": "https://origami.arcticleaf.com/files/fil_…" }]
  }
}

Verify the signature over the raw body before parsing, with a constant-time comparison. Two things to plan for: delivery is attempted once, with a ten-second timeout and no automatic retry, so keep your handler fast and idempotent; and the url on a file needs a signed-in Origami session with access to that form. It is not a public download link.

The other three

  • Email notifications on every submission, to whoever you list on the form.
  • CSV export of the whole history, timestamped in your workspace's time zone.
  • The dashboard, with search, read and unread, and per-form analytics.

Before you go live

Six things worth confirming with a real submission.

  • Every name matches a data key in the builder, copied not guessed. Unknown keys are dropped in silence.
  • Your form enforces the same required fields as Origami, so nobody meets the hosted error page.
  • Number and dropdown values are normalised to what the field accepts.
  • No _ts is sent from a server, and _website is absent or empty.
  • A Redirect URL is set if you're using the plain HTML path.
  • You've submitted the live form once and watched it reach the dashboard, with the notification email arriving.
Stuck on a specific form?

Reply to your welcome email with the form slug and the exact values you're posting, and we'll tell you which field is unhappy.