Make your first onboarding form

A hands-on walkthrough: create a flow, publish it, submit data with curl, read it back.

Time: ~10 minutes · You'll use: curl only (no frontend required)


What you'll build

A simple onboarding intake: name + email, saved when someone submits. Optional: webhook notification.

flowchart LR
  dev[Developer_API_key] --> createFlow[Create_flow]
  createFlow --> addVersion[Add_draft_version]
  addVersion --> publish[Publish_v1]
  publish --> user[User_embed_key]
  user --> submit[Submit_intake]
  submit --> db[(Intake_Relay)]
  submit --> webhook[Webhook_optional]

Concepts (30 seconds)

Term Plain English
Organization Your account
API key (mfk_...) Admin key — create flows, read all submissions
Flow One intake process (e.g. "SaaS onboarding")
Version A snapshot of fields + steps (v1, v2, …)
Publish Lock a version so embeds can use it
Embed key (ifk_...) Public key — submit data, read published schema
Intake One person's submitted answers

Rule: API key in your backend only. Embed key in your frontend.


Before you begin

1. Create an account

Create a free account and verify your email.

2. Mint an API key

curl -s -X POST https://willowriverautomation.com/relay/v1/auth/api-key \
  -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com","password":"your-password"}'

Copy the mfk_... value from the response. It is shown once.

3. Set shell variables

export API_KEY="mfk_..."   # paste yours
export BASE="https://willowriverautomation.com/relay"

All API requests in this tutorial use $BASE/v1/....


Step 1 — Health check

What: Confirm the API is reachable.

curl -s "$BASE/health"

Expected: {"status":"ok"}


Step 2 — Create a flow

What: Create a named intake process. Intake Relay returns an embed key for this flow.

Why: A flow is the container — versions and submissions live under it.

curl -s -X POST "$BASE/v1/flows" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"My Onboarding","slug":"onboarding"}'

Save from the response:

export FLOW_ID="<id from response>"
export EMBED_KEY="<embed_key from response>"

Step 3 — Define the form (draft version)

What: Add fields and steps as JSON. Status starts as draft.

Why: Draft lets you edit before going live. Published versions are immutable.

curl -s -X POST "$BASE/v1/flows/$FLOW_ID/versions" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "schema": {
      "fields": [
        {"id":"name","type":"text","label":"Your name","validation":{"required":true}},
        {"id":"email","type":"email","label":"Work email","validation":{"required":true}}
      ],
      "steps": [
        {"id":"about","title":"About you","fieldIds":["name","email"]}
      ]
    }
  }'

Expected: "version_number": 1, "status": "draft"


Step 4 — Publish

What: Make version 1 live. Embeds and intake submissions can now target it.

curl -s -X POST "$BASE/v1/flows/$FLOW_ID/versions/1/publish" \
  -H "Authorization: Bearer $API_KEY"

Expected: "status": "published"


Step 5 — Submit intake (as a user)

What: Send answers like a user filling the form.

Why: Uses embed key, not API key — same as your frontend would.

curl -s -X POST "$BASE/v1/flows/$FLOW_ID/intake?version=1" \
  -H "X-Embed-Key: $EMBED_KEY" \
  -H "Content-Type: application/json" \
  -d '{"data":{"name":"Alex","email":"alex@example.com"}}'

Save:

export SUBMISSION_ID="<id from response>"

Expected: "status": "accepted", HTTP 201


Step 6 — Read it back (admin)

What: Fetch the saved submission with your API key.

curl -s "$BASE/v1/intake/$SUBMISSION_ID" \
  -H "Authorization: Bearer $API_KEY"

Expected: "data": {"name": "Alex", "email": "alex@example.com"}


Optional extras

Safe retries (idempotency)

Same key = same submission, no duplicate:

curl -s -X POST "$BASE/v1/flows/$FLOW_ID/intake?version=1" \
  -H "X-Embed-Key: $EMBED_KEY" \
  -H "Idempotency-Key: signup-alex-001" \
  -H "Content-Type: application/json" \
  -d '{"data":{"name":"Alex","email":"alex@example.com"}}'

Run twice — second response has "idempotent_replay": true.

Register a webhook

Get notified on every submission:

curl -s -X POST "$BASE/v1/flows/$FLOW_ID/webhooks" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/webhook","events":["intake.submitted"]}'

See Webhooks for signature verification.

Embed in a webpage

<div id="onboarding"></div>
<script
  src="https://willowriverautomation.com/relay/v1/intake.js"
  data-flow-id="YOUR_FLOW_ID"
  data-version="1"
  data-embed-key="YOUR_EMBED_KEY"
  data-target="#onboarding">
</script>

See Embedding for React and callbacks.


You did it

You just:

  1. Created a flow
  2. Defined and published a version
  3. Submitted intake with an embed key
  4. Read the submission with an API key

Go deeper:


Troubleshooting

Problem Fix
401 unauthorized Check Authorization: Bearer $API_KEY or X-Embed-Key: $EMBED_KEY
400 version_not_published Run the publish step first
400 validation Missing required field — check schema
429 quota_exceeded Free tier limits — see billing or upgrade
Connection errors Confirm $BASE is https://willowriverautomation.com/relay and your network allows HTTPS